face) { ++return nil ++} ++m.emit(newInterface, 0) ++return nil ++} ++ ++// getDefaultInterfaceBySocket detects the default interface by attempting a ++// TCP connect and observing the locally assigned source address. ++func (m *defaultInterfaceMonitor) getDefaultInterfaceBySocket() (*control.Interface, error) { ++socketFd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0) ++if err != nil { ++return nil, E.Cause(err, "create file descriptor") ++} ++defer unix.Close(socketFd) ++go unix.Connect(socketFd, &unix.SockaddrInet4{ ++Addr: [4]byte{10, 255, 255, 255}, ++Port: 80, ++}) ++result := make(chan netip.Addr, 1) ++done := make(chan struct{}) ++defer close(done) ++go func() { ++for { ++sockname, sockErr := unix.Getsockname(socketFd) ++if sockErr != nil { ++break ++} ++sockaddr, isInet4Sockaddr := sockname.(*unix.SockaddrInet4) ++if !isInet4Sockaddr { ++break ++} ++addr := netip.AddrFrom4(sockaddr.Addr) ++if addr.IsUnspecified() { ++select { ++case <-done: ++return ++default: ++time.Sleep(10 * time.Millisecond) ++continue ++} ++} ++result <- addr ++break ++} ++}() ++var selectedAddr netip.Addr ++select { ++case selectedAddr = <-result: ++case <-time.After(time.Second): ++return nil, nil ++} ++return m.interfaceFinder.ByAddr(selectedAddr) ++} diff --git a/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_monitor__other.go b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_monitor__other.go new file mode 100644 index 000000000000..c1c61db50a08 --- /dev/null +++ b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_monitor__other.go @@ -0,0 +1,10 @@ +-- Exclude FreeBSD from the stub monitor that returns os.ErrInvalid. +-- FreeBSD now has a real monitor implementation in monitor_freebsd.go. +--- vendor/github.com/sagernet/sing-tun/monitor_other.go.orig 2026-05-04 16:34:14 UTC ++++ vendor/github.com/sagernet/sing-tun/monitor_other.go +@@ -1,4 +1,4 @@ +-//go:build !(linux || windows || darwin) ++//go:build !(linux || windows || darwin || freebsd) + + package tun + diff --git a/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_monitor__shared.go b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_monitor__shared.go new file mode 100644 index 000000000000..930778e3037d --- /dev/null +++ b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_monitor__shared.go @@ -0,0 +1,11 @@ +-- Include FreeBSD in the shared defaultInterfaceMonitor implementation. +-- The shared code uses AF_ROUTE sockets and golang.org/x/net/route which +-- work identically on FreeBSD and Darwin. +--- vendor/github.com/sagernet/sing-tun/monitor_shared.go.orig 2026-05-04 16:34:14 UTC ++++ vendor/github.com/sagernet/sing-tun/monitor_shared.go +@@ -1,4 +1,4 @@ +-//go:build linux || windows || darwin ++//go:build linux || windows || darwin || freebsd + + package tun + diff --git a/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__freebsd.go b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__freebsd.go new file mode 100644 index 000000000000..ee4661cc5711 --- /dev/null +++ b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__freebsd.go @@ -0,0 +1,435 @@ +-- Add FreeBSD TUN interface support to the sing-tun library. +-- FreeBSD uses /dev/tunN character devices. TUNSIFHEAD is enabled so each +-- packet carries a 4-byte AF-family prefix (PacketOffset=4), matching the +-- Darwin layout. The interface is auto-created via SIOCIFCREATE2 if it does +-- not already exist. IPv4/IPv6 addresses are configured via SIOCAIFADDR and +-- SIOCAIFADDR_IN6 ioctls; routes are managed through AF_ROUTE sockets. +--- vendor/github.com/sagernet/sing-tun/tun_freebsd.go.orig 2026-05-04 17:28:35 UTC ++++ vendor/github.com/sagernet/sing-tun/tun_freebsd.go +@@ -0,0 +1,426 @@ ++//go:build freebsd ++ ++package tun ++ ++import ( ++"fmt" ++"net" ++"net/netip" ++"os" ++"syscall" ++"unsafe" ++ ++"github.com/sagernet/sing/common" ++E "github.com/sagernet/sing/common/exceptions" ++ ++"golang.org/x/net/route" ++"golang.org/x/sys/unix" ++) ++ ++const PacketOffset = 4 ++ ++// FreeBSD-specific ioctl constants (computed from /usr/include/net/if_tun.h ++// and /usr/include/netinet6/in6_var.h). ++const ( ++_TUNSIFHEAD = 0x80047460 // _IOW('t', 96, int) — enable multi-AF header mode ++_SIOCAIFADDR_IN6 = 0x8088691b // _IOW('i', 27, struct in6_aliasreq) ++ ++_IN6_IFF_NODAD = 0x0020 ++_IN6_IFF_SECURED = 0x0400 ++_ND6_INFINITE_LIFETIME = 0xFFFFFFFF ++) ++ ++// ifreqFlags is used with SIOCGIFFLAGS / SIOCSIFFLAGS. ++type ifreqFlags struct { ++Name [unix.IFNAMSIZ]byte ++Flags int16 ++_ [14]byte ++} ++ ++// ifreqMTU is used with SIOCSIFMTU. ++type ifreqMTU struct { ++Name [unix.IFNAMSIZ]byte ++MTU int32 ++_ [12]byte ++} ++ ++// ifAliasReq is used with SIOCAIFADDR (IPv4). ++type ifAliasReq struct { ++Name [unix.IFNAMSIZ]byte ++Addr unix.RawSockaddrInet4 ++Dstaddr unix.RawSockaddrInet4 ++Mask unix.RawSockaddrInet4 ++} ++ ++// addrLifetime6 mirrors struct in6_addrlifetime from netinet6/in6_var.h. ++type addrLifetime6 struct { ++Expire int64 // time_t ia6t_expire ++Preferred int64 // time_t ia6t_preferred ++Vltime uint32 // ia6t_vltime ++Pltime uint32 // ia6t_pltime ++} ++ ++// ifAliasReq6 mirrors struct in6_aliasreq from netinet6/in6_var.h. ++// FreeBSD adds ifra_vhid relative to the Darwin version. ++// sizeof = 136 bytes (verified against the C struct). ++type ifAliasReq6 struct { ++Name [unix.IFNAMSIZ]byte ++Addr unix.RawSockaddrInet6 ++Dstaddr unix.RawSockaddrInet6 ++Mask unix.RawSockaddrInet6 ++Flags int32 ++Lifetime addrLifetime6 ++Vhid int32 ++} ++ ++// NativeTun implements Tun for FreeBSD using /dev/tunN character devices. ++// TUNSIFHEAD is enabled so each packet carries a 4-byte AF-family prefix ++// (PacketOffset = 4), matching the Darwin layout. ++type NativeTun struct { ++tunFd int ++tunFile *os.File ++options Options ++routeSet bool ++iovecsOutputDefault []unix.Iovec ++} ++ ++func New(options Options) (Tun, error) { ++if options.Name == "" { ++return nil, E.New("tun name is required") ++} ++var tunIndex int ++if _, err := fmt.Sscanf(options.Name, "tun%d", &tunIndex); err != nil { ++return nil, E.New("bad tun name (expected tunN): ", options.Name) ++} ++ ++// Create the interface. If it already exists from a previous run (EEXIST), ++// destroy it first so we start with a clean slate — no stale addresses, ++// routes, or flags. ++if err := useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(fd int) error { ++var ifr ifreqMTU ++copy(ifr.Name[:], options.Name) ++_, _, errno := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), ++uintptr(unix.SIOCIFCREATE2), uintptr(unsafe.Pointer(&ifr))) ++if errno == unix.EEXIST { ++// Interface exists: destroy it so we can recreate it cleanly. ++if _, _, errno2 := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), ++uintptr(unix.SIOCIFDESTROY), uintptr(unsafe.Pointer(&ifr))); errno2 != 0 { ++return os.NewSyscallError("SIOCIFDESTROY", errno2) ++} ++if _, _, errno2 := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), ++uintptr(unix.SIOCIFCREATE2), uintptr(unsafe.Pointer(&ifr))); errno2 != 0 { ++return os.NewSyscallError("SIOCIFCREATE2", errno2) ++} ++} else if errno != 0 { ++return os.NewSyscallError("SIOCIFCREATE2", errno) ++} ++return nil ++}); err != nil { ++return nil, err ++} ++ ++devPath := fmt.Sprintf("/dev/%s", options.Name) ++tunFd, err := unix.Open(devPath, unix.O_RDWR, 0) ++if err != nil { ++return nil, os.NewSyscallError("open "+devPath, err) ++} ++ ++// Enable multi-AF header mode: each packet gains a 4-byte network-byte-order ++// AF_family prefix. Without this, the kernel assumes AF_INET for every write ++// and IPv6 packets would be misdelivered. ++one := 1 ++if _, _, errno := unix.Syscall( ++syscall.SYS_IOCTL, ++uintptr(tunFd), ++uintptr(_TUNSIFHEAD), ++uintptr(unsafe.Pointer(&one)), ++); errno != 0 { ++unix.Close(tunFd) ++return nil, os.NewSyscallError("TUNSIFHEAD", errno) ++} ++ ++// Set interface MTU ++if err = useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(fd int) error { ++ifr := ifreqMTU{MTU: int32(options.MTU)} ++copy(ifr.Name[:], options.Name) ++if _, _, errno := unix.Syscall( ++syscall.SYS_IOCTL, ++uintptr(fd), ++uintptr(unix.SIOCSIFMTU), ++uintptr(unsafe.Pointer(&ifr)), ++); errno != 0 { ++return os.NewSyscallError("SIOCSIFMTU", errno) ++} ++return nil ++}); err != nil { ++unix.Close(tunFd) ++return nil, err ++} ++ ++if !options.EXP_ExternalConfiguration { ++for _, address := range options.Inet4Address { ++if err = setInet4Address(options.Name, address); err != nil { ++unix.Close(tunFd) ++return nil, err ++} ++} ++for _, address := range options.Inet6Address { ++if err = setInet6Address(options.Name, address); err != nil { ++unix.Close(tunFd) ++return nil, err ++} ++} ++if err = bringUp(options.Name); err != nil { ++unix.Close(tunFd) ++return nil, err ++} ++} ++ ++return &NativeTun{ ++tunFd: tunFd, ++tunFile: os.NewFile(uintptr(tunFd), options.Name), ++options: options, ++}, nil ++} ++ ++func setInet4Address(name string, address netip.Prefix) error { ++ifReq := ifAliasReq{ ++Addr: unix.RawSockaddrInet4{ ++Len: unix.SizeofSockaddrInet4, ++Family: unix.AF_INET, ++Addr: address.Addr().As4(), ++}, ++Dstaddr: unix.RawSockaddrInet4{ ++Len: unix.SizeofSockaddrInet4, ++Family: unix.AF_INET, ++Addr: address.Addr().As4(), ++}, ++Mask: unix.RawSockaddrInet4{ ++Len: unix.SizeofSockaddrInet4, ++Family: unix.AF_INET, ++Addr: netip.MustParseAddr(net.IP(net.CIDRMask(address.Bits(), 32)).String()).As4(), ++}, ++} ++copy(ifReq.Name[:], name) ++return useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(fd int) error { ++if _, _, errno := unix.Syscall( ++syscall.SYS_IOCTL, ++uintptr(fd), ++uintptr(unix.SIOCAIFADDR), ++uintptr(unsafe.Pointer(&ifReq)), ++); errno != 0 { ++return os.NewSyscallError("SIOCAIFADDR", errno) ++} ++return nil ++}) ++} ++ ++func setInet6Address(name string, address netip.Prefix) error { ++ifReq6 := ifAliasReq6{ ++Addr: unix.RawSockaddrInet6{ ++Len: unix.SizeofSockaddrInet6, ++Family: unix.AF_INET6, ++Addr: address.Addr().As16(), ++}, ++Mask: unix.RawSockaddrInet6{ ++Len: unix.SizeofSockaddrInet6, ++Family: unix.AF_INET6, ++Addr: netip.MustParseAddr(net.IP(net.CIDRMask(address.Bits(), 128)).String()).As16(), ++}, ++Flags: _IN6_IFF_NODAD | _IN6_IFF_SECURED, ++Lifetime: addrLifetime6{ ++Vltime: _ND6_INFINITE_LIFETIME, ++Pltime: _ND6_INFINITE_LIFETIME, ++}, ++} ++if address.Bits() == 128 { ++ifReq6.Dstaddr = unix.RawSockaddrInet6{ ++Len: unix.SizeofSockaddrInet6, ++Family: unix.AF_INET6, ++Addr: address.Addr().Next().As16(), ++} ++} ++copy(ifReq6.Name[:], name) ++return useSocket(unix.AF_INET6, unix.SOCK_DGRAM, 0, func(fd int) error { ++if _, _, errno := unix.Syscall( ++syscall.SYS_IOCTL, ++uintptr(fd), ++uintptr(_SIOCAIFADDR_IN6), ++uintptr(unsafe.Pointer(&ifReq6)), ++); errno != 0 { ++return os.NewSyscallError("SIOCAIFADDR_IN6", errno) ++} ++return nil ++}) ++} ++ ++func bringUp(name string) error { ++return useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(fd int) error { ++var ifr ifreqFlags ++copy(ifr.Name[:], name) ++if _, _, errno := unix.Syscall( ++syscall.SYS_IOCTL, ++uintptr(fd), ++uintptr(unix.SIOCGIFFLAGS), ++uintptr(unsafe.Pointer(&ifr)), ++); errno != 0 { ++return os.NewSyscallError("SIOCGIFFLAGS", errno) ++} ++ifr.Flags |= unix.IFF_UP ++if _, _, errno := unix.Syscall( ++syscall.SYS_IOCTL, ++uintptr(fd), ++uintptr(unix.SIOCSIFFLAGS), ++uintptr(unsafe.Pointer(&ifr)), ++); errno != 0 { ++return os.NewSyscallError("SIOCSIFFLAGS", errno) ++} ++return nil ++}) ++} ++ ++func (t *NativeTun) Name() (string, error) { ++return t.options.Name, nil ++} ++ ++func (t *NativeTun) Start() error { ++if t.options.EXP_ExternalConfiguration { ++return nil ++} ++t.options.InterfaceMonitor.RegisterMyInterface(t.options.Name) ++return t.setRoutes() ++} ++ ++func (t *NativeTun) Close() error { ++if t.options.EXP_ExternalConfiguration { ++return t.tunFile.Close() ++} ++err := E.Errors(t.unsetRoutes(), t.tunFile.Close()) ++// Destroy the interface so it doesn't leave stale addresses/routes behind. ++destroyErr := useSocket(unix.AF_INET, unix.SOCK_DGRAM, 0, func(fd int) error { ++var ifr ifreqMTU ++copy(ifr.Name[:], t.options.Name) ++_, _, errno := unix.Syscall(syscall.SYS_IOCTL, uintptr(fd), ++uintptr(unix.SIOCIFDESTROY), uintptr(unsafe.Pointer(&ifr))) ++if errno != 0 && errno != unix.ENXIO { ++return os.NewSyscallError("SIOCIFDESTROY", errno) ++} ++return nil ++}) ++return E.Errors(err, destroyErr) ++} ++ ++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) UpdateRouteOptions(tunOptions Options) error { ++t.options = tunOptions ++if t.options.EXP_ExternalConfiguration { ++return nil ++} ++if err := t.unsetRoutes(); err != nil { ++return err ++} ++return t.setRoutes() ++} ++ ++func (t *NativeTun) setRoutes() error { ++if t.options.FileDescriptor != 0 { ++return nil ++} ++routeRanges, err := t.options.BuildAutoRouteRanges(false) ++if err != nil { ++return err ++} ++if len(routeRanges) == 0 { ++return nil ++} ++gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr() ++for _, destination := range routeRanges { ++var gateway netip.Addr ++if destination.Addr().Is4() { ++gateway = gateway4 ++} else { ++gateway = gateway6 ++} ++err = execRoute(unix.RTM_ADD, destination, gateway) ++if err != nil { ++if err == unix.EEXIST { ++_ = execRoute(unix.RTM_DELETE, destination, gateway) ++err = execRoute(unix.RTM_ADD, destination, gateway) ++} ++if err != nil { ++return E.Cause(err, "add route: ", destination) ++} ++} ++} ++t.routeSet = true ++return nil ++} ++ ++func (t *NativeTun) unsetRoutes() error { ++if !t.routeSet { ++return nil ++} ++routeRanges, err := t.options.BuildAutoRouteRanges(false) ++if err != nil { ++return err ++} ++gateway4, gateway6 := t.options.Inet4GatewayAddr(), t.options.Inet6GatewayAddr() ++for _, destination := range routeRanges { ++var gateway netip.Addr ++if destination.Addr().Is4() { ++gateway = gateway4 ++} else { ++gateway = gateway6 ++} ++_ = execRoute(unix.RTM_DELETE, destination, gateway) ++} ++return nil ++} ++ ++func useSocket(domain, typ, proto int, block func(fd int) error) error { ++fd, err := unix.Socket(domain, typ, proto) ++if err != nil { ++return err ++} ++defer unix.Close(fd) ++return block(fd) ++} ++ ++func execRoute(rtmType int, destination netip.Prefix, gateway netip.Addr) error { ++msg := route.RouteMessage{ ++Type: rtmType, ++Version: unix.RTM_VERSION, ++Flags: unix.RTF_STATIC | unix.RTF_GATEWAY, ++Seq: 1, ++} ++if rtmType == unix.RTM_ADD { ++msg.Flags |= unix.RTF_UP ++} ++if gateway.Is4() { ++msg.Addrs = []route.Addr{ ++syscall.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()}, ++syscall.RTAX_NETMASK: &route.Inet4Addr{IP: netip.MustParseAddr(net.IP(net.CIDRMask(destination.Bits(), 32)).String()).As4()}, ++syscall.RTAX_GATEWAY: &route.Inet4Addr{IP: gateway.As4()}, ++} ++} else { ++msg.Addrs = []route.Addr{ ++syscall.RTAX_DST: &route.Inet6Addr{IP: destination.Addr().As16()}, ++syscall.RTAX_NETMASK: &route.Inet6Addr{IP: netip.MustParseAddr(net.IP(net.CIDRMask(destination.Bits(), 128)).String()).As16()}, ++syscall.RTAX_GATEWAY: &route.Inet6Addr{IP: gateway.As16()}, ++} ++} ++request, err := msg.Marshal() ++if err != nil { ++return err ++} ++return useSocket(unix.AF_ROUTE, unix.SOCK_RAW, 0, func(fd int) error { ++return common.Error(unix.Write(fd, request)) ++}) ++} diff --git a/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__freebsd__gvisor.go b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__freebsd__gvisor.go new file mode 100644 index 000000000000..4de5c81237e0 --- /dev/null +++ b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__freebsd__gvisor.go @@ -0,0 +1,116 @@ +-- Add gVisor (GVisorTun) support to FreeBSD's NativeTun. +-- Implements WritePacket (writes gVisor PacketBuffer to the TUN fd with the +-- 4-byte AF-family prefix required by TUNSIFHEAD) and NewEndpoint (returns a +-- channel.Endpoint with a reader goroutine for the pure gVisor stack). +-- Required by the "mixed" and "gvisor" network stacks when sing-box is built +-- with the with_gvisor tag. +--- vendor/github.com/sagernet/sing-tun/tun_freebsd_gvisor.go.orig 2026-05-04 17:24:50 UTC ++++ vendor/github.com/sagernet/sing-tun/tun_freebsd_gvisor.go +@@ -0,0 +1,107 @@ ++//go:build with_gvisor && freebsd ++ ++package tun ++ ++import ( ++ "encoding/binary" ++ "syscall" ++ "unsafe" ++ ++ "github.com/sagernet/gvisor/pkg/buffer" ++ "github.com/sagernet/gvisor/pkg/tcpip" ++ gHdr "github.com/sagernet/gvisor/pkg/tcpip/header" ++ "github.com/sagernet/gvisor/pkg/tcpip/link/channel" ++ "github.com/sagernet/gvisor/pkg/tcpip/stack" ++ ++ "golang.org/x/sys/unix" ++) ++ ++// packetHeader{4,6} are the 4-byte big-endian AF-family prefixes written before ++// each packet when TUNSIFHEAD mode is active. ++var ( ++ packetHeader4 = [4]byte{0, 0, 0, unix.AF_INET} ++ packetHeader6 = [4]byte{0, 0, 0, unix.AF_INET6} ++ packetHeaderVec4 unix.Iovec ++ packetHeaderVec6 unix.Iovec ++) ++ ++func init() { ++ packetHeaderVec4 = unix.Iovec{Base: &packetHeader4[0]} ++ packetHeaderVec4.SetLen(4) ++ packetHeaderVec6 = unix.Iovec{Base: &packetHeader6[0]} ++ packetHeaderVec6.SetLen(4) ++} ++ ++var _ GVisorTun = (*NativeTun)(nil) ++ ++// WritePacket writes a gVisor packet buffer to the TUN device, prepending the ++// 4-byte big-endian AF-family header required by TUNSIFHEAD mode. ++func (t *NativeTun) WritePacket(pkt *stack.PacketBuffer) (int, error) { ++ iovecs := t.iovecsOutputDefault ++ if pkt.NetworkProtocolNumber == gHdr.IPv4ProtocolNumber { ++ iovecs = append(iovecs, packetHeaderVec4) ++ } else { ++ iovecs = append(iovecs, packetHeaderVec6) ++ } ++ var dataLen int ++ for _, s := range pkt.AsSlices() { ++ if len(s) == 0 { ++ continue ++ } ++ dataLen += len(s) ++ iov := unix.Iovec{Base: &s[0]} ++ iov.SetLen(len(s)) ++ iovecs = append(iovecs, iov) ++ } ++ if cap(iovecs) > cap(t.iovecsOutputDefault) { ++ t.iovecsOutputDefault = iovecs[:0] ++ } ++ _, _, errno := unix.Syscall(syscall.SYS_WRITEV, ++ uintptr(t.tunFd), ++ uintptr(unsafe.Pointer(&iovecs[0])), ++ uintptr(len(iovecs))) ++ if errno != 0 { ++ return 0, errno ++ } ++ return dataLen, nil ++} ++ ++// NewEndpoint creates a gVisor link endpoint backed by the TUN device fd. ++// A goroutine is started to read packets from the TUN fd and inject them into ++// the channel endpoint. This is used by the pure gVisor stack; the Mixed ++// stack drives packet flow itself via WritePacket / Tun.Read. ++func (t *NativeTun) NewEndpoint() (stack.LinkEndpoint, stack.NICOptions, error) { ++ ep := channel.New(1024, uint32(t.options.MTU), "") ++ go t.gvisorReadLoop(ep) ++ return ep, stack.NICOptions{}, nil ++} ++ ++func (t *NativeTun) gvisorReadLoop(ep *channel.Endpoint) { ++ buf := make([]byte, t.options.MTU+PacketOffset) ++ for { ++ n, err := t.tunFile.Read(buf) ++ if err != nil { ++ return ++ } ++ if n < PacketOffset+gHdr.IPv4MinimumSize { ++ continue ++ } ++ af := binary.BigEndian.Uint32(buf[:PacketOffset]) ++ rawPkt := make([]byte, n-PacketOffset) ++ copy(rawPkt, buf[PacketOffset:n]) ++ var proto tcpip.NetworkProtocolNumber ++ switch uint32(af) { ++ case uint32(unix.AF_INET): ++ proto = gHdr.IPv4ProtocolNumber ++ case uint32(unix.AF_INET6): ++ proto = gHdr.IPv6ProtocolNumber ++ default: ++ continue ++ } ++ pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ ++ Payload: buffer.MakeWithData(rawPkt), ++ }) ++ ep.InjectInbound(proto, pkt) ++ pkt.DecRef() ++ } ++} diff --git a/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__nondarwin.go b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__nondarwin.go new file mode 100644 index 000000000000..e9508bea502b --- /dev/null +++ b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__nondarwin.go @@ -0,0 +1,12 @@ +-- Exclude FreeBSD from the !darwin PacketOffset=0 fallback. +-- FreeBSD uses TUNSIFHEAD multi-AF mode, which requires PacketOffset=4 +-- (same as Darwin). FreeBSD gets its own PacketOffset constant in +-- tun_freebsd.go. +--- vendor/github.com/sagernet/sing-tun/tun_nondarwin.go.orig 2026-05-04 16:16:29 UTC ++++ vendor/github.com/sagernet/sing-tun/tun_nondarwin.go +@@ -1,4 +1,4 @@ +-//go:build !darwin ++//go:build !darwin && !freebsd + + package tun + diff --git a/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__other.go b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__other.go new file mode 100644 index 000000000000..4254e059c543 --- /dev/null +++ b/net/sing-box/files/patch-vendor_github.com_sagernet_sing-tun_tun__other.go @@ -0,0 +1,10 @@ +-- Exclude FreeBSD from the stub that returns os.ErrInvalid. +-- FreeBSD now has a real TUN implementation in tun_freebsd.go. +--- vendor/github.com/sagernet/sing-tun/tun_other.go.orig 2026-05-04 16:34:14 UTC ++++ vendor/github.com/sagernet/sing-tun/tun_other.go +@@ -1,4 +1,4 @@ +-//go:build !(linux || windows || darwin) ++//go:build !(linux || windows || darwin || freebsd) + + package tun + diff --git a/net/sing-box/files/sing-box-tun-test.json b/net/sing-box/files/sing-box-tun-test.json new file mode 100644 index 000000000000..4ec78cdfb1b4 --- /dev/null +++ b/net/sing-box/files/sing-box-tun-test.json @@ -0,0 +1,24 @@ +{ + "log": { + "level": "info" + }, + "inbounds": [ + { + "type": "tun", + "tag": "tun-in", + "interface_name": "tun0", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], + "mtu": 1500, + "auto_route": false + } + ], + "outbounds": [ + { + "type": "direct", + "tag": "direct" + } + ] +}