Files
nebula/header/header.go
T
Wade Simmons fb20de39b2 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.
2026-09-02 14:48:17 -04:00

242 lines
6.5 KiB
Go

package header
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
)
//Version 1 header:
// 0 31
// |-----------------------------------------------------------------------|
// | Version (uint4) | Type (uint4) | Subtype (uint8) | Reserved (uint16) | 32
// |-----------------------------------------------------------------------|
// The low 8 bits of Reserved carry the multiport lane index (0 is the base
// tunnel, which is what every non-multiport sender emits). The high 8 bits
// remain reserved and are always sent as zero.
// | Remote index (uint32) | 64
// |-----------------------------------------------------------------------|
// | Message counter | 96
// | (uint64) | 128
// |-----------------------------------------------------------------------|
// | payload... |
type m = map[string]any
const (
Version uint8 = 1
Len = 16
)
type MessageType uint8
type MessageSubType uint8
const (
Handshake MessageType = 0
Message MessageType = 1
RecvError MessageType = 2
LightHouse MessageType = 3
Test MessageType = 4
CloseTunnel MessageType = 5
Control MessageType = 6
)
var typeMap = map[MessageType]string{
Handshake: "handshake",
Message: "message",
RecvError: "recvError",
LightHouse: "lightHouse",
Test: "test",
CloseTunnel: "closeTunnel",
Control: "control",
}
const (
MessageNone MessageSubType = 0
MessageRelay MessageSubType = 1
)
const (
TestRequest MessageSubType = 0
TestReply MessageSubType = 1
// LaneProbe is sent on a multiport lane to prove the lane's 5-tuple is
// usable; LaneProbeAck answers it on the base tunnel.
LaneProbe MessageSubType = 2
LaneProbeAck MessageSubType = 3
)
// MaxLane is the largest lane index the header can carry.
const MaxLane = 0xff
// laneMask covers the bits of Reserved that hold the lane index.
const laneMask uint16 = 0x00ff
const (
HandshakeIXPSK0 MessageSubType = 0
HandshakeXXPSK0 MessageSubType = 1
)
var ErrHeaderTooShort = errors.New("header is too short")
var subTypeTestMap = map[MessageSubType]string{
TestRequest: "testRequest",
TestReply: "testReply",
LaneProbe: "laneProbe",
LaneProbeAck: "laneProbeAck",
}
var subTypeNoneMap = map[MessageSubType]string{0: "none"}
var subTypeMap = map[MessageType]*map[MessageSubType]string{
Message: {
MessageNone: "none",
MessageRelay: "relay",
},
RecvError: &subTypeNoneMap,
LightHouse: &subTypeNoneMap,
Test: &subTypeTestMap,
CloseTunnel: &subTypeNoneMap,
Handshake: {
HandshakeIXPSK0: "ix_psk0",
},
Control: &subTypeNoneMap,
}
type H struct {
Version uint8
Type MessageType
Subtype MessageSubType
Reserved uint16
RemoteIndex uint32
MessageCounter uint64
}
// Encode uses the provided byte array to encode the provided header values into.
// Byte array must be capped higher than HeaderLen or this will panic
func Encode(b []byte, v uint8, t MessageType, st MessageSubType, ri uint32, c uint64) []byte {
return EncodeLane(b, v, t, st, ri, c, 0)
}
// EncodeLane is Encode with an explicit multiport lane index, which is carried
// in the low 8 bits of Reserved.
func EncodeLane(b []byte, v uint8, t MessageType, st MessageSubType, ri uint32, c uint64, lane uint8) []byte {
b = b[:Len]
b[0] = v<<4 | byte(t&0x0f)
b[1] = byte(st)
binary.BigEndian.PutUint16(b[2:4], uint16(lane))
binary.BigEndian.PutUint32(b[4:8], ri)
binary.BigEndian.PutUint64(b[8:16], c)
return b
}
// String creates a readable string representation of a header
func (h *H) String() string {
if h == nil {
return "<nil>"
}
return fmt.Sprintf("ver=%d type=%s subtype=%s reserved=%#x remoteindex=%v messagecounter=%v",
h.Version, h.TypeName(), h.SubTypeName(), h.Reserved, h.RemoteIndex, h.MessageCounter)
}
// MarshalJSON creates a json string representation of a header
func (h *H) MarshalJSON() ([]byte, error) {
return json.Marshal(m{
"version": h.Version,
"type": h.TypeName(),
"subType": h.SubTypeName(),
"reserved": h.Reserved,
"remoteIndex": h.RemoteIndex,
"messageCounter": h.MessageCounter,
})
}
// Encode turns header into bytes
func (h *H) Encode(b []byte) ([]byte, error) {
if h == nil {
return nil, errors.New("nil header")
}
return EncodeLane(b, h.Version, h.Type, h.Subtype, h.RemoteIndex, h.MessageCounter, h.Lane()), nil
}
// Lane returns the multiport lane index carried in Reserved. Lane 0 is the base
// tunnel, which is what any sender that does not know about lanes will report.
func (h *H) Lane() uint8 {
return uint8(h.Reserved & laneMask)
}
// Parse is a helper function to parses given bytes into new Header struct
func (h *H) Parse(b []byte) error {
if len(b) < Len {
return ErrHeaderTooShort
}
// get upper 4 bytes
h.Version = uint8((b[0] >> 4) & 0x0f)
// get lower 4 bytes
h.Type = MessageType(b[0] & 0x0f)
h.Subtype = MessageSubType(b[1])
h.Reserved = binary.BigEndian.Uint16(b[2:4])
h.RemoteIndex = binary.BigEndian.Uint32(b[4:8])
h.MessageCounter = binary.BigEndian.Uint64(b[8:16])
return nil
}
// TypeName will transform the headers message type into a human string
func (h *H) TypeName() string {
return TypeName(h.Type)
}
// TypeName will transform a nebula message type into a human string
func TypeName(t MessageType) string {
if n, ok := typeMap[t]; ok {
return n
}
return "unknown"
}
// SubTypeName will transform the headers message sub type into a human string
func (h *H) SubTypeName() string {
return SubTypeName(h.Type, h.Subtype)
}
func (h *H) IsValidSubType() bool {
return IsValidSubType(h.Type, h.Subtype)
}
// SubTypeName will transform a nebula message sub type into a human string
func SubTypeName(t MessageType, s MessageSubType) string {
if n, ok := subTypeMap[t]; ok {
if x, ok := (*n)[s]; ok {
return x
}
}
return "unknown"
}
func IsValidSubType(t MessageType, s MessageSubType) bool {
switch t {
case Message:
return s == MessageNone || s == MessageRelay
case Handshake:
return s == HandshakeIXPSK0
case Test:
return s == TestReply || s == TestRequest || s == LaneProbe || s == LaneProbeAck
case Control, CloseTunnel, RecvError, LightHouse:
return s == 0
default:
return false
}
}
// NewHeader turns bytes into a header
func NewHeader(b []byte) (*H, error) {
h := new(H)
if err := h.Parse(b); err != nil {
return nil, err
}
return h, nil
}