JackDoan 3 недель назад
Родитель
Сommit
355b3dcbdb
4 измененных файлов с 41 добавлено и 9 удалено
  1. 6 3
      connection_state.go
  2. 1 1
      handshake_manager.go
  3. 5 1
      lanes.go
  4. 29 4
      lanes_test.go

+ 6 - 3
connection_state.go

@@ -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
handshake_manager.go

@@ -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
lanes.go

@@ -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
lanes_test.go

@@ -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")