From 6780045550410f869fd87820ae4b672b0c3ac85d Mon Sep 17 00:00:00 2001 From: Boris Korzun Date: Wed, 15 Apr 2026 15:40:19 +0300 Subject: [PATCH] TUN inbound: Add FreeBSD support (#5891) And reverts "refactor `mtu` to support setting IPv4/v6 separately" https://github.com/XTLS/Xray-core/pull/5891#issuecomment-4245677624 And fixes `autoOutboundsInterface` on Windows https://github.com/XTLS/Xray-core/pull/5887#issuecomment-4251719900 --------- Co-authored-by: LjhAUMEM --- infra/conf/tun.go | 9 +-- proxy/tun/README.md | 19 +++++ proxy/tun/config.pb.go | 8 +-- proxy/tun/config.proto | 2 +- proxy/tun/tun_android.go | 2 +- proxy/tun/tun_darwin.go | 28 ++++---- proxy/tun/tun_default.go | 2 +- proxy/tun/tun_freebsd.go | 147 +++++++++++++++++++++++++++++++++++++++ proxy/tun/tun_linux.go | 4 +- proxy/tun/tun_windows.go | 33 ++++----- 10 files changed, 209 insertions(+), 45 deletions(-) create mode 100644 proxy/tun/tun_freebsd.go diff --git a/infra/conf/tun.go b/infra/conf/tun.go index 4a3b297a..4ba9165c 100644 --- a/infra/conf/tun.go +++ b/infra/conf/tun.go @@ -7,7 +7,7 @@ import ( type TunConfig struct { Name string `json:"name"` - MTU []uint32 `json:"mtu"` + MTU uint32 `json:"mtu"` Gateway []string `json:"gateway"` DNS []string `json:"dns"` UserLevel uint32 `json:"userLevel"` @@ -34,11 +34,8 @@ func (v *TunConfig) Build() (proto.Message, error) { if config.Name == "" { config.Name = "xray0" } - if len(config.MTU) == 0 { - config.MTU = []uint32{1500, 1280} - } - if len(config.MTU) == 1 { - config.MTU = append(config.MTU, config.MTU[0]) + if config.MTU == 0 { + config.MTU = 1500 } return config, nil } diff --git a/proxy/tun/README.md b/proxy/tun/README.md index ca081f5a..51aaea37 100644 --- a/proxy/tun/README.md +++ b/proxy/tun/README.md @@ -173,6 +173,25 @@ Note on ipv6 support. \ Despite Windows also giving the adapter autoconfigured ipv6 address, the ipv6 is not possible until the interface has any _routable_ ipv6 address (given link-local address will not accept traffic from external addresses). \ So everything applicable for ipv4 above also works for ipv6, you only need to give the interface some address manually, e.g. anything private like fc00::a:b:c:d/64 will do just fine +## FreeBSD SUPPORT + +FreeBSD support of the same functionality is implemented through tun(4). + +Interface name in the configuration must comply to the scheme "tunN", where N is some number. \ +It's necessary to set an IP address to the interface, ex.: +``` +ifconfig tun0 inet 169.254.10.1/30 +``` +To attach routing to the interface, route command like following can be executed: +``` +route add -net 1.1.1.0/24 -iface tun10 +``` +``` +route add -inet6 -host 2606:4700:4700::1111 -iface tun10 +route add -inet6 -host 2606:4700:4700::1001 -iface tun10 +``` +Important to remember that everything written above about Linux routing concept, also apply to FreeBSD. If you simply route default route through tun interface, that will result network loop and immediate network failure. + ## MAC OS X SUPPORT Darwin (Mac OS X) support of the same functionality is implemented through utun (userspace tunnel). diff --git a/proxy/tun/config.pb.go b/proxy/tun/config.pb.go index 83b7afce..04ce01d8 100644 --- a/proxy/tun/config.pb.go +++ b/proxy/tun/config.pb.go @@ -24,7 +24,7 @@ const ( type Config struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - MTU []uint32 `protobuf:"varint,2,rep,packed,name=MTU,proto3" json:"MTU,omitempty"` + MTU uint32 `protobuf:"varint,2,opt,name=MTU,proto3" json:"MTU,omitempty"` Gateway []string `protobuf:"bytes,3,rep,name=gateway,proto3" json:"gateway,omitempty"` DNS []string `protobuf:"bytes,4,rep,name=DNS,proto3" json:"DNS,omitempty"` UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` @@ -71,11 +71,11 @@ func (x *Config) GetName() string { return "" } -func (x *Config) GetMTU() []uint32 { +func (x *Config) GetMTU() uint32 { if x != nil { return x.MTU } - return nil + return 0 } func (x *Config) GetGateway() []string { @@ -120,7 +120,7 @@ const file_proxy_tun_config_proto_rawDesc = "" + "\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" + "\x06Config\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + - "\x03MTU\x18\x02 \x03(\rR\x03MTU\x12\x18\n" + + "\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" + "\agateway\x18\x03 \x03(\tR\agateway\x12\x10\n" + "\x03DNS\x18\x04 \x03(\tR\x03DNS\x12\x1d\n" + "\n" + diff --git a/proxy/tun/config.proto b/proxy/tun/config.proto index 70413b73..50d43ce2 100644 --- a/proxy/tun/config.proto +++ b/proxy/tun/config.proto @@ -8,7 +8,7 @@ option java_multiple_files = true; message Config { string name = 1; - repeated uint32 MTU = 2; + uint32 MTU = 2; repeated string gateway = 3; repeated string DNS = 4; uint32 user_level = 5; diff --git a/proxy/tun/tun_android.go b/proxy/tun/tun_android.go index 9d7923b6..16dd5161 100644 --- a/proxy/tun/tun_android.go +++ b/proxy/tun/tun_android.go @@ -73,7 +73,7 @@ func (t *AndroidTun) Index() (int, error) { func (t *AndroidTun) newEndpoint() (stack.LinkEndpoint, error) { return fdbased.New(&fdbased.Options{ FDs: []int{t.tunFd}, - MTU: t.options.MTU[0], + MTU: t.options.MTU, RXChecksumOffload: true, }) } diff --git a/proxy/tun/tun_darwin.go b/proxy/tun/tun_darwin.go index 5025a470..955f3614 100644 --- a/proxy/tun/tun_darwin.go +++ b/proxy/tun/tun_darwin.go @@ -3,7 +3,7 @@ package tun import ( - go_errors "errors" + "errors" "fmt" "net" "net/netip" @@ -12,7 +12,6 @@ import ( "unsafe" "github.com/xtls/xray-core/common/buf" - "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/platform" "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/buffer" @@ -76,7 +75,7 @@ func NewTun(options *Config) (Tun, error) { return nil, err } - err = setup(options.Name, options.MTU[0]) + err = setup(options.Name, options.MTU) if err != nil { _ = tunFile.Close() return nil, err @@ -121,7 +120,7 @@ func (t *DarwinTun) Index() (int, error) { // WritePacket implements GVisorDevice method to write one packet to the tun device func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { // request memory to write from reusable buffer pool - b := buf.NewWithSize(int32(t.options.MTU[0]) + utunHeaderSize) + b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize) defer b.Release() // prepare Darwin specific packet header @@ -143,7 +142,7 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { b.SetByte(3, family) if _, err := t.tunFile.Write(b.Bytes()); err != nil { - if go_errors.Is(err, unix.EAGAIN) { + if errors.Is(err, unix.EAGAIN) { return &tcpip.ErrWouldBlock{} } return &tcpip.ErrAborted{} @@ -156,11 +155,11 @@ func (t *DarwinTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { // which will make the stack call Wait which should implement desired push-back func (t *DarwinTun) ReadPacket() (byte, *stack.PacketBuffer, error) { // request memory to write from reusable buffer pool - b := buf.NewWithSize(int32(t.options.MTU[0]) + utunHeaderSize) + b := buf.NewWithSize(int32(t.options.MTU) + utunHeaderSize) // read the bytes to the interface file n, err := b.ReadFrom(t.tunFile) - if go_errors.Is(err, unix.EAGAIN) || go_errors.Is(err, unix.EINTR) { + if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { b.Release() return 0, nil, ErrQueueEmpty } @@ -193,7 +192,7 @@ func (t *DarwinTun) Wait() { } func (t *DarwinTun) newEndpoint() (stack.LinkEndpoint, error) { - return &LinkEndpoint{deviceMTU: t.options.MTU[0], device: t}, nil + return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil } // open the interface, by creating new utunN if in the system and returning its file descriptor @@ -373,12 +372,17 @@ func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error { } func setinterface(network, address string, fd uintptr, iface *net.Interface) error { + var err1, err2 error + switch network { - case "tcp4", "udp4", "ip4": - return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index) case "tcp6", "udp6", "ip6": - return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index) + err1 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index) + fallthrough + case "tcp4", "udp4", "ip4": + err2 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index) default: - return errors.New("unknown network ", network) + panic(network + " " + address) } + + return errors.Join(err1, err2) } diff --git a/proxy/tun/tun_default.go b/proxy/tun/tun_default.go index 9c5c8e83..24e6d08b 100644 --- a/proxy/tun/tun_default.go +++ b/proxy/tun/tun_default.go @@ -1,4 +1,4 @@ -//go:build !linux && !windows && !android && !darwin +//go:build !linux && !windows && !android && !darwin && !freebsd package tun diff --git a/proxy/tun/tun_freebsd.go b/proxy/tun/tun_freebsd.go new file mode 100644 index 00000000..8833a753 --- /dev/null +++ b/proxy/tun/tun_freebsd.go @@ -0,0 +1,147 @@ +//go:build freebsd + +package tun + +import ( + "errors" + "net" + _ "unsafe" + + "golang.zx2c4.com/wireguard/tun" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/stack" + + "golang.org/x/sys/unix" + + "github.com/xtls/xray-core/common/buf" +) + +const tunHeaderSize = 4 + +//go:linkname procyield runtime.procyield +func procyield(cycles uint32) + +type FreeBSDTun struct { + device tun.Device + mtu uint32 +} + +var _ Tun = (*FreeBSDTun)(nil) +var _ GVisorDevice = (*FreeBSDTun)(nil) + +// NewTun builds new tun interface handler +func NewTun(options *Config) (Tun, error) { + tunDev, err := tun.CreateTUN(options.Name, int(options.MTU)) + if err != nil { + return nil, err + } + + return &FreeBSDTun{device: tunDev, mtu: options.MTU}, nil +} + +func (t *FreeBSDTun) Start() error { + return nil +} + +func (t *FreeBSDTun) Close() error { + return t.device.Close() +} + +func (t *FreeBSDTun) Name() (string, error) { + return t.device.Name() +} + +func (t *FreeBSDTun) Index() (int, error) { + name, err := t.Name() + if err != nil { + return 0, err + } + iface, err := net.InterfaceByName(name) + if err != nil { + return 0, err + } + return iface.Index, nil +} + +// WritePacket implements GVisorDevice method to write one packet to the tun device +func (t *FreeBSDTun) WritePacket(packet *stack.PacketBuffer) tcpip.Error { + // request memory to write from reusable buffer pool + b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize) + defer b.Release() + + // prepare Unix specific packet header + _, _ = b.Write([]byte{0x0, 0x0, 0x0, 0x0}) + // copy the bytes of slices that compose the packet into the allocated buffer + for _, packetElement := range packet.AsSlices() { + _, _ = b.Write(packetElement) + } + // fill Unix specific header from the first raw packet byte, that we can access now + var family byte + switch b.Byte(4) >> 4 { + case 4: + family = unix.AF_INET + case 6: + family = unix.AF_INET6 + default: + return &tcpip.ErrAborted{} + } + b.SetByte(3, family) + + if _, err := t.device.File().Write(b.Bytes()); err != nil { + if errors.Is(err, unix.EAGAIN) { + return &tcpip.ErrWouldBlock{} + } + return &tcpip.ErrAborted{} + } + return nil +} + +// ReadPacket implements GVisorDevice method to read one packet from the tun device +// It is expected that the method will not block, rather return ErrQueueEmpty when there is nothing on the line, +// which will make the stack call Wait which should implement desired push-back +func (t *FreeBSDTun) ReadPacket() (byte, *stack.PacketBuffer, error) { + // request memory to write from reusable buffer pool + b := buf.NewWithSize(int32(t.mtu) + tunHeaderSize) + + // read the bytes to the interface file + n, err := b.ReadFrom(t.device.File()) + if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { + b.Release() + return 0, nil, ErrQueueEmpty + } + if err != nil { + b.Release() + return 0, nil, err + } + + // discard empty or sub-empty packets + if n <= tunHeaderSize { + b.Release() + return 0, nil, ErrQueueEmpty + } + + // network protocol version from first byte of the raw packet, the one that follows Unix specific header + version := b.Byte(tunHeaderSize) >> 4 + packetBuffer := buffer.MakeWithData(b.BytesFrom(tunHeaderSize)) + return version, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: packetBuffer, + IsForwardedPacket: true, + OnRelease: func() { + b.Release() + }, + }), nil +} + +// Wait some cpu cycles +func (t *FreeBSDTun) Wait() { + procyield(1) +} + +func (t *FreeBSDTun) newEndpoint() (stack.LinkEndpoint, error) { + return &LinkEndpoint{deviceMTU: t.mtu, device: t}, nil +} + +func setinterface(network, address string, fd uintptr, iface *net.Interface) error { + return nil +} diff --git a/proxy/tun/tun_linux.go b/proxy/tun/tun_linux.go index 24d0fdba..ef3f3505 100644 --- a/proxy/tun/tun_linux.go +++ b/proxy/tun/tun_linux.go @@ -30,7 +30,7 @@ func NewTun(options *Config) (Tun, error) { return nil, err } - tunLink, err := setup(options.Name, int(options.MTU[0])) + tunLink, err := setup(options.Name, int(options.MTU)) if err != nil { _ = unix.Close(tunFd) return nil, err @@ -121,7 +121,7 @@ func (t *LinuxTun) Index() (int, error) { func (t *LinuxTun) newEndpoint() (stack.LinkEndpoint, error) { return fdbased.New(&fdbased.Options{ FDs: []int{t.tunFd}, - MTU: t.options.MTU[0], + MTU: t.options.MTU, RXChecksumOffload: true, }) } diff --git a/proxy/tun/tun_windows.go b/proxy/tun/tun_windows.go index 9ca51f5a..c8c00d4e 100644 --- a/proxy/tun/tun_windows.go +++ b/proxy/tun/tun_windows.go @@ -134,7 +134,7 @@ func (t *WindowsTun) Start() error { ipif.DadTransmits = 0 ipif.ManagedAddressConfigurationSupported = false ipif.OtherStatefulConfigurationSupported = false - ipif.NLMTU = t.options.MTU[0] + ipif.NLMTU = t.options.MTU ipif.UseAutomaticMetric = false ipif.Metric = 0 err = ipif.Set() @@ -151,7 +151,7 @@ func (t *WindowsTun) Start() error { ipif.DadTransmits = 0 ipif.ManagedAddressConfigurationSupported = false ipif.OtherStatefulConfigurationSupported = false - ipif.NLMTU = t.options.MTU[1] + ipif.NLMTU = t.options.MTU ipif.UseAutomaticMetric = false ipif.Metric = 0 err = ipif.Set() @@ -278,7 +278,7 @@ func (t *WindowsTun) Wait() { } func (t *WindowsTun) newEndpoint() (stack.LinkEndpoint, error) { - return &LinkEndpoint{deviceMTU: t.options.MTU[0], device: t}, nil + return &LinkEndpoint{deviceMTU: t.options.MTU, device: t}, nil } const ( @@ -290,26 +290,23 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err var index [4]byte binary.BigEndian.PutUint32(index[:], uint32(iface.Index)) + var err1, err2, err3, err4 error + switch network { - case "tcp4", "udp4", "ip4": - err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0]))) - if err != nil { - return err - } - if network == "udp4" { - return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, windows.IP_MULTICAST_IF, *(*int)(unsafe.Pointer(&index[0]))) - } case "tcp6", "udp6", "ip6": - err := windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index) - if err != nil { - return err - } + err1 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index) if network == "udp6" { - return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index) + err2 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index) + } + fallthrough + case "tcp4", "udp4", "ip4": + err3 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0]))) + if network == "udp4" || network == "udp6" { + err4 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, windows.IP_MULTICAST_IF, *(*int)(unsafe.Pointer(&index[0]))) } default: - return errors.New("unknown network ", network) + panic(network + " " + address) } - return nil + return errors.Combine(err1, err2, err3, err4) }