mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 15:06:52 +01:00
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:
1 parent
8b914e67f4
commit
fb20de39b2
19 files changed
+1223
-1273
No files matched your search
@@ -42,10 +42,11 @@ type Result struct {
|
||||
|
||||
// Multiport lane negotiation, from the peer's LaneDetails. All zero when
|
||||
// the peer did not advertise (vanilla peer or multiport disabled).
|
||||
// PeerLaneIndex is nonzero only on the responder side of a lane handshake.
|
||||
// PeerTxLanes is how many lanes the peer may send on, which is how many lane
|
||||
// sessions we need in order to receive everything it sends.
|
||||
PeerPortCount uint32
|
||||
PeerBasePort uint32
|
||||
PeerLaneIndex uint32
|
||||
PeerTxLanes uint32
|
||||
}
|
||||
|
||||
// Machine drives a Noise handshake through N messages. It handles Noise
|
||||
@@ -344,18 +345,18 @@ func (m *Machine) processPayload(msg []byte, flags msgFlags) error {
|
||||
// Multiport advert from the peer's side of the exchange. Out-of-range
|
||||
// values mean a peer we can't pair lanes with; ignore the advert
|
||||
// rather than failing the handshake — the tunnel itself is fine, it
|
||||
// just won't get lanes. Semantic policing (index bounds vs advert,
|
||||
// port-count caps) belongs to the handshake manager.
|
||||
// just won't get lanes. Semantic policing (port-count caps, lane
|
||||
// clamping) belongs to the handshake manager.
|
||||
var peerLanes *LaneDetails
|
||||
if m.result.Initiator {
|
||||
peerLanes = payload.ResponderLanes
|
||||
} else {
|
||||
peerLanes = payload.InitiatorLanes
|
||||
}
|
||||
if peerLanes != nil && peerLanes.BasePort <= 0xffff && peerLanes.PortCount <= 0xffff {
|
||||
if peerLanes != nil && peerLanes.BasePort <= 0xffff && peerLanes.PortCount <= 0xffff && peerLanes.TxLanes <= 0xffff {
|
||||
m.result.PeerPortCount = peerLanes.PortCount
|
||||
m.result.PeerBasePort = peerLanes.BasePort
|
||||
m.result.PeerLaneIndex = peerLanes.LaneIndex
|
||||
m.result.PeerTxLanes = peerLanes.TxLanes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,30 +61,18 @@ func doFullLaneHandshake(t *testing.T, initLanes, respLanes *LaneDetails) (initR
|
||||
|
||||
func TestMachineLaneAdvertBothSides(t *testing.T) {
|
||||
initR, respR := doFullLaneHandshake(t,
|
||||
&LaneDetails{PortCount: 8, BasePort: 4242},
|
||||
&LaneDetails{PortCount: 4, BasePort: 5353},
|
||||
&LaneDetails{PortCount: 8, BasePort: 4242, TxLanes: 8},
|
||||
&LaneDetails{PortCount: 4, BasePort: 5353, TxLanes: 3},
|
||||
)
|
||||
|
||||
// Each side's Result carries the peer's advert.
|
||||
assert.Equal(t, uint32(4), initR.PeerPortCount)
|
||||
assert.Equal(t, uint32(5353), initR.PeerBasePort)
|
||||
assert.Equal(t, uint32(0), initR.PeerLaneIndex)
|
||||
assert.Equal(t, uint32(3), initR.PeerTxLanes)
|
||||
|
||||
assert.Equal(t, uint32(8), respR.PeerPortCount)
|
||||
assert.Equal(t, uint32(4242), respR.PeerBasePort)
|
||||
assert.Equal(t, uint32(0), respR.PeerLaneIndex)
|
||||
}
|
||||
|
||||
func TestMachineLaneHandshakeCarriesLaneIndex(t *testing.T) {
|
||||
// A lane handshake: initiator tags its lane number; responder still
|
||||
// adverts (harmlessly).
|
||||
initR, respR := doFullLaneHandshake(t,
|
||||
&LaneDetails{PortCount: 8, BasePort: 4242, LaneIndex: 3},
|
||||
&LaneDetails{PortCount: 4, BasePort: 5353},
|
||||
)
|
||||
|
||||
assert.Equal(t, uint32(3), respR.PeerLaneIndex)
|
||||
assert.Equal(t, uint32(0), initR.PeerLaneIndex)
|
||||
assert.Equal(t, uint32(8), respR.PeerTxLanes)
|
||||
}
|
||||
|
||||
func TestMachineLaneAdvertAsymmetric(t *testing.T) {
|
||||
@@ -103,11 +91,21 @@ func TestMachineLaneAdvertAsymmetric(t *testing.T) {
|
||||
func TestMachineLaneAdvertOutOfRangeIgnored(t *testing.T) {
|
||||
// A BasePort that can't be a real UDP port is ignored, not fatal.
|
||||
initR, respR := doFullLaneHandshake(t,
|
||||
&LaneDetails{PortCount: 8, BasePort: 70000},
|
||||
&LaneDetails{PortCount: 4, BasePort: 5353},
|
||||
&LaneDetails{PortCount: 8, BasePort: 70000, TxLanes: 8},
|
||||
&LaneDetails{PortCount: 4, BasePort: 5353, TxLanes: 4},
|
||||
)
|
||||
assert.Equal(t, uint32(0), respR.PeerPortCount)
|
||||
assert.Equal(t, uint32(0), respR.PeerBasePort)
|
||||
assert.Equal(t, uint32(0), respR.PeerTxLanes)
|
||||
// The sane side still negotiates.
|
||||
assert.Equal(t, uint32(4), initR.PeerPortCount)
|
||||
|
||||
// An out-of-range TxLanes drops the whole advert the same way: a lane index
|
||||
// that does not fit the header is as unusable as an impossible port.
|
||||
initR, respR = doFullLaneHandshake(t,
|
||||
&LaneDetails{PortCount: 8, BasePort: 4242, TxLanes: 0x10000},
|
||||
&LaneDetails{PortCount: 4, BasePort: 5353, TxLanes: 4},
|
||||
)
|
||||
assert.Equal(t, uint32(0), respR.PeerPortCount)
|
||||
assert.Equal(t, uint32(4), initR.PeerPortCount)
|
||||
}
|
||||
+13
-10
@@ -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:
|
||||
|
||||
@@ -146,14 +146,14 @@ func TestPayloadLaneDetails(t *testing.T) {
|
||||
data := MarshalPayload(nil, Payload{
|
||||
InitiatorIndex: 12345,
|
||||
Time: 999,
|
||||
InitiatorLanes: &LaneDetails{PortCount: 8, BasePort: 4242, LaneIndex: 3},
|
||||
InitiatorLanes: &LaneDetails{PortCount: 8, BasePort: 4242, TxLanes: 3},
|
||||
ResponderLanes: &LaneDetails{PortCount: 4, BasePort: 5353},
|
||||
})
|
||||
|
||||
got, err := UnmarshalPayload(data)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got.InitiatorLanes)
|
||||
assert.Equal(t, LaneDetails{PortCount: 8, BasePort: 4242, LaneIndex: 3}, *got.InitiatorLanes)
|
||||
assert.Equal(t, LaneDetails{PortCount: 8, BasePort: 4242, TxLanes: 3}, *got.InitiatorLanes)
|
||||
require.NotNil(t, got.ResponderLanes)
|
||||
assert.Equal(t, LaneDetails{PortCount: 4, BasePort: 5353}, *got.ResponderLanes)
|
||||
})
|
||||
@@ -237,7 +237,7 @@ func TestPayloadLaneDetails(t *testing.T) {
|
||||
|
||||
t.Run("lane subfield varint overflow rejected", func(t *testing.T) {
|
||||
var lane []byte
|
||||
lane = protowire.AppendTag(lane, fieldLaneLaneIndex, protowire.VarintType)
|
||||
lane = protowire.AppendTag(lane, fieldLaneTxLanes, protowire.VarintType)
|
||||
lane = protowire.AppendVarint(lane, math.MaxUint32+1)
|
||||
|
||||
var details []byte
|
||||
@@ -442,7 +442,7 @@ func FuzzPayload(f *testing.F) {
|
||||
f.Add(MarshalPayload(nil, Payload{
|
||||
InitiatorIndex: 1,
|
||||
Time: 3,
|
||||
InitiatorLanes: &LaneDetails{PortCount: 8, BasePort: 4242, LaneIndex: 2},
|
||||
InitiatorLanes: &LaneDetails{PortCount: 8, BasePort: 4242, TxLanes: 2},
|
||||
ResponderLanes: &LaneDetails{PortCount: 4, BasePort: 5353},
|
||||
}))
|
||||
f.Add([]byte{})
|
||||
|
||||
Reference in new issue
Block a user