peer: move the Noise handshake operations onto the control machine

The pending connection drove its own Noise handshake while the control
machine held it, so the crypto and the state it produces lived on the
carrier that is going away. Move the six operations onto the machine:
starting and completing an initiation, processing an inbound
initiation, taking the session, testing for one, and dropping the
handle on failure. Bodies are unchanged apart from reaching the
handles through the attached connection.

Each operation now records its results on both carriers. The learned
identity, the remote epoch, and the activity stamp are written to the
machine's own bookkeeping at the same point and with the same value as
they are written to the connection's. The connection's copies still
have readers until those reads are repointed, so both have to be
written; the machine's copy of the learned identity was previously
never populated for an inbound connection, which is why an inbound
pending row showed no expected peer.

Driving the handshake from the machine means the machine has to exist
before the crypto runs. On the inbound path it is built above the
message-1 processing and still kept local, so a rejected message
leaves no registry entry and allocates no index. On the outbound path
it already existed from the dial, so it simply takes the connection
before the index allocation, and both failure arms unwind it as they
did.

Completing message 2 no longer mirrors the activity stamp separately,
since the completion itself now writes both carriers at the point the
mirror was approximating.

Three tests cover what the compiler cannot. A connection whose
handshake failed holds neither Noise handle yet must stay visible to
the stale-connection sweep, or every failed connection would leak and
hold a peering-budget slot forever. A message 1 rejected by the crypto
or by the ACL must leave no machine, no index, and the same rejection
count as before. A dial whose message-1 preparation fails must unwind
the machine registered at dial time; that test drives the
index-allocation failure rather than a crypto failure, which is the
arm this change actually widens, since the allocation now happens with
the connection already attached.
This commit is contained in:
Johnathan Corgan
2026-07-18 22:55:38 +00:00
parent 11ec16777c
commit 347cbe60bd
10 changed files with 747 additions and 411 deletions
+51 -34
View File
@@ -585,7 +585,20 @@ impl Node {
// Create connection in handshake phase (outbound knows expected identity)
let current_time_ms = Self::now_ms();
let mut connection = PeerConnection::outbound(link_id, peer_identity, current_time_ms);
let connection = PeerConnection::outbound(link_id, peer_identity, current_time_ms);
// The control machine drives the handshake, so it takes the connection
// before the crypto runs. The machine was born at dial and persisted in
// `initiate_connection`, so every live caller already has one; recover
// with a fresh one if a direct caller ever skips the dial.
debug_assert!(
self.peer_machines.contains_key(&link_id),
"outbound msg1 prepared for link {link_id} with no dial-time machine"
);
self.peer_machines
.entry(link_id)
.or_insert_with(|| PeerMachine::new_outbound(link_id, peer_identity, current_time_ms))
.set_leg(connection);
// Allocate a session index for this handshake
let our_index = match self.index_allocator.allocate() {
@@ -602,23 +615,35 @@ impl Node {
// Start the Noise handshake and get message 1
let our_keypair = self.identity().keypair();
let noise_msg1 =
match connection.start_handshake(our_keypair, self.startup_epoch(), current_time_ms) {
Ok(msg) => msg,
Err(e) => {
// Clean up the index, link, and dial-time machine
let _ = self.index_allocator.free(our_index);
self.links.remove(&link_id);
self.addr_to_link
.remove(&(transport_id, remote_addr.clone()));
self.remove_peer_machine(link_id);
return Err(NodeError::HandshakeFailed(e.to_string()));
}
};
let startup_epoch = self.startup_epoch();
let noise_msg1 = match self
.peer_machines
.get_mut(&link_id)
.expect("dial-time machine carries the connection")
.start_handshake(our_keypair, startup_epoch, current_time_ms)
{
Ok(msg) => msg,
Err(e) => {
// Clean up the index, link, and dial-time machine
let _ = self.index_allocator.free(our_index);
self.links.remove(&link_id);
self.addr_to_link
.remove(&(transport_id, remote_addr.clone()));
self.remove_peer_machine(link_id);
return Err(NodeError::HandshakeFailed(e.to_string()));
}
};
// Set index and transport info on the connection
connection.set_our_index(our_index);
connection.set_source_addr(remote_addr.clone());
{
let conn = self
.peer_machines
.get_mut(&link_id)
.and_then(|machine| machine.leg_mut())
.expect("dial-time machine carries the connection");
conn.set_our_index(our_index);
conn.set_source_addr(remote_addr.clone());
}
// Build wire format msg1: [0x01][sender_idx:4 LE][noise_msg1:82]
let wire_msg1 = build_msg1(our_index, &noise_msg1);
@@ -641,21 +666,13 @@ impl Node {
self.pending_outbound
.insert((transport_id, our_index.as_u32()), link_id);
// The dial-born machine (persisted in `initiate_connection`) carries
// the prepared connection from here. Every live caller dialed first,
// so the machine exists; recover with a fresh one if a direct caller
// ever skips the dial.
debug_assert!(
self.peer_machines.contains_key(&link_id),
"outbound msg1 prepared for link {link_id} with no dial-time machine"
);
let machine = self
.peer_machines
.entry(link_id)
.or_insert_with(|| PeerMachine::new_outbound(link_id, peer_identity, current_time_ms));
.get_mut(&link_id)
.expect("dial-time machine carries the connection");
// The dial-born machine carrier was stamped at dial; re-stamp it with the
// leg's msg1-prep clock so the surviving `started_at`/`last_activity`
// carry the leg's provenance. The two clocks differ when a connect
// msg1-prep clock so the surviving `started_at`/`last_activity` carry
// the preparation's provenance. The two clocks differ when a connect
// round-trip separates dial from msg1 preparation.
machine.set_conn_started_at(current_time_ms);
machine.touch_conn(current_time_ms);
@@ -663,14 +680,14 @@ impl Node {
// projects it to the promotion hand-off); holds even if a direct caller
// reached here without the dial-time `on_dial` write.
machine.set_conn_transport_id(transport_id);
// Record our session index on the surviving carrier — the same index just
// written on the leg above — so the carrier is the single index home on
// the outbound path (the inbound path writes it at authorize).
// Record our session index on the surviving carrier — the same index
// just written on the connection above — so the carrier is the single
// index home on the outbound path (the inbound path writes it at
// authorize).
machine.set_conn_our_index(our_index);
// Store the msg1 wire on the surviving carrier (the leg no longer holds
// the resend source); the retransmit driver reads it from here.
// Store the msg1 wire on the surviving carrier (the connection does not
// hold the resend source); the retransmit driver reads it from here.
machine.set_conn_handshake_msg1(wire_msg1, first_resend_at_ms);
machine.set_leg(connection);
Ok(())
}