Files
nebula/header/header.go
T
Wade Simmons be77501f14 header: show the lane field in the version 1 header diagram
The lane byte was described in prose wedged between the first and second
rows of the table, which both broke up the diagram and left the wire layout
still claiming a single Reserved (uint16). Make it a field: widen the table
to fit Reserved (uint8) and Lane (uint8) with their types, and move the
explanation below the whole diagram.

The prose now covers what the picture can't: lane 0 is what a lanes-unaware
sender and every non-lane packet carry, so the field is compatible in both
directions; the remaining 8 bits stay reserved and zero; the H struct still
stores the pair as one Reserved field, so callers reach the low byte through
H.Lane and EncodeLane; and lane is part of the AEAD's associated data, so it
cannot be altered in flight.
2026-09-03 14:25:01 -04:00

251 lines
7.2 KiB
Go

package header
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
)
//Version 1 header:
// 0 31
// |------------------------------------------------------------------------------------|
// | Version (uint4) | Type (uint4) | Subtype (uint8) | Reserved (uint8) | Lane (uint8) | 32
// |------------------------------------------------------------------------------------|
// | Remote index (uint32) | 64
// |------------------------------------------------------------------------------------|
// | Message counter | 96
// | (uint64) | 128
// |------------------------------------------------------------------------------------|
// | payload... |
//
// Lane is the multiport lane index, carved out of the low 8 bits of what was a
// single Reserved (uint16) before multiport. Lane 0 is the base tunnel, which is
// what every sender that does not know about lanes emits and what every non-lane
// packet — handshakes, lighthouse, relays, close — carries, so the field is
// compatible in both directions with a peer that has never heard of it. The
// remaining 8 bits stay reserved and are always sent as zero. The H struct still
// holds the pair as one Reserved field; use H.Lane and EncodeLane to reach the
// low byte.
//
// Lane is part of the AEAD's associated data, so a lane index cannot be altered
// in flight: a packet decrypts on the lane it claims or not at all.
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
}