Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.idea/
/vendor/
.DS_Store
.worktrees/
!/README.md
/*.md
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ build:
GOOS=linux GOARCH=386 go build -v -tags with_gvisor .
GOOS=linux GOARCH=arm go build -v -tags with_gvisor .
GOOS=android GOARCH=arm64 go build -v -tags with_gvisor .
GOOS=freebsd GOARCH=amd64 go build -v -tags with_gvisor .
GOOS=windows GOARCH=amd64 go build -v -tags with_gvisor .
GOOS=freebsd GOARCH=amd64 go build -v -tags with_gvisor .

fmt:
@gofumpt -l -w .
Expand Down
174 changes: 174 additions & 0 deletions monitor_freebsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package tun

import (
"net"
"net/netip"
"os"
"sync"

"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/x/list"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)

var _ NetworkUpdateMonitor = (*networkUpdateMonitor)(nil)

type networkUpdateMonitor struct {
access sync.Mutex
callbacks list.List[NetworkUpdateCallback]
routeSocketFile *os.File
closeOnce sync.Once
done chan struct{}
logger logger.Logger
}

func NewNetworkUpdateMonitor(logger logger.Logger) (NetworkUpdateMonitor, error) {

return &networkUpdateMonitor{
logger: logger,
done: make(chan struct{}),
}, nil
}

// Close implements NetworkUpdateMonitor.
func (m *networkUpdateMonitor) Close() error {
m.closeOnce.Do(func() {
close(m.done)
})
return nil
}

// Start implements NetworkUpdateMonitor.
func (m *networkUpdateMonitor) Start() error {
go m.loopUpdate()
return nil
}

func (m *networkUpdateMonitor) loopUpdate() {
for {
select {
case <-m.done:
return
default:
}
err := m.loopUpdate0()
if err != nil {
m.logger.Error("listen network update: ", err)
return
}
}
}

func (m *networkUpdateMonitor) loopUpdate0() error {
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return err
}
err = unix.SetNonblock(routeSocket, true)
if err != nil {
unix.Close(routeSocket)
return err
}
routeSocketFile := os.NewFile(uintptr(routeSocket), "route")
defer routeSocketFile.Close()
m.routeSocketFile = routeSocketFile
m.loopUpdate1(routeSocketFile)
return nil
}

func (m *networkUpdateMonitor) loopUpdate1(routeSocketFile *os.File) {
buffer := buf.NewPacket()
defer buffer.Release()

done := make(chan struct{})
go func() {
select {
case <-m.done:
routeSocketFile.Close()
case <-done:
}
}()
n, err := routeSocketFile.Read(buffer.FreeBytes())
close(done)
if err != nil {
return
}
buffer.Truncate(n)

messages, err := route.ParseRIB(route.RIBTypeRoute, buffer.Bytes())
if err != nil {
return
}

for _, message := range messages {
if _, isRouteMessage := message.(*route.RouteMessage); isRouteMessage {
m.emit()
return
}
}
}

// checkUpdate finds the first IPv4 default gateway and emits an update event.
func (m *defaultInterfaceMonitor) checkUpdate() error {
var defaultInterface *control.Interface
ribMessage, err := route.FetchRIB(unix.AF_INET, route.RIBTypeRoute, 0)
if err != nil {
return err
}
routeMessages, err := route.ParseRIB(route.RIBTypeRoute, ribMessage)
if err != nil {
return err
}

for _, rawRouteMessage := range routeMessages {
routeMessage := rawRouteMessage.(*route.RouteMessage)
if len(routeMessage.Addrs) <= unix.RTAX_NETMASK {
continue
}
destination, isIPv4Destination := routeMessage.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
if !isIPv4Destination || destination.IP != netip.IPv4Unspecified().As4() {
continue
}
mask, isIPv4Mask := routeMessage.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr)
if !isIPv4Mask {
continue
}
if ones, _ := net.IPMask(mask.IP[:]).Size(); ones != 0 {
continue
}
flag := unix.RTF_UP | unix.RTF_GATEWAY | unix.RTF_STATIC
if routeMessage.Flags&(flag) != flag {
continue
}
routeInterface, err := m.interfaceFinder.ByIndex(routeMessage.Index)
if err != nil {
return err
}
if routeInterface.Flags&net.FlagLoopback != 0 {
continue
}
defaultInterface = routeInterface
break
}

if defaultInterface == nil {
if m.underNetworkExtension {
m.logger.Warn("Not implemented: UnderNetworkExtension")
}
return ErrNoRoute
}
newInterface, err := m.interfaceFinder.ByIndex(defaultInterface.Index)
if err != nil {
return E.Cause(err, "find updated interface: ", defaultInterface.Name)
}
oldInterface := m.defaultInterface.Swap(newInterface)
if oldInterface != nil && oldInterface.Equals(*newInterface) {
return nil
}
m.emit(newInterface, 0)
return nil
}
2 changes: 1 addition & 1 deletion monitor_other.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !(linux || windows || darwin)
//go:build !(linux || windows || darwin || freebsd)

package tun

Expand Down
2 changes: 1 addition & 1 deletion monitor_shared.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build linux || windows || darwin
//go:build linux || windows || darwin || freebsd

package tun

Expand Down
Loading