diff --git a/README.md b/README.md index 6a8a9c08..3accbfea 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Simple transparent proxy library. -For Linux, Windows, macOS and iOS. +For Linux, FreeBSD, Windows, macOS and iOS. ## License diff --git a/monitor_freebsd.go b/monitor_freebsd.go new file mode 100644 index 00000000..452730ac --- /dev/null +++ b/monitor_freebsd.go @@ -0,0 +1,167 @@ +package tun + +import ( + "net" + "net/netip" + "os" + "sync" + + "github.com/metacubex/sing/common/buf" + "github.com/metacubex/sing/common/control" + E "github.com/metacubex/sing/common/exceptions" + "github.com/metacubex/sing/common/logger" + "github.com/metacubex/sing/common/x/list" + + "golang.org/x/net/route" + "golang.org/x/sys/unix" +) + +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 +} + +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 + } + } +} + +func (m *networkUpdateMonitor) Close() error { + m.closeOnce.Do(func() { + close(m.done) + }) + return nil +} + +func (m *defaultInterfaceMonitor) checkUpdate() error { + ribMessage, err := route.FetchRIB(unix.AF_UNSPEC, route.RIBTypeRoute, 0) + if err != nil { + return err + } + routeMessages, err := route.ParseRIB(route.RIBTypeRoute, ribMessage) + if err != nil { + return err + } + var defaultInterface *control.Interface + 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 { + continue + } + if destination.IP != netip.IPv4Unspecified().As4() { + continue + } + mask, isIPv4Mask := routeMessage.Addrs[unix.RTAX_NETMASK].(*route.Inet4Addr) + if !isIPv4Mask { + continue + } + ones, _ := net.IPMask(mask.IP[:]).Size() + if ones != 0 { + continue + } + routeInterface, err := m.interfaceFinder.ByIndex(routeMessage.Index) + if err != nil { + return err + } + if routeMessage.Flags&unix.RTF_UP == 0 { + continue + } + if routeMessage.Flags&unix.RTF_GATEWAY == 0 { + continue + } + if routeInterface.Flags&net.FlagLoopback != 0 { + continue + } + defaultInterface = routeInterface + break + } + if defaultInterface == nil { + 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 +} diff --git a/monitor_other.go b/monitor_other.go index 40a936e9..f4e0b330 100644 --- a/monitor_other.go +++ b/monitor_other.go @@ -1,4 +1,4 @@ -//go:build !(linux || windows || darwin) +//go:build !(linux || windows || darwin || freebsd) package tun diff --git a/monitor_shared.go b/monitor_shared.go index c2b4254a..004700c9 100644 --- a/monitor_shared.go +++ b/monitor_shared.go @@ -1,4 +1,4 @@ -//go:build linux || windows || darwin +//go:build linux || windows || darwin || freebsd package tun diff --git a/tun.go b/tun.go index c4ba540c..0d023a0f 100644 --- a/tun.go +++ b/tun.go @@ -110,6 +110,14 @@ type Options struct { FileDescriptor int Logger logger.Logger + // FreeBSDInterfaceDescription, when non-empty, is written to the tun(4) + // interface's description field (ifconfig description) on FreeBSD. FreeBSD + // preserves this description after the owning process exits, so the caller + // can use it as a marker to identify and clean up tun devices it created. + // sing-tun itself attaches no meaning to the value; the caller (e.g. mihomo) + // owns both the marker string and any cleanup logic. Ignored on other platforms. + FreeBSDInterfaceDescription string + // No work for TCP, do not use. _TXChecksumOffload bool diff --git a/tun_freebsd.go b/tun_freebsd.go new file mode 100644 index 00000000..9409c825 --- /dev/null +++ b/tun_freebsd.go @@ -0,0 +1,327 @@ +package tun + +import ( + "net" + "os" + "os/exec" + "strconv" + "strings" + "syscall" + "unsafe" + + E "github.com/metacubex/sing/common/exceptions" + "github.com/metacubex/sing/common/shell" + + "golang.org/x/sys/unix" +) + +var _ Tun = (*NativeTun)(nil) + +// PacketOffset is the size of the 4-byte address family header that the +// FreeBSD tun(4) driver prepends to each packet once TUNSIFHEAD is enabled. +const PacketOffset = 4 + +// directFib is the alternate routing table (FIB) that carries the original +// physical routes while TUN auto-route takes over the default FIB (0). mihomo's +// DIRECT outbound sockets bind to this FIB via SO_SETFIB so their traffic +// bypasses the tun interface instead of looping back into the proxy. This is +// FreeBSD's equivalent of Linux's policy-routing table / SO_BINDTODEVICE. +const directFib = 1 + +// TUNSIFHEAD = _IOW('t', 96, int) on FreeBSD (net/if_tun.h). +// _IOW(g,n,t) = IOC_IN | ((sizeof(t)&IOCPARM_MASK)<<16) | (g<<8) | n +// IOC_IN=0x80000000, IOCPARM_MASK=0x1fff, sizeof(int)=4, 't'=0x74, n=96(0x60). +const _TUNSIFHEAD = 0x80000000 | (4 << 16) | (0x74 << 8) | 96 // 0x80047460 + +type NativeTun struct { + tunFd int + tunFile *os.File + options Options + inet4Address [4]byte + inet6Address [16]byte +} + +func New(options Options) (Tun, error) { + var ( + tunFd int + tunFile *os.File + err error + ) + if options.FileDescriptor == 0 { + tunFile, err = openTun(options.Name) + if err != nil { + return nil, err + } + tunFd = int(tunFile.Fd()) + } else { + tunFd = options.FileDescriptor + tunFile = os.NewFile(uintptr(tunFd), "tun") + } + + nativeTun := &NativeTun{ + tunFd: tunFd, + tunFile: tunFile, + options: options, + } + if len(options.Inet4Address) > 0 { + nativeTun.inet4Address = options.Inet4Address[0].Addr().As4() + } + if len(options.Inet6Address) > 0 { + nativeTun.inet6Address = options.Inet6Address[0].Addr().As16() + } + + err = nativeTun.configure() + if err != nil { + tunFile.Close() + return nil, err + } + return nativeTun, nil +} + +// openTun opens the tun device matching options.Name (e.g. "tun0"). If Name is +// empty or "tun", it uses the cloning device /dev/tun to auto-allocate one. +func openTun(name string) (*os.File, error) { + devPath := "/dev/" + name + if name == "" || name == "tun" { + devPath = "/dev/tun" + } + file, err := os.OpenFile(devPath, os.O_RDWR, 0) + if err != nil { + return nil, E.Cause(err, "open ", devPath) + } + // Enable the 4-byte address-family header so we can carry both IPv4 and IPv6. + headerMode := 1 + _, _, errno := unix.Syscall( + syscall.SYS_IOCTL, + file.Fd(), + uintptr(_TUNSIFHEAD), + uintptr(unsafe.Pointer(&headerMode)), + ) + if errno != 0 { + file.Close() + return nil, os.NewSyscallError("TUNSIFHEAD", errno) + } + return file, nil +} + +func (t *NativeTun) configure() error { + name := t.options.Name + // Tag the interface with the caller-supplied description, if any. FreeBSD + // keeps this description even after the process that opened the tun dies, so + // the caller (e.g. mihomo) can use it as a marker to identify and clean up + // tun devices it created. sing-tun assigns no meaning to the value and skips + // this step entirely when no description was provided. + if t.options.FreeBSDInterfaceDescription != "" { + if err := shell.Exec("ifconfig", name, "description", t.options.FreeBSDInterfaceDescription).Run(); err != nil { + return E.Cause(err, "set interface description") + } + } + // Configure MTU and addresses via ifconfig(8). Using the userspace tool + // rather than hand-rolled ioctl structs avoids any risk of passing a + // malformed request to the kernel. + if t.options.MTU > 0 { + err := shell.Exec("ifconfig", name, "mtu", strconv.Itoa(int(t.options.MTU))).Run() + if err != nil { + return E.Cause(err, "set mtu") + } + } + err := t.setAddresses() + if err != nil { + return err + } + if t.options.AutoRoute { + // Mirror the physical default route into the alternate FIB *before* + // installing the tun routes, so DIRECT traffic has a working escape + // path that does not loop back through the tun interface. + err = t.setupDirectFib() + if err != nil { + return err + } + err = t.setRoutes() + if err != nil { + return err + } + } + return nil +} + +func (t *NativeTun) Read(p []byte) (n int, err error) { + return t.tunFile.Read(p) +} + +func (t *NativeTun) Write(p []byte) (n int, err error) { + return t.tunFile.Write(p) +} + +func (t *NativeTun) Close() error { + if t.options.AutoRoute { + t.teardownDirectFib() + } + err := t.tunFile.Close() + // On FreeBSD a tun(4) interface created by opening /dev/tunN persists after + // its file descriptor is closed (unlike Linux, where it disappears). Without + // an explicit destroy, every enable/disable cycle would leak another tunN + // device and the next start would advance to a higher index. Tear down the + // interface we created so devices do not accumulate. We only destroy + // interfaces we opened ourselves; when the fd was supplied by the caller + // (FileDescriptor != 0) its lifetime is the caller's responsibility. + if t.options.FileDescriptor == 0 && t.options.Name != "" { + if destroyErr := exec.Command("ifconfig", t.options.Name, "destroy").Run(); destroyErr != nil && err == nil { + err = E.Cause(destroyErr, "destroy interface ", t.options.Name) + } + } + return err +} + +func (t *NativeTun) setAddresses() error { + name := t.options.Name + for _, address := range t.options.Inet4Address { + // ifconfig tunN inet / alias + // The repeated address sets the point-to-point peer, matching how + // tun(4) interfaces are conventionally addressed. + output, err := shell.Exec("ifconfig", name, "inet", address.String(), address.Addr().String(), "alias").Read() + if err != nil { + return E.Cause(err, "add inet4 address: ", address, ": ", strings.TrimSpace(output)) + } + } + for _, address := range t.options.Inet6Address { + output, err := shell.Exec("ifconfig", name, "inet6", address.String(), "alias").Read() + if err != nil { + return E.Cause(err, "add inet6 address: ", address, ": ", strings.TrimSpace(output)) + } + } + return nil +} + +func (t *NativeTun) setRoutes() error { + routeRanges, err := t.options.BuildAutoRouteRanges(false) + if err != nil { + return err + } + for _, routeRange := range routeRanges { + var family string + if routeRange.Addr().Is4() { + family = "-inet" + } else { + family = "-inet6" + } + // FreeBSD's route(8) rejects a literal default prefix (0.0.0.0/0 or + // ::/0) with "route already in table" because it aliases the existing + // `default` route. Split it into two half-ranges that together cover + // the whole address space without colliding with the default route. + var prefixes []string + if routeRange.Bits() == 0 { + if routeRange.Addr().Is4() { + prefixes = []string{"0.0.0.0/1", "128.0.0.0/1"} + } else { + prefixes = []string{"::/1", "8000::/1"} + } + } else { + prefixes = []string{routeRange.String()} + } + for _, prefix := range prefixes { + // route -n add -interface + // Routing through the interface itself is appropriate for a + // point-to-point tun device serving as the system gateway. + // + // FreeBSD's route(8) fails with "route already in table" if the + // prefix is still present from a previous tun that was not fully + // torn down (e.g. when the interface monitor re-runs configure, or + // after a crash). Delete any stale entry first, ignoring errors when + // nothing is there, so reconfiguration is idempotent and does not + // leave the TUN half-configured. + _ = shell.Exec("route", "-n", "delete", family, prefix).Run() + // Use Read() to capture route(8)'s stderr so failures surface the + // actual reason (e.g. "network is unreachable") instead of a bare + // "exit status 1". + output, err := shell.Exec("route", "-n", "add", family, prefix, "-interface", t.options.Name).Read() + if err != nil { + return E.Cause(err, "add route: ", prefix, ": ", strings.TrimSpace(output)) + } + } + } + return nil +} + +// setupDirectFib ensures the system has at least two FIBs and mirrors the +// current physical IPv4 default route into the alternate FIB (directFib). This +// FIB is later selected by mihomo's DIRECT sockets via SO_SETFIB so their +// traffic egresses the physical interface instead of the tun device. +func (t *NativeTun) setupDirectFib() error { + if err := ensureFibs(directFib + 1); err != nil { + return err + } + gateway, device, err := defaultInet4Route() + if err != nil { + // No usable physical default route: nothing to mirror. Leave the FIB + // empty rather than failing TUN startup. + return nil + } + fib := strconv.Itoa(directFib) + // Clear any stale entries from a previous run, ignoring errors. + _ = exec.Command("route", "-n", "delete", "-fib", fib, "default").Run() + _ = exec.Command("route", "-n", "delete", "-fib", fib, "-host", gateway).Run() + // A host route to the gateway via the physical interface makes the gateway + // reachable inside the otherwise-empty FIB; the default route then resolves + // through it. This avoids needing the interface's subnet mask. + if err = shell.Exec("route", "-n", "add", "-fib", fib, "-host", gateway, "-interface", device).Run(); err != nil { + return E.Cause(err, "mirror gateway route into fib ", fib) + } + if err = shell.Exec("route", "-n", "add", "-fib", fib, "default", gateway).Run(); err != nil { + return E.Cause(err, "mirror default route into fib ", fib) + } + return nil +} + +// teardownDirectFib removes the mirrored routes from the alternate FIB. Errors +// are ignored because the routes may already be gone. +func (t *NativeTun) teardownDirectFib() { + fib := strconv.Itoa(directFib) + _ = exec.Command("route", "-n", "delete", "-fib", fib, "default").Run() + if gateway, _, err := defaultInet4Route(); err == nil { + _ = exec.Command("route", "-n", "delete", "-fib", fib, "-host", gateway).Run() + } +} + +// ensureFibs makes sure net.fibs is at least want. On FreeBSD 15 net.fibs is +// writable at runtime (it can only grow), so a fresh sysctl write is enough and +// no reboot/loader.conf change is required. +func ensureFibs(want int) error { + out, err := exec.Command("sysctl", "-n", "net.fibs").Output() + if err != nil { + return E.Cause(err, "read net.fibs") + } + current, err := strconv.Atoi(strings.TrimSpace(string(out))) + if err != nil { + return E.Cause(err, "parse net.fibs") + } + if current >= want { + return nil + } + if err = shell.Exec("sysctl", "net.fibs="+strconv.Itoa(want)).Run(); err != nil { + return E.Cause(err, "raise net.fibs to ", want) + } + return nil +} + +// defaultInet4Route returns the gateway and interface of the current IPv4 +// default route in the default FIB, parsed from `netstat -rn -f inet`. +func defaultInet4Route() (gateway string, device string, err error) { + out, err := exec.Command("netstat", "-rn", "-f", "inet").Output() + if err != nil { + return "", "", E.Cause(err, "read routing table") + } + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(line) + if len(fields) >= 4 && fields[0] == "default" { + gateway = fields[1] + device = fields[len(fields)-1] + // Sanity check the gateway is an IP, not an interface link. + if net.ParseIP(gateway) != nil { + return gateway, device, nil + } + } + } + return "", "", E.New("no inet default route found") +} + diff --git a/tun_freebsd_gvisor.go b/tun_freebsd_gvisor.go new file mode 100644 index 00000000..4704f22e --- /dev/null +++ b/tun_freebsd_gvisor.go @@ -0,0 +1,164 @@ +//go:build with_gvisor && freebsd + +package tun + +import ( + "sync" + + "github.com/metacubex/gvisor/pkg/buffer" + "github.com/metacubex/gvisor/pkg/tcpip" + "github.com/metacubex/gvisor/pkg/tcpip/header" + "github.com/metacubex/gvisor/pkg/tcpip/stack" + + "golang.org/x/sys/unix" +) + +var _ GVisorTun = (*NativeTun)(nil) + +func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { + var packetHeader [PacketOffset]byte + if pkt.NetworkProtocolNumber == header.IPv6ProtocolNumber { + packetHeader[3] = unix.AF_INET6 + } else { + packetHeader[3] = unix.AF_INET + } + views := pkt.AsSlices() + packet := make([]byte, 0, PacketOffset+pkt.Size()) + packet = append(packet, packetHeader[:]...) + for _, view := range views { + packet = append(packet, view...) + } + _, err := t.tunFile.Write(packet) + if err != nil { + return 0, err + } + return pkt.Size(), nil +} + +func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { + return &FreeBSDEndpoint{tun: t}, stack.NICOptions{}, nil +} + +var _ stack.LinkEndpoint = (*FreeBSDEndpoint)(nil) + +type FreeBSDEndpoint struct { + tun *NativeTun + mu sync.RWMutex // mu guards dispatcher + dispatcher stack.NetworkDispatcher +} + +func (e *FreeBSDEndpoint) MTU() uint32 { + return e.tun.options.MTU +} + +func (e *FreeBSDEndpoint) SetMTU(mtu uint32) { +} + +func (e *FreeBSDEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (e *FreeBSDEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (e *FreeBSDEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { +} + +func (e *FreeBSDEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (e *FreeBSDEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + e.mu.Lock() + defer e.mu.Unlock() + if dispatcher == nil && e.dispatcher != nil { + e.dispatcher = nil + return + } + if dispatcher != nil && e.dispatcher == nil { + e.dispatcher = dispatcher + go e.dispatchLoop() + } +} + +func (e *FreeBSDEndpoint) dispatchLoop() { + mtu := int(e.tun.options.MTU) + for { + readBuffer := make([]byte, PacketOffset+mtu) + n, err := e.tun.tunFile.Read(readBuffer) + if err != nil { + break + } + if n <= PacketOffset { + continue + } + packetBuffer := buffer.MakeWithData(readBuffer[PacketOffset:n]) + ihl, ok := packetBuffer.PullUp(0, 1) + if !ok { + packetBuffer.Release() + continue + } + var networkProtocol tcpip.NetworkProtocolNumber + switch header.IPVersion(ihl.AsSlice()) { + case header.IPv4Version: + networkProtocol = header.IPv4ProtocolNumber + case header.IPv6Version: + networkProtocol = header.IPv6ProtocolNumber + default: + packetBuffer.Release() + continue + } + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: packetBuffer, + IsForwardedPacket: true, + }) + e.mu.RLock() + dispatcher := e.dispatcher + e.mu.RUnlock() + if dispatcher == nil { + pkt.DecRef() + return + } + dispatcher.DeliverNetworkPacket(networkProtocol, pkt) + pkt.DecRef() + } +} + +func (e *FreeBSDEndpoint) IsAttached() bool { + e.mu.RLock() + defer e.mu.RUnlock() + return e.dispatcher != nil +} + +func (e *FreeBSDEndpoint) Wait() { +} + +func (e *FreeBSDEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (e *FreeBSDEndpoint) AddHeader(buffer *stack.PacketBuffer) { +} + +func (e *FreeBSDEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { + return true +} + +func (e *FreeBSDEndpoint) WritePackets(packetBufferList stack.PacketBufferList) (int, tcpip.Error) { + var n int + for _, packet := range packetBufferList.AsSlice() { + _, err := e.tun.WritePacket(packet) + if err != nil { + return n, &tcpip.ErrAborted{} + } + n++ + } + return n, nil +} + +func (e *FreeBSDEndpoint) Close() { +} + +func (e *FreeBSDEndpoint) SetOnCloseAction(f func()) { +} diff --git a/tun_nondarwin.go b/tun_nondarwin.go index 0faa2c9e..053b931d 100644 --- a/tun_nondarwin.go +++ b/tun_nondarwin.go @@ -1,4 +1,4 @@ -//go:build !darwin +//go:build !darwin && !freebsd package tun diff --git a/tun_other.go b/tun_other.go index 1db48f93..432d26bf 100644 --- a/tun_other.go +++ b/tun_other.go @@ -1,4 +1,4 @@ -//go:build !(linux || windows || darwin) +//go:build !(linux || windows || darwin || freebsd) package tun