Skip to content

Commit 405a78f

Browse files
committed
port offsets for peer mismatch
1 parent 0488793 commit 405a78f

4 files changed

Lines changed: 111 additions & 6 deletions

File tree

examples/config.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ listen:
175175
# listen.port+i instead of sharing one port via SO_REUSEPORT, and one extra
176176
# tunnel ("lane") per routine is negotiated with capable peers: lane i
177177
# handshakes from local port listen.port+i to the peer's advertised
178-
# base+(i mod peer_ports). Each lane is a full Noise session with its own
178+
# base+((i + pair_offset) mod peer_ports), where pair_offset is a per-pair
179+
# hash that spreads many small peers across a big peer's whole port range.
180+
# Each lane is a full Noise session with its own
179181
# keys, nonce counter and replay window, so flows taking different paths
180182
# never fight over shared replay state.
181183
#

handshake_manager.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,11 @@ func (hm *HandshakeManager) maybeAllocLaneState(hostinfo *HostInfo, result *hand
740740
// A certified-but-hostile peer doesn't get to size our state.
741741
peerPorts = 256
742742
}
743-
hostinfo.lanes = newLaneState(hm.f.routines, uint16(peerPorts), uint16(result.PeerBasePort))
743+
var offset uint16
744+
if len(hm.f.myVpnAddrs) > 0 && len(hostinfo.vpnAddrs) > 0 {
745+
offset = lanePortOffset(hm.f.myVpnAddrs[0], hostinfo.vpnAddrs[0], uint16(peerPorts))
746+
}
747+
hostinfo.lanes = newLaneState(hm.f.routines, uint16(peerPorts), uint16(result.PeerBasePort), offset)
744748
}
745749

746750
// EnsureLanes starts lane handshakes for every empty, non-pending, retry-due
@@ -785,7 +789,7 @@ func (hm *HandshakeManager) startLaneHandshake(base *HostInfo, i int) {
785789
ls.noteLaneFailure(i)
786790
return
787791
}
788-
target := netip.AddrPortFrom(remote.Addr(), ls.peerBasePort+uint16(i%int(ls.peerPortCount)))
792+
target := netip.AddrPortFrom(remote.Addr(), ls.laneTargetPort(i))
789793

790794
hostinfo := &HostInfo{
791795
vpnAddrs: slices.Clone(base.vpnAddrs),

hostmap.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"hash/fnv"
89
"log/slog"
910
"net"
1011
"net/netip"
@@ -316,9 +317,11 @@ type laneState struct {
316317
sync.Mutex
317318

318319
// peerPortCount/peerBasePort are the peer's advert from the base
319-
// handshake; lane i targets peerBasePort + (i % peerPortCount).
320+
// handshake; portOffset is the per-pair rotation from lanePortOffset.
321+
// Lane i targets peerBasePort + ((i + portOffset) % peerPortCount).
320322
peerPortCount uint16
321323
peerBasePort uint16
324+
portOffset uint16
322325

323326
// txLanes[i] is our established, initiator-owned lane for routine i, or
324327
// nil. Index 0 is always nil — the base tunnel is lane 0. A pointer is
@@ -337,17 +340,57 @@ type laneState struct {
337340
peerLanes []*HostInfo
338341
}
339342

340-
func newLaneState(laneCount int, peerPortCount, peerBasePort uint16) *laneState {
343+
func newLaneState(laneCount int, peerPortCount, peerBasePort, portOffset uint16) *laneState {
341344
return &laneState{
342345
peerPortCount: peerPortCount,
343346
peerBasePort: peerBasePort,
347+
portOffset: portOffset,
344348
txLanes: make([]atomic.Pointer[HostInfo], laneCount),
345349
txPending: make([]bool, laneCount),
346350
txFails: make([]uint8, laneCount),
347351
txRetryAt: make([]time.Time, laneCount),
348352
}
349353
}
350354

355+
// laneTargetPort returns the peer port that owned lane i handshakes to and
356+
// egresses toward. The caller must ensure peerPortCount != 0.
357+
func (ls *laneState) laneTargetPort(i int) uint16 {
358+
return ls.peerBasePort + uint16((i+int(ls.portOffset))%int(ls.peerPortCount))
359+
}
360+
361+
// lanePortOffset returns the rotation applied to this pair's lane target
362+
// ports, in [0, peerPortCount). Without it every low-routine peer would aim
363+
// its few lanes at a big peer's first few ports, concentrating the big
364+
// peer's receive work on a couple of sockets; the hash spreads pairs across
365+
// the whole advertised range.
366+
//
367+
// Both sides hash the same sorted vpn-address pair and the higher address
368+
// negates the result, so when port counts match the two sides' rotations
369+
// cancel: our lane i's 4-tuple is still the reverse of a peer-owned lane's,
370+
// and each outbound lane handshake opens the conntrack entry its partner
371+
// arrives through. (The one lane a nonzero rotation lands on the peer's base
372+
// port has no partner lane; behind a port-restricted NAT it may not form and
373+
// its routine rides the base tunnel — the standard lane fallback.)
374+
func lanePortOffset(myAddr, peerAddr netip.Addr, peerPortCount uint16) uint16 {
375+
if peerPortCount == 0 {
376+
return 0
377+
}
378+
lo, hi := myAddr, peerAddr
379+
if hi.Less(lo) {
380+
lo, hi = hi, lo
381+
}
382+
h := fnv.New32a()
383+
b := lo.As16()
384+
h.Write(b[:])
385+
b = hi.As16()
386+
h.Write(b[:])
387+
o := uint16(h.Sum32() % uint32(peerPortCount))
388+
if myAddr == hi {
389+
o = (peerPortCount - o) % peerPortCount
390+
}
391+
return o
392+
}
393+
351394
const (
352395
laneRetryBase = 5 * time.Second
353396
laneRetryMax = 60 * time.Second

lanes_test.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func newTestBaseHostInfo(vpnIp netip.Addr, localIdx, remoteIdx uint32, laneCount
2727
HandshakePacket: map[uint8][]byte{},
2828
}
2929
base.SetRemote(netip.MustParseAddrPort("192.0.2.1:4242"))
30-
base.lanes = newLaneState(laneCount, uint16(laneCount), 4242)
30+
base.lanes = newLaneState(laneCount, uint16(laneCount), 4242, 0)
3131
return base
3232
}
3333

@@ -47,6 +47,62 @@ func newTestLaneHostInfo(base *HostInfo, laneIndex uint16, localIdx, remoteIdx u
4747
return lane
4848
}
4949

50+
func TestLanePortOffset(t *testing.T) {
51+
a := netip.MustParseAddr("10.0.0.1")
52+
b := netip.MustParseAddr("10.0.0.2")
53+
54+
// Deterministic and in range.
55+
for _, count := range []uint16{1, 2, 3, 4, 16, 256} {
56+
o := lanePortOffset(a, b, count)
57+
assert.Equal(t, o, lanePortOffset(a, b, count), "count %d not deterministic", count)
58+
assert.Less(t, o, count, "count %d out of range", count)
59+
}
60+
assert.Equal(t, uint16(0), lanePortOffset(a, b, 0), "zero port count")
61+
62+
// The two sides' rotations cancel when port counts match, preserving the
63+
// lane-i-reverses-lane-j conntrack pairing.
64+
for _, count := range []uint16{2, 3, 4, 7, 16} {
65+
for i := range 32 {
66+
peer := netip.AddrFrom4([4]byte{192, 0, 2, byte(i)})
67+
oA := lanePortOffset(a, peer, count)
68+
oB := lanePortOffset(peer, a, count)
69+
assert.Equal(t, uint16(0), (oA+oB)%count,
70+
"offsets don't cancel for peer %s count %d", peer, count)
71+
}
72+
}
73+
74+
// Distinct small peers land on distinct rotations of a big peer's range,
75+
// not all on the same first ports.
76+
const bigPeerPorts = 16
77+
distinct := map[uint16]struct{}{}
78+
for i := range 64 {
79+
client := netip.AddrFrom4([4]byte{192, 0, 2, byte(i)})
80+
distinct[lanePortOffset(client, a, bigPeerPorts)] = struct{}{}
81+
}
82+
assert.GreaterOrEqual(t, len(distinct), 8, "64 clients only produced %d distinct offsets", len(distinct))
83+
}
84+
85+
func TestLaneTargetPort(t *testing.T) {
86+
// No rotation: lane i targets base+i, wrapping past the peer's range.
87+
ls := newLaneState(4, 4, 4242, 0)
88+
for i, want := range map[int]uint16{1: 4243, 2: 4244, 3: 4245, 5: 4243} {
89+
assert.Equal(t, want, ls.laneTargetPort(i), "lane %d", i)
90+
}
91+
92+
// Rotation shifts the whole mapping; the wrapped lane lands on the base
93+
// port itself, which is a valid distinct 4-tuple (our source port differs).
94+
ls = newLaneState(4, 4, 4242, 3)
95+
for i, want := range map[int]uint16{1: 4242, 2: 4243, 3: 4244} {
96+
assert.Equal(t, want, ls.laneTargetPort(i), "rotated lane %d", i)
97+
}
98+
99+
// Fewer peer ports than local lanes: rotation still spreads across all of
100+
// the peer's ports.
101+
ls = newLaneState(16, 2, 4242, 1)
102+
assert.Equal(t, uint16(4242), ls.laneTargetPort(1))
103+
assert.Equal(t, uint16(4243), ls.laneTargetPort(2))
104+
}
105+
50106
func TestLaneHostmapLifecycle(t *testing.T) {
51107
l := test.NewLogger()
52108
hostMap := newHostMap(l)

0 commit comments

Comments
 (0)