crazy multiport stuff

This commit is contained in:
JackDoan
2026-07-21 10:52:24 -05:00
parent 59ecea92ce
commit 0488793a62
19 changed files with 1801 additions and 76 deletions
+14 -2
View File
@@ -23,7 +23,19 @@ message NebulaHandshakeDetails {
// hand-written parser silently skips it on read.
uint64 Cookie = 4 [deprecated = true];
uint64 Time = 5;
// Multiport lane negotiation. Absent on hosts without multiport enabled;
// vanilla nebula treats 6 and 7 as unknown fields and skips them.
LaneDetails InitiatorLanes = 6;
LaneDetails ResponderLanes = 7;
uint32 CertVersion = 8;
// reserved for WIP multiport
reserved 6, 7;
}
// LaneDetails advertises a host's multiport lane capability. On a base
// handshake LaneIndex is 0 and PortCount/BasePort describe the sender's
// consecutively bound UDP ports. On a lane handshake the initiator sets
// LaneIndex to its (nonzero) lane number.
message LaneDetails {
uint32 PortCount = 1;
uint32 BasePort = 2;
uint32 LaneIndex = 3;
}
+1
View File
@@ -71,6 +71,7 @@ func newTestMachine(
cs.version, cs.getCredential,
verifier, func() (uint32, error) { return localIndex, nil },
initiator, header.HandshakeIXPSK0,
nil,
)
require.NoError(t, err)
return m
+33 -1
View File
@@ -39,6 +39,13 @@ type Result struct {
HandshakeTime uint64
MessageIndex uint64 // number of messages exchanged during the handshake
Initiator bool
// 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.
PeerPortCount uint32
PeerBasePort uint32
PeerLaneIndex uint32
}
// Machine drives a Noise handshake through N messages. It handles Noise
@@ -61,6 +68,7 @@ type Machine struct {
verifier CertVerifier
result *Result
msgs []msgFlags
lanes *LaneDetails // our multiport advert; nil emits a vanilla payload
myVersion cert.Version
subtype header.MessageSubType
indexAllocated bool
@@ -73,6 +81,8 @@ type Machine struct {
// the noise pattern and the per-message content layout. The credential for
// `version` is fetched via getCred and used to seed the noise.HandshakeState.
// IndexAllocator is called lazily when the first outgoing payload is built.
// lanes, when non-nil, is emitted as this side's multiport advert on every
// payload-bearing message; nil produces byte-identical vanilla payloads.
func NewMachine(
version cert.Version,
getCred GetCredentialFunc,
@@ -80,6 +90,7 @@ func NewMachine(
allocIndex IndexAllocator,
initiator bool,
subtype header.MessageSubType,
lanes *LaneDetails,
) (*Machine, error) {
info, err := subtypeInfoFor(subtype)
if err != nil {
@@ -103,6 +114,7 @@ func NewMachine(
getCred: getCred,
allocIndex: allocIndex,
verifier: verifier,
lanes: lanes,
myVersion: version,
result: &Result{
Initiator: initiator,
@@ -298,7 +310,8 @@ func (m *Machine) processPayload(msg []byte, flags msgFlags) error {
}
// Assert the payload contains exactly what we expect
hasPayloadData := payload.InitiatorIndex != 0 || payload.ResponderIndex != 0 || payload.Time != 0
hasPayloadData := payload.InitiatorIndex != 0 || payload.ResponderIndex != 0 || payload.Time != 0 ||
payload.InitiatorLanes != nil || payload.ResponderLanes != nil
if hasPayloadData != flags.expectsPayload {
m.failed = true
return ErrUnexpectedContent
@@ -327,6 +340,23 @@ func (m *Machine) processPayload(msg []byte, flags msgFlags) error {
m.result.RemoteIndex = remoteIndex
m.result.HandshakeTime = payload.Time
m.payloadSet = true
// 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.
var peerLanes *LaneDetails
if m.result.Initiator {
peerLanes = payload.ResponderLanes
} else {
peerLanes = payload.InitiatorLanes
}
if peerLanes != nil && peerLanes.BasePort <= 0xffff && peerLanes.PortCount <= 0xffff {
m.result.PeerPortCount = peerLanes.PortCount
m.result.PeerBasePort = peerLanes.BasePort
m.result.PeerLaneIndex = peerLanes.LaneIndex
}
}
// Process certificate
@@ -397,9 +427,11 @@ func (m *Machine) marshalOutgoing(flags msgFlags) ([]byte, error) {
if m.result.Initiator {
p.InitiatorIndex = m.result.LocalIndex
p.InitiatorLanes = m.lanes
} else {
p.ResponderIndex = m.result.LocalIndex
p.InitiatorIndex = m.result.RemoteIndex
p.ResponderLanes = m.lanes
}
p.Time = uint64(time.Now().UnixNano())
}
+113
View File
@@ -0,0 +1,113 @@
package handshake
import (
"net/netip"
"testing"
"time"
"github.com/slackhq/nebula/cert"
ct "github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/header"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newTestLaneMachine is newTestMachine with a lane advert attached.
func newTestLaneMachine(
t *testing.T,
cs *testCertState,
verifier CertVerifier,
initiator bool,
localIndex uint32,
lanes *LaneDetails,
) *Machine {
t.Helper()
m, err := NewMachine(
cs.version, cs.getCredential,
verifier, func() (uint32, error) { return localIndex, nil },
initiator, header.HandshakeIXPSK0,
lanes,
)
require.NoError(t, err)
return m
}
func doFullLaneHandshake(t *testing.T, initLanes, respLanes *LaneDetails) (initR, respR *Result) {
t.Helper()
ca, _, caKey, _ := ct.NewTestCaCert(
cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil,
)
caPool := ct.NewTestCAPool(ca)
v := testVerifier(caPool)
initCS := newTestCertState(t, ca, caKey, "initiator", []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")})
respCS := newTestCertState(t, ca, caKey, "responder", []netip.Prefix{netip.MustParsePrefix("10.0.0.2/24")})
initM := newTestLaneMachine(t, initCS, v, true, 1000, initLanes)
respM := newTestLaneMachine(t, respCS, v, false, 2000, respLanes)
msg1, err := initM.Initiate(nil)
require.NoError(t, err)
resp, respR, err := respM.ProcessPacket(nil, msg1)
require.NoError(t, err)
require.NotNil(t, respR)
_, initR, err = initM.ProcessPacket(nil, resp)
require.NoError(t, err)
require.NotNil(t, initR)
return initR, respR
}
func TestMachineLaneAdvertBothSides(t *testing.T) {
initR, respR := doFullLaneHandshake(t,
&LaneDetails{PortCount: 8, BasePort: 4242},
&LaneDetails{PortCount: 4, BasePort: 5353},
)
// 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(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)
}
func TestMachineLaneAdvertAsymmetric(t *testing.T) {
// Vanilla initiator, multiport responder and vice versa: the nil side
// yields all-zero peer fields on the other end.
initR, respR := doFullLaneHandshake(t, nil, &LaneDetails{PortCount: 4, BasePort: 5353})
assert.Equal(t, uint32(4), initR.PeerPortCount)
assert.Equal(t, uint32(0), respR.PeerPortCount)
assert.Equal(t, uint32(0), respR.PeerBasePort)
initR, respR = doFullLaneHandshake(t, &LaneDetails{PortCount: 8, BasePort: 4242}, nil)
assert.Equal(t, uint32(0), initR.PeerPortCount)
assert.Equal(t, uint32(8), respR.PeerPortCount)
}
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},
)
assert.Equal(t, uint32(0), respR.PeerPortCount)
assert.Equal(t, uint32(0), respR.PeerBasePort)
// The sane side still negotiates.
assert.Equal(t, uint32(4), initR.PeerPortCount)
}
+2
View File
@@ -444,6 +444,7 @@ func TestMachineThreeMessagePattern(t *testing.T) {
initCS.getCredential, v,
func() (uint32, error) { return 1000, nil },
true, header.HandshakeXXPSK0,
nil,
)
require.NoError(t, err)
@@ -452,6 +453,7 @@ func TestMachineThreeMessagePattern(t *testing.T) {
respCS.getCredential, v,
func() (uint32, error) { return 2000, nil },
false, header.HandshakeXXPSK0,
nil,
)
require.NoError(t, err)
+110
View File
@@ -20,6 +20,19 @@ type Payload struct {
ResponderIndex uint32
Time uint64
CertVersion uint32
// Multiport lane negotiation; nil when the sender has multiport disabled
// (which keeps the encoded payload byte-identical to a vanilla one).
InitiatorLanes *LaneDetails
ResponderLanes *LaneDetails
}
// LaneDetails advertises multiport lane capability. LaneIndex is zero on base
// handshakes and the initiator's lane number (>= 1) on lane handshakes.
type LaneDetails struct {
PortCount uint32
BasePort uint32
LaneIndex uint32
}
// Proto field numbers for NebulaHandshakeDetails
@@ -28,9 +41,18 @@ const (
fieldInitiatorIndex = 2 // uint32
fieldResponderIndex = 3 // uint32
fieldTime = 5 // uint64
fieldInitiatorLanes = 6 // LaneDetails
fieldResponderLanes = 7 // LaneDetails
fieldCertVersion = 8 // uint32
)
// Proto field numbers for LaneDetails
const (
fieldLanePortCount = 1 // uint32
fieldLaneBasePort = 2 // uint32
fieldLaneLaneIndex = 3 // uint32
)
// MarshalPayload encodes a handshake payload in protobuf wire format compatible
// with NebulaHandshake{Details: NebulaHandshakeDetails{...}}.
// Returns out (which may be nil), with the marshalled Payload appended to it.
@@ -53,6 +75,14 @@ func MarshalPayload(out []byte, p Payload) []byte {
details = protowire.AppendTag(details, fieldTime, protowire.VarintType)
details = protowire.AppendVarint(details, p.Time)
}
if p.InitiatorLanes != nil {
details = protowire.AppendTag(details, fieldInitiatorLanes, protowire.BytesType)
details = protowire.AppendBytes(details, p.InitiatorLanes.marshal(nil))
}
if p.ResponderLanes != nil {
details = protowire.AppendTag(details, fieldResponderLanes, protowire.BytesType)
details = protowire.AppendBytes(details, p.ResponderLanes.marshal(nil))
}
if p.CertVersion != 0 {
details = protowire.AppendTag(details, fieldCertVersion, protowire.VarintType)
details = protowire.AppendVarint(details, uint64(p.CertVersion))
@@ -64,6 +94,20 @@ func MarshalPayload(out []byte, p Payload) []byte {
return out
}
// marshal appends the LaneDetails submessage fields to out. All fields are
// emitted unconditionally: a LaneDetails is only present at all when multiport
// is negotiating, and explicit zeros keep the parser's presence semantics
// trivial.
func (d *LaneDetails) marshal(out []byte) []byte {
out = protowire.AppendTag(out, fieldLanePortCount, protowire.VarintType)
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))
return out
}
// UnmarshalPayload decodes a protobuf-encoded NebulaHandshake message.
func UnmarshalPayload(b []byte) (Payload, error) {
var p Payload
@@ -161,6 +205,72 @@ func unmarshalPayloadDetails(p *Payload, b []byte) error {
}
p.CertVersion = uint32(v)
b = b[n:]
case fieldInitiatorLanes:
if typ != protowire.BytesType {
return errInvalidHandshakeDetails
}
v, n := protowire.ConsumeBytes(b)
if n < 0 {
return errInvalidHandshakeDetails
}
p.InitiatorLanes = new(LaneDetails)
if err := unmarshalLaneDetails(p.InitiatorLanes, v); err != nil {
return err
}
b = b[n:]
case fieldResponderLanes:
if typ != protowire.BytesType {
return errInvalidHandshakeDetails
}
v, n := protowire.ConsumeBytes(b)
if n < 0 {
return errInvalidHandshakeDetails
}
p.ResponderLanes = new(LaneDetails)
if err := unmarshalLaneDetails(p.ResponderLanes, v); err != nil {
return err
}
b = b[n:]
default:
n := protowire.ConsumeFieldValue(num, typ, b)
if n < 0 {
return errInvalidHandshakeDetails
}
b = b[n:]
}
}
return nil
}
func unmarshalLaneDetails(d *LaneDetails, b []byte) error {
for len(b) > 0 {
num, typ, n := protowire.ConsumeTag(b)
if n < 0 {
return errInvalidHandshakeDetails
}
b = b[n:]
// Same contract as the details parser: known fields hard-fail on a
// wire-type mismatch, unknown fields are skipped, repeated singular
// fields follow proto3 last-wins.
switch num {
case fieldLanePortCount, fieldLaneBasePort, fieldLaneLaneIndex:
if typ != protowire.VarintType {
return errInvalidHandshakeDetails
}
v, n := protowire.ConsumeVarint(b)
if n < 0 || v > math.MaxUint32 {
return errInvalidHandshakeDetails
}
switch num {
case fieldLanePortCount:
d.PortCount = uint32(v)
case fieldLaneBasePort:
d.BasePort = uint32(v)
case fieldLaneLaneIndex:
d.LaneIndex = uint32(v)
}
b = b[n:]
default:
n := protowire.ConsumeFieldValue(num, typ, b)
if n < 0 {
+137 -11
View File
@@ -117,23 +117,134 @@ func TestPayloadUnknownFields(t *testing.T) {
assert.Equal(t, uint32(88), got.ResponderIndex)
})
t.Run("reserved fields 6 and 7 are skipped", func(t *testing.T) {
// Fields 6 and 7 are reserved in the proto definition
t.Run("unknown field inside LaneDetails is skipped", func(t *testing.T) {
var lane []byte
lane = protowire.AppendTag(lane, fieldLanePortCount, protowire.VarintType)
lane = protowire.AppendVarint(lane, 4)
lane = protowire.AppendTag(lane, 50, protowire.VarintType) // unknown subfield
lane = protowire.AppendVarint(lane, 9999)
lane = protowire.AppendTag(lane, fieldLaneBasePort, protowire.VarintType)
lane = protowire.AppendVarint(lane, 4242)
var details []byte
details = protowire.AppendTag(details, fieldInitiatorIndex, protowire.VarintType)
details = protowire.AppendVarint(details, 100)
details = protowire.AppendTag(details, 6, protowire.VarintType)
details = protowire.AppendVarint(details, 1)
details = protowire.AppendTag(details, 7, protowire.VarintType)
details = protowire.AppendVarint(details, 2)
details = protowire.AppendTag(details, fieldInitiatorLanes, protowire.BytesType)
details = protowire.AppendBytes(details, lane)
var data []byte
data = protowire.AppendTag(data, 1, protowire.BytesType)
data = protowire.AppendBytes(data, details)
got, err := UnmarshalPayload(wrapDetails(details))
require.NoError(t, err)
assert.Equal(t, uint32(100), got.InitiatorIndex)
require.NotNil(t, got.InitiatorLanes)
assert.Equal(t, uint32(4), got.InitiatorLanes.PortCount)
assert.Equal(t, uint32(4242), got.InitiatorLanes.BasePort)
})
}
func TestPayloadLaneDetails(t *testing.T) {
t.Run("round trip both sides", func(t *testing.T) {
data := MarshalPayload(nil, Payload{
InitiatorIndex: 12345,
Time: 999,
InitiatorLanes: &LaneDetails{PortCount: 8, BasePort: 4242, LaneIndex: 3},
ResponderLanes: &LaneDetails{PortCount: 4, BasePort: 5353},
})
got, err := UnmarshalPayload(data)
require.NoError(t, err)
assert.Equal(t, uint32(100), got.InitiatorIndex)
require.NotNil(t, got.InitiatorLanes)
assert.Equal(t, LaneDetails{PortCount: 8, BasePort: 4242, LaneIndex: 3}, *got.InitiatorLanes)
require.NotNil(t, got.ResponderLanes)
assert.Equal(t, LaneDetails{PortCount: 4, BasePort: 5353}, *got.ResponderLanes)
})
t.Run("zero-valued LaneDetails survives the round trip", func(t *testing.T) {
// Presence is what negotiation keys on; an all-zero advert must not
// decay to nil.
data := MarshalPayload(nil, Payload{
InitiatorIndex: 1,
InitiatorLanes: &LaneDetails{},
})
got, err := UnmarshalPayload(data)
require.NoError(t, err)
require.NotNil(t, got.InitiatorLanes)
assert.Equal(t, LaneDetails{}, *got.InitiatorLanes)
assert.Nil(t, got.ResponderLanes)
})
t.Run("nil lanes marshal byte-identical to a vanilla payload", func(t *testing.T) {
p := Payload{
Cert: []byte("cert"),
CertVersion: 2,
InitiatorIndex: 100,
Time: 999,
}
// The vanilla encoding of the same fields, built by hand in field order.
var details []byte
details = protowire.AppendTag(details, fieldCert, protowire.BytesType)
details = protowire.AppendBytes(details, p.Cert)
details = protowire.AppendTag(details, fieldInitiatorIndex, protowire.VarintType)
details = protowire.AppendVarint(details, uint64(p.InitiatorIndex))
details = protowire.AppendTag(details, fieldTime, protowire.VarintType)
details = protowire.AppendVarint(details, p.Time)
details = protowire.AppendTag(details, fieldCertVersion, protowire.VarintType)
details = protowire.AppendVarint(details, uint64(p.CertVersion))
assert.Equal(t, wrapDetails(details), MarshalPayload(nil, p))
})
t.Run("lane field with wrong wire type rejected", func(t *testing.T) {
for _, field := range []protowire.Number{fieldInitiatorLanes, fieldResponderLanes} {
var details []byte
details = protowire.AppendTag(details, field, protowire.VarintType)
details = protowire.AppendVarint(details, 1)
_, err := UnmarshalPayload(wrapDetails(details))
assert.Error(t, err)
}
})
t.Run("lane subfield with wrong wire type rejected", func(t *testing.T) {
var lane []byte
lane = protowire.AppendTag(lane, fieldLanePortCount, protowire.BytesType)
lane = protowire.AppendBytes(lane, []byte{1, 2, 3})
var details []byte
details = protowire.AppendTag(details, fieldInitiatorLanes, protowire.BytesType)
details = protowire.AppendBytes(details, lane)
_, err := UnmarshalPayload(wrapDetails(details))
assert.Error(t, err)
})
t.Run("truncated LaneDetails submessage rejected", func(t *testing.T) {
var details []byte
details = protowire.AppendTag(details, fieldInitiatorLanes, protowire.BytesType)
details = append(details, 0x0a, 0x01, 0x02) // length 10, only 2 bytes
_, err := UnmarshalPayload(wrapDetails(details))
assert.Error(t, err)
})
t.Run("truncated varint inside LaneDetails rejected", func(t *testing.T) {
var lane []byte
lane = protowire.AppendTag(lane, fieldLaneBasePort, protowire.VarintType)
lane = append(lane, 0x80) // incomplete varint
var details []byte
details = protowire.AppendTag(details, fieldResponderLanes, protowire.BytesType)
details = protowire.AppendBytes(details, lane)
_, err := UnmarshalPayload(wrapDetails(details))
assert.Error(t, err)
})
t.Run("lane subfield varint overflow rejected", func(t *testing.T) {
var lane []byte
lane = protowire.AppendTag(lane, fieldLaneLaneIndex, protowire.VarintType)
lane = protowire.AppendVarint(lane, math.MaxUint32+1)
var details []byte
details = protowire.AppendTag(details, fieldInitiatorLanes, protowire.BytesType)
details = protowire.AppendBytes(details, lane)
_, err := UnmarshalPayload(wrapDetails(details))
assert.Error(t, err)
})
}
@@ -328,6 +439,12 @@ func FuzzPayload(f *testing.F) {
Time: 3,
CertVersion: 2,
}))
f.Add(MarshalPayload(nil, Payload{
InitiatorIndex: 1,
Time: 3,
InitiatorLanes: &LaneDetails{PortCount: 8, BasePort: 4242, LaneIndex: 2},
ResponderLanes: &LaneDetails{PortCount: 4, BasePort: 5353},
}))
f.Add([]byte{})
f.Add([]byte{0xff})
@@ -357,5 +474,14 @@ func payloadsEqual(a, b Payload) bool {
a.InitiatorIndex == b.InitiatorIndex &&
a.ResponderIndex == b.ResponderIndex &&
a.Time == b.Time &&
a.CertVersion == b.CertVersion
a.CertVersion == b.CertVersion &&
laneDetailsEqual(a.InitiatorLanes, b.InitiatorLanes) &&
laneDetailsEqual(a.ResponderLanes, b.ResponderLanes)
}
func laneDetailsEqual(a, b *LaneDetails) bool {
if a == nil || b == nil {
return a == b
}
return *a == *b
}