diff --git a/component/dialer/bind_freebsd.go b/component/dialer/bind_freebsd.go new file mode 100644 index 0000000000..4c54e1653f --- /dev/null +++ b/component/dialer/bind_freebsd.go @@ -0,0 +1,54 @@ +package dialer + +import ( + "context" + "net" + "net/netip" + "syscall" + + "golang.org/x/sys/unix" +) + +// directFib is the alternate routing table (FIB) that sing-tun populates with +// the original physical routes while TUN auto-route takes over the default FIB. +// Binding DIRECT sockets to this FIB makes their traffic egress the physical +// interface instead of looping back through the tun device. +// +// FreeBSD has neither Linux's SO_BINDTODEVICE nor macOS's IP_BOUND_IF; SO_SETFIB +// is the platform's mechanism for steering a socket onto an alternate routing +// table, so it is what we use to prevent the DIRECT-outbound loopback. +const directFib = 1 + +func bindControl(fib int) controlFn { + return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) { + addrPort, err := netip.ParseAddrPort(address) + if err == nil && !addrPort.Addr().IsGlobalUnicast() { + return + } + + var innerErr error + err = c.Control(func(fd uintptr) { + innerErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SETFIB, fib) + }) + + if innerErr != nil { + err = innerErr + } + + return + } +} + +func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error { + addControlToDialer(dialer, bindControl(directFib)) + return nil +} + +func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string, rAddrPort netip.AddrPort) (string, error) { + addControlToListenConfig(lc, bindControl(directFib)) + return address, nil +} + +func ParseNetwork(network string, addr netip.Addr) string { + return network +} diff --git a/component/dialer/bind_others.go b/component/dialer/bind_others.go index b7db962a6c..4ccf1cbf14 100644 --- a/component/dialer/bind_others.go +++ b/component/dialer/bind_others.go @@ -1,4 +1,4 @@ -//go:build !linux && !darwin && !windows +//go:build !linux && !darwin && !windows && !freebsd package dialer diff --git a/go.mod b/go.mod index 424097dfcc..538a3cd466 100644 --- a/go.mod +++ b/go.mod @@ -151,3 +151,5 @@ require ( // for https://github.com/golang/protobuf/issues/1704 replace google.golang.org/protobuf => github.com/metacubex/protobuf-go v0.0.0-20260306035419-7ceee0674686 + +replace github.com/metacubex/sing-tun => ../sing-tun-freebsd diff --git a/go.sum b/go.sum index db0c5b30a7..682b4414d0 100644 --- a/go.sum +++ b/go.sum @@ -165,8 +165,6 @@ github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6w github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= github.com/metacubex/sing-shadowtls v0.0.0-20260517015314-c11c36474edc h1:8wLoFfYQ88iGPL+krQ5tJsI8IAmkFjKpQL2q+y3pvss= github.com/metacubex/sing-shadowtls v0.0.0-20260517015314-c11c36474edc/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= -github.com/metacubex/sing-tun v0.4.20 h1:xdupzizRoZKyDzP0l68WAx5Sk4ooiuT1GiWsiJyOGPw= -github.com/metacubex/sing-tun v0.4.20/go.mod h1:g4I/JNplDBhXLF+aQWgFbhNeJPSXQOWS9HvLeNvkgeA= github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE= github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q= github.com/metacubex/sing-wireguard v0.0.0-20260520151737-7e7c7c1b854c h1:tH9FuQW357zp2xAGzkoZTGpNGMVmEFZov0iV5M2S5ew= diff --git a/listener/sing_tun/server.go b/listener/sing_tun/server.go index ede0b6ac28..992e78bd18 100644 --- a/listener/sing_tun/server.go +++ b/listener/sing_tun/server.go @@ -7,6 +7,7 @@ import ( "net" "net/netip" "os" + "os/exec" "runtime" "strconv" "strings" @@ -80,6 +81,18 @@ var emptyAddressSet = []*netipx.IPSet{{}} func CalculateInterfaceName(name string) (tunName string) { if runtime.GOOS == "darwin" { tunName = "utun" + } else if runtime.GOOS == "freebsd" { + // FreeBSD tun(4) interfaces must be named "tunN"; ignore any custom + // name and scan for a free index below. + // + // FreeBSD tun(4) devices are cloning interfaces that persist after the + // process that opened them dies. When mihomo is killed without a clean + // shutdown (e.g. logout/reboot while TUN mode is on), the tun it created + // is left behind as an orphan, so the next start would skip it and + // advance to a higher index (tun0 -> tun1 -> ...), leaking a device each + // cycle. Destroy those orphans first so we can reuse the low index. + destroyOrphanTunInterfaces() + tunName = "tun" } else if name != "" { tunName = name return @@ -113,6 +126,71 @@ func CalculateInterfaceName(name string) (tunName string) { return } +// freebsdTunDescription is the marker mihomo asks sing-tun to write into the +// ifconfig description of every tun(4) interface it creates on FreeBSD. mihomo +// owns this string (sing-tun stays generic); it is the sole criterion used to +// identify our own orphan tun devices for cleanup, so it must be specific +// enough to never collide with descriptions other software might use. +const freebsdTunDescription = "clash-verge-rev-tun" + +// destroyOrphanTunInterfaces removes leftover FreeBSD tun(4) interfaces that a +// previous, abnormally terminated mihomo created but never tore down. +// +// Safety: we ONLY destroy interfaces whose ifconfig description equals +// freebsdTunDescription (the marker mihomo asked sing-tun to stamp on the tun +// devices it created). FreeBSD preserves that description after the owning +// process dies, so it reliably identifies our own orphans. Interfaces created +// by any other software (VPN clients, manually created tun, etc.) never carry +// this marker and are therefore never touched — even if they look unused. +func destroyOrphanTunInterfaces() { + interfaces, err := net.Interfaces() + if err != nil { + return + } + for _, netInterface := range interfaces { + if !strings.HasPrefix(netInterface.Name, "tun") { + continue + } + // Numeric suffix only ("tunN"), to match real tun(4) devices. + if _, parseErr := strconv.ParseInt(netInterface.Name[len("tun"):], 10, 16); parseErr != nil { + continue + } + // An interface that is currently UP is in active use; never touch it. + if netInterface.Flags&net.FlagUp != 0 { + continue + } + // Only destroy interfaces that carry our own description marker. + if !tunHasClashDescription(netInterface.Name) { + continue + } + if destroyErr := exec.Command("ifconfig", netInterface.Name, "destroy").Run(); destroyErr != nil { + log.Warnln("[TUN] failed to destroy orphan tun interface %s: %v", netInterface.Name, destroyErr) + } else { + log.Infoln("[TUN] destroyed orphan tun interface %s left by a previous run", netInterface.Name) + } + } +} + +// tunHasClashDescription reports whether the given tun interface's ifconfig +// description equals the marker sing-tun stamps on tun devices it creates. +func tunHasClashDescription(name string) bool { + // `ifconfig ` prints the full interface info including a + // "description: " line; `ifconfig description` (no value) + // would instead be a set request and error out, so always read the full + // output and parse it. + output, err := exec.Command("ifconfig", name).Output() + if err != nil { + return false + } + for _, line := range strings.Split(string(output), "\n") { + line = strings.TrimSpace(line) + if desc, ok := strings.CutPrefix(line, "description: "); ok { + return strings.TrimSpace(desc) == freebsdTunDescription + } + } + return false +} + func checkTunName(tunName string) (ok bool) { defer func() { if !ok { @@ -129,6 +207,18 @@ func checkTunName(tunName string) (ok bool) { if _, parseErr := strconv.ParseInt(tunName[4:], 10, 16); parseErr != nil { return false } + } else if runtime.GOOS == "freebsd" { + // FreeBSD tun(4) devices must be named "tunN" (N numeric); the kernel + // derives the device node /dev/tunN from the name. + if len(tunName) <= 3 { + return false + } + if tunName[:3] != "tun" { + return false + } + if _, parseErr := strconv.ParseInt(tunName[3:], 10, 16); parseErr != nil { + return false + } } return true } @@ -410,6 +500,7 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis ExcludeMACAddress: excludeMACAddress, FileDescriptor: options.FileDescriptor, InterfaceMonitor: defaultInterfaceMonitor, + FreeBSDInterfaceDescription: freebsdTunDescription, EXP_RecvMsgX: options.RecvMsgX, EXP_SendMsgX: options.SendMsgX, } diff --git a/listener/sing_tun/tun_name_freebsd.go b/listener/sing_tun/tun_name_freebsd.go new file mode 100644 index 0000000000..33dfa6127c --- /dev/null +++ b/listener/sing_tun/tun_name_freebsd.go @@ -0,0 +1,37 @@ +//go:build freebsd + +package sing_tun + +import ( + "os" + "unsafe" + + "golang.org/x/sys/unix" +) + +// getTunnelName resolves the interface name for a tun device opened from a raw +// file descriptor. The FreeBSD port creates the tun device by name rather than +// passing a descriptor, so this path is normally unused; we still attempt to +// resolve it via the device's TUNGIFNAME ioctl for completeness. +func getTunnelName(fd int32) (string, error) { + // TUNGIFNAME = _IOR('t', 89, struct ifreq) on FreeBSD. + const tunGifName = 0x4020745d + var ifr [unix.IFNAMSIZ + 16]byte + _, _, errno := unix.Syscall( + unix.SYS_IOCTL, + uintptr(fd), + uintptr(tunGifName), + uintptr(unsafe.Pointer(&ifr[0])), + ) + if errno != 0 { + return "", os.NewSyscallError("TUNGIFNAME", errno) + } + n := 0 + for n < len(ifr) && ifr[n] != 0 { + n++ + } + if n == 0 { + return "", os.ErrInvalid + } + return string(ifr[:n]), nil +} diff --git a/listener/sing_tun/tun_name_other.go b/listener/sing_tun/tun_name_other.go index c47c8cbee4..167000b466 100644 --- a/listener/sing_tun/tun_name_other.go +++ b/listener/sing_tun/tun_name_other.go @@ -1,4 +1,4 @@ -//go:build !(darwin || linux) +//go:build !(darwin || linux || freebsd) package sing_tun