Skip to content
Draft
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
54 changes: 54 additions & 0 deletions component/dialer/bind_freebsd.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion component/dialer/bind_others.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !linux && !darwin && !windows
//go:build !linux && !darwin && !windows && !freebsd

package dialer

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
91 changes: 91 additions & 0 deletions listener/sing_tun/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"net/netip"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <name>` prints the full interface info including a
// "description: <text>" line; `ifconfig <name> 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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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,
}
Expand Down
37 changes: 37 additions & 0 deletions listener/sing_tun/tun_name_freebsd.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion listener/sing_tun/tun_name_other.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !(darwin || linux)
//go:build !(darwin || linux || freebsd)

package sing_tun

Expand Down