Fix cross-connection session alignment for simultaneous auto-connect

When both nodes simultaneously initiate connections, each promotes its
inbound handshake first, leaving both sides with mismatched Noise sessions
and session indices. Resolve this in handle_msg2 using the existing
tie-breaker (smaller node_addr's outbound wins):

- Winner (smaller node): swaps ActivePeer to outbound session + indices
  via new replace_session() method
- Loser (larger node): keeps inbound session and original their_index
  (already references winner's outbound our_index from msg1)

Key insight: the loser must NOT update their_index from msg2, because the
msg2 sender_idx carries the winner's inbound index which becomes stale
after the winner swaps.

Defer outbound connection cleanup from promote_connection() to handle_msg2()
so outbound session state is available for the swap.

Verified with live two-node test over UDP in network namespaces: both nodes
connect, exchange TreeAnnounce, and converge to spanning tree within ~2s.
This commit is contained in:
Johnathan Corgan
2026-02-11 01:41:21 +00:00
parent 74eaeab3d4
commit 7bc5b21c3a
3 changed files with 154 additions and 25 deletions
+28
View File
@@ -287,6 +287,34 @@ impl ActivePeer {
self.their_index
}
/// Update their session index (used during cross-connection resolution
/// when the losing node keeps its inbound session but needs the peer's
/// outbound index).
pub fn set_their_index(&mut self, index: SessionIndex) {
self.their_index = Some(index);
}
/// Replace the Noise session and indices during cross-connection resolution.
///
/// When both nodes simultaneously initiate, each promotes its inbound
/// handshake first. When the peer's msg2 arrives, we learn the correct
/// session — the outbound handshake that pairs with the peer's inbound.
/// This replaces the entire session so both nodes use matching keys.
///
/// Returns the old our_index so the caller can update peers_by_index.
pub fn replace_session(
&mut self,
new_session: NoiseSession,
new_our_index: SessionIndex,
new_their_index: SessionIndex,
) -> Option<SessionIndex> {
let old_our_index = self.our_index;
self.noise_session = Some(new_session);
self.our_index = Some(new_our_index);
self.their_index = Some(new_their_index);
old_our_index
}
/// Get the transport ID for this peer.
pub fn transport_id(&self) -> Option<TransportId> {
self.transport_id