node: drive net-new outbound establish through the per-peer machine

Cut the net-new outbound handshake completion (a received msg2 that
promotes a fresh outbound leg to a new peer) over to the per-peer
control machine, mirroring the inbound cutover. handle_msg2 still runs
the msg2 prologue, the ACL check, and the cross-connection swap/keep
arms inline; for the net-new promote it now builds a transient machine,
steps it, and drives promote_connection through the executor. The
session index was already allocated at dial, so there is no two-phase
authorize here.

The wire, index sequence, and peer registry state after promote are
byte-neutral. To keep the promote-failure path neutral, the executor's
cleanup now distinguishes inbound from outbound: an outbound promote
failure records the reject only, matching the prior handler, rather
than the inbound path's link and index teardown.

Cross-connection swap/keep, rekey-msg2, and the dial path stay inline;
the loser-link surgery for the currently unreachable driven
cross-connection case lands with the register relocation next.
This commit is contained in:
Johnathan Corgan
2026-07-13 13:42:08 +00:00
parent c80a7fdea5
commit e9112cc1bb
3 changed files with 162 additions and 93 deletions
+49 -15
View File
@@ -54,6 +54,14 @@ pub(in crate::node) struct PeerActionCtx {
pub(in crate::node) their_index: Option<SessionIndex>,
/// The wire timestamp driving this step (promotion ts / loss-report clock).
pub(in crate::node) now_ms: u64,
/// Establish direction for this exchange. Discriminates the
/// `PromoteToActive` failure cleanup: the pre-refactor inbound
/// (`handle_msg1`) and outbound (`handle_msg2`) promote-Err arms were NOT
/// byte-identical, so the executor must reproduce each. `false` = inbound
/// (drop link + reverse map + free index), `true` = outbound (record the
/// reject only; leave the dead link/`addr_to_link` for the stale-connection
/// reaper, matching old `handle_msg2`).
pub(in crate::node) is_outbound: bool,
}
impl Node {
@@ -179,22 +187,48 @@ impl Node {
}
Err(e) => {
// GAP-4: promotion failed. `promote_connection` already
// removed `connections[link]`; mirror the pre-refactor
// cleanup (`handle_msg1` L587-591): drop the link +
// reverse map, free our index, discard the machine, and
// record the reject. The queue is drained (PromoteToActive
// is the last establish action), so no explicit abort.
//
// Restored pre-refactor promote-failure warn!
// (`handle_msg1` L757).
warn!(link_id = %promote_link, error = %e, "Failed to promote inbound connection");
self.remove_link(&promote_link);
if let Some(idx) = ambient.our_index {
let _ = self.index_allocator.free(idx);
// removed `connections[link]` and (on error) handled its
// own index internally. The pre-refactor inbound and
// outbound promote-Err arms were NOT byte-identical, so
// discriminate on `ambient.is_outbound`. The queue is
// drained (PromoteToActive is the last establish action),
// so no explicit abort.
if ambient.is_outbound {
// OLD outbound (`handle_msg2` promote-Err): warn +
// record_reject ONLY. NO `remove_link`, NO
// `index_allocator.free`, NO `addr_to_link` removal —
// the dead link/addr_to_link/pending_outbound were
// left for the 30s stale-connection reaper
// (`promote_connection` already handled
// `connections[link]`/its index on error). Restored
// pre-refactor outbound warn! ("Failed to promote
// connection").
//
// The transient outbound machine was inserted BEFORE
// execute (Model A); it is additive C3-1 state that
// did not exist pre-refactor, so removing the just-
// inserted machine on failure is neutral vs old and
// prevents a leak.
warn!(link_id = %promote_link, error = %e, "Failed to promote connection");
self.stats_mut().record_reject(RejectReason::Handshake(
HandshakeReject::BadState,
));
self.peer_machines.remove(&promote_link);
} else {
// OLD inbound (`handle_msg1` L587-591): drop the link
// + reverse map, free our index, discard the machine,
// and record the reject. Restored pre-refactor inbound
// promote-failure warn! (`handle_msg1` L757).
warn!(link_id = %promote_link, error = %e, "Failed to promote inbound connection");
self.remove_link(&promote_link);
if let Some(idx) = ambient.our_index {
let _ = self.index_allocator.free(idx);
}
self.peer_machines.remove(&promote_link);
self.stats_mut().record_reject(RejectReason::Handshake(
HandshakeReject::BadState,
));
}
self.peer_machines.remove(&promote_link);
self.stats_mut()
.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
}
}
}