From e14772ac0cc2fc4929f28f269b352c049c658886 Mon Sep 17 00:00:00 2001 From: Jay Wren Date: Wed, 21 Oct 2026 10:22:27 -0400 Subject: [PATCH] multiport: fix panic sending to IPv6 remotes over the raw socket RawConn builds IPv4 headers only, but WriteTo called As4() on every destination. Once a handshake passed tun.multiport.tx_handshake_delay, the handshake manager sent a multiport copy to every remote, and the first IPv6 address panicked with "As4 called on IPv6 address". The tunnel send path in inside.go had the same problem for any multiport tunnel with an IPv6 remote. Add udp.RawSupported and only use the raw socket for IPv4 (and IPv4-mapped) destinations; IPv6 remotes fall back to the normal UDP writer. RawConn.WriteTo now returns an error rather than panicking, matching ErrInvalidIPv6RemoteForSocket on the regular UDP sockets. Seen in production on v1.11.1-slack1: https://slack-pde.slack.com/archives/GK7NPNGPP/p1792590741819449 Co-Authored-By: Claude --- handshake_manager.go | 2 +- inside.go | 4 ++-- udp/udp_raw.go | 13 ++++++++++++- udp/udp_raw_linux.go | 4 ++++ udp/udp_raw_test.go | 15 +++++++++++++++ 5 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 udp/udp_raw_test.go diff --git a/handshake_manager.go b/handshake_manager.go index 8d841756..481395a5 100644 --- a/handshake_manager.go +++ b/handshake_manager.go @@ -318,7 +318,7 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered } // Attempt a multiport handshake if we are past the TxHandshakeDelay attempts - if hm.multiPort.TxHandshake && hm.udpRaw != nil && hh.counter >= hm.multiPort.TxHandshakeDelay { + if hm.multiPort.TxHandshake && hm.udpRaw != nil && hh.counter >= hm.multiPort.TxHandshakeDelay && udp.RawSupported(addr) { sentMultiport = true // We need to re-allocate with 8 bytes at the start of SOCK_RAW raw := hostinfo.HandshakePacket[0x80] diff --git a/inside.go b/inside.go index 32b02668..454b6d0c 100644 --- a/inside.go +++ b/inside.go @@ -414,7 +414,7 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType } if remote.IsValid() { - if multiport { + if multiport && udp.RawSupported(remote) { rawOut = rawOut[:len(out)+udp.RawOverhead] port := udpPortGetter.UDPSendPort(f.multiPort.TxPorts) err = f.udpRaw.WriteTo(rawOut, port, remote) @@ -428,7 +428,7 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType ) } } else if hr := hostinfo.GetRemote(); hr.IsValid() { - if multiport { + if multiport && udp.RawSupported(hr) { rawOut = rawOut[:len(out)+udp.RawOverhead] port := udpPortGetter.UDPSendPort(f.multiPort.TxPorts) err = f.udpRaw.WriteTo(rawOut, port, hr) diff --git a/udp/udp_raw.go b/udp/udp_raw.go index 4c5c2879..b9408e8c 100644 --- a/udp/udp_raw.go +++ b/udp/udp_raw.go @@ -1,6 +1,9 @@ package udp -import mathrand "math/rand" +import ( + mathrand "math/rand" + "net/netip" +) type SendPortGetter interface { // UDPSendPort returns the port to use @@ -14,3 +17,11 @@ func (randomSendPort) UDPSendPort(maxPort int) uint16 { } var RandomSendPort = randomSendPort{} + +// RawSupported reports whether (*RawConn).WriteTo can send to addr. The raw +// socket only builds IPv4 headers, so IPv6 destinations must use the normal +// UDP socket. +func RawSupported(addr netip.AddrPort) bool { + a := addr.Addr() + return a.Is4() || a.Is4In6() +} diff --git a/udp/udp_raw_linux.go b/udp/udp_raw_linux.go index 5093dd45..ff23fa12 100644 --- a/udp/udp_raw_linux.go +++ b/udp/udp_raw_linux.go @@ -76,6 +76,10 @@ func NewRawConn(l *slog.Logger, ip string, port int, basePort uint16) (*RawConn, // WriteTo must be called with raw leaving the first `udp.RawOverhead` bytes empty, // for the IP/UDP headers. func (u *RawConn) WriteTo(raw []byte, fromPort uint16, ip netip.AddrPort) error { + if !RawSupported(ip) { + return fmt.Errorf("raw socket does not support sending to %s", ip) + } + var rsa unix.RawSockaddrInet4 rsa.Family = unix.AF_INET rsa.Addr = ip.Addr().As4() diff --git a/udp/udp_raw_test.go b/udp/udp_raw_test.go new file mode 100644 index 00000000..85c086bb --- /dev/null +++ b/udp/udp_raw_test.go @@ -0,0 +1,15 @@ +package udp + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRawSupported(t *testing.T) { + assert.True(t, RawSupported(netip.MustParseAddrPort("1.2.3.4:4242"))) + assert.True(t, RawSupported(netip.MustParseAddrPort("[::ffff:1.2.3.4]:4242"))) + assert.False(t, RawSupported(netip.MustParseAddrPort("[2001:db8::1]:4242"))) + assert.False(t, RawSupported(netip.AddrPort{})) +}