mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 15:46: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
+34
-423
@@ -52,7 +52,7 @@ type HandshakeConfig struct {
|
||||
|
||||
// Multiport lane parameters; laneCount == 0 means multiport is disabled.
|
||||
// laneCount includes implicit lane 0 (the base tunnel), so lanes
|
||||
// 1..laneCount-1 are initiated. lanePortCount/laneBasePort describe our
|
||||
// 1..laneCount-1 may carry traffic. lanePortCount/laneBasePort describe our
|
||||
// own bound port range and are advertised in every handshake payload.
|
||||
laneCount int
|
||||
lanePortCount uint16
|
||||
@@ -73,15 +73,11 @@ type HandshakeManager struct {
|
||||
outside udp.Conn
|
||||
config HandshakeConfig
|
||||
OutboundHandshakeTimer *LockingTimerWheel[netip.Addr]
|
||||
// OutboundLaneTimer drives lane handshake retries. Lanes never enter
|
||||
// vpnIps (they would collide with base handshakes for the same address),
|
||||
// so their wheel is keyed by pending localIndexId instead.
|
||||
OutboundLaneTimer *LockingTimerWheel[uint32]
|
||||
messageMetrics *MessageMetrics
|
||||
metricInitiated metrics.Counter
|
||||
metricTimedOut metrics.Counter
|
||||
f *Interface
|
||||
l *slog.Logger
|
||||
messageMetrics *MessageMetrics
|
||||
metricInitiated metrics.Counter
|
||||
metricTimedOut metrics.Counter
|
||||
f *Interface
|
||||
l *slog.Logger
|
||||
|
||||
// can be used to trigger outbound handshake for the given vpnIp
|
||||
trigger chan netip.Addr
|
||||
@@ -100,11 +96,6 @@ type HandshakeHostInfo struct {
|
||||
|
||||
hostinfo *HostInfo
|
||||
machine *handshake.Machine // The handshake state machine, set during stage 0 (initiator) or beginHandshake (responder multi-message)
|
||||
|
||||
// laneTarget is the single pinned destination for a lane handshake
|
||||
// (base-tunnel remote IP at the peer's lane port). Lane handshakes never
|
||||
// broadcast to the RemoteList.
|
||||
laneTarget netip.AddrPort
|
||||
}
|
||||
|
||||
func (hh *HandshakeHostInfo) cachePacket(l *slog.Logger, t header.MessageType, st header.MessageSubType, packet []byte, f packetCallback, m *cachedPacketMetrics) {
|
||||
@@ -142,7 +133,6 @@ func NewHandshakeManager(l *slog.Logger, mainHostMap *HostMap, lightHouse *Light
|
||||
config: config,
|
||||
trigger: make(chan netip.Addr, config.triggerBuffer),
|
||||
OutboundHandshakeTimer: NewLockingTimerWheel[netip.Addr](config.tryInterval, hsTimeout(config.retries, config.tryInterval)),
|
||||
OutboundLaneTimer: NewLockingTimerWheel[uint32](config.tryInterval, hsTimeout(config.retries, config.tryInterval)),
|
||||
messageMetrics: config.messageMetrics,
|
||||
metricInitiated: metrics.GetOrRegisterCounter("handshake_manager.initiated", nil),
|
||||
metricTimedOut: metrics.GetOrRegisterCounter("handshake_manager.timed_out", nil),
|
||||
@@ -162,7 +152,6 @@ func (hm *HandshakeManager) Run(ctx context.Context) {
|
||||
hm.handleOutbound(vpnIP, true)
|
||||
case now := <-clockSource.C:
|
||||
hm.NextOutboundHandshakeTimerTick(now)
|
||||
hm.NextOutboundLaneTimerTick(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,10 +543,9 @@ func (hm *HandshakeManager) DeleteHostInfo(hostinfo *HostInfo) {
|
||||
|
||||
func (hm *HandshakeManager) unlockedDeleteHostInfo(hostinfo *HostInfo) {
|
||||
for _, addr := range hostinfo.vpnAddrs {
|
||||
// Only delete the pending entry if it is actually ours. Lane
|
||||
// handshakes never live in vpnIps, and an unconditional delete here
|
||||
// could evict a concurrently pending base handshake for the same
|
||||
// address.
|
||||
// Only delete the pending entry if it is actually ours: an
|
||||
// unconditional delete could evict a concurrently pending handshake for
|
||||
// the same address.
|
||||
if cur, ok := hm.vpnIps[addr]; ok && cur.hostinfo == hostinfo {
|
||||
delete(hm.vpnIps, addr)
|
||||
}
|
||||
@@ -695,7 +683,7 @@ func (hm *HandshakeManager) buildStage0Packet(hh *HandshakeHostInfo) bool {
|
||||
v, cs.GetCredential,
|
||||
hm.certVerifier(), func() (uint32, error) { return hm.allocateIndex(hh) },
|
||||
true, header.HandshakeIXPSK0,
|
||||
hm.laneAdvert(uint32(hh.hostinfo.laneIndex)),
|
||||
hm.laneAdvert(),
|
||||
)
|
||||
if err != nil {
|
||||
hm.f.l.Error("Failed to create handshake machine",
|
||||
@@ -721,227 +709,38 @@ func (hm *HandshakeManager) buildStage0Packet(hh *HandshakeHostInfo) bool {
|
||||
|
||||
// laneAdvert returns our multiport advert for a handshake payload, or nil
|
||||
// when multiport is disabled (which keeps the payload byte-identical to
|
||||
// vanilla). laneIndex is 0 for base handshakes and our lane number for lane
|
||||
// handshakes.
|
||||
func (hm *HandshakeManager) laneAdvert(laneIndex uint32) *handshake.LaneDetails {
|
||||
// vanilla).
|
||||
func (hm *HandshakeManager) laneAdvert() *handshake.LaneDetails {
|
||||
if hm.config.laneCount == 0 {
|
||||
return nil
|
||||
}
|
||||
return &handshake.LaneDetails{
|
||||
PortCount: uint32(hm.config.lanePortCount),
|
||||
BasePort: uint32(hm.config.laneBasePort),
|
||||
LaneIndex: laneIndex,
|
||||
TxLanes: uint32(hm.config.laneCount),
|
||||
}
|
||||
}
|
||||
|
||||
// maybeAllocLaneState attaches a laneState to a just-completed base tunnel
|
||||
// when both sides advertised multiport. Must run before the hostinfo becomes
|
||||
// visible in the hostmap: the data plane reads base.lanes lock-free.
|
||||
func (hm *HandshakeManager) maybeAllocLaneState(hostinfo *HostInfo, result *handshake.Result) {
|
||||
if hm.config.laneCount == 0 || result.PeerPortCount == 0 || result.PeerLaneIndex != 0 {
|
||||
return
|
||||
}
|
||||
peerPorts := result.PeerPortCount
|
||||
if peerPorts > 256 {
|
||||
// A certified-but-hostile peer doesn't get to size our state.
|
||||
peerPorts = 256
|
||||
}
|
||||
var offset uint16
|
||||
if len(hm.f.myVpnAddrs) > 0 && len(hostinfo.vpnAddrs) > 0 {
|
||||
offset = lanePortOffset(hm.f.myVpnAddrs[0], hostinfo.vpnAddrs[0], uint16(peerPorts))
|
||||
}
|
||||
hostinfo.lanes = newLaneState(hm.config.laneCount, uint16(peerPorts), uint16(result.PeerBasePort), offset)
|
||||
}
|
||||
|
||||
// EnsureLanes starts lane handshakes for every empty, non-pending, retry-due
|
||||
// slot of a base tunnel that the data plane has asked for. Called on base
|
||||
// handshake completion (both sides) and from the connection manager's
|
||||
// per-tunnel tick, so a demanded lane is established, and a dead one
|
||||
// re-established, within one check interval subject to per-slot backoff.
|
||||
// maybeAllocLanes derives the multiport lane sessions for a just-completed
|
||||
// tunnel. Must run before the hostinfo becomes visible in the hostmap: the data
|
||||
// plane reads hostinfo.lanes with no synchronization at all.
|
||||
//
|
||||
// Demand is what makes lanes lazy: nothing is built when the base tunnel
|
||||
// completes, only when a routine actually has traffic for the peer and finds
|
||||
// its slot empty (see noteLaneDemand). Consuming the flag here rather than
|
||||
// leaving it set also means a lane that dies after its routine went quiet
|
||||
// stays dead instead of being rebuilt forever.
|
||||
func (hm *HandshakeManager) EnsureLanes(base *HostInfo) {
|
||||
ls := base.lanes
|
||||
if ls == nil || hm.config.laneCount <= 1 {
|
||||
// A failure here costs the tunnel its lanes, not the tunnel: the base session is
|
||||
// already usable and every lane the data plane wants falls back to it.
|
||||
func (hm *HandshakeManager) maybeAllocLanes(hostinfo *HostInfo, result *handshake.Result) {
|
||||
if hm.config.laneCount == 0 || result.PeerPortCount == 0 {
|
||||
return
|
||||
}
|
||||
if len(hm.f.myVpnAddrs) == 0 || len(hostinfo.vpnAddrs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var starts []int
|
||||
ls.Lock()
|
||||
n := min(len(ls.txLanes), hm.config.laneCount)
|
||||
for i := 1; i < n; i++ {
|
||||
if ls.txLanes[i].Load() != nil || ls.txPending[i] || now.Before(ls.txRetryAt[i]) {
|
||||
continue
|
||||
}
|
||||
// Checked last: a slot that isn't startable keeps its demand for the
|
||||
// tick that can act on it.
|
||||
if !ls.takeLaneDemand(i) {
|
||||
continue
|
||||
}
|
||||
ls.txPending[i] = true
|
||||
starts = append(starts, i)
|
||||
}
|
||||
ls.Unlock()
|
||||
|
||||
for _, i := range starts {
|
||||
hm.startLaneHandshake(base, i)
|
||||
}
|
||||
}
|
||||
|
||||
// startLaneHandshake initiates lane i of base: a full Noise handshake from
|
||||
// local socket i to the peer's advertised lane port. The caller has already
|
||||
// claimed the slot (txPending[i] = true); every failure path must release it
|
||||
// via noteLaneFailure.
|
||||
func (hm *HandshakeManager) startLaneHandshake(base *HostInfo, i int) {
|
||||
ls := base.lanes
|
||||
|
||||
remote := base.GetRemote()
|
||||
if !remote.IsValid() || ls.peerPortCount == 0 {
|
||||
// Relay-only peer (or a zero advert that should not have allocated
|
||||
// lanes). Retried if a direct path shows up later.
|
||||
ls.noteLaneFailure(i)
|
||||
return
|
||||
}
|
||||
target := netip.AddrPortFrom(remote.Addr(), ls.laneTargetPort(i))
|
||||
|
||||
hostinfo := &HostInfo{
|
||||
vpnAddrs: slices.Clone(base.vpnAddrs),
|
||||
HandshakePacket: make(map[uint8][]byte, 0),
|
||||
relayState: RelayState{
|
||||
relays: nil,
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
// A private RemoteList: SetRemote must never leak lane ports into the
|
||||
// shared lighthouse-learned cache that base handshakes broadcast to.
|
||||
remotes: NewRemoteList(base.vpnAddrs, nil),
|
||||
sockIdx: i,
|
||||
laneIndex: uint16(i),
|
||||
laneOwned: true,
|
||||
parent: base,
|
||||
}
|
||||
|
||||
hh := &HandshakeHostInfo{
|
||||
hostinfo: hostinfo,
|
||||
startTime: time.Now(),
|
||||
laneTarget: target,
|
||||
}
|
||||
if cs := base.ConnectionState; cs != nil && cs.myCert != nil {
|
||||
// Pin the lane to the base tunnel's negotiated cert version rather
|
||||
// than re-running version selection.
|
||||
hh.initiatingVersionOverride = cs.myCert.Version()
|
||||
}
|
||||
|
||||
// Build stage 0 eagerly: allocateIndex registers hh in hm.indexes so
|
||||
// continueHandshake can find it, and gives us the key for the lane timer.
|
||||
hh.Lock()
|
||||
ok := hm.buildStage0Packet(hh)
|
||||
hh.Unlock()
|
||||
if !ok {
|
||||
ls.noteLaneFailure(i)
|
||||
return
|
||||
}
|
||||
|
||||
hostinfo.logger(hm.l).Info("Lane handshake started",
|
||||
"laneIndex", i,
|
||||
"udpAddr", target,
|
||||
"initiatorIndex", hostinfo.localIndexId,
|
||||
)
|
||||
hm.metricInitiated.Inc(1)
|
||||
hm.OutboundLaneTimer.Add(hostinfo.localIndexId, hm.config.tryInterval)
|
||||
hm.handleOutboundLane(hostinfo.localIndexId)
|
||||
}
|
||||
|
||||
func (hm *HandshakeManager) NextOutboundLaneTimerTick(now time.Time) {
|
||||
hm.OutboundLaneTimer.Advance(now)
|
||||
for {
|
||||
idx, has := hm.OutboundLaneTimer.Purge()
|
||||
if !has {
|
||||
break
|
||||
}
|
||||
hm.handleOutboundLane(idx)
|
||||
}
|
||||
}
|
||||
|
||||
// handleOutboundLane is handleOutbound for lane handshakes: single pinned
|
||||
// target, egress via the lane's own socket, no lighthouse and no relays.
|
||||
func (hm *HandshakeManager) handleOutboundLane(localIndex uint32) {
|
||||
hh := hm.queryIndex(localIndex)
|
||||
if hh == nil || !hh.hostinfo.isLane() {
|
||||
return
|
||||
}
|
||||
hh.Lock()
|
||||
defer hh.Unlock()
|
||||
|
||||
hostinfo := hh.hostinfo
|
||||
if hh.counter >= hm.config.retries {
|
||||
hostinfo.logger(hm.l).Info("Lane handshake timed out",
|
||||
"laneIndex", hostinfo.laneIndex,
|
||||
"udpAddr", hh.laneTarget,
|
||||
"initiatorIndex", hostinfo.localIndexId,
|
||||
"durationNs", time.Since(hh.startTime).Nanoseconds(),
|
||||
)
|
||||
hm.metricTimedOut.Inc(1)
|
||||
hm.DeleteHostInfo(hostinfo)
|
||||
hostinfo.parent.lanes.noteLaneFailure(int(hostinfo.laneIndex))
|
||||
return
|
||||
}
|
||||
hh.counter++
|
||||
|
||||
stage0 := hostinfo.HandshakePacket[handshakePacketStage0]
|
||||
hm.messageMetrics.Tx(header.Handshake, hh.machine.Subtype(), 1)
|
||||
err := hm.f.writers[hostinfo.sockIdx].WriteTo(stage0, hh.laneTarget)
|
||||
ls, err := newLaneSet(result, hm.config.laneCount, hm.f.myVpnAddrs[0], hostinfo.vpnAddrs[0])
|
||||
if err != nil {
|
||||
hostinfo.logger(hm.l).Error("Failed to send lane handshake message",
|
||||
"laneIndex", hostinfo.laneIndex,
|
||||
"udpAddr", hh.laneTarget,
|
||||
"initiatorIndex", hostinfo.localIndexId,
|
||||
"error", err,
|
||||
)
|
||||
} else if hm.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(hm.l).Debug("Lane handshake message sent",
|
||||
"laneIndex", hostinfo.laneIndex,
|
||||
"udpAddr", hh.laneTarget,
|
||||
"initiatorIndex", hostinfo.localIndexId,
|
||||
)
|
||||
hostinfo.logger(hm.l).Error("Failed to derive multiport lanes", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
hm.OutboundLaneTimer.Add(localIndex, hm.config.tryInterval*time.Duration(hh.counter))
|
||||
}
|
||||
|
||||
// deletePendingHostInfo abandons a pending handshake. For lanes it also
|
||||
// releases the base's slot claim with failure backoff so ensureLanes can
|
||||
// retry later.
|
||||
func (hm *HandshakeManager) deletePendingHostInfo(hostinfo *HostInfo) {
|
||||
hm.DeleteHostInfo(hostinfo)
|
||||
if hostinfo.isLane() {
|
||||
hostinfo.parent.lanes.noteLaneFailure(int(hostinfo.laneIndex))
|
||||
}
|
||||
}
|
||||
|
||||
// completeLane moves a finished lane out of the pending map and registers it
|
||||
// in the main hostmap's index maps (never Hosts). The caller publishes it to
|
||||
// the base's txLanes/peerLanes afterwards.
|
||||
func (hm *HandshakeManager) completeLane(hostinfo *HostInfo, f *Interface) {
|
||||
hm.mainHostMap.Lock()
|
||||
defer hm.mainHostMap.Unlock()
|
||||
hm.Lock()
|
||||
defer hm.Unlock()
|
||||
|
||||
existingRemoteIndex, found := hm.mainHostMap.RemoteIndexes[hostinfo.remoteIndexId]
|
||||
if found && existingRemoteIndex != nil {
|
||||
hostinfo.logger(hm.l).Info("New lane shadows existing host remoteIndex",
|
||||
"collision", existingRemoteIndex.vpnAddrs,
|
||||
)
|
||||
}
|
||||
|
||||
hm.unlockedDeleteHostInfo(hostinfo)
|
||||
hm.mainHostMap.unlockedAddLane(hostinfo, f)
|
||||
hostinfo.lanes = ls
|
||||
}
|
||||
|
||||
// beginHandshake handles an incoming handshake packet that doesn't match any
|
||||
@@ -962,7 +761,7 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
|
||||
v, cs.GetCredential,
|
||||
hm.certVerifier(), func() (uint32, error) { return generateIndex(f.l) },
|
||||
false, header.HandshakeIXPSK0,
|
||||
hm.laneAdvert(0),
|
||||
hm.laneAdvert(),
|
||||
)
|
||||
if err != nil {
|
||||
f.l.Error("Failed to create handshake machine", "from", via, "error", err)
|
||||
@@ -999,13 +798,6 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
|
||||
return
|
||||
}
|
||||
|
||||
// A nonzero lane tag makes this a lane handshake for an existing base
|
||||
// tunnel rather than a (possibly duplicate) tunnel of its own.
|
||||
if result.PeerLaneIndex > 0 {
|
||||
hm.completeLaneResponder(via, packet, response, result, vpnAddrs)
|
||||
return
|
||||
}
|
||||
|
||||
connState, err := newConnectionStateFromResult(result)
|
||||
if err != nil {
|
||||
f.l.Error("Discarding handshake with an invalid message index", "error", err, "vpnAddrs", vpnAddrs)
|
||||
@@ -1056,7 +848,7 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
|
||||
hostinfo.SetRemote(via.UdpAddr)
|
||||
}
|
||||
hostinfo.buildNetworks(f.myVpnNetworksTable, remoteCert.Certificate)
|
||||
hm.maybeAllocLaneState(hostinfo, result)
|
||||
hm.maybeAllocLanes(hostinfo, result)
|
||||
|
||||
existing, err := hm.CheckAndComplete(hostinfo, handshakePacketStage0, f)
|
||||
if err != nil {
|
||||
@@ -1066,7 +858,6 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
|
||||
|
||||
hm.sendHandshakeResponse(via, response, hostinfo, false)
|
||||
hostinfo.remotes.RefreshFromHandshake(vpnAddrs)
|
||||
hm.EnsureLanes(hostinfo)
|
||||
|
||||
// Don't wait for UpdateWorker
|
||||
if f.lightHouse.IsAnyLighthouseAddr(vpnAddrs) {
|
||||
@@ -1074,146 +865,6 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
|
||||
}
|
||||
}
|
||||
|
||||
// completeLaneResponder finishes the responder side of a lane handshake:
|
||||
// associate with the base tunnel by the peer's certified vpn address, dedup
|
||||
// replays per lane, register the lane in the index maps, and reply from the
|
||||
// arrival socket. Handshakes with no live base are dropped — the initiator's
|
||||
// retry loop converges once the base exists.
|
||||
func (hm *HandshakeManager) completeLaneResponder(via ViaSender, packet, response []byte, result *handshake.Result, vpnAddrs []netip.Addr) {
|
||||
f := hm.f
|
||||
|
||||
if via.IsRelayed {
|
||||
hm.l.Debug("dropping relayed lane handshake", "vpnAddrs", vpnAddrs, "from", via)
|
||||
return
|
||||
}
|
||||
if hm.config.laneCount == 0 {
|
||||
hm.l.Debug("dropping lane handshake, multiport is disabled", "vpnAddrs", vpnAddrs, "from", via)
|
||||
return
|
||||
}
|
||||
|
||||
base := hm.mainHostMap.QueryVpnAddr(vpnAddrs[0])
|
||||
if base == nil || base.ConnectionState == nil || base.lanes == nil {
|
||||
hm.l.Debug("dropping lane handshake with no base tunnel",
|
||||
"vpnAddrs", vpnAddrs, "from", via, "laneIndex", result.PeerLaneIndex)
|
||||
return
|
||||
}
|
||||
ls := base.lanes
|
||||
|
||||
// The owner's lane index is bounded by its own advertised port count
|
||||
// (lanes are one-per-routine and PortCount == routines on the owner).
|
||||
laneIndex := result.PeerLaneIndex
|
||||
if laneIndex >= uint32(ls.peerPortCount) || laneIndex > 256 {
|
||||
hm.l.Debug("dropping lane handshake with out-of-range lane index",
|
||||
"vpnAddrs", vpnAddrs, "from", via, "laneIndex", laneIndex)
|
||||
return
|
||||
}
|
||||
|
||||
// Per-lane replay dedup against the existing lane with the same index.
|
||||
stage0 := packet[header.Len:]
|
||||
var existing *HostInfo
|
||||
ls.Lock()
|
||||
for _, h := range ls.peerLanes {
|
||||
if h.laneIndex == uint16(laneIndex) {
|
||||
existing = h
|
||||
break
|
||||
}
|
||||
}
|
||||
ls.Unlock()
|
||||
if existing != nil {
|
||||
if bytes.Equal(stage0, existing.HandshakePacket[handshakePacketStage0]) {
|
||||
// Stage-0 retransmit: the peer is committed to the original
|
||||
// response's ephemeral keys, resend it from the arrival socket.
|
||||
if msg := existing.HandshakePacket[handshakePacketStage2]; msg != nil {
|
||||
hm.sendHandshakeResponse(via, msg, existing, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
if existing.lastHandshakeTime >= result.HandshakeTime {
|
||||
existing.logger(hm.l).Debug("dropping stale lane handshake",
|
||||
"laneIndex", laneIndex, "from", via)
|
||||
return
|
||||
}
|
||||
// Newer handshake wins; the initiator only re-initiates after it
|
||||
// declared the old lane dead. Silent local teardown.
|
||||
hm.mainHostMap.DeleteHostInfo(existing)
|
||||
}
|
||||
|
||||
connState, err := newConnectionStateFromResult(result)
|
||||
if err != nil {
|
||||
hm.l.Error("Discarding lane handshake with an invalid message index",
|
||||
"error", err, "vpnAddrs", vpnAddrs, "laneIndex", laneIndex)
|
||||
return
|
||||
}
|
||||
|
||||
hostinfo := &HostInfo{
|
||||
ConnectionState: connState,
|
||||
localIndexId: result.LocalIndex,
|
||||
remoteIndexId: result.RemoteIndex,
|
||||
vpnAddrs: vpnAddrs,
|
||||
HandshakePacket: make(map[uint8][]byte, 0),
|
||||
lastHandshakeTime: result.HandshakeTime,
|
||||
relayState: RelayState{
|
||||
relays: nil,
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
// Private RemoteList: lane roaming must not touch the shared
|
||||
// lighthouse-learned cache.
|
||||
remotes: NewRemoteList(vpnAddrs, nil),
|
||||
sockIdx: via.SockIdx,
|
||||
laneIndex: uint16(laneIndex),
|
||||
laneOwned: false,
|
||||
parent: base,
|
||||
}
|
||||
|
||||
// packet aliases the listener's incoming buffer, so this copy must stay.
|
||||
hostinfo.HandshakePacket[handshakePacketStage0] = make([]byte, len(stage0))
|
||||
copy(hostinfo.HandshakePacket[handshakePacketStage0], stage0)
|
||||
if response != nil {
|
||||
hostinfo.HandshakePacket[handshakePacketStage2] = response
|
||||
}
|
||||
hostinfo.SetRemote(via.UdpAddr)
|
||||
hostinfo.buildNetworks(f.myVpnNetworksTable, result.RemoteCert.Certificate)
|
||||
|
||||
// Index-collision checks, mirroring CheckAndComplete minus the Hosts
|
||||
// dedup (lanes are keyed by (base, laneIndex), not by address).
|
||||
hm.mainHostMap.Lock()
|
||||
hm.Lock()
|
||||
if existingIndex, found := hm.mainHostMap.Indexes[hostinfo.localIndexId]; found && existingIndex != hostinfo {
|
||||
hm.Unlock()
|
||||
hm.mainHostMap.Unlock()
|
||||
hostinfo.logger(hm.l).Error("Failed to add lane due to localIndex collision", "laneIndex", laneIndex)
|
||||
return
|
||||
}
|
||||
if existingPendingIndex, found := hm.indexes[hostinfo.localIndexId]; found && existingPendingIndex.hostinfo != hostinfo {
|
||||
hm.Unlock()
|
||||
hm.mainHostMap.Unlock()
|
||||
hostinfo.logger(hm.l).Error("Failed to add lane due to pending localIndex collision", "laneIndex", laneIndex)
|
||||
return
|
||||
}
|
||||
if existingRemoteIndex, found := hm.mainHostMap.RemoteIndexes[hostinfo.remoteIndexId]; found && existingRemoteIndex != nil {
|
||||
hostinfo.logger(hm.l).Info("New lane shadows existing host remoteIndex",
|
||||
"collision", existingRemoteIndex.vpnAddrs,
|
||||
)
|
||||
}
|
||||
hm.mainHostMap.unlockedAddLane(hostinfo, f)
|
||||
hm.Unlock()
|
||||
hm.mainHostMap.Unlock()
|
||||
|
||||
ls.Lock()
|
||||
ls.peerLanes = append(ls.peerLanes, hostinfo)
|
||||
ls.Unlock()
|
||||
|
||||
hostinfo.logger(hm.l).Info("Lane handshake received",
|
||||
"laneIndex", laneIndex,
|
||||
"from", via,
|
||||
"initiatorIndex", result.RemoteIndex,
|
||||
"responderIndex", result.LocalIndex,
|
||||
)
|
||||
|
||||
hm.sendHandshakeResponse(via, response, hostinfo, false)
|
||||
}
|
||||
|
||||
// continueHandshake feeds an incoming packet to an existing pending handshake Machine.
|
||||
func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostInfo, packet []byte) {
|
||||
f := hm.f
|
||||
@@ -1234,14 +885,6 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
|
||||
}
|
||||
|
||||
hostinfo := hh.hostinfo
|
||||
if hostinfo.isLane() && via.IsRelayed {
|
||||
// Lane handshakes are direct-only; a relayed continuation is a stray
|
||||
// or a protocol violation. Drop without failing the machine so the
|
||||
// direct stage-2 can still land.
|
||||
f.l.Debug("dropping relayed lane handshake continuation",
|
||||
"vpnAddrs", hostinfo.vpnAddrs, "from", via)
|
||||
return
|
||||
}
|
||||
if !via.IsRelayed {
|
||||
if !f.lightHouse.GetRemoteAllowList().AllowAll(hostinfo.vpnAddrs, via.UdpAddr.Addr()) {
|
||||
f.l.Debug("lighthouse.remote_allow_list denied incoming handshake",
|
||||
@@ -1254,7 +897,7 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
|
||||
if machine == nil {
|
||||
f.l.Error("No handshake machine available for continuation",
|
||||
"vpnAddrs", hostinfo.vpnAddrs, "from", via)
|
||||
hm.deletePendingHostInfo(hostinfo)
|
||||
hm.DeleteHostInfo(hostinfo)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1264,7 +907,7 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
|
||||
if machine.Failed() {
|
||||
f.l.Warn("Failed to process handshake packet, abandoning",
|
||||
"vpnAddrs", hostinfo.vpnAddrs, "from", via, "error", err)
|
||||
hm.deletePendingHostInfo(hostinfo)
|
||||
hm.DeleteHostInfo(hostinfo)
|
||||
} else {
|
||||
f.l.Debug("Failed to process handshake packet",
|
||||
"vpnAddrs", hostinfo.vpnAddrs, "from", via, "error", err)
|
||||
@@ -1293,7 +936,7 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
|
||||
if remoteCert == nil {
|
||||
f.l.Error("Handshake completed without peer certificate",
|
||||
"vpnAddrs", hostinfo.vpnAddrs, "from", via)
|
||||
hm.deletePendingHostInfo(hostinfo)
|
||||
hm.DeleteHostInfo(hostinfo)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1327,7 +970,7 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
|
||||
"issuer", remoteCert.Certificate.Issuer(),
|
||||
"handshake", m{"stage": uint64(machine.MessageIndex()), "style": header.SubTypeName(header.Handshake, machine.Subtype())},
|
||||
)
|
||||
hm.deletePendingHostInfo(hostinfo)
|
||||
hm.DeleteHostInfo(hostinfo)
|
||||
return
|
||||
}
|
||||
vpnAddrs[i] = network.Addr()
|
||||
@@ -1351,15 +994,6 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
|
||||
"handshake", m{"stage": uint64(machine.MessageIndex()), "style": header.SubTypeName(header.Handshake, machine.Subtype())},
|
||||
)
|
||||
|
||||
if hostinfo.isLane() {
|
||||
// No restart dance for lanes: close what the peer completed,
|
||||
// release the slot with backoff, and let ensureLanes retry.
|
||||
hm.deletePendingHostInfo(hostinfo)
|
||||
hostinfo.vpnAddrs = vpnAddrs
|
||||
f.sendCloseTunnel(hostinfo)
|
||||
return
|
||||
}
|
||||
|
||||
hm.DeleteHostInfo(hostinfo)
|
||||
hm.StartHandshake(hostinfo.vpnAddrs[0], func(newHH *HandshakeHostInfo) {
|
||||
newHH.hostinfo.remotes = hostinfo.remotes
|
||||
@@ -1394,31 +1028,8 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
|
||||
hostinfo.vpnAddrs = vpnAddrs
|
||||
hostinfo.buildNetworks(f.myVpnNetworksTable, remoteCert.Certificate)
|
||||
|
||||
if hostinfo.isLane() {
|
||||
hm.completeLane(hostinfo, f)
|
||||
|
||||
// Publish to the data plane only after the lane is registered and its
|
||||
// ConnectionState is fully populated; a routine that Loads non-nil
|
||||
// must always see a usable tunnel.
|
||||
ls := hostinfo.parent.lanes
|
||||
i := int(hostinfo.laneIndex)
|
||||
ls.Lock()
|
||||
if i < len(ls.txPending) {
|
||||
ls.txPending[i] = false
|
||||
ls.txFails[i] = 0
|
||||
}
|
||||
ls.Unlock()
|
||||
if i < len(ls.txLanes) {
|
||||
ls.txLanes[i].Store(hostinfo)
|
||||
}
|
||||
|
||||
f.metricHandshakes.Update(duration)
|
||||
return
|
||||
}
|
||||
|
||||
hm.maybeAllocLaneState(hostinfo, result)
|
||||
hm.maybeAllocLanes(hostinfo, result)
|
||||
hm.Complete(hostinfo, f)
|
||||
hm.EnsureLanes(hostinfo)
|
||||
|
||||
if len(hh.packetStore) > 0 {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
|
||||
Reference in new issue
Block a user