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 <llnu14702@gmail.com>
This commit is contained in:
Boris Korzun
2026-04-15 15:40:19 +03:00
committed by GitHub
parent ff6126463b
commit 6780045550
10 changed files with 209 additions and 45 deletions

View File

@@ -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).

View File

@@ -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" +

View File

@@ -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;

View File

@@ -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,
})
}

View File

@@ -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)
}

View File

@@ -1,4 +1,4 @@
//go:build !linux && !windows && !android && !darwin
//go:build !linux && !windows && !android && !darwin && !freebsd
package tun

147
proxy/tun/tun_freebsd.go Normal file
View File

@@ -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
}

View File

@@ -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,
})
}

View File

@@ -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)
}