From 6a4274ab7584b9b57bbbec3b157be2f993b00b9b Mon Sep 17 00:00:00 2001 From: patterniha <71074308+patterniha@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:04:40 +0330 Subject: [PATCH] TUN inbound: `autoOutboundsInterface` should not set IP_BOUND_IF on IPv6 sockets on Darwin (mainly macOS) (#7230) Fixes https://github.com/XTLS/Xray-core/issues/7228 --------- Co-authored-by: Claude Opus 5.5 --- proxy/tun/tun_darwin.go | 11 ++++------ proxy/tun/tun_darwin_test.go | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/proxy/tun/tun_darwin.go b/proxy/tun/tun_darwin.go index 299720cbd..a005681f4 100644 --- a/proxy/tun/tun_darwin.go +++ b/proxy/tun/tun_darwin.go @@ -558,19 +558,16 @@ 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 "tcp6", "udp6", "ip6": - err1 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index) - fallthrough + // IPV6_BOUND_IF binds the IPv4 traffic of a dual-stack socket as + // well, and an IPv6 socket rejects IP_BOUND_IF with EINVAL. + return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index) case "tcp4", "udp4", "ip4": - err2 = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index) + return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index) default: panic(network + " " + address) } - - return errors.Join(err1, err2) } func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) { diff --git a/proxy/tun/tun_darwin_test.go b/proxy/tun/tun_darwin_test.go index 2c299e378..2656e62b6 100644 --- a/proxy/tun/tun_darwin_test.go +++ b/proxy/tun/tun_darwin_test.go @@ -3,7 +3,10 @@ package tun import ( + "context" + "net" "sync" + "syscall" "testing" "time" @@ -231,3 +234,40 @@ func TestWaitKqueueReportsPersistentFailure(t *testing.T) { } } } + +// TestSetinterfaceSocketFamilies binds each kind of socket Xray's dialers +// open to lo0, including the dual-stack one Go opens for UDP. +func TestSetinterfaceSocketFamilies(t *testing.T) { + lo, err := net.InterfaceByName("lo0") + if err != nil { + t.Skip(err) + } + lc := net.ListenConfig{Control: func(network, address string, c syscall.RawConn) error { + var err error + if controlErr := c.Control(func(fd uintptr) { + err = setinterface(network, address, fd, lo) + }); controlErr != nil { + return controlErr + } + return err + }} + for _, s := range []struct{ network, address string }{ + {"udp", "0.0.0.0:0"}, // dual-stack + {"udp4", "127.0.0.1:0"}, + {"udp6", "[::1]:0"}, + {"tcp4", "127.0.0.1:0"}, + {"tcp6", "[::1]:0"}, + } { + var closer interface{ Close() error } + if s.network[:3] == "udp" { + closer, err = lc.ListenPacket(context.Background(), s.network, s.address) + } else { + closer, err = lc.Listen(context.Background(), s.network, s.address) + } + if err != nil { + t.Errorf("%s %s: %v", s.network, s.address, err) + continue + } + closer.Close() + } +}