multiport: make a lane a session on the tunnel, not a tunnel

Lanes used to be separate HostInfos, each with its own handshake, its own
half-established states, its own lifetime and its own slot bookkeeping. That
bought nothing: a lane is the same tunnel over a different underlay 5-tuple.

Derive lane sessions instead. Noise leaves us with A.eKey == B.dKey, so both
sides HKDF-expand the same two keys with the same per-lane label and land on a
matched pair without exchanging anything. Lanes now cost no handshake, have no
half-established state, and die exactly when their base tunnel does. Which lane
a packet belongs to rides the low byte of the nebula header's Reserved field,
inside the AEAD's associated data.

Receiving on a lane needs no permission, since the session exists the moment the
base handshake completes. Sending on one needs proof the new 5-tuple works, so a
lane stays down until a probe on it is acked and falls back to the base tunnel
the moment it stops being acked. Probing is demand-driven off the connection
manager's per-tunnel traffic tick: a peer we exchange a trickle with never costs
more than its base tunnel, however many lanes are configured. The ack rides the
base session on purpose, so a broken reverse lane can't fail a working one.

Because the data now rides lane counters, the rehandshake, exhaustion and
swap-primary checks take the max counter across the base session and its lanes;
otherwise the base counter would sit near zero while a lane ran its keys past
the nonce ceiling.

Removes OutboundLaneTimer, EnsureLanes, startLaneHandshake, completeLane,
completeLaneResponder, makeLaneTrafficDecision and the lane fields on HostInfo.
Handshake payload field 3 (the per-lane handshake index) is permanently
reserved; peers advertise a TxLanes count instead.
This commit is contained in:
Wade Simmons committed 2026-09-02 14:48:17 -04:00
1 parent 8b914e67f4
commit fb20de39b2
19 files changed
+1223 -1273

No files matched your search

+13 -10
View File
@@ -27,12 +27,14 @@ type Payload struct {
ResponderLanes *LaneDetails
}
// LaneDetails advertises multiport lane capability. LaneIndex is zero on base
// handshakes and the initiator's lane number (>= 1) on lane handshakes.
// LaneDetails advertises multiport lane capability: the contiguous UDP port
// range the sender bound, and how many lanes it may send on. The receiver needs
// PortCount/BasePort to aim its own lanes and TxLanes to know how many lane
// sessions to derive for receiving.
type LaneDetails struct {
PortCount uint32
BasePort uint32
LaneIndex uint32
TxLanes uint32
}
// Proto field numbers for NebulaHandshakeDetails
@@ -46,11 +48,12 @@ const (
fieldResponderLanes = 10 // LaneDetails
)
// Proto field numbers for LaneDetails
// Proto field numbers for LaneDetails.
// Field 3 was a per-lane handshake index and is permanently reserved.
const (
fieldLanePortCount = 1 // uint32
fieldLaneBasePort = 2 // uint32
fieldLaneLaneIndex = 3 // uint32
fieldLaneTxLanes = 4 // uint32
)
// MarshalPayload encodes a handshake payload in protobuf wire format compatible
@@ -105,8 +108,8 @@ func (d *LaneDetails) marshal(out []byte) []byte {
out = protowire.AppendVarint(out, uint64(d.PortCount))
out = protowire.AppendTag(out, fieldLaneBasePort, protowire.VarintType)
out = protowire.AppendVarint(out, uint64(d.BasePort))
out = protowire.AppendTag(out, fieldLaneLaneIndex, protowire.VarintType)
out = protowire.AppendVarint(out, uint64(d.LaneIndex))
out = protowire.AppendTag(out, fieldLaneTxLanes, protowire.VarintType)
out = protowire.AppendVarint(out, uint64(d.TxLanes))
return out
}
@@ -256,7 +259,7 @@ func unmarshalLaneDetails(d *LaneDetails, b []byte) error {
// wire-type mismatch, unknown fields are skipped, repeated singular
// fields follow proto3 last-wins.
switch num {
case fieldLanePortCount, fieldLaneBasePort, fieldLaneLaneIndex:
case fieldLanePortCount, fieldLaneBasePort, fieldLaneTxLanes:
if typ != protowire.VarintType {
return errInvalidHandshakeDetails
}
@@ -269,8 +272,8 @@ func unmarshalLaneDetails(d *LaneDetails, b []byte) error {
d.PortCount = uint32(v)
case fieldLaneBasePort:
d.BasePort = uint32(v)
case fieldLaneLaneIndex:
d.LaneIndex = uint32(v)
case fieldLaneTxLanes:
d.TxLanes = uint32(v)
}
b = b[n:]
default: