Sockopt: Handle single-family interface binding on Windows, including TUN inbound's autoOutboundsInterface (#7214)

And https://github.com/XTLS/Xray-core/pull/7214#issuecomment-6329118637

https://github.com/XTLS/Xray-core/pull/7201#issuecomment-6314453444

Fixes https://github.com/XTLS/Xray-core/issues/7160#issuecomment-6246944012

---------

Co-authored-by: patterniha <71074308+patterniha@users.noreply.github.com>
This commit is contained in:
Eliot the Cougar
2026-10-28 10:15:58 +00:00
committed by GitHub
co-authored by patterniha
parent 1576e79563
commit 3071fbfa25
4 changed files with 110 additions and 1 deletions
+10
View File
@@ -2,6 +2,7 @@ package localdns
import (
"context"
"net/netip"
"syscall"
"time"
@@ -100,6 +101,15 @@ func New() *Client {
if internet.IsSkippedDNSServer(address) {
return nil, errors.New("skipped DNS server ", address)
}
// A socket of the server's IP version only needs that version bound
// to the interface, which a dual-stack one would need both of.
if ap, err := netip.ParseAddrPort(address); err == nil && (network == "udp" || network == "tcp") {
if ap.Addr().Unmap().Is4() {
network += "4"
} else {
network += "6"
}
}
return d.DialContext(ctx, network, address)
},
}
+25 -1
View File
@@ -401,6 +401,15 @@ func resolveOnOwn() *savedResolver {
if internet.IsSkippedDNSServer(address) {
return nil, errors.New("skipped DNS server ", address)
}
// A socket of the server's IP version only needs that version bound
// to the interface, which a dual-stack one would need both of.
if ap, err := netip.ParseAddrPort(address); err == nil && (network == "udp" || network == "tcp") {
if ap.Addr().Unmap().Is4() {
network += "4"
} else {
network += "6"
}
}
return dialer.DialContext(ctx, network, address)
}
net.DefaultResolver.PreferGo = true
@@ -558,10 +567,22 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
switch network {
case "tcp6", "udp6", "ip6":
dualStack := false
if network == "udp6" {
ipv6Only, err := windows.GetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_V6ONLY)
if err != nil {
return internet.NewBindError(err)
}
dualStack = ipv6Only == 0
}
err1 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, IPV6_UNICAST_IF, iface.Index)
if network == "udp6" {
err2 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_MULTICAST_IF, iface.Index)
}
// Only dual-stack UDP can send to both IPv4 and IPv6 destinations.
if !dualStack {
break
}
fallthrough
case "tcp4", "udp4", "ip4":
err3 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, IP_UNICAST_IF, *(*int)(unsafe.Pointer(&index[0])))
@@ -572,7 +593,10 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
panic(network + " " + address)
}
return errors.Combine(err1, err2, err3, err4)
if err := errors.Combine(err1, err2, err3, err4); err != nil {
return internet.NewBindError(err)
}
return nil
}
func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, error) {
+12
View File
@@ -2,6 +2,8 @@ package internet
import (
"context"
go_errors "errors"
"runtime"
"sync"
"syscall"
"time"
@@ -92,6 +94,16 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
}
}
packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String())
var bindErr *BindError
if err != nil && runtime.GOOS == "windows" && go_errors.As(err, &bindErr) {
// Keep dual-stack UDP when possible. If the interface cannot bind both
// families, retry a socket that cannot send on the unbound family.
network := "udp6"
if destAddr.IP.To4() != nil {
network = "udp4"
}
packetConn, err = lc.ListenPacket(ctx, network, srcAddr.String())
}
if err != nil {
return nil, err
}
@@ -0,0 +1,63 @@
package internet_test
import (
"context"
"errors"
"slices"
"syscall"
"testing"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"golang.org/x/sys/windows"
)
// TestDialUDPBindFallback checks that a UDP dial whose dual-stack socket
// cannot be bound to both IP versions, as on an interface without one of
// them, moves to a socket of the destination's version only, and still
// fails when that one cannot be bound either.
func TestDialUDPBindFallback(t *testing.T) {
for _, c := range []struct {
dest string
reject []string // the kinds of socket that cannot be bound
tried []string // the kinds of socket tried, in order
ok bool
}{
{"127.0.0.1", nil, []string{"dual"}, true},
{"127.0.0.1", []string{"dual"}, []string{"dual", "udp4"}, true},
{"::1", []string{"dual"}, []string{"dual", "udp6"}, true},
{"127.0.0.1", []string{"dual", "udp4"}, []string{"dual", "udp4"}, false},
} {
var tried []string
internet.ControllersLock.Lock()
saved := internet.Controllers
internet.Controllers = []func(string, string, syscall.RawConn) error{func(network, _ string, rc syscall.RawConn) error {
kind := network
if network == "udp6" {
rc.Control(func(fd uintptr) {
if v6only, _ := windows.GetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_V6ONLY); v6only == 0 {
kind = "dual"
}
})
}
tried = append(tried, kind)
if slices.Contains(c.reject, kind) {
return internet.NewBindError(errors.New("cannot bind " + kind))
}
return nil
}}
internet.ControllersLock.Unlock()
conn, err := internet.DialSystem(context.Background(), net.UDPDestination(net.ParseAddress(c.dest), 53), nil)
if conn != nil {
conn.Close()
}
internet.ControllersLock.Lock()
internet.Controllers = saved
internet.ControllersLock.Unlock()
if (err == nil) != c.ok || !slices.Equal(tried, c.tried) {
t.Errorf("%s rejecting %v: tried %v, err %v", c.dest, c.reject, tried, err)
}
}
}