sing-bind-windows-v6.patch
Summary
On Windows, binding an IPv6-only socket (udp6 / tcp6 / ip6) to an interface always
fails with WSAEINVAL when the address has no host part (e.g. :0, :41641).
common/control/bind_windows.go, wildcard branch:
if M.ParseSocksaddr(address).AddrString() == "" {
err := bind4(handle, interfaceIndex)
if err != nil {
return err
}
// try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6
bind6(handle, interfaceIndex)
return nil
}
switch network {
case "tcp4", "udp4", "ip4":
return bind4(handle, interfaceIndex)
default:
return bind6(handle, interfaceIndex)
}
bind4 sets IP_UNICAST_IF at level IPPROTO_IP. Go creates sockets for network
"udp6" with IPV6_V6ONLY set, and Windows rejects IPPROTO_IP level options on such
a socket with WSAEINVAL ("An invalid argument was supplied").
Because the address has no host part, the wildcard branch is taken and bind4 is called
unconditionally. Its failure is returned, so the whole ListenPacket fails.
Two things suggest this is an oversight rather than intended:
- The
switch network below already distinguishes v4 from v6 correctly. Only the
wildcard branch does not.
- Inside the wildcard branch,
bind6 failure is explicitly tolerated but bind4
failure is not. On an AF_INET socket this works out (bind4 succeeds, bind6 fails and
is ignored); on an IPv6-only socket the asymmetry breaks it.
Dual-stack networks ("udp" / "tcp" / "ip") are unaffected, since IP_UNICAST_IF is
valid on a dual-stack socket.
Observed effect
Found via sing-box's Tailscale endpoint on Windows. tailscale's magicsock cannot bind its
udp6 socket, so it reports ipv6=false to the control plane. Every peer then refuses to
attempt IPv6 direct connections and falls back to DERP permanently, even when both sides
have native public IPv6 and no NAT in between.
Before:
magicsock: unable to bind udp6 port 41641: listen udp6 :41641: An invalid argument was supplied.
magicsock: Rebind ignoring IPv6 bind failure: failed to bind any ports (tried [41641 0])
netcheck: report: udp=true v6=false v6os=true mapvarydest=false portmap= v4a=198.51.100.10:24497 derp=1
control: NetInfo{varies=false ipv6=false ipv6os=true udp=true icmpv4=false derp=#1}
Note v6os=true while v6=false: the OS supports IPv6 and the interface has global IPv6
addresses, but no socket could be bound. The link state seen by tailscale confirms this:
link state: interfaces.State{defaultRoute=Ethernet ifs={... Ethernet:[192.0.2.2/24 2001:db8:1234:5678::1/64 llu6] ...} v4=true v6=true}
The failure is independent of interface selection: it reproduces with
auto_detect_interface: true, with an explicit default_interface, and with both
options removed. It also reproduces with both an explicit listen_port and a random
port (tried [41641 0]).
The OS itself is fine — binding an IPv6 UDP socket from PowerShell on the same machine
works, and other processes hold [::] UDP endpoints:
$s=[System.Net.Sockets.UdpClient]::new(0,[System.Net.Sockets.AddressFamily]::InterNetworkV6)
$s.Client.LocalEndPoint # InterNetworkV6 :: 53123
After applying the patch below, same machine, same configuration:
netcheck: report: udp=true v6=true mapvarydest=false portmap= v4a=198.51.100.10:24399 v6a=[2001:db8:1234:5678::1]:49154 derp=1 derpdist=1v4:18ms,1v6:11ms
control: NetInfo{varies=false ipv6=true ipv6os=true udp=true icmpv4=false derp=#1}
magicsock: endpoints changed: 198.51.100.10:24399 (stun), [2001:db8:1234:5678::1]:49154 (stun), 192.0.2.2:49155 (local), [2001:db8:1234:5678::1]:49155 (local)
No unable to bind udp6 lines, and STUN now returns an IPv6 mapped address, so the
socket both binds and carries traffic.
Proposed fix
Handle explicitly-v6 networks in the wildcard branch the same way the switch network
below already does:
--- a/common/control/bind_windows.go
+++ b/common/control/bind_windows.go
@@ -23,6 +23,11 @@ func bindToInterface(conn syscall.RawConn, network string, address string, finder
}
handle := syscall.Handle(fd)
if M.ParseSocksaddr(address).AddrString() == "" {
+ switch network {
+ case "tcp6", "udp6", "ip6":
+ // IPv6-only socket: IPPROTO_IP options are rejected with WSAEINVAL.
+ return bind6(handle, interfaceIndex)
+ }
err := bind4(handle, interfaceIndex)
if err != nil {
return err
Possibly related
#2900 reported the same error for the WireGuard endpoint; this code path may be the same root cause.
Environment
- sing v0.9.0-beta.4 (reproduced through sing-box v1.14.0 and v1.15.0-alpha.2, identical
error on both)
- Windows 11, build 10.0.26200
- Interface has native global IPv6 from router advertisement; IPv6 is not disabled in the
registry
sing-bind-windows-v6.patch
Summary
On Windows, binding an IPv6-only socket (
udp6/tcp6/ip6) to an interface alwaysfails with
WSAEINVALwhen the address has no host part (e.g.:0,:41641).common/control/bind_windows.go, wildcard branch:bind4setsIP_UNICAST_IFat levelIPPROTO_IP. Go creates sockets for network"udp6"withIPV6_V6ONLYset, and Windows rejectsIPPROTO_IPlevel options on sucha socket with
WSAEINVAL("An invalid argument was supplied").Because the address has no host part, the wildcard branch is taken and
bind4is calledunconditionally. Its failure is returned, so the whole
ListenPacketfails.Two things suggest this is an oversight rather than intended:
switch networkbelow already distinguishes v4 from v6 correctly. Only thewildcard branch does not.
bind6failure is explicitly tolerated butbind4failure is not. On an AF_INET socket this works out (bind4 succeeds, bind6 fails and
is ignored); on an IPv6-only socket the asymmetry breaks it.
Dual-stack networks (
"udp"/"tcp"/"ip") are unaffected, sinceIP_UNICAST_IFisvalid on a dual-stack socket.
Observed effect
Found via sing-box's Tailscale endpoint on Windows. tailscale's magicsock cannot bind its
udp6socket, so it reportsipv6=falseto the control plane. Every peer then refuses toattempt IPv6 direct connections and falls back to DERP permanently, even when both sides
have native public IPv6 and no NAT in between.
Before:
Note
v6os=truewhilev6=false: the OS supports IPv6 and the interface has global IPv6addresses, but no socket could be bound. The link state seen by tailscale confirms this:
The failure is independent of interface selection: it reproduces with
auto_detect_interface: true, with an explicitdefault_interface, and with bothoptions removed. It also reproduces with both an explicit
listen_portand a randomport (
tried [41641 0]).The OS itself is fine — binding an IPv6 UDP socket from PowerShell on the same machine
works, and other processes hold
[::]UDP endpoints:After applying the patch below, same machine, same configuration:
No
unable to bind udp6lines, and STUN now returns an IPv6 mapped address, so thesocket both binds and carries traffic.
Proposed fix
Handle explicitly-v6 networks in the wildcard branch the same way the
switch networkbelow already does:
Possibly related
#2900 reported the same error for the WireGuard endpoint; this code path may be the same root cause.
Environment
error on both)
registry