Skip to content

[BUG]: bindToInterface on Windows fails for IPv6-only sockets when the listen address has no host part #109

Description

@KuroIVeko

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions