make all lane epochs match

This commit is contained in:
JackDoan committed 2026-09-25 14:47:13 -05:00
1 parent 0ee7ace67e
commit 355b3dcbdb
4 files changed
+41 -9

No files matched your search

+6 -3
View File
@@ -29,12 +29,15 @@ const (
// RehandshakeAfterMessages must stay below RejectAfterMessages so tunnels roll before the hard send stop.
const _ = RejectAfterMessages - RehandshakeAfterMessages
// sessionEpoch hands out a receiver-local ordinal to every ConnectionState at creation. The RX
// sessionEpoch hands out a receiver-local ordinal to every tunnel at creation. The RX
// staging sort (overlay/batch) orders packets by (epoch, message counter). A re-handshake never
// rekeys an existing tunnel; it brings up a new hostinfo and ConnectionState with a counter space
// starting near zero, while the old tunnel keeps decrypting until torn down. During that cutover
// one flush batch can hold packets from both tunnels, and the epoch keeps the old tunnel's
// packets sorted first.
//
// It is drawn once per tunnel, by the base session: a tunnel's multiport lane sessions copy it
// rather than draw their own.
var sessionEpoch atomic.Uint64
type ConnectionState struct {
@@ -85,7 +88,7 @@ func newConnectionStateFromResult(r *handshake.Result) (*ConnectionState, error)
//
// The lane gets its own counter and replay window starting from zero. No
// handshake messages were spent on it, so unlike the base session there is
// nothing to seed.
// nothing to seed. The epoch is the tunnel's, not the lane's; see sessionEpoch.
func newLaneConnectionState(m *laneMaterial, lane uint8) (*ConnectionState, error) {
if lane == 0 {
return nil, fmt.Errorf("lane 0 is the base session")
@@ -107,7 +110,7 @@ func newLaneConnectionState(m *laneMaterial, lane uint8) (*ConnectionState, erro
eKey: noiseutil.NewCipherStateFromKey(eKey, m.cipher),
dKey: noiseutil.NewCipherStateFromKey(dKey, m.cipher),
window: NewBits(ReplayWindow),
epoch: sessionEpoch.Add(1),
epoch: m.epoch,
}, nil
}
+1 -1
View File
@@ -733,7 +733,7 @@ func (hm *HandshakeManager) maybeAllocLanes(hostinfo *HostInfo, result *handshak
return
}
hostinfo.lanes = newLaneSet(result, hm.config.laneCount, hm.f.myVpnAddrs[0], hostinfo.vpnAddrs[0])
hostinfo.lanes = newLaneSet(result, hm.config.laneCount, hostinfo.ConnectionState.epoch, hm.f.myVpnAddrs[0], hostinfo.vpnAddrs[0])
}
// beginHandshake handles an incoming handshake packet that doesn't match any
+5 -1
View File
@@ -145,6 +145,7 @@ type laneMaterial struct {
myCert cert.Certificate
peerCert *cert.CachedCertificate
initiator bool
epoch uint64
}
type laneProbeState struct {
@@ -174,7 +175,9 @@ type laneProbeState struct {
// returns nil when the pair has no lane beyond the base tunnel, which is the
// normal answer for a peer running without multiport. No session is derived
// here; each is derived on the first packet that needs it.
func newLaneSet(r *handshake.Result, myLanes int, myAddr, peerAddr netip.Addr) *laneSet {
//
// epoch is the base session's, which every lane session will share.
func newLaneSet(r *handshake.Result, myLanes int, epoch uint64, myAddr, peerAddr netip.Addr) *laneSet {
// PeerPortCount and PeerBasePort are already bounded to uint16 by the
// handshake payload parser. A zero port count is a peer that did not
// advertise multiport at all, so there is no lane to be had in either
@@ -218,6 +221,7 @@ func newLaneSet(r *handshake.Result, myLanes int, myAddr, peerAddr netip.Addr) *
myCert: r.MyCert,
peerCert: r.RemoteCert,
initiator: r.Initiator,
epoch: epoch,
},
txAddr: make([]atomic.Pointer[netip.AddrPort], txLanes),
demand: make([]atomic.Bool, txLanes),
+29 -4
View File
@@ -27,12 +27,17 @@ var (
// newTestLaneSet builds a lane set from a real handshake result so the sessions
// it derives hold usable keys.
// testLaneEpoch stands in for the base session's epoch. It is deliberately not a
// value sessionEpoch would hand out early in a test run, so a lane session that
// went back to drawing its own cannot match it by accident.
const testLaneEpoch = uint64(0xe90c)
func newTestLaneSet(t *testing.T, r *handshake.Result, myLanes int, peerPorts, peerBase, peerTxLanes uint32) *laneSet {
t.Helper()
r.PeerPortCount = peerPorts
r.PeerBasePort = peerBase
r.PeerTxLanes = peerTxLanes
return newLaneSet(r, myLanes, testMyAddr, testPeerAddr)
return newLaneSet(r, myLanes, testLaneEpoch, testMyAddr, testPeerAddr)
}
// laneSessionFor derives lane s's session and installs it, standing in for the
@@ -153,10 +158,10 @@ func TestNewLaneSetSizing(t *testing.T) {
initR, _ := runTestHandshake(t)
// A peer with no multiport advert gets no lanes at all.
assert.Nil(t, newLaneSet(&handshake.Result{}, 4, testMyAddr, testPeerAddr))
assert.Nil(t, newLaneSet(&handshake.Result{}, 4, testLaneEpoch, testMyAddr, testPeerAddr))
// One lane means only the base tunnel, which is not a lane set.
assert.Nil(t, newLaneSet(&handshake.Result{PeerPortCount: 4, PeerTxLanes: 1}, 1, testMyAddr, testPeerAddr))
assert.Nil(t, newLaneSet(&handshake.Result{PeerPortCount: 4, PeerTxLanes: 1}, 1, testLaneEpoch, testMyAddr, testPeerAddr))
// Sessions cover both directions: enough for everything the peer may send,
// even though we may only send on a few.
@@ -262,6 +267,26 @@ func TestLaneSessionRxDerivation(t *testing.T) {
"the loser's counter was not carried to the surviving window")
}
// Every session on a tunnel reports the tunnel's epoch. The RX staging sort keys
// off (epoch, counter) and lanes derive lazily, so a lane drawing its own ordinal
// would be ordered by when its first packet landed: during a rehandshake cutover
// a late-derived lane of the *old* tunnel would outrank the new tunnel and sort
// its packets last, the exact inversion the epoch exists to prevent.
func TestLaneSessionsShareTheTunnelEpoch(t *testing.T) {
initR, _ := runTestHandshake(t)
ls := newTestLaneSet(t, initR, 4, 4, 4242, 4)
for s := 1; s < 4; s++ {
// Move the global on between derivations, so a session that drew its own
// would land on a different value for every lane.
sessionEpoch.Add(1)
cs, err := ls.session(s)
require.NoError(t, err)
assert.Equal(t, testLaneEpoch, cs.epoch, "lane %d did not inherit the tunnel's epoch", s)
}
}
func TestLaneTxGate(t *testing.T) {
initR, _ := runTestHandshake(t)
ls := newTestLaneSet(t, initR, 4, 4, 4242, 4)
@@ -976,7 +1001,7 @@ func TestLaneFlowSymmetry(t *testing.T) {
const ourBase, peerBase = 4242, 5353
ourLS := newTestLaneSet(t, initR, 4, 4, peerBase, 4)
respR.PeerPortCount, respR.PeerBasePort, respR.PeerTxLanes = 4, ourBase, 4
peerLS := newLaneSet(respR, 4, testPeerAddr, testMyAddr)
peerLS := newLaneSet(respR, 4, testLaneEpoch, testPeerAddr, testMyAddr)
require.NotNil(t, peerLS)
require.Equal(t, uint16(0), ourLS.laneBias, "the low address hashes straight")