mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Merge branch 'refactor-node' into refactor-node-next
Brings the handshake-leg deletion onto the XX line: the leg struct and src/peer/connection.rs are gone, the Noise handles and crypto methods now live on PeerMachine, and every leg accessor reads the machine's own ConnectionState. The incoming branch speaks Noise IK, this one speaks XX, so the crypto methods could not be taken as they arrived. Git added the IK versions to machine.rs with no conflict marker, and complete_handshake_msg3 -- which XX needs and IK has no counterpart for -- was absent from the merged tree entirely. All four are re-expressed here: the incoming structure (the Option<HandshakeCrypto> handle, the self.conn reads, the borrow scoping that hoists carrier writes out of the leg borrow) carrying this line's XX bodies. start_handshake is the one that mattered most: its signature is identical on both lines, so it compiled silently, and left alone it would have emitted an IK msg1 through the two-argument new_initiator and panicked every anonymous dial on an expect for an identity XX does not have at that point. It now uses the one-argument XX form and no expect. handle_msg1 keeps this line's flow, not the incoming one's. The two functions share a name but not a shape -- the admission gate moved out of msg1 into msg3 when the line went to XX, and identity is unknown until msg3. The machine is built above the crypto because it now drives it, but it stays a local until the late insert, so a rejected msg1 still leaves no registry trace and the drop-the-local error arms are unchanged. Parking it at SentMsg2 after the index is allocated needed a transition the birth constructor could not provide, so that constructor now delegates to it. Node::start_handshake is re-expressed the same way and for the same reason: keeping the machine a local until all fallible setup has succeeded preserves the existing error arms exactly, with no machine disposal to add. The two leg-to-carrier mirror blocks collapse. With one carrier the writes they mirrored are self-assignment. Their content is preserved: the completion touch comes from the relocated complete_handshake, on the same clock, and the negotiated profile from process_fmp_negotiation, which now takes the machine directly and writes through set_conn_peer_profile. Worth recording, because the collapse was justified on a narrower claim that does not hold: complete_handshake writes only the touch. It never wrote the profile on either line. The two writes land on the same carrier at the same point, so the collapse is neutral, but it is discharged jointly by two methods rather than by complete_handshake alone. Three inbound tests are dropped rather than carried. Each asserts a property at msg1 that XX does not have there: an ACL decision, a no-registry-trace guard on that decision, and an identity learn on the carrier. Under XX msg1 is ephemeral-only, so none of the three has a landing site at that step. A comment at each drop site records where the property actually lives on this line. One of them was a deletion here that the incoming branch had merely modified, so its removal is this line's own prior decision rather than a new one. The same is true of the craft_and_send_msg1 helper and of HandshakeSeed::inbound, both left without callers once those tests went. Also carried: peer_actions.rs had no conflict and no incoming change, but three sites reached deleted Node accessors and are repointed at the machine; the EstablishView snapshot is not resurrected, as this line has never had it; and the connection-state doc keeps this line's XX wording with the registry-independence invariant grafted in.
This commit is contained in:
@@ -1316,17 +1316,18 @@ pub fn show_connections(node: &Node) -> Value {
|
||||
let now = now_ms();
|
||||
let connections: Vec<Value> = node
|
||||
.connections()
|
||||
.map(|conn| {
|
||||
.map(|(_, machine)| {
|
||||
let link_id = machine.link_id();
|
||||
let mut conn_json = json!({
|
||||
"link_id": conn.link_id().as_u64(),
|
||||
"direction": format!("{}", conn.direction()),
|
||||
"handshake_state": node.connection_handshake_state(conn.link_id()),
|
||||
"started_at_ms": node.connection_started_at(conn.link_id()),
|
||||
"idle_ms": now.saturating_sub(node.connection_last_activity(conn.link_id())),
|
||||
"resend_count": node.connection_resend_count(conn.link_id()),
|
||||
"link_id": link_id.as_u64(),
|
||||
"direction": format!("{}", machine.conn_direction()),
|
||||
"handshake_state": node.connection_handshake_state(link_id),
|
||||
"started_at_ms": node.connection_started_at(link_id),
|
||||
"idle_ms": now.saturating_sub(node.connection_last_activity(link_id)),
|
||||
"resend_count": node.connection_resend_count(link_id),
|
||||
});
|
||||
|
||||
if let Some(identity) = node.connection_expected_identity(conn.link_id()) {
|
||||
if let Some(identity) = node.connection_expected_identity(link_id) {
|
||||
conn_json["expected_peer"] = json!(identity.npub());
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ pub use cache::{CacheEntry, CacheError, CacheStats, CoordCache};
|
||||
pub use proto::fmp::{PromotionResult, cross_connection_winner};
|
||||
|
||||
// Re-export peer types
|
||||
pub use peer::{ActivePeer, ConnectivityState, PeerConnection, PeerError};
|
||||
pub use peer::{ActivePeer, ConnectivityState, PeerError};
|
||||
|
||||
// Re-export node types
|
||||
pub use node::{Node, NodeError, NodeState, UpdatePeersOutcome};
|
||||
|
||||
@@ -251,8 +251,9 @@ impl Node {
|
||||
let wire_msg2 = if ambient.is_outbound {
|
||||
None
|
||||
} else {
|
||||
self.leg(&promote_link)
|
||||
.and_then(|c| c.handshake_msg2().map(|m| m.to_vec()))
|
||||
self.peer_machines
|
||||
.get(&promote_link)
|
||||
.and_then(|m| m.conn_handshake_msg2().map(|b| b.to_vec()))
|
||||
};
|
||||
|
||||
if ambient.is_outbound {
|
||||
@@ -595,18 +596,21 @@ impl Node {
|
||||
if our_inbound_wins {
|
||||
// Larger node side: swap to the inbound session so it pairs
|
||||
// with the peer's kept outbound session.
|
||||
let inbound_session =
|
||||
match self.leg_mut(&link).and_then(|c| c.take_session()) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
self.remove_link(&link);
|
||||
self.remove_peer_machine(link);
|
||||
self.stats_mut().record_reject(RejectReason::Handshake(
|
||||
HandshakeReject::BadState,
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let inbound_session = match self
|
||||
.peer_machines
|
||||
.get_mut(&link)
|
||||
.and_then(|m| m.take_session())
|
||||
{
|
||||
Some(s) => s,
|
||||
None => {
|
||||
self.remove_link(&link);
|
||||
self.remove_peer_machine(link);
|
||||
self.stats_mut().record_reject(RejectReason::Handshake(
|
||||
HandshakeReject::BadState,
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(peer_ref) = self.peers.get_mut(&peer) {
|
||||
let old_our_index =
|
||||
peer_ref.replace_session(inbound_session, our_index, their_index);
|
||||
@@ -686,7 +690,7 @@ impl Node {
|
||||
|
||||
// Rekey: process as responder, store new session as pending.
|
||||
let noise_session = {
|
||||
let Some(conn) = self.leg_mut(&link) else {
|
||||
let Some(machine) = self.peer_machines.get_mut(&link) else {
|
||||
warn!(link_id = %link, "Connection removed during rekey msg3 processing");
|
||||
self.links.remove(&link);
|
||||
self.remove_peer_machine(link);
|
||||
@@ -695,7 +699,7 @@ impl Node {
|
||||
));
|
||||
return;
|
||||
};
|
||||
conn.take_session()
|
||||
machine.take_session()
|
||||
};
|
||||
let our_new_index = our_index;
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ use crate::node::acl::PeerAclContext;
|
||||
use crate::node::dataplane::PeerActionCtx;
|
||||
use crate::node::reject::{HandshakeReject, RejectReason};
|
||||
use crate::node::{Node, NodeError};
|
||||
use crate::peer::machine::{CrossConnOutcome, PeerAction, PeerEvent, PeerMachine, TimerKind};
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
use crate::peer::ActivePeer;
|
||||
use crate::peer::machine::{
|
||||
CrossConnOutcome, HandshakeCrypto, PeerAction, PeerEvent, PeerMachine, TimerKind,
|
||||
};
|
||||
use crate::proto::fmp::wire::{Msg1Header, Msg2Header, Msg3Header, build_msg2, build_msg3};
|
||||
use crate::proto::fmp::{
|
||||
Disconnect, DisconnectReason, EstablishSnapshot, InboundDecision, InboundReject,
|
||||
@@ -247,19 +249,24 @@ impl Node {
|
||||
|
||||
// === CRYPTO COST PAID HERE ===
|
||||
let link_id = self.allocate_link_id();
|
||||
let mut conn = PeerConnection::inbound_with_transport(
|
||||
link_id,
|
||||
packet.transport_id,
|
||||
packet.remote_addr.clone(),
|
||||
packet.timestamp_ms,
|
||||
);
|
||||
|
||||
// The control machine drives the handshake, so it is built here, above
|
||||
// the crypto. It stays a local: it enters `peer_machines` only at the
|
||||
// promote tails, so a rejected msg1 still leaves no registry trace and
|
||||
// allocates no index.
|
||||
let mut machine = PeerMachine::new_inbound(link_id, packet.timestamp_ms);
|
||||
// Seed the carrier with the transport and address msg1 arrived on, so
|
||||
// the promotion hand-off reads them from it.
|
||||
machine.set_conn_transport_id(packet.transport_id);
|
||||
machine.set_conn_source_addr(packet.remote_addr.clone());
|
||||
machine.set_leg(HandshakeCrypto::new());
|
||||
|
||||
// Create FMP negotiation payload for msg2 (includes profile, MMP bits, bloom TLV)
|
||||
let neg_payload = NegotiationPayload::fmp(1, 1, self.node_profile()).encode();
|
||||
|
||||
let our_keypair = self.identity().keypair();
|
||||
let noise_msg1 = &packet.data[header.noise_msg1_offset..];
|
||||
let msg2_response = match conn.receive_handshake_init(
|
||||
let msg2_response = match machine.receive_handshake_init(
|
||||
our_keypair,
|
||||
self.startup_epoch(),
|
||||
noise_msg1,
|
||||
@@ -301,8 +308,7 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_their_index(header.sender_idx);
|
||||
machine.set_conn_their_index(header.sender_idx);
|
||||
|
||||
// Create link
|
||||
let link = Link::connectionless(
|
||||
@@ -317,27 +323,19 @@ impl Node {
|
||||
self.addr_to_link.insert(addr_key, link_id);
|
||||
|
||||
// Build the msg2 response, storing it on the surviving carrier for
|
||||
// potential resend before the connection is embedded on the machine
|
||||
// below.
|
||||
// potential resend before the machine enters the registry below.
|
||||
let wire_msg2 = build_msg2(our_index, header.sender_idx, &msg2_response);
|
||||
|
||||
// The leg's persistent control machine is born carrying its pending
|
||||
// connection, parked at `SentMsg2` awaiting msg3 (identity is unknown
|
||||
// until then). Inserted before the msg2 send below so no suspension
|
||||
// point observes a leg in flight without a machine. `handle_msg3`
|
||||
// steps this same machine; every teardown path disposes it with the
|
||||
// embedded leg.
|
||||
let mut machine = PeerMachine::inbound_msg2_sent(link_id, our_index, packet.timestamp_ms);
|
||||
// The inbound leg carries the transport ID and peer index from msg1, but
|
||||
// the machine's carrier is only written on the outbound dial and at msg3.
|
||||
// Seed both here so the promotion hand-off reads them from the surviving
|
||||
// carrier, matching the leg's inbound seeds.
|
||||
machine.set_conn_transport_id(packet.transport_id);
|
||||
machine.set_conn_their_index(header.sender_idx);
|
||||
// The machine built above the crypto now parks at `SentMsg2` awaiting
|
||||
// msg3 (identity is unknown until then), recording the index it owns.
|
||||
// It enters the registry before the msg2 send below so no suspension
|
||||
// point observes a handshake in flight without a machine. `handle_msg3`
|
||||
// steps this same machine; every teardown path disposes it along with
|
||||
// the Noise handles it carries.
|
||||
machine.park_inbound_msg2_sent(our_index);
|
||||
// Store the framed msg2 on the surviving carrier for duplicate-msg1
|
||||
// resend while the connection is still pending.
|
||||
// resend while the handshake is still pending.
|
||||
machine.set_conn_handshake_msg2(wire_msg2.clone());
|
||||
machine.set_leg(conn);
|
||||
self.peer_machines.insert(link_id, machine);
|
||||
|
||||
if let Some(transport) = self.transports.get(&packet.transport_id) {
|
||||
@@ -357,8 +355,8 @@ impl Node {
|
||||
error = %e,
|
||||
"Failed to send msg2"
|
||||
);
|
||||
// Clean up on failure (the machine disposal drops the
|
||||
// embedded connection with it)
|
||||
// Clean up on failure (disposing the machine drops the
|
||||
// Noise handles it carries with it)
|
||||
self.links.remove(&link_id);
|
||||
self.addr_to_link
|
||||
.remove(&(packet.transport_id, packet.remote_addr));
|
||||
@@ -439,8 +437,8 @@ impl Node {
|
||||
};
|
||||
|
||||
// Check if this is a rekey msg2: the handshake state is on the
|
||||
// ActivePeer (not a PeerConnection), so the link's machine — if one
|
||||
// survives at all — carries no pending connection. A bare machine
|
||||
// ActivePeer, not in a handshake carrier, so the link's machine — if
|
||||
// one survives at all — carries no pending handshake. A bare machine
|
||||
// lookup would NOT discriminate here: an established peer's machine
|
||||
// stays keyed by this link, so the pending connection's presence is
|
||||
// what marks a fresh establish. Look for a peer with matching
|
||||
@@ -627,7 +625,7 @@ impl Node {
|
||||
|
||||
let our_profile = self.node_profile();
|
||||
let (peer_identity, msg3_bytes, our_index) = {
|
||||
let Some(conn) = self.leg_mut(&link_id) else {
|
||||
let Some(machine) = self.peer_machines.get_mut(&link_id) else {
|
||||
warn!(link_id = %link_id, "Connection removed during msg2 processing");
|
||||
self.pending_outbound.remove(&key);
|
||||
self.stats_mut()
|
||||
@@ -640,7 +638,7 @@ impl Node {
|
||||
|
||||
// Process Noise msg2 and generate msg3
|
||||
let noise_msg2 = &packet.data[header.noise_msg2_offset..];
|
||||
let (msg3_bytes, received_negotiation) = match conn.complete_handshake(
|
||||
let (msg3_bytes, received_negotiation) = match machine.complete_handshake(
|
||||
noise_msg2,
|
||||
Some(&neg_payload),
|
||||
packet.timestamp_ms,
|
||||
@@ -652,18 +650,15 @@ impl Node {
|
||||
error = %e,
|
||||
"Handshake completion failed"
|
||||
);
|
||||
// Drop the leg's Noise handle (byte-identical point) and
|
||||
// record the failure on the control machine as `send_failed`
|
||||
// — the failure state's new home. The machine PHASE stays
|
||||
// exactly where the old leg-carried failure left it
|
||||
// (`Handshaking{SentMsg1}`): the stale-connection sweep
|
||||
// reclaims the leg via the machine `is_failed()` at the next
|
||||
// tick, before any projection or resend, byte-identical to
|
||||
// the pre-collapse leg mark.
|
||||
conn.mark_failed();
|
||||
if let Some(machine) = self.peer_machines.get_mut(&link_id) {
|
||||
machine.mark_send_failed();
|
||||
}
|
||||
// Drop the Noise handle (byte-identical point) and record
|
||||
// the failure on the control machine as `send_failed` — the
|
||||
// failure state's home. The machine PHASE stays exactly
|
||||
// where the old failure left it (`Handshaking{SentMsg1}`):
|
||||
// the stale-connection sweep reclaims the handshake via the
|
||||
// machine `is_failed()` at the next tick, before any
|
||||
// projection or resend.
|
||||
machine.mark_failed();
|
||||
machine.mark_send_failed();
|
||||
self.stats_mut()
|
||||
.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
|
||||
return;
|
||||
@@ -672,17 +667,15 @@ impl Node {
|
||||
|
||||
// Process peer's FMP negotiation payload from msg2
|
||||
if let Some(neg_bytes) = &received_negotiation {
|
||||
match process_fmp_negotiation(our_profile, conn, neg_bytes) {
|
||||
match process_fmp_negotiation(our_profile, machine, neg_bytes) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
warn!(link_id = %link_id, our_profile = %our_profile, error = %e, "FMP negotiation failed");
|
||||
// Failure moves to the machine (`send_failed`); the phase
|
||||
// stays `Handshaking{SentMsg1}` so the sweep reclaims the
|
||||
// leg exactly as the pre-collapse leg mark did.
|
||||
conn.mark_failed();
|
||||
if let Some(machine) = self.peer_machines.get_mut(&link_id) {
|
||||
machine.mark_send_failed();
|
||||
}
|
||||
// handshake exactly as the pre-collapse mark did.
|
||||
machine.mark_failed();
|
||||
machine.mark_send_failed();
|
||||
self.stats_mut()
|
||||
.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
|
||||
return;
|
||||
@@ -691,11 +684,11 @@ impl Node {
|
||||
}
|
||||
|
||||
// Store their index
|
||||
conn.set_their_index(header.sender_idx);
|
||||
conn.set_source_addr(packet.remote_addr.clone());
|
||||
machine.set_conn_their_index(header.sender_idx);
|
||||
machine.set_conn_source_addr(packet.remote_addr.clone());
|
||||
|
||||
// Get peer identity for promotion (learned from msg2 in XX)
|
||||
let peer_identity = match conn.expected_identity() {
|
||||
let peer_identity = match machine.conn_expected_identity() {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
warn!(link_id = %link_id, "No identity after handshake");
|
||||
@@ -705,24 +698,17 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
let our_index = conn.our_index();
|
||||
let our_index = machine.our_index();
|
||||
|
||||
(peer_identity, msg3_bytes, our_index)
|
||||
};
|
||||
|
||||
let peer_node_addr = *peer_identity.node_addr();
|
||||
|
||||
// Mirror the leg's completion `touch` and the negotiated peer profile
|
||||
// onto the surviving carrier so the connection's last-activity advances
|
||||
// at msg2 completion (matching the leg's clock) and the promotion
|
||||
// hand-off reads the profile from the carrier.
|
||||
let negotiated_profile = self.leg(&link_id).and_then(|conn| conn.peer_profile());
|
||||
if let Some(machine) = self.peer_machines.get_mut(&link_id) {
|
||||
machine.touch_conn(packet.timestamp_ms);
|
||||
if let Some(profile) = negotiated_profile {
|
||||
machine.set_conn_peer_profile(profile);
|
||||
}
|
||||
}
|
||||
// The completion `touch` and the negotiated peer profile are written
|
||||
// directly onto the surviving carrier by `complete_handshake` and
|
||||
// `process_fmp_negotiation` above, so last-activity advances at msg2
|
||||
// completion and the promotion hand-off reads the profile from it.
|
||||
|
||||
// ACL check: with XX, this is the first point where the initiator
|
||||
// knows the responder's identity.
|
||||
@@ -784,12 +770,10 @@ impl Node {
|
||||
);
|
||||
// Failure moves to the machine (`send_failed`); the phase
|
||||
// stays `Handshaking{SentMsg1}` (promote has not run yet) so
|
||||
// the sweep reclaims the leg exactly as the pre-collapse leg
|
||||
// mark did.
|
||||
if let Some(conn) = self.leg_mut(&link_id) {
|
||||
conn.mark_failed();
|
||||
}
|
||||
// the sweep reclaims the handshake exactly as the
|
||||
// pre-collapse mark did.
|
||||
if let Some(machine) = self.peer_machines.get_mut(&link_id) {
|
||||
machine.mark_failed();
|
||||
machine.mark_send_failed();
|
||||
}
|
||||
self.stats_mut()
|
||||
@@ -887,10 +871,10 @@ impl Node {
|
||||
// right after the take — unconditionally, whether or not a
|
||||
// connection was carried — so none of this block's exits leave a
|
||||
// dangling machine.
|
||||
let taken_conn = self
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.and_then(|machine| machine.take_leg());
|
||||
let (taken_conn, carrier_our_index) = match self.peer_machines.get_mut(&link_id) {
|
||||
Some(machine) => (machine.take_leg(), machine.our_index()),
|
||||
None => (None, None),
|
||||
};
|
||||
self.remove_peer_machine(link_id);
|
||||
|
||||
let mut conn = match taken_conn {
|
||||
@@ -907,8 +891,8 @@ impl Node {
|
||||
if swap {
|
||||
// We're the smaller node. Swap to outbound session + indices.
|
||||
// The peer will keep their inbound session (complement of ours).
|
||||
let outbound_our_index = conn.our_index();
|
||||
let outbound_session = conn.take_session();
|
||||
let outbound_our_index = carrier_our_index;
|
||||
let outbound_session = conn.noise_session.take();
|
||||
|
||||
let (outbound_session, outbound_our_index) = match (
|
||||
outbound_session,
|
||||
@@ -976,7 +960,7 @@ impl Node {
|
||||
// their outbound session, that index is exactly what they'll use.
|
||||
// The msg2 sender_idx we see here is the peer's INBOUND our_index,
|
||||
// which becomes stale after the peer swaps.
|
||||
let outbound_our_index = conn.our_index();
|
||||
let outbound_our_index = carrier_our_index;
|
||||
|
||||
if let Some(peer) = self.peers.get(&peer_node_addr) {
|
||||
debug!(
|
||||
@@ -1106,8 +1090,8 @@ impl Node {
|
||||
let our_profile = self.node_profile();
|
||||
let (peer_identity, our_index, remote_epoch) = {
|
||||
// Get the pending connection
|
||||
let conn = match self.leg_mut(&link_id) {
|
||||
Some(c) => c,
|
||||
let machine = match self.peer_machines.get_mut(&link_id) {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
debug!(
|
||||
link_id = %link_id,
|
||||
@@ -1122,7 +1106,7 @@ impl Node {
|
||||
// Process msg3 — learns initiator's identity and epoch
|
||||
let noise_msg3 = &packet.data[header.noise_msg3_offset..];
|
||||
let received_negotiation =
|
||||
match conn.complete_handshake_msg3(noise_msg3, packet.timestamp_ms) {
|
||||
match machine.complete_handshake_msg3(noise_msg3, packet.timestamp_ms) {
|
||||
Ok(neg) => neg,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@@ -1131,10 +1115,11 @@ impl Node {
|
||||
"Msg3 processing failed"
|
||||
);
|
||||
// Clean up. Capture the index before disposing the
|
||||
// machine (and the connection embedded on it); reading
|
||||
// machine (and the Noise handles it carries); reading
|
||||
// it after the disposal would always return None and
|
||||
// leak the allocated index.
|
||||
let our_idx_to_free = self.leg(&link_id).and_then(|c| c.our_index());
|
||||
let our_idx_to_free =
|
||||
self.peer_machines.get(&link_id).and_then(|m| m.our_index());
|
||||
self.remove_link(&link_id);
|
||||
self.remove_peer_machine(link_id);
|
||||
if let Some(idx) = our_idx_to_free {
|
||||
@@ -1148,7 +1133,7 @@ impl Node {
|
||||
|
||||
// Process peer's FMP negotiation payload from msg3
|
||||
if let Some(neg_bytes) = &received_negotiation {
|
||||
match process_fmp_negotiation(our_profile, conn, neg_bytes) {
|
||||
match process_fmp_negotiation(our_profile, machine, neg_bytes) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
warn!(link_id = %link_id, our_profile = %our_profile, error = %e, "FMP negotiation failed");
|
||||
@@ -1162,7 +1147,7 @@ impl Node {
|
||||
}
|
||||
|
||||
// Learn peer identity from msg3
|
||||
let peer_identity = match conn.expected_identity() {
|
||||
let peer_identity = match machine.conn_expected_identity() {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
warn!("Identity not learned from msg3");
|
||||
@@ -1174,22 +1159,17 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
let our_index = conn.our_index();
|
||||
let remote_epoch = conn.remote_epoch();
|
||||
let our_index = machine.our_index();
|
||||
let remote_epoch = machine.conn_remote_epoch();
|
||||
|
||||
(peer_identity, our_index, remote_epoch)
|
||||
};
|
||||
|
||||
let peer_node_addr = *peer_identity.node_addr();
|
||||
|
||||
// Mirror the negotiated peer profile onto the surviving carrier so the
|
||||
// promotion hand-off reads it from the carrier, not the leg.
|
||||
let negotiated_profile = self.leg(&link_id).and_then(|conn| conn.peer_profile());
|
||||
if let Some(profile) = negotiated_profile
|
||||
&& let Some(machine) = self.peer_machines.get_mut(&link_id)
|
||||
{
|
||||
machine.set_conn_peer_profile(profile);
|
||||
}
|
||||
// The negotiated peer profile is written straight onto the surviving
|
||||
// carrier by `process_fmp_negotiation` above, so the promotion hand-off
|
||||
// reads it from there.
|
||||
|
||||
// ACL check: with XX, this is the first point where the responder
|
||||
// knows the initiator's identity.
|
||||
@@ -1208,8 +1188,8 @@ impl Node {
|
||||
// and the initiator has a matching session from processing
|
||||
// msg2. Reason `Other` is used instead of `SecurityViolation`
|
||||
// to avoid naming the ACL mechanism on the wire.
|
||||
let reject_info = match self.leg_mut(&link_id) {
|
||||
Some(conn) => match (conn.their_index(), conn.take_session()) {
|
||||
let reject_info = match self.peer_machines.get_mut(&link_id) {
|
||||
Some(machine) => match (machine.conn_their_index(), machine.take_session()) {
|
||||
(Some(idx), Some(session)) => Some((idx, session)),
|
||||
_ => None,
|
||||
},
|
||||
@@ -1486,14 +1466,14 @@ impl Node {
|
||||
link_id = %link_id,
|
||||
"Leaf node rejecting additional peer (single-peer enforcement)"
|
||||
);
|
||||
// Clean up the connection (taken off its machine first) and its
|
||||
// control machine
|
||||
if let Some(conn) = self
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.and_then(|machine| machine.take_leg())
|
||||
&& let Some(idx) = conn.our_index()
|
||||
{
|
||||
// Detach the Noise handles from the machine and free the index the
|
||||
// handshake allocated, then dispose of the machine itself.
|
||||
if let Some(idx) = self.peer_machines.get_mut(&link_id).and_then(|machine| {
|
||||
// Freeing stays conditional on a handshake actually being
|
||||
// attached, as it was when the index lived on the leg.
|
||||
machine.take_leg()?;
|
||||
machine.our_index()
|
||||
}) {
|
||||
let _ = self.index_allocator.free(idx);
|
||||
}
|
||||
self.remove_link(&link_id);
|
||||
@@ -1501,67 +1481,65 @@ impl Node {
|
||||
return Err(NodeError::MaxPeersExceeded { max: 1 });
|
||||
}
|
||||
|
||||
// Take the pending connection off its control machine. The machine
|
||||
// Take the pending connection off its control machine, and read the
|
||||
// carrier fields the promotion needs in the same borrow. The machine
|
||||
// survives the promotion (it becomes the active peer's control
|
||||
// machine), left with no pending connection.
|
||||
let mut connection = self
|
||||
//
|
||||
// The connection is detached before anything is validated, so every
|
||||
// error return below leaves the machine leg-less — the caller disposes
|
||||
// of it. Gathering the carrier reads up front is only a borrow shape:
|
||||
// they are infallible, so the order in which the missing-field errors
|
||||
// are reported below is unchanged.
|
||||
let machine = self
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.and_then(|machine| machine.take_leg())
|
||||
.ok_or(NodeError::ConnectionNotFound(link_id))?;
|
||||
let mut connection = machine
|
||||
.take_leg()
|
||||
.ok_or(NodeError::ConnectionNotFound(link_id))?;
|
||||
let carrier_our_index = machine.our_index();
|
||||
let carrier_their_index = machine.conn_their_index();
|
||||
let carrier_transport_id = machine.conn_transport_id();
|
||||
let carrier_source_addr = machine.conn_source_addr().cloned();
|
||||
let carrier_is_outbound = machine.conn_is_outbound();
|
||||
let carrier_remote_epoch = machine.conn_remote_epoch();
|
||||
let link_stats = machine.conn_link_stats().clone();
|
||||
// The negotiated peer profile, read from the same carrier borrow.
|
||||
let peer_profile = machine
|
||||
.conn_peer_profile()
|
||||
.unwrap_or(crate::proto::fmp::NodeProfile::Full);
|
||||
|
||||
// Verify handshake is complete and extract session
|
||||
if !connection.has_session() {
|
||||
if connection.noise_session.is_none() {
|
||||
return Err(NodeError::HandshakeIncomplete(link_id));
|
||||
}
|
||||
|
||||
let noise_session = connection
|
||||
.take_session()
|
||||
.noise_session
|
||||
.take()
|
||||
.ok_or(NodeError::NoSession(link_id))?;
|
||||
|
||||
let our_index = connection
|
||||
.our_index()
|
||||
.ok_or_else(|| NodeError::PromotionFailed {
|
||||
link_id,
|
||||
reason: "missing our_index".into(),
|
||||
})?;
|
||||
let their_index = self
|
||||
.peer_machines
|
||||
.get(&link_id)
|
||||
.and_then(|machine| machine.conn_their_index())
|
||||
.ok_or_else(|| NodeError::PromotionFailed {
|
||||
link_id,
|
||||
reason: "missing their_index".into(),
|
||||
})?;
|
||||
let transport_id = self
|
||||
.peer_machines
|
||||
.get(&link_id)
|
||||
.and_then(|machine| machine.conn_transport_id())
|
||||
.ok_or_else(|| NodeError::PromotionFailed {
|
||||
link_id,
|
||||
reason: "missing transport_id".into(),
|
||||
})?;
|
||||
let current_addr = connection
|
||||
.source_addr()
|
||||
.ok_or_else(|| NodeError::PromotionFailed {
|
||||
link_id,
|
||||
reason: "missing source_addr".into(),
|
||||
})?
|
||||
.clone();
|
||||
let link_stats = self
|
||||
.peer_machines
|
||||
.get(&link_id)
|
||||
.map(|machine| machine.conn_link_stats().clone())
|
||||
.unwrap_or_default();
|
||||
let remote_epoch = connection.remote_epoch();
|
||||
let peer_profile = self
|
||||
.peer_machines
|
||||
.get(&link_id)
|
||||
.and_then(|machine| machine.conn_peer_profile())
|
||||
.unwrap_or(crate::proto::fmp::NodeProfile::Full);
|
||||
let our_index = carrier_our_index.ok_or_else(|| NodeError::PromotionFailed {
|
||||
link_id,
|
||||
reason: "missing our_index".into(),
|
||||
})?;
|
||||
let their_index = carrier_their_index.ok_or_else(|| NodeError::PromotionFailed {
|
||||
link_id,
|
||||
reason: "missing their_index".into(),
|
||||
})?;
|
||||
let transport_id = carrier_transport_id.ok_or_else(|| NodeError::PromotionFailed {
|
||||
link_id,
|
||||
reason: "missing transport_id".into(),
|
||||
})?;
|
||||
let current_addr = carrier_source_addr.ok_or_else(|| NodeError::PromotionFailed {
|
||||
link_id,
|
||||
reason: "missing source_addr".into(),
|
||||
})?;
|
||||
let remote_epoch = carrier_remote_epoch;
|
||||
|
||||
let peer_node_addr = *verified_identity.node_addr();
|
||||
let is_outbound = connection.is_outbound();
|
||||
let is_outbound = carrier_is_outbound;
|
||||
|
||||
// Check for cross-connection
|
||||
if let Some(existing_peer) = self.peers.get(&peer_node_addr) {
|
||||
@@ -1719,12 +1697,13 @@ impl Node {
|
||||
// the 30s handshake timeout.
|
||||
let pending_to_same_peer: Vec<LinkId> = self
|
||||
.connections()
|
||||
.filter(|conn| {
|
||||
conn.expected_identity()
|
||||
.filter(|(_, machine)| {
|
||||
machine
|
||||
.conn_expected_identity()
|
||||
.map(|id| *id.node_addr() == peer_node_addr)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|conn| conn.link_id())
|
||||
.map(|(_, machine)| machine.link_id())
|
||||
.collect();
|
||||
|
||||
for pending_link_id in &pending_to_same_peer {
|
||||
@@ -1819,20 +1798,20 @@ impl Node {
|
||||
/// Process an FMP negotiation payload received from a peer.
|
||||
///
|
||||
/// Decodes the payload, validates profile pairing, and stores the
|
||||
/// results on the PeerConnection.
|
||||
/// results on the peer's control machine.
|
||||
fn process_fmp_negotiation(
|
||||
our_profile: crate::proto::fmp::NodeProfile,
|
||||
conn: &mut PeerConnection,
|
||||
machine: &mut PeerMachine,
|
||||
neg_bytes: &[u8],
|
||||
) -> Result<(), crate::proto::Error> {
|
||||
// The decode -> validate -> profile decision is the pure core split; the
|
||||
// shell records the result on the connection and logs.
|
||||
let their_profile = decide_fmp_negotiation(our_profile, neg_bytes)?;
|
||||
|
||||
conn.set_negotiation_results(their_profile);
|
||||
machine.set_conn_peer_profile(their_profile);
|
||||
|
||||
debug!(
|
||||
link_id = %conn.link_id(),
|
||||
link_id = %machine.link_id(),
|
||||
our_profile = %our_profile,
|
||||
peer_profile = %their_profile,
|
||||
"FMP negotiation complete"
|
||||
|
||||
@@ -270,7 +270,7 @@ impl Node {
|
||||
///
|
||||
/// Creates a new XX handshake as initiator, sends msg1 over the existing
|
||||
/// link (same transport, same remote address), and stores the handshake
|
||||
/// state on the ActivePeer. No new Link or PeerConnection is created.
|
||||
/// state on the ActivePeer. No new Link or handshake leg is created.
|
||||
async fn initiate_rekey(&mut self, node_addr: &NodeAddr) {
|
||||
let peer = match self.peers.get(node_addr) {
|
||||
Some(p) => p,
|
||||
@@ -302,7 +302,7 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
// Create XX initiator handshake directly (no PeerConnection)
|
||||
// Create XX initiator handshake directly (no handshake leg)
|
||||
let our_keypair = self.identity().keypair();
|
||||
let mut hs = HandshakeState::new_initiator(our_keypair);
|
||||
hs.set_local_epoch(self.startup_epoch());
|
||||
@@ -348,7 +348,7 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
// Store handshake state on the ActivePeer (not a separate PeerConnection)
|
||||
// Store handshake state on the ActivePeer (not a separate handshake leg)
|
||||
let resend_interval = self.config().node.rate_limit.handshake_resend_interval_ms;
|
||||
let now_ms = Self::now_ms();
|
||||
if let Some(peer) = self.peers.get_mut(node_addr) {
|
||||
|
||||
@@ -19,18 +19,18 @@ impl LifecycleView for Node {
|
||||
// reap.
|
||||
self.peer_machines
|
||||
.iter()
|
||||
.filter_map(|(link_id, machine)| machine.leg().map(|conn| (link_id, machine, conn)))
|
||||
.filter(|(link_id, machine, _conn)| {
|
||||
.filter(|(_, machine)| machine.leg().is_some())
|
||||
.filter(|(link_id, machine)| {
|
||||
machine.is_failed()
|
||||
|| (machine.conn_is_timed_out(now_ms, timeout_ms)
|
||||
&& !self.peer_timers.get(*link_id).is_some_and(|timers| {
|
||||
timers.contains_key(&TimerKind::HandshakeTimeout)
|
||||
}))
|
||||
})
|
||||
.map(|(link_id, _machine, conn)| ConnSnapshot {
|
||||
.map(|(link_id, machine)| ConnSnapshot {
|
||||
link: *link_id,
|
||||
is_outbound: conn.is_outbound(),
|
||||
retry_addr: conn.expected_identity().map(|id| *id.node_addr()),
|
||||
is_outbound: machine.conn_is_outbound(),
|
||||
retry_addr: machine.conn_expected_identity().map(|id| *id.node_addr()),
|
||||
resend_count: 0,
|
||||
msg1: Vec::new(),
|
||||
})
|
||||
@@ -77,8 +77,12 @@ impl Node {
|
||||
.peer_machines
|
||||
.get(&link)
|
||||
.is_some_and(|machine| machine.is_failed());
|
||||
if let Some(conn) = self.leg(&link) {
|
||||
let direction = conn.direction();
|
||||
if let Some(machine) = self
|
||||
.peer_machines
|
||||
.get(&link)
|
||||
.filter(|machine| machine.leg().is_some())
|
||||
{
|
||||
let direction = machine.conn_direction();
|
||||
if is_failed {
|
||||
debug!(
|
||||
link_id = %link,
|
||||
@@ -116,7 +120,7 @@ impl Node {
|
||||
// dangling machine. A no-op for promoted peers — `promote_connection`
|
||||
// already consumed their connection, so this reaper never runs for
|
||||
// them.
|
||||
let conn = match self
|
||||
let _detached_leg = match self
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.and_then(|machine| machine.take_leg())
|
||||
@@ -124,16 +128,16 @@ impl Node {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
// Read the transport ID off the surviving carrier before disposing the
|
||||
// machine (the leg no longer projects it).
|
||||
let transport_id = self
|
||||
.peer_machines
|
||||
.get(&link_id)
|
||||
.and_then(|machine| machine.conn_transport_id());
|
||||
// Read the transport ID and session index off the surviving carrier
|
||||
// before disposing the machine (the leg no longer projects them).
|
||||
let (transport_id, our_index) = match self.peer_machines.get(&link_id) {
|
||||
Some(machine) => (machine.conn_transport_id(), machine.our_index()),
|
||||
None => (None, None),
|
||||
};
|
||||
self.remove_peer_machine(link_id);
|
||||
|
||||
// Free session index and pending_outbound/pending_inbound if allocated
|
||||
if let Some(idx) = conn.our_index() {
|
||||
if let Some(idx) = our_index {
|
||||
if let Some(tid) = transport_id {
|
||||
self.pending_outbound.remove(&(tid, idx.as_u32()));
|
||||
self.pending_inbound.remove(&(tid, idx.as_u32()));
|
||||
@@ -190,24 +194,31 @@ impl Node {
|
||||
.collect();
|
||||
for link in timer_links {
|
||||
// The idle-timeout threshold reads the survivor carrier's
|
||||
// last-activity (the leg no longer projects it); the leg still
|
||||
// supplies direction/identity for the retry decision below.
|
||||
// last-activity; presence of a pending handshake is what decides
|
||||
// between reaping and dropping an orphan timer.
|
||||
let timed_out = self
|
||||
.peer_machines
|
||||
.get(&link)
|
||||
.is_some_and(|machine| machine.conn_is_timed_out(now_ms, timeout_ms));
|
||||
let (reap, retry_peer) = match self.leg(&link) {
|
||||
Some(conn) if timed_out => {
|
||||
let retry_peer = if conn.is_outbound() {
|
||||
conn.expected_identity().map(|id| *id.node_addr())
|
||||
let (reap, retry_peer) = match self.has_pending_leg(&link) {
|
||||
true if timed_out => {
|
||||
let retry_peer = if self
|
||||
.peer_machines
|
||||
.get(&link)
|
||||
.is_some_and(|machine| machine.conn_is_outbound())
|
||||
{
|
||||
self.peer_machines
|
||||
.get(&link)
|
||||
.and_then(|machine| machine.conn_expected_identity())
|
||||
.map(|id| *id.node_addr())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(true, retry_peer)
|
||||
}
|
||||
// Not yet idle-timed-out: leave the timer for a later tick.
|
||||
Some(_) => (false, None),
|
||||
None => {
|
||||
true => (false, None),
|
||||
false => {
|
||||
// Orphan timer (connection already reaped elsewhere) — drop it.
|
||||
if let Some(timers) = self.peer_timers.get_mut(&link) {
|
||||
timers.remove(&TimerKind::HandshakeTimeout);
|
||||
@@ -266,9 +277,10 @@ impl Node {
|
||||
// Skip a resend whose target peer already promoted via the inbound
|
||||
// cross-connection path — resending msg1 would start a new handshake
|
||||
// (session mismatch).
|
||||
if let Some(conn) = self.leg(&link)
|
||||
&& conn
|
||||
.expected_identity()
|
||||
if let Some(machine) = self.peer_machines.get(&link)
|
||||
&& machine.leg().is_some()
|
||||
&& machine
|
||||
.conn_expected_identity()
|
||||
.map(|id| self.peers.contains_key(id.node_addr()))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
@@ -327,17 +339,14 @@ impl Node {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (transport_id, remote_addr) = match self.leg(&link) {
|
||||
Some(conn) => match (
|
||||
self.peer_machines
|
||||
.get(&link)
|
||||
.and_then(|machine| machine.conn_transport_id()),
|
||||
conn.source_addr(),
|
||||
) {
|
||||
(Some(tid), Some(addr)) => (tid, addr.clone()),
|
||||
_ => continue,
|
||||
},
|
||||
None => continue,
|
||||
let (transport_id, remote_addr) = match self.peer_machines.get(&link) {
|
||||
Some(machine) if machine.leg().is_some() => {
|
||||
match (machine.conn_transport_id(), machine.conn_source_addr()) {
|
||||
(Some(tid), Some(addr)) => (tid, addr.clone()),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let sent = if let Some(transport) = self.transports.get(&transport_id) {
|
||||
|
||||
@@ -15,8 +15,7 @@ use crate::node::acl::PeerAclContext;
|
||||
use crate::node::dataplane::PeerActionCtx;
|
||||
use crate::nostr::{BootstrapEvent, NostrRendezvous};
|
||||
use crate::nostr::{BootstrapHandoffResult, EstablishedTraversal};
|
||||
use crate::peer::PeerConnection;
|
||||
use crate::peer::machine::{PeerEvent, PeerMachine};
|
||||
use crate::peer::machine::{HandshakeCrypto, PeerEvent, PeerMachine};
|
||||
use crate::proto::fmp::wire::build_msg1;
|
||||
use crate::proto::fmp::{Disconnect, DisconnectReason};
|
||||
use crate::transport::{Link, LinkDirection, LinkId, TransportAddr, TransportId, packet_channel};
|
||||
@@ -372,27 +371,28 @@ impl Node {
|
||||
}
|
||||
|
||||
fn is_connecting_to_peer(&self, peer_node_addr: &NodeAddr) -> bool {
|
||||
self.connections().any(|conn| {
|
||||
conn.expected_identity()
|
||||
self.connections().any(|(_, machine)| {
|
||||
machine
|
||||
.conn_expected_identity()
|
||||
.map(|id| id.node_addr() == peer_node_addr)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_connecting_to_peer_on_path(
|
||||
pub(in crate::node) fn is_connecting_to_peer_on_path(
|
||||
&self,
|
||||
peer_node_addr: &NodeAddr,
|
||||
transport_id: TransportId,
|
||||
remote_addr: &TransportAddr,
|
||||
) -> bool {
|
||||
self.peer_machines.values().any(|machine| {
|
||||
machine.leg().is_some_and(|conn| {
|
||||
conn.expected_identity()
|
||||
machine.leg().is_some()
|
||||
&& machine
|
||||
.conn_expected_identity()
|
||||
.map(|id| id.node_addr() == peer_node_addr)
|
||||
.unwrap_or(false)
|
||||
&& machine.conn_transport_id() == Some(transport_id)
|
||||
&& conn.source_addr() == Some(remote_addr)
|
||||
})
|
||||
&& machine.conn_transport_id() == Some(transport_id)
|
||||
&& machine.conn_source_addr() == Some(remote_addr)
|
||||
}) || self.peering.pending_connects.iter().any(|pending| {
|
||||
pending
|
||||
.peer_identity
|
||||
@@ -607,9 +607,22 @@ impl Node {
|
||||
) -> Result<(), NodeError> {
|
||||
let peer_node_addr = *peer_identity.node_addr();
|
||||
|
||||
// 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);
|
||||
|
||||
// The control machine drives the handshake, so it takes the crypto
|
||||
// carrier 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, Some(peer_identity), current_time_ms)
|
||||
})
|
||||
.set_leg(HandshakeCrypto::new());
|
||||
|
||||
// Allocate a session index for this handshake
|
||||
let our_index = match self.index_allocator.allocate() {
|
||||
@@ -626,23 +639,30 @@ 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());
|
||||
self.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.expect("dial-time machine carries the connection")
|
||||
.set_conn_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);
|
||||
@@ -665,20 +685,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, Some(peer_identity), current_time_ms)
|
||||
});
|
||||
let machine = self
|
||||
.peer_machines
|
||||
.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);
|
||||
@@ -686,14 +699,12 @@ 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 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(())
|
||||
}
|
||||
@@ -712,14 +723,17 @@ impl Node {
|
||||
remote_addr: TransportAddr,
|
||||
peer_identity: Option<PeerIdentity>,
|
||||
) -> Result<(), NodeError> {
|
||||
// Create connection in handshake phase. Anonymous discovery
|
||||
// Create the control machine in handshake phase. Anonymous discovery
|
||||
// (no peer_identity) leaves identity to be learned from XX msg2.
|
||||
//
|
||||
// The machine drives the Noise crypto, so it is built before the leaf
|
||||
// runs — but it stays a LOCAL until all fallible setup has succeeded.
|
||||
// The index-alloc and Noise Err returns below therefore still just drop
|
||||
// it, leaving no registry trace and needing no disposal, exactly as the
|
||||
// pre-collapse local connection did.
|
||||
let current_time_ms = Self::now_ms();
|
||||
let mut connection = if let Some(identity) = peer_identity {
|
||||
PeerConnection::outbound(link_id, identity, current_time_ms)
|
||||
} else {
|
||||
PeerConnection::outbound_anonymous(link_id, current_time_ms)
|
||||
};
|
||||
let mut machine = PeerMachine::new_outbound(link_id, peer_identity, current_time_ms);
|
||||
machine.set_leg(HandshakeCrypto::new());
|
||||
|
||||
// Allocate a session index for this handshake
|
||||
let our_index = match self.index_allocator.allocate() {
|
||||
@@ -735,7 +749,7 @@ 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) {
|
||||
match machine.start_handshake(our_keypair, self.startup_epoch(), current_time_ms) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
// Clean up the index and link
|
||||
@@ -746,15 +760,15 @@ impl Node {
|
||||
}
|
||||
};
|
||||
|
||||
// Set index and transport info on the connection
|
||||
connection.set_our_index(our_index);
|
||||
connection.set_transport_id(transport_id);
|
||||
connection.set_source_addr(remote_addr.clone());
|
||||
// Set index and transport info on the surviving carrier
|
||||
machine.set_conn_our_index(our_index);
|
||||
machine.set_conn_transport_id(transport_id);
|
||||
machine.set_conn_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);
|
||||
|
||||
if let Some(id) = connection.expected_identity() {
|
||||
if let Some(id) = machine.conn_expected_identity() {
|
||||
debug!(
|
||||
peer = %self.peer_display_name(id.node_addr()),
|
||||
transport_id = %transport_id,
|
||||
@@ -773,37 +787,28 @@ impl Node {
|
||||
);
|
||||
}
|
||||
|
||||
// Store msg1 for resend and schedule first resend
|
||||
// Store msg1 for resend and schedule first resend. The carrier holds
|
||||
// the resend wire; the retransmit driver reads it from there.
|
||||
let resend_interval = self.config().node.rate_limit.handshake_resend_interval_ms;
|
||||
let first_resend_at_ms = current_time_ms + resend_interval;
|
||||
connection.set_handshake_msg1(wire_msg1.clone(), first_resend_at_ms);
|
||||
machine.set_conn_handshake_msg1(wire_msg1.clone(), first_resend_at_ms);
|
||||
|
||||
// Track in pending_outbound for msg2 dispatch
|
||||
self.pending_outbound
|
||||
.insert((transport_id, our_index.as_u32()), link_id);
|
||||
// The leg's persistent control machine is born carrying its pending
|
||||
// connection, after all fallible setup (the index-alloc and Noise Err
|
||||
// returns above predate it and need no disposal). Both production
|
||||
// callers dial anonymously (identified dials persist their machine at
|
||||
// dial and go through `prepare_outbound_msg1`), so the machine starts
|
||||
// identity-less; `handle_msg2` crystallizes the identity the XX
|
||||
// handshake learns. Inserted before the send below so no suspension
|
||||
// point observes a leg in flight without a machine. The send-failure
|
||||
// arm retains the failed connection, and the machine keeps carrying
|
||||
// it — the stale-connection reaper disposes both together. No timers
|
||||
// are armed and no event is dispatched: this path sends msg1 inline,
|
||||
// so the machine parks at `Discovered` until msg2.
|
||||
let mut machine = PeerMachine::new_outbound(link_id, peer_identity, current_time_ms);
|
||||
// Seed the surviving carrier with the leg's msg1-prep provenance so the
|
||||
// started_at/last_activity timestamps, the transport ID, our index, and
|
||||
// the msg1 resend wire all live on the carrier — the promotion hand-off
|
||||
// and the retransmit driver read them there, not the leg.
|
||||
machine.set_conn_started_at(current_time_ms);
|
||||
machine.touch_conn(current_time_ms);
|
||||
machine.set_conn_transport_id(transport_id);
|
||||
machine.set_conn_our_index(our_index);
|
||||
machine.set_conn_handshake_msg1(wire_msg1.clone(), first_resend_at_ms);
|
||||
machine.set_leg(connection);
|
||||
// The persistent control machine enters the registry here, after all
|
||||
// fallible setup. Both production callers dial anonymously (identified
|
||||
// dials persist their machine at dial and go through
|
||||
// `prepare_outbound_msg1`), so the machine starts identity-less;
|
||||
// `handle_msg2` crystallizes the identity the XX handshake learns.
|
||||
// Inserted before the send below so no suspension point observes a
|
||||
// handshake in flight without a machine. The send-failure arm retains
|
||||
// the failed handshake and the machine keeps carrying it — the
|
||||
// stale-connection reaper disposes both together. No timers are armed
|
||||
// and no event is dispatched: this path sends msg1 inline, so the
|
||||
// machine parks at `Discovered` until msg2. The carrier was born on
|
||||
// this same `current_time_ms`, so its `started_at`/`last_activity`
|
||||
// already carry the msg1-prep provenance.
|
||||
self.peer_machines.insert(link_id, machine);
|
||||
|
||||
// Send the wire format handshake message
|
||||
@@ -866,7 +871,11 @@ impl Node {
|
||||
Some(w) => w.to_vec(),
|
||||
None => return,
|
||||
};
|
||||
let our_index = self.leg(&link_id).and_then(|c| c.our_index());
|
||||
let our_index = self
|
||||
.peer_machines
|
||||
.get(&link_id)
|
||||
.filter(|machine| machine.leg().is_some())
|
||||
.and_then(|machine| machine.our_index());
|
||||
|
||||
// Send the wire format handshake message
|
||||
if let Some(transport) = self.transports.get(&transport_id) {
|
||||
@@ -1130,12 +1139,13 @@ impl Node {
|
||||
let now_ms = Self::now_ms();
|
||||
let stale: Vec<LinkId> = self
|
||||
.connections()
|
||||
.filter(|conn| {
|
||||
conn.expected_identity()
|
||||
.filter(|(_, machine)| {
|
||||
machine
|
||||
.conn_expected_identity()
|
||||
.map(|id| id.node_addr() == &peer_addr)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|conn| conn.link_id())
|
||||
.map(|(_, machine)| machine.link_id())
|
||||
.collect();
|
||||
for link_id in stale {
|
||||
self.cleanup_stale_connection(link_id, now_ms);
|
||||
@@ -2959,11 +2969,11 @@ impl Node {
|
||||
let connected: HashSet<NodeAddr> = self.peers.keys().copied().collect();
|
||||
let connecting: HashSet<NodeAddr> = self
|
||||
.connections()
|
||||
.filter_map(|conn| conn.expected_identity().map(|id| *id.node_addr()))
|
||||
.filter_map(|(_, machine)| machine.conn_expected_identity().map(|id| *id.node_addr()))
|
||||
.collect();
|
||||
let mut in_flight_by_peer: HashMap<NodeAddr, usize> = HashMap::new();
|
||||
for conn in self.connections() {
|
||||
if let Some(id) = conn.expected_identity() {
|
||||
for (_, machine) in self.connections() {
|
||||
if let Some(id) = machine.conn_expected_identity() {
|
||||
*in_flight_by_peer.entry(*id.node_addr()).or_default() += 1;
|
||||
}
|
||||
}
|
||||
@@ -3035,8 +3045,9 @@ impl Node {
|
||||
|
||||
let in_flight_for_peer = self
|
||||
.connections()
|
||||
.filter(|conn| {
|
||||
conn.expected_identity()
|
||||
.filter(|(_, machine)| {
|
||||
machine
|
||||
.conn_expected_identity()
|
||||
.map(|identity| identity.node_addr() == peer_node_addr)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
|
||||
205
src/node/mod.rs
205
src/node/mod.rs
@@ -50,8 +50,8 @@ use self::reloadable::Reloadable;
|
||||
pub(crate) const REKEY_JITTER_SECS: i64 = 15;
|
||||
use crate::cache::CoordCache;
|
||||
use crate::node::session::SessionEntry;
|
||||
use crate::peer::ActivePeer;
|
||||
use crate::peer::machine::{PeerMachine, TimerKind};
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
use crate::proto::bloom::{BloomFilter, BloomState};
|
||||
use crate::proto::fmp::Fmp;
|
||||
use crate::proto::fmp::NodeProfile;
|
||||
@@ -367,9 +367,9 @@ pub struct Node {
|
||||
|
||||
// === Per-Peer Control Machines ===
|
||||
/// Per-peer lifecycle control FSMs, keyed by the stable `LinkId` that spans
|
||||
/// the handshake→active lifetime. Each machine owns its pending handshake
|
||||
/// connection (the `PeerConnection` leg) while the handshake is in
|
||||
/// progress — the single LinkId-keyed per-peer map on `Node`; `peers`
|
||||
/// the handshake→active lifetime. Each machine owns its handshake crypto
|
||||
/// carrier while the handshake is in progress — the single LinkId-keyed
|
||||
/// per-peer map on `Node`; `peers`
|
||||
/// stays byte-unchanged (hot path pristine).
|
||||
/// Machines are inserted at dial and inbound msg1, and stepped in production
|
||||
/// by the handshake handlers, the rekey-cadence and liveness-reap routers,
|
||||
@@ -2018,15 +2018,17 @@ impl Node {
|
||||
// --- connections (show_connections) ---
|
||||
let connection_rows: Vec<snap::ConnectionRow> = self
|
||||
.connections()
|
||||
.map(|conn| snap::ConnectionRow {
|
||||
link_id: conn.link_id().as_u64(),
|
||||
direction: format!("{}", conn.direction()),
|
||||
handshake_state: self.connection_handshake_state(conn.link_id()).to_string(),
|
||||
started_at_ms: self.connection_started_at(conn.link_id()),
|
||||
last_activity_ms: self.connection_last_activity(conn.link_id()),
|
||||
resend_count: self.connection_resend_count(conn.link_id()),
|
||||
.map(|(_, machine)| snap::ConnectionRow {
|
||||
link_id: machine.link_id().as_u64(),
|
||||
direction: format!("{}", machine.conn_direction()),
|
||||
handshake_state: self
|
||||
.connection_handshake_state(machine.link_id())
|
||||
.to_string(),
|
||||
started_at_ms: self.connection_started_at(machine.link_id()),
|
||||
last_activity_ms: self.connection_last_activity(machine.link_id()),
|
||||
resend_count: self.connection_resend_count(machine.link_id()),
|
||||
expected_peer: self
|
||||
.connection_expected_identity(conn.link_id())
|
||||
.connection_expected_identity(machine.link_id())
|
||||
.map(|id| id.npub()),
|
||||
})
|
||||
.collect();
|
||||
@@ -2442,35 +2444,29 @@ impl Node {
|
||||
|
||||
// === Connection Management (Handshake Phase) ===
|
||||
|
||||
/// The pending connection for `link_id`, read through the control machine
|
||||
/// that carries it.
|
||||
fn leg(&self, link_id: &LinkId) -> Option<&PeerConnection> {
|
||||
/// Whether `link_id` has a pending handshake, read through the control
|
||||
/// machine that carries it.
|
||||
fn has_pending_leg(&self, link_id: &LinkId) -> bool {
|
||||
self.peer_machines
|
||||
.get(link_id)
|
||||
.and_then(|machine| machine.leg())
|
||||
}
|
||||
|
||||
/// Mutable access to the pending connection for `link_id`.
|
||||
fn leg_mut(&mut self, link_id: &LinkId) -> Option<&mut PeerConnection> {
|
||||
self.peer_machines
|
||||
.get_mut(link_id)
|
||||
.and_then(|machine| machine.leg_mut())
|
||||
}
|
||||
|
||||
/// Add a pending connection.
|
||||
///
|
||||
/// Seeds a control machine for the leg when none exists yet and embeds the
|
||||
/// connection on it, for callers that insert a connection directly rather
|
||||
/// than through the dial or inbound-msg1 paths, which build the machine
|
||||
/// themselves.
|
||||
pub fn add_connection(&mut self, connection: PeerConnection) -> Result<(), NodeError> {
|
||||
let link_id = connection.link_id();
|
||||
|
||||
if self
|
||||
.peer_machines
|
||||
.get(&link_id)
|
||||
.is_some_and(|machine| machine.leg().is_some())
|
||||
{
|
||||
}
|
||||
|
||||
/// Test-support: seed a control machine for `seed.link_id` directly,
|
||||
/// without the caller having to stage a pending handshake by hand.
|
||||
///
|
||||
/// The machine is chosen the way the establish paths choose it — outbound
|
||||
/// when the seed names a peer, inbound otherwise — and its carrier is
|
||||
/// seeded with every field a promotion reads. Built through
|
||||
/// `entry(..).or_insert_with(..)`, so an existing handshake-less machine
|
||||
/// keeps its constructor-side fields rather than being rebuilt.
|
||||
/// Post-construction `started_at` and the stored handshake bytes are not
|
||||
/// seeded; the establish paths write those at their own points.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn seed_handshake_machine(&mut self, seed: HandshakeSeed) -> Result<(), NodeError> {
|
||||
let link_id = seed.link_id;
|
||||
|
||||
if self.has_pending_leg(&link_id) {
|
||||
return Err(NodeError::ConnectionAlreadyExists(link_id));
|
||||
}
|
||||
|
||||
@@ -2480,55 +2476,45 @@ impl Node {
|
||||
});
|
||||
}
|
||||
|
||||
let machine = self.peer_machines.entry(link_id).or_insert_with(|| {
|
||||
let now = connection.started_at();
|
||||
if connection.is_outbound() {
|
||||
PeerMachine::new_outbound(link_id, connection.expected_identity().copied(), now)
|
||||
} else {
|
||||
PeerMachine::new_inbound(link_id, now)
|
||||
}
|
||||
});
|
||||
// Seed the surviving carrier's peer index and transport from the
|
||||
// pre-built leg so the promotion hand-off reads them from the machine,
|
||||
// matching the establish paths that write them on the machine directly.
|
||||
if let Some(their) = connection.their_index() {
|
||||
machine.set_conn_their_index(their);
|
||||
let started_at_ms = seed.started_at_ms;
|
||||
let expected_identity = seed.expected_identity;
|
||||
let machine =
|
||||
self.peer_machines
|
||||
.entry(link_id)
|
||||
.or_insert_with(|| match expected_identity {
|
||||
Some(_) => PeerMachine::new_outbound(link_id, expected_identity, started_at_ms),
|
||||
None => PeerMachine::new_inbound(link_id, started_at_ms),
|
||||
});
|
||||
if let Some(index) = seed.our_index {
|
||||
machine.set_conn_our_index(index);
|
||||
}
|
||||
if let Some(tid) = connection.transport_id() {
|
||||
machine.set_conn_transport_id(tid);
|
||||
if let Some(index) = seed.their_index {
|
||||
machine.set_conn_their_index(index);
|
||||
}
|
||||
machine.set_leg(connection);
|
||||
if let Some(id) = seed.transport_id {
|
||||
machine.set_conn_transport_id(id);
|
||||
}
|
||||
if let Some(addr) = seed.source_addr {
|
||||
machine.set_conn_source_addr(addr);
|
||||
}
|
||||
machine.set_leg(crate::peer::machine::HandshakeCrypto::new());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a connection by LinkId.
|
||||
pub fn get_connection(&self, link_id: &LinkId) -> Option<&PeerConnection> {
|
||||
self.leg(link_id)
|
||||
}
|
||||
|
||||
/// Get a mutable connection by LinkId.
|
||||
pub fn get_connection_mut(&mut self, link_id: &LinkId) -> Option<&mut PeerConnection> {
|
||||
self.leg_mut(link_id)
|
||||
}
|
||||
|
||||
/// Remove a connection, disposing its control machine alongside
|
||||
/// (the disposal complement of `add_connection`'s machine seeding). The
|
||||
/// connection is taken off the machine BEFORE the machine is dropped, so
|
||||
/// the caller still receives it.
|
||||
pub fn remove_connection(&mut self, link_id: &LinkId) -> Option<PeerConnection> {
|
||||
let connection = self
|
||||
.peer_machines
|
||||
.get_mut(link_id)
|
||||
.and_then(|machine| machine.take_leg());
|
||||
self.remove_peer_machine(*link_id);
|
||||
connection
|
||||
}
|
||||
|
||||
/// Iterate over all connections.
|
||||
pub fn connections(&self) -> impl Iterator<Item = &PeerConnection> {
|
||||
/// Iterate over the control machines that carry a pending connection.
|
||||
///
|
||||
/// Carrying a pending connection is what makes a machine handshake-phase,
|
||||
/// so the filter below is the membership rule. It is the same predicate
|
||||
/// that `connection_count` applies, and the one the stale-connection sweep
|
||||
/// narrows further.
|
||||
///
|
||||
/// Internal to the crate: this yields the control machine, which is not
|
||||
/// part of the published surface. Callers outside the crate that need a
|
||||
/// view of the pending handshakes go through the operator queries.
|
||||
pub(crate) fn connections(&self) -> impl Iterator<Item = (&LinkId, &PeerMachine)> {
|
||||
self.peer_machines
|
||||
.values()
|
||||
.filter_map(|machine| machine.leg())
|
||||
.iter()
|
||||
.filter(|(_, machine)| machine.leg().is_some())
|
||||
}
|
||||
|
||||
// === Peer Management (Active Phase) ===
|
||||
@@ -3289,3 +3275,60 @@ impl fmt::Debug for Node {
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-support seed spec for [`Node::seed_handshake_machine`].
|
||||
///
|
||||
/// Carries the carrier fields a seeded machine needs before any crypto runs.
|
||||
/// Only fields the establish paths write at seed time belong here; the Noise
|
||||
/// handshake is driven afterwards through the machine's own crypto methods.
|
||||
#[cfg(test)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct HandshakeSeed {
|
||||
link_id: LinkId,
|
||||
expected_identity: Option<PeerIdentity>,
|
||||
started_at_ms: u64,
|
||||
transport_id: Option<TransportId>,
|
||||
source_addr: Option<TransportAddr>,
|
||||
our_index: Option<crate::utils::index::SessionIndex>,
|
||||
their_index: Option<crate::utils::index::SessionIndex>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl HandshakeSeed {
|
||||
/// Outbound leg: we know who we are dialing.
|
||||
pub(crate) fn outbound(
|
||||
link_id: LinkId,
|
||||
expected_identity: PeerIdentity,
|
||||
started_at_ms: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
link_id,
|
||||
expected_identity: Some(expected_identity),
|
||||
started_at_ms,
|
||||
transport_id: None,
|
||||
source_addr: None,
|
||||
our_index: None,
|
||||
their_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_transport_id(mut self, transport_id: TransportId) -> Self {
|
||||
self.transport_id = Some(transport_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_source_addr(mut self, source_addr: TransportAddr) -> Self {
|
||||
self.source_addr = Some(source_addr);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_our_index(mut self, index: crate::utils::index::SessionIndex) -> Self {
|
||||
self.our_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_their_index(mut self, index: crate::utils::index::SessionIndex) -> Self {
|
||||
self.their_index = Some(index);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ async fn test_outbound_connect_denied_by_denylist() {
|
||||
assert_eq!(node.peer_count(), 0);
|
||||
}
|
||||
|
||||
// The master-line `test_inbound_msg1_denied_by_acl` has no counterpart here.
|
||||
// Under XX, msg1 is an ephemeral-only exchange that carries no identity, so
|
||||
// there is no peer to authorize at that step and no ACL decision to assert.
|
||||
// The equivalent gate on this line runs once msg3 has revealed the initiator's
|
||||
// static key; `test_inbound_msg3_denied_by_acl` covers it there.
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_outbound_msg2_denied_after_acl_reload() {
|
||||
let (dir, mut node_a) = make_acl_node();
|
||||
@@ -56,14 +62,23 @@ async fn test_outbound_msg2_denied_after_acl_reload() {
|
||||
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
|
||||
|
||||
let link_id_a = node_a.allocate_link_id();
|
||||
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
|
||||
let our_index_a = node_a.index_allocator.allocate().unwrap();
|
||||
let noise_msg1 = conn_a
|
||||
.start_handshake(node_a.identity().keypair(), node_a.startup_epoch(), 1000)
|
||||
node_a
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id_a, peer_b_identity, 1000)
|
||||
.with_our_index(our_index_a)
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(remote_addr.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let keypair_a = node_a.identity().keypair();
|
||||
let epoch_a = node_a.startup_epoch();
|
||||
let noise_msg1 = node_a
|
||||
.peer_machines
|
||||
.get_mut(&link_id_a)
|
||||
.unwrap()
|
||||
.start_handshake(keypair_a, epoch_a, 1000)
|
||||
.unwrap();
|
||||
conn_a.set_our_index(our_index_a);
|
||||
conn_a.set_transport_id(transport_id);
|
||||
conn_a.set_source_addr(remote_addr.clone());
|
||||
|
||||
let link_a = Link::connectionless(
|
||||
link_id_a,
|
||||
@@ -76,12 +91,11 @@ async fn test_outbound_msg2_denied_after_acl_reload() {
|
||||
node_a
|
||||
.addr_to_link
|
||||
.insert((transport_id, remote_addr.clone()), link_id_a);
|
||||
node_a.add_connection(conn_a).unwrap();
|
||||
node_a
|
||||
.pending_outbound
|
||||
.insert((transport_id, our_index_a.as_u32()), link_id_a);
|
||||
|
||||
let mut conn_b = PeerConnection::inbound(LinkId::new(2), 1000);
|
||||
let mut conn_b = inbound_leg(LinkId::new(2), 1000);
|
||||
let responder_epoch = [0x11; 8];
|
||||
let noise_msg2 = conn_b
|
||||
.receive_handshake_init(
|
||||
@@ -175,14 +189,25 @@ async fn test_inbound_msg3_denied_triggers_disconnect() {
|
||||
|
||||
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
|
||||
let link_id_a = node_a.allocate_link_id();
|
||||
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
|
||||
let our_index_a = node_a.index_allocator.allocate().unwrap();
|
||||
let noise_msg1 = conn_a
|
||||
.start_handshake(node_a.identity().keypair(), node_a.startup_epoch(), 1000)
|
||||
// Mirror the production dial path: the seam seeds the outbound leg's
|
||||
// control machine, which owns the handshake crypto.
|
||||
node_a
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id_a, peer_b_identity, 1000)
|
||||
.with_our_index(our_index_a)
|
||||
.with_transport_id(transport_id_a)
|
||||
.with_source_addr(addr_b.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair_a = node_a.identity().keypair();
|
||||
let startup_epoch_a = node_a.startup_epoch();
|
||||
let noise_msg1 = node_a
|
||||
.peer_machines
|
||||
.get_mut(&link_id_a)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair_a, startup_epoch_a, 1000)
|
||||
.unwrap();
|
||||
conn_a.set_our_index(our_index_a);
|
||||
conn_a.set_transport_id(transport_id_a);
|
||||
conn_a.set_source_addr(addr_b.clone());
|
||||
|
||||
let wire_msg1 = build_msg1(our_index_a, &noise_msg1);
|
||||
let link_a = Link::connectionless(
|
||||
@@ -196,9 +221,6 @@ async fn test_inbound_msg3_denied_triggers_disconnect() {
|
||||
node_a
|
||||
.addr_to_link
|
||||
.insert((transport_id_a, addr_b.clone()), link_id_a);
|
||||
// Mirror the production dial path: the seam seeds the outbound leg's
|
||||
// control machine and embeds the connection on it.
|
||||
node_a.add_connection(conn_a).unwrap();
|
||||
node_a
|
||||
.pending_outbound
|
||||
.insert((transport_id_a, our_index_a.as_u32()), link_id_a);
|
||||
@@ -308,3 +330,9 @@ async fn test_outbound_connect_not_denied_by_allowlist_miss() {
|
||||
|
||||
assert!(!matches!(result, Err(NodeError::AccessDenied(_))));
|
||||
}
|
||||
|
||||
// The master-line `test_acl_rejected_msg1_leaves_no_registry_trace` asserts a
|
||||
// no-registry-trace property at the msg1 ACL denial. XX has no ACL decision at
|
||||
// msg1 (identity is unknown until msg3), so the property has no landing site at
|
||||
// that step. The same guard belongs at the msg3 `authorize_peer` gate, where
|
||||
// this line actually rejects an inbound peer.
|
||||
|
||||
@@ -143,9 +143,8 @@ async fn test_adopted_traversal_skips_already_connected_peer() {
|
||||
|
||||
let transport_id = TransportId::new(1);
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1_000);
|
||||
let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1_000);
|
||||
let peer_node_addr = *peer_identity.node_addr();
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, peer_identity, 2_000)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -28,10 +28,9 @@ fn test_decrypt_failure_threshold_removes_peer() {
|
||||
|
||||
// Build a fully-promoted active peer with our_index/transport_id set
|
||||
// so peers_by_index is populated by promote_connection.
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1_000);
|
||||
let identity = seed_completed_connection(&mut node, link_id, transport_id, 1_000);
|
||||
let node_addr = *identity.node_addr();
|
||||
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, identity, 2_000).unwrap();
|
||||
|
||||
// Sanity: peer is registered and indexed.
|
||||
|
||||
@@ -56,19 +56,28 @@ async fn test_two_node_handshake_udp() {
|
||||
let peer_b_node_addr = *peer_b_identity.node_addr();
|
||||
|
||||
let link_id_a = node_a.allocate_link_id();
|
||||
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
|
||||
|
||||
// Allocate session index for A's outbound
|
||||
let our_index_a = node_a.index_allocator.allocate().unwrap();
|
||||
|
||||
node_a
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id_a, peer_b_identity, 1000)
|
||||
.with_our_index(our_index_a)
|
||||
.with_transport_id(transport_id_a)
|
||||
.with_source_addr(remote_addr_b.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Start handshake (generates Noise XX msg1)
|
||||
let our_keypair_a = node_a.identity().keypair();
|
||||
let noise_msg1 = conn_a
|
||||
.start_handshake(our_keypair_a, node_a.startup_epoch(), 1000)
|
||||
let startup_epoch_a = node_a.startup_epoch();
|
||||
let noise_msg1 = node_a
|
||||
.peer_machines
|
||||
.get_mut(&link_id_a)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair_a, startup_epoch_a, 1000)
|
||||
.unwrap();
|
||||
conn_a.set_our_index(our_index_a);
|
||||
conn_a.set_transport_id(transport_id_a);
|
||||
conn_a.set_source_addr(remote_addr_b.clone());
|
||||
|
||||
// Build wire msg1 and track in node state
|
||||
let wire_msg1 = build_msg1(our_index_a, &noise_msg1);
|
||||
@@ -81,7 +90,6 @@ async fn test_two_node_handshake_udp() {
|
||||
Duration::from_millis(100),
|
||||
);
|
||||
node_a.links.insert(link_id_a, link_a);
|
||||
node_a.add_connection(conn_a).unwrap();
|
||||
node_a
|
||||
.pending_outbound
|
||||
.insert((transport_id_a, our_index_a.as_u32()), link_id_a);
|
||||
@@ -319,16 +327,24 @@ async fn test_run_rx_loop_handshake() {
|
||||
let peer_b_node_addr = *peer_b_identity.node_addr();
|
||||
|
||||
let link_id_a = node_a.allocate_link_id();
|
||||
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
|
||||
|
||||
let our_index_a = node_a.index_allocator.allocate().unwrap();
|
||||
let our_keypair_a = node_a.identity().keypair();
|
||||
let noise_msg1 = conn_a
|
||||
.start_handshake(our_keypair_a, node_a.startup_epoch(), 1000)
|
||||
node_a
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id_a, peer_b_identity, 1000)
|
||||
.with_our_index(our_index_a)
|
||||
.with_transport_id(transport_id_a)
|
||||
.with_source_addr(remote_addr_b.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair_a = node_a.identity().keypair();
|
||||
let startup_epoch_a = node_a.startup_epoch();
|
||||
let noise_msg1 = node_a
|
||||
.peer_machines
|
||||
.get_mut(&link_id_a)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair_a, startup_epoch_a, 1000)
|
||||
.unwrap();
|
||||
conn_a.set_our_index(our_index_a);
|
||||
conn_a.set_transport_id(transport_id_a);
|
||||
conn_a.set_source_addr(remote_addr_b.clone());
|
||||
|
||||
let wire_msg1 = build_msg1(our_index_a, &noise_msg1);
|
||||
|
||||
@@ -340,7 +356,6 @@ async fn test_run_rx_loop_handshake() {
|
||||
Duration::from_millis(100),
|
||||
);
|
||||
node_a.links.insert(link_id_a, link_a);
|
||||
node_a.add_connection(conn_a).unwrap();
|
||||
node_a
|
||||
.pending_outbound
|
||||
.insert((transport_id_a, our_index_a.as_u32()), link_id_a);
|
||||
@@ -505,15 +520,23 @@ async fn test_cross_connection_both_initiate() {
|
||||
|
||||
// Node A initiates to Node B
|
||||
let link_id_a_out = node_a.allocate_link_id();
|
||||
let mut conn_a = PeerConnection::outbound(link_id_a_out, peer_b_identity, 1000);
|
||||
let our_index_a = node_a.index_allocator.allocate().unwrap();
|
||||
let our_keypair_a = node_a.identity().keypair();
|
||||
let noise_msg1_a = conn_a
|
||||
.start_handshake(our_keypair_a, node_a.startup_epoch(), 1000)
|
||||
node_a
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id_a_out, peer_b_identity, 1000)
|
||||
.with_our_index(our_index_a)
|
||||
.with_transport_id(transport_id_a)
|
||||
.with_source_addr(remote_addr_b.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair_a = node_a.identity().keypair();
|
||||
let startup_epoch_a = node_a.startup_epoch();
|
||||
let noise_msg1_a = node_a
|
||||
.peer_machines
|
||||
.get_mut(&link_id_a_out)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair_a, startup_epoch_a, 1000)
|
||||
.unwrap();
|
||||
conn_a.set_our_index(our_index_a);
|
||||
conn_a.set_transport_id(transport_id_a);
|
||||
conn_a.set_source_addr(remote_addr_b.clone());
|
||||
|
||||
let wire_msg1_a = build_msg1(our_index_a, &noise_msg1_a);
|
||||
|
||||
@@ -528,22 +551,29 @@ async fn test_cross_connection_both_initiate() {
|
||||
node_a
|
||||
.addr_to_link
|
||||
.insert((transport_id_a, remote_addr_b.clone()), link_id_a_out);
|
||||
node_a.add_connection(conn_a).unwrap();
|
||||
node_a
|
||||
.pending_outbound
|
||||
.insert((transport_id_a, our_index_a.as_u32()), link_id_a_out);
|
||||
|
||||
// Node B initiates to Node A
|
||||
let link_id_b_out = node_b.allocate_link_id();
|
||||
let mut conn_b = PeerConnection::outbound(link_id_b_out, peer_a_identity, 1000);
|
||||
let our_index_b = node_b.index_allocator.allocate().unwrap();
|
||||
let our_keypair_b = node_b.identity().keypair();
|
||||
let noise_msg1_b = conn_b
|
||||
.start_handshake(our_keypair_b, node_b.startup_epoch(), 1000)
|
||||
node_b
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id_b_out, peer_a_identity, 1000)
|
||||
.with_our_index(our_index_b)
|
||||
.with_transport_id(transport_id_b)
|
||||
.with_source_addr(remote_addr_a.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair_b = node_b.identity().keypair();
|
||||
let startup_epoch_b = node_b.startup_epoch();
|
||||
let noise_msg1_b = node_b
|
||||
.peer_machines
|
||||
.get_mut(&link_id_b_out)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair_b, startup_epoch_b, 1000)
|
||||
.unwrap();
|
||||
conn_b.set_our_index(our_index_b);
|
||||
conn_b.set_transport_id(transport_id_b);
|
||||
conn_b.set_source_addr(remote_addr_a.clone());
|
||||
|
||||
let wire_msg1_b = build_msg1(our_index_b, &noise_msg1_b);
|
||||
|
||||
@@ -558,7 +588,6 @@ async fn test_cross_connection_both_initiate() {
|
||||
node_b
|
||||
.addr_to_link
|
||||
.insert((transport_id_b, remote_addr_a.clone()), link_id_b_out);
|
||||
node_b.add_connection(conn_b).unwrap();
|
||||
node_b
|
||||
.pending_outbound
|
||||
.insert((transport_id_b, our_index_b.as_u32()), link_id_b_out);
|
||||
@@ -703,17 +732,24 @@ async fn test_stale_connection_cleanup() {
|
||||
// Create outbound connection with a timestamp far in the past
|
||||
let past_time_ms = 1000; // A very early timestamp
|
||||
let link_id = node.allocate_link_id();
|
||||
let mut conn = PeerConnection::outbound(link_id, peer_identity, past_time_ms);
|
||||
|
||||
// Allocate session index and set transport info
|
||||
let our_index = node.index_allocator.allocate().unwrap();
|
||||
node.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id, peer_identity, past_time_ms)
|
||||
.with_our_index(our_index)
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(remote_addr.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair = node.identity().keypair();
|
||||
let _noise_msg1 = conn
|
||||
.start_handshake(our_keypair, node.startup_epoch(), past_time_ms)
|
||||
let startup_epoch = node.startup_epoch();
|
||||
let _noise_msg1 = node
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair, startup_epoch, past_time_ms)
|
||||
.unwrap();
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_transport_id(transport_id);
|
||||
conn.set_source_addr(remote_addr.clone());
|
||||
|
||||
// Set up all the state that initiate_peer_connection would create
|
||||
let link = Link::connectionless(
|
||||
@@ -726,7 +762,6 @@ async fn test_stale_connection_cleanup() {
|
||||
node.links.insert(link_id, link);
|
||||
node.addr_to_link
|
||||
.insert((transport_id, remote_addr.clone()), link_id);
|
||||
node.add_connection(conn).unwrap();
|
||||
node.pending_outbound
|
||||
.insert((transport_id, our_index.as_u32()), link_id);
|
||||
|
||||
@@ -782,16 +817,23 @@ async fn test_failed_connection_cleanup() {
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let link_id = node.allocate_link_id();
|
||||
let mut conn = PeerConnection::outbound(link_id, peer_identity, now_ms);
|
||||
|
||||
let our_index = node.index_allocator.allocate().unwrap();
|
||||
node.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id, peer_identity, now_ms)
|
||||
.with_our_index(our_index)
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(remote_addr.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair = node.identity().keypair();
|
||||
let _noise_msg1 = conn
|
||||
.start_handshake(our_keypair, node.startup_epoch(), now_ms)
|
||||
let startup_epoch = node.startup_epoch();
|
||||
let _noise_msg1 = node
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair, startup_epoch, now_ms)
|
||||
.unwrap();
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_transport_id(transport_id);
|
||||
conn.set_source_addr(remote_addr.clone());
|
||||
|
||||
let link = Link::connectionless(
|
||||
link_id,
|
||||
@@ -803,7 +845,6 @@ async fn test_failed_connection_cleanup() {
|
||||
node.links.insert(link_id, link);
|
||||
node.addr_to_link
|
||||
.insert((transport_id, remote_addr.clone()), link_id);
|
||||
node.add_connection(conn).unwrap();
|
||||
node.pending_outbound
|
||||
.insert((transport_id, our_index.as_u32()), link_id);
|
||||
|
||||
@@ -814,7 +855,7 @@ async fn test_failed_connection_cleanup() {
|
||||
let machine = node
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.expect("machine seeded by add_connection");
|
||||
.expect("machine seeded by the handshake seeder");
|
||||
let alloc = &mut node.index_allocator;
|
||||
let actions = machine.step(
|
||||
crate::peer::machine::PeerEvent::HandshakeSendFailed,
|
||||
@@ -859,24 +900,24 @@ async fn test_msg1_stored_for_resend() {
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let link_id = node.allocate_link_id();
|
||||
let mut conn = PeerConnection::outbound(link_id, peer_identity, now_ms);
|
||||
let mut conn = outbound_leg(link_id, peer_identity, now_ms);
|
||||
|
||||
let our_index = node.index_allocator.allocate().unwrap();
|
||||
let our_keypair = node.identity().keypair();
|
||||
let noise_msg1 = conn
|
||||
.start_handshake(our_keypair, node.startup_epoch(), now_ms)
|
||||
.unwrap();
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_transport_id(transport_id);
|
||||
conn.set_source_addr(remote_addr.clone());
|
||||
conn.set_conn_our_index(our_index);
|
||||
conn.set_conn_transport_id(transport_id);
|
||||
conn.set_conn_source_addr(remote_addr.clone());
|
||||
|
||||
// Build wire msg1 and store it (as initiate_peer_connection does)
|
||||
let wire_msg1 = build_msg1(our_index, &noise_msg1);
|
||||
let resend_interval = node.config().node.rate_limit.handshake_resend_interval_ms;
|
||||
conn.set_handshake_msg1(wire_msg1.clone(), now_ms + resend_interval);
|
||||
conn.set_conn_handshake_msg1(wire_msg1.clone(), now_ms + resend_interval);
|
||||
|
||||
// Verify stored msg1 matches what was built
|
||||
assert_eq!(conn.handshake_msg1().unwrap(), &wire_msg1);
|
||||
assert_eq!(conn.conn_handshake_msg1().unwrap(), &wire_msg1);
|
||||
}
|
||||
|
||||
/// Test that resend scheduling respects max_resends and backoff.
|
||||
@@ -890,20 +931,17 @@ async fn test_resend_scheduling() {
|
||||
|
||||
let now_ms = 100_000u64; // Use a fixed time for predictable testing
|
||||
let link_id = node.allocate_link_id();
|
||||
let mut conn = PeerConnection::outbound(link_id, peer_identity, now_ms);
|
||||
let mut conn = outbound_leg(link_id, peer_identity, now_ms);
|
||||
|
||||
let our_index = node.index_allocator.allocate().unwrap();
|
||||
let our_keypair = node.identity().keypair();
|
||||
let noise_msg1 = conn
|
||||
.start_handshake(our_keypair, node.startup_epoch(), now_ms)
|
||||
.unwrap();
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_transport_id(transport_id);
|
||||
conn.set_source_addr(remote_addr.clone());
|
||||
conn.set_conn_source_addr(remote_addr.clone());
|
||||
|
||||
// Store msg1 with first resend at now + 1000ms
|
||||
let wire_msg1 = crate::proto::fmp::wire::build_msg1(our_index, &noise_msg1);
|
||||
conn.set_handshake_msg1(wire_msg1.clone(), now_ms + 1000);
|
||||
|
||||
let link = Link::connectionless(
|
||||
link_id,
|
||||
@@ -937,7 +975,9 @@ async fn test_resend_scheduling() {
|
||||
// The msg1 wire lives on the machine's carrier (the retransmit driver's
|
||||
// resend source), mirroring `prepare_outbound_msg1`.
|
||||
machine.set_conn_handshake_msg1(wire_msg1, now_ms + 1000);
|
||||
machine.set_leg(conn);
|
||||
machine.set_conn_our_index(our_index);
|
||||
machine.set_conn_transport_id(transport_id);
|
||||
machine.set_leg(conn.take_leg().unwrap());
|
||||
node.peer_machines.insert(link_id, machine);
|
||||
node.peer_timers.entry(link_id).or_default().insert(
|
||||
crate::peer::machine::TimerKind::HandshakeRetransmit,
|
||||
@@ -976,15 +1016,13 @@ async fn test_handshake_timeout_drive() {
|
||||
|
||||
let dial_ms = 1000u64;
|
||||
let link_id = node.allocate_link_id();
|
||||
let mut conn = PeerConnection::outbound(link_id, peer_identity, dial_ms);
|
||||
let mut conn = outbound_leg(link_id, peer_identity, dial_ms);
|
||||
let our_index = node.index_allocator.allocate().unwrap();
|
||||
let our_keypair = node.identity().keypair();
|
||||
let _ = conn
|
||||
.start_handshake(our_keypair, node.startup_epoch(), dial_ms)
|
||||
.unwrap();
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_transport_id(transport_id);
|
||||
conn.set_source_addr(remote_addr.clone());
|
||||
conn.set_conn_source_addr(remote_addr.clone());
|
||||
|
||||
let link = Link::connectionless(
|
||||
link_id,
|
||||
@@ -1013,7 +1051,9 @@ async fn test_handshake_timeout_drive() {
|
||||
dial_ms,
|
||||
&mut node.index_allocator,
|
||||
);
|
||||
machine.set_leg(conn);
|
||||
machine.set_conn_our_index(our_index);
|
||||
machine.set_conn_transport_id(transport_id);
|
||||
machine.set_leg(conn.take_leg().unwrap());
|
||||
node.peer_machines.insert(link_id, machine);
|
||||
node.peer_timers.entry(link_id).or_default().insert(
|
||||
crate::peer::machine::TimerKind::HandshakeTimeout,
|
||||
@@ -1041,17 +1081,17 @@ async fn test_handshake_timeout_drive() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that msg2 is stored on PeerConnection for responder resend.
|
||||
/// Test that msg2 is stored on the control machine's carrier for responder resend.
|
||||
#[test]
|
||||
fn test_msg2_stored_on_connection() {
|
||||
let mut conn = PeerConnection::inbound(LinkId::new(1), 1000);
|
||||
let mut machine = crate::peer::machine::PeerMachine::new_inbound(LinkId::new(1), 1000);
|
||||
|
||||
assert!(conn.handshake_msg2().is_none());
|
||||
assert!(machine.conn_handshake_msg2().is_none());
|
||||
|
||||
let msg2_bytes = vec![0x01, 0x02, 0x03, 0x04];
|
||||
conn.set_handshake_msg2(msg2_bytes.clone());
|
||||
machine.set_conn_handshake_msg2(msg2_bytes.clone());
|
||||
|
||||
assert_eq!(conn.handshake_msg2().unwrap(), &msg2_bytes);
|
||||
assert_eq!(machine.conn_handshake_msg2().unwrap(), &msg2_bytes);
|
||||
}
|
||||
|
||||
/// Test that duplicate msg2 is silently dropped when pending_outbound is already cleared.
|
||||
@@ -1480,15 +1520,28 @@ async fn drive_to_msg3(
|
||||
let peer_identity = PeerIdentity::from_pubkey_full(responder.node.identity().pubkey_full());
|
||||
|
||||
let link_id = initiator.node.allocate_link_id();
|
||||
let mut conn = PeerConnection::outbound(link_id, peer_identity, now_ms);
|
||||
let our_index = initiator.node.index_allocator.allocate().unwrap();
|
||||
let our_keypair = initiator.node.identity().keypair();
|
||||
let noise_msg1 = conn
|
||||
.start_handshake(our_keypair, initiator.node.startup_epoch(), now_ms)
|
||||
// Mirror the production dial path: the seam seeds the identified outbound
|
||||
// leg's control machine at dial, and the promote feedback later
|
||||
// crystallizes that same machine in place.
|
||||
initiator
|
||||
.node
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id, peer_identity, now_ms)
|
||||
.with_our_index(our_index)
|
||||
.with_transport_id(initiator.transport_id)
|
||||
.with_source_addr(responder.addr.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair = initiator.node.identity().keypair();
|
||||
let startup_epoch = initiator.node.startup_epoch();
|
||||
let noise_msg1 = initiator
|
||||
.node
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair, startup_epoch, now_ms)
|
||||
.unwrap();
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_transport_id(initiator.transport_id);
|
||||
conn.set_source_addr(responder.addr.clone());
|
||||
|
||||
let wire_msg1 = build_msg1(our_index, &noise_msg1);
|
||||
let link = Link::connectionless(
|
||||
@@ -1503,10 +1556,6 @@ async fn drive_to_msg3(
|
||||
.node
|
||||
.addr_to_link
|
||||
.insert((initiator.transport_id, responder.addr.clone()), link_id);
|
||||
// Mirror the production dial path: the seam seeds the identified outbound
|
||||
// leg's control machine (carrying the connection) at dial, and the promote
|
||||
// feedback later crystallizes that same machine in place.
|
||||
initiator.node.add_connection(conn).unwrap();
|
||||
initiator
|
||||
.node
|
||||
.pending_outbound
|
||||
@@ -1693,10 +1742,11 @@ async fn test_inbound_machine_born_at_msg1_and_crystallized_at_promote() {
|
||||
// After msg1 the responder's window leg carries a machine parked at
|
||||
// `SentMsg2`, seeded with the leg's msg1-allocated index.
|
||||
assert_eq!(responder.node.connection_count(), 1);
|
||||
let leg_link = responder.node.connections().next().unwrap().link_id();
|
||||
let leg_link = responder.node.connections().next().unwrap().1.link_id();
|
||||
let leg_index = responder
|
||||
.node
|
||||
.get_connection(&leg_link)
|
||||
.peer_machines
|
||||
.get(&leg_link)
|
||||
.unwrap()
|
||||
.our_index();
|
||||
assert!(leg_index.is_some(), "msg1 allocated the leg index");
|
||||
@@ -1862,7 +1912,7 @@ async fn test_anonymous_dial_births_identityless_machine_at_leg_birth() {
|
||||
.expect("anonymous dial");
|
||||
|
||||
assert_eq!(initiator.node.connection_count(), 1);
|
||||
let leg_link = initiator.node.connections().next().unwrap().link_id();
|
||||
let leg_link = initiator.node.connections().next().unwrap().1.link_id();
|
||||
let machine = initiator
|
||||
.node
|
||||
.peer_machines
|
||||
@@ -1897,7 +1947,7 @@ async fn test_anonymous_msg2_crystallizes_identity_and_promotes() {
|
||||
.initiate_connection(initiator.transport_id, responder.addr.clone(), None)
|
||||
.await
|
||||
.expect("anonymous dial");
|
||||
let leg_link = initiator.node.connections().next().unwrap().link_id();
|
||||
let leg_link = initiator.node.connections().next().unwrap().1.link_id();
|
||||
initiator.node.debug_assert_peer_maps_coherent();
|
||||
|
||||
// Responder answers msg1 with msg2; the initiator's msg2 processing learns
|
||||
@@ -1957,7 +2007,7 @@ async fn test_anonymous_self_connect_drop_disposes_machine() {
|
||||
.initiate_connection(node.transport_id, self_addr, None)
|
||||
.await
|
||||
.expect("anonymous self dial");
|
||||
let leg_link = node.node.connections().next().unwrap().link_id();
|
||||
let leg_link = node.node.connections().next().unwrap().1.link_id();
|
||||
assert_eq!(node.node.peer_machines.len(), 1);
|
||||
|
||||
// We answer our own msg1, then our msg2 processing discovers the learned
|
||||
@@ -1969,7 +2019,7 @@ async fn test_anonymous_self_connect_drop_disposes_machine() {
|
||||
|
||||
assert_eq!(node.node.peer_count(), 0, "no promotion");
|
||||
assert!(
|
||||
node.node.get_connection(&leg_link).is_none(),
|
||||
!node.node.has_pending_leg(&leg_link),
|
||||
"self-connect drop removes the outbound leg"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::PeerIdentity;
|
||||
use crate::peer::machine::HandshakeCrypto;
|
||||
use crate::transport::{LinkDirection, ReceivedPacket, TransportAddr, packet_channel};
|
||||
use crate::utils::index::SessionIndex;
|
||||
use std::time::Duration;
|
||||
@@ -83,31 +84,83 @@ pub(super) fn make_peer_identity() -> PeerIdentity {
|
||||
PeerIdentity::from_pubkey(identity.pubkey())
|
||||
}
|
||||
|
||||
/// Create a PeerConnection with a completed Noise XX handshake.
|
||||
/// A control machine carrying a fresh outbound connection, for tests that
|
||||
/// drive one end of a handshake without a whole node behind it. The machine
|
||||
/// owns the handshake operations, so it is what the crypto runs on.
|
||||
pub(super) fn outbound_leg(
|
||||
link_id: LinkId,
|
||||
expected_identity: PeerIdentity,
|
||||
current_time_ms: u64,
|
||||
) -> PeerMachine {
|
||||
let mut machine = PeerMachine::new_outbound(link_id, Some(expected_identity), current_time_ms);
|
||||
machine.set_leg(HandshakeCrypto::new());
|
||||
machine
|
||||
}
|
||||
|
||||
/// The responder twin of [`outbound_leg`].
|
||||
pub(super) fn inbound_leg(link_id: LinkId, current_time_ms: u64) -> PeerMachine {
|
||||
let mut machine = PeerMachine::new_inbound(link_id, current_time_ms);
|
||||
machine.set_leg(HandshakeCrypto::new());
|
||||
machine
|
||||
}
|
||||
|
||||
/// Seed a control machine whose leg carries a completed Noise XX handshake.
|
||||
///
|
||||
/// Returns (connection, peer_identity) where the connection is outbound,
|
||||
/// in Complete state, with session, indices, and transport info set.
|
||||
pub(super) fn make_completed_connection(
|
||||
/// Returns the peer identity. The leg is outbound, in Complete state, with
|
||||
/// session, indices, and transport info set, and is installed on the node
|
||||
/// through [`Node::seed_handshake_machine`].
|
||||
pub(super) fn seed_completed_connection(
|
||||
node: &mut Node,
|
||||
link_id: LinkId,
|
||||
transport_id: TransportId,
|
||||
current_time_ms: u64,
|
||||
) -> (PeerConnection, PeerIdentity) {
|
||||
) -> PeerIdentity {
|
||||
let our_index = node.index_allocator.allocate().unwrap();
|
||||
seed_completed_connection_with(node, link_id, current_time_ms, |seed| {
|
||||
seed.with_our_index(our_index)
|
||||
.with_their_index(SessionIndex::new(42))
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(TransportAddr::from_string("127.0.0.1:5000"))
|
||||
})
|
||||
}
|
||||
|
||||
/// [`seed_completed_connection`] with the seed left to the caller, for tests
|
||||
/// that need a leg deliberately missing one of the fields promotion requires.
|
||||
///
|
||||
/// The Noise exchange runs on the already-seeded machine, where it used to
|
||||
/// run before the leg was handed over. That reordering is neutral: on XX the
|
||||
/// responder's `receive_handshake_init` learns nothing (identity arrives at
|
||||
/// msg3), and the seeded reads (`link_id`, `started_at`, `is_outbound`,
|
||||
/// `their_index`, `transport_id`) are untouched by the handshake.
|
||||
pub(super) fn seed_completed_connection_with(
|
||||
node: &mut Node,
|
||||
link_id: LinkId,
|
||||
current_time_ms: u64,
|
||||
shape: impl FnOnce(HandshakeSeed) -> HandshakeSeed,
|
||||
) -> PeerIdentity {
|
||||
let peer_identity_full = Identity::generate();
|
||||
// Must use from_pubkey_full to preserve parity for ECDH
|
||||
let peer_identity = PeerIdentity::from_pubkey_full(peer_identity_full.pubkey_full());
|
||||
|
||||
// Create outbound connection
|
||||
let mut conn = PeerConnection::outbound(link_id, peer_identity, current_time_ms);
|
||||
node.seed_handshake_machine(shape(HandshakeSeed::outbound(
|
||||
link_id,
|
||||
peer_identity,
|
||||
current_time_ms,
|
||||
)))
|
||||
.unwrap();
|
||||
|
||||
// Run initiator side of handshake
|
||||
let our_keypair = node.identity().keypair();
|
||||
let msg1 = conn
|
||||
.start_handshake(our_keypair, node.startup_epoch(), current_time_ms)
|
||||
let startup_epoch = node.startup_epoch();
|
||||
let msg1 = node
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair, startup_epoch, current_time_ms)
|
||||
.unwrap();
|
||||
|
||||
// Run responder side to generate msg2
|
||||
let mut resp_conn = PeerConnection::inbound(LinkId::new(999), current_time_ms);
|
||||
let mut resp_conn = inbound_leg(LinkId::new(999), current_time_ms);
|
||||
let peer_keypair = peer_identity_full.keypair();
|
||||
let mut resp_epoch = [0u8; 8];
|
||||
rand::Rng::fill_bytes(&mut rand::rng(), &mut resp_epoch);
|
||||
@@ -116,7 +169,10 @@ pub(super) fn make_completed_connection(
|
||||
.unwrap();
|
||||
|
||||
// Complete initiator handshake (XX: generates msg3)
|
||||
let (msg3, _neg) = conn
|
||||
let (msg3, _neg) = node
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.unwrap()
|
||||
.complete_handshake(&msg2, None, current_time_ms)
|
||||
.unwrap();
|
||||
|
||||
@@ -125,12 +181,5 @@ pub(super) fn make_completed_connection(
|
||||
.complete_handshake_msg3(&msg3, current_time_ms)
|
||||
.unwrap();
|
||||
|
||||
// Set indices and transport info
|
||||
let our_index = node.index_allocator.allocate().unwrap();
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_their_index(SessionIndex::new(42));
|
||||
conn.set_transport_id(transport_id);
|
||||
conn.set_source_addr(TransportAddr::from_string("127.0.0.1:5000"));
|
||||
|
||||
(conn, peer_identity)
|
||||
peer_identity
|
||||
}
|
||||
|
||||
@@ -29,9 +29,8 @@ fn test_routing_direct_peer() {
|
||||
let transport_id = TransportId::new(1);
|
||||
let link_id = LinkId::new(1);
|
||||
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let peer_addr = *identity.node_addr();
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, identity, 2000).unwrap();
|
||||
|
||||
let result = node.find_next_hop(&peer_addr);
|
||||
@@ -58,15 +57,13 @@ fn test_routing_bloom_filter_hit() {
|
||||
|
||||
// Create two peers
|
||||
let link_id1 = LinkId::new(1);
|
||||
let (conn1, id1) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let id1 = seed_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let peer1_addr = *id1.node_addr();
|
||||
node.add_connection(conn1).unwrap();
|
||||
node.promote_connection(link_id1, id1, 2000).unwrap();
|
||||
|
||||
let link_id2 = LinkId::new(2);
|
||||
let (conn2, id2) = make_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
let id2 = seed_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
let peer2_addr = *id2.node_addr();
|
||||
node.add_connection(conn2).unwrap();
|
||||
node.promote_connection(link_id2, id2, 2000).unwrap();
|
||||
|
||||
// Set up tree: we are root, both peers are our children
|
||||
@@ -115,10 +112,9 @@ fn test_routing_bloom_filter_multiple_hits_tiebreak() {
|
||||
let mut peer_addrs = Vec::new();
|
||||
for i in 1..=3 {
|
||||
let link_id = LinkId::new(i);
|
||||
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let id = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let addr = *id.node_addr();
|
||||
peer_addrs.push(addr);
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
}
|
||||
|
||||
@@ -166,9 +162,8 @@ fn test_routing_tree_fallback() {
|
||||
|
||||
// Create a peer
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let id = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let peer_addr = *id.node_addr();
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
|
||||
// Set up tree state through the public API.
|
||||
@@ -220,9 +215,8 @@ fn test_routing_bloom_hit_not_closer_falls_through_to_tree() {
|
||||
|
||||
// tree_peer: child of self, on the path to dest (greedy tree pick).
|
||||
let tree_link = LinkId::new(1);
|
||||
let (tree_conn, tree_id) = make_completed_connection(&mut node, tree_link, transport_id, 1000);
|
||||
let tree_id = seed_completed_connection(&mut node, tree_link, transport_id, 1000);
|
||||
let tree_peer_addr = *tree_id.node_addr();
|
||||
node.add_connection(tree_conn).unwrap();
|
||||
node.promote_connection(tree_link, tree_id, 2000).unwrap();
|
||||
|
||||
// bloom_peer: also a child of self, but with a stale/false-positive
|
||||
@@ -230,10 +224,8 @@ fn test_routing_bloom_hit_not_closer_falls_through_to_tree() {
|
||||
// ours, so the self-distance check in select_best_candidate excludes
|
||||
// it — leaving zero viable bloom candidates.
|
||||
let bloom_link = LinkId::new(2);
|
||||
let (bloom_conn, bloom_id) =
|
||||
make_completed_connection(&mut node, bloom_link, transport_id, 1000);
|
||||
let bloom_id = seed_completed_connection(&mut node, bloom_link, transport_id, 1000);
|
||||
let bloom_peer_addr = *bloom_id.node_addr();
|
||||
node.add_connection(bloom_conn).unwrap();
|
||||
node.promote_connection(bloom_link, bloom_id, 2000).unwrap();
|
||||
|
||||
// Tree topology (we are root):
|
||||
@@ -300,8 +292,7 @@ fn test_routing_tree_no_coords_in_cache() {
|
||||
|
||||
// Create a peer
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.add_connection(conn).unwrap();
|
||||
let id = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
|
||||
// Destination not in bloom filters and not in coord cache
|
||||
@@ -319,9 +310,8 @@ fn test_routing_refreshes_coord_cache_ttl() {
|
||||
|
||||
// Create a peer
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let id = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let peer_addr = *id.node_addr();
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
|
||||
// Set up tree coordinates
|
||||
@@ -364,15 +354,13 @@ fn test_routing_bloom_hit_without_coords_returns_none() {
|
||||
|
||||
// Create two peers
|
||||
let link_id1 = LinkId::new(1);
|
||||
let (conn1, id1) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let id1 = seed_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let peer1_addr = *id1.node_addr();
|
||||
node.add_connection(conn1).unwrap();
|
||||
node.promote_connection(link_id1, id1, 2000).unwrap();
|
||||
|
||||
let link_id2 = LinkId::new(2);
|
||||
let (conn2, id2) = make_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
let id2 = seed_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
let peer2_addr = *id2.node_addr();
|
||||
node.add_connection(conn2).unwrap();
|
||||
node.promote_connection(link_id2, id2, 2000).unwrap();
|
||||
|
||||
let dest = make_node_addr(99);
|
||||
@@ -404,9 +392,8 @@ fn test_routing_discovery_coord_cache() {
|
||||
|
||||
// Create a peer
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let id = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let peer_addr = *id.node_addr();
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
|
||||
// Set up tree: we are root, peer is our child
|
||||
|
||||
@@ -1005,9 +1005,7 @@ fn test_identity_cache_populated_on_promote() {
|
||||
let transport_id = TransportId::new(1);
|
||||
let link_id = LinkId::new(1);
|
||||
|
||||
let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
|
||||
node.add_connection(conn).unwrap();
|
||||
let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
|
||||
// Promote
|
||||
let result = node
|
||||
|
||||
@@ -137,16 +137,26 @@ pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usiz
|
||||
let transport_id = initiator.transport_id;
|
||||
|
||||
let link_id = initiator.node.allocate_link_id();
|
||||
let mut conn = PeerConnection::outbound(link_id, peer_identity, 1000);
|
||||
|
||||
let our_index = initiator.node.index_allocator.allocate().unwrap();
|
||||
let our_keypair = initiator.node.identity().keypair();
|
||||
let noise_msg1 = conn
|
||||
.start_handshake(our_keypair, initiator.node.startup_epoch(), 1000)
|
||||
initiator
|
||||
.node
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id, peer_identity, 1000)
|
||||
.with_our_index(our_index)
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(responder_addr.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair = initiator.node.identity().keypair();
|
||||
let startup_epoch = initiator.node.startup_epoch();
|
||||
let noise_msg1 = initiator
|
||||
.node
|
||||
.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair, startup_epoch, 1000)
|
||||
.unwrap();
|
||||
conn.set_our_index(our_index);
|
||||
conn.set_transport_id(transport_id);
|
||||
conn.set_source_addr(responder_addr.clone());
|
||||
|
||||
let wire_msg1 = build_msg1(our_index, &noise_msg1);
|
||||
|
||||
@@ -162,7 +172,6 @@ pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usiz
|
||||
.node
|
||||
.addr_to_link
|
||||
.insert((transport_id, responder_addr.clone()), link_id);
|
||||
initiator.node.add_connection(conn).unwrap();
|
||||
initiator
|
||||
.node
|
||||
.pending_outbound
|
||||
|
||||
@@ -138,7 +138,7 @@ async fn test_try_peer_addresses_races_all_concrete_udp_candidates() {
|
||||
|
||||
let mut addrs = node
|
||||
.connections()
|
||||
.filter_map(|conn| conn.source_addr().and_then(|addr| addr.as_str()))
|
||||
.filter_map(|(_, machine)| machine.conn_source_addr().and_then(|addr| addr.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
addrs.sort();
|
||||
assert_eq!(addrs, vec!["127.0.0.1:10", "127.0.0.1:9"]);
|
||||
@@ -337,14 +337,14 @@ fn test_node_connection_management() {
|
||||
|
||||
let identity = make_peer_identity();
|
||||
let link_id = LinkId::new(1);
|
||||
let conn = PeerConnection::outbound(link_id, identity, 1000);
|
||||
node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 1000))
|
||||
.unwrap();
|
||||
|
||||
node.add_connection(conn).unwrap();
|
||||
assert_eq!(node.connection_count(), 1);
|
||||
|
||||
assert!(node.get_connection(&link_id).is_some());
|
||||
assert!(node.has_pending_leg(&link_id));
|
||||
|
||||
node.remove_connection(&link_id);
|
||||
node.remove_peer_machine(link_id);
|
||||
assert_eq!(node.connection_count(), 0);
|
||||
}
|
||||
|
||||
@@ -354,29 +354,27 @@ fn test_node_connection_duplicate() {
|
||||
|
||||
let identity = make_peer_identity();
|
||||
let link_id = LinkId::new(1);
|
||||
let conn1 = PeerConnection::outbound(link_id, identity, 1000);
|
||||
let conn2 = PeerConnection::outbound(link_id, identity, 2000);
|
||||
node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 1000))
|
||||
.unwrap();
|
||||
|
||||
node.add_connection(conn1).unwrap();
|
||||
let result = node.add_connection(conn2);
|
||||
let result = node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 2000));
|
||||
|
||||
assert!(matches!(result, Err(NodeError::ConnectionAlreadyExists(_))));
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[test]
|
||||
fn test_peer_maps_coherent_after_add_connection() {
|
||||
fn test_peer_maps_coherent_after_seeding_a_handshake() {
|
||||
let mut node = make_node();
|
||||
|
||||
let identity = make_peer_identity();
|
||||
let link_id = LinkId::new(1);
|
||||
let conn = PeerConnection::outbound(link_id, identity, 1000);
|
||||
|
||||
node.add_connection(conn).unwrap();
|
||||
node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 1000))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
node.peer_machines.contains_key(&link_id),
|
||||
"add_connection seeds a control machine for its leg"
|
||||
"seeding a handshake creates its control machine"
|
||||
);
|
||||
node.debug_assert_peer_maps_coherent();
|
||||
}
|
||||
@@ -388,9 +386,8 @@ fn test_peer_maps_coherent_through_establish() {
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
|
||||
node.add_connection(conn).unwrap();
|
||||
node.debug_assert_peer_maps_coherent();
|
||||
|
||||
let result = node.promote_connection(link_id, identity, 2000).unwrap();
|
||||
@@ -421,16 +418,111 @@ fn test_peer_maps_coherence_detects_orphaned_machine() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Promotion detaches the pending connection before it validates anything, and
|
||||
/// then validates the required fields in a fixed order. Both properties are
|
||||
/// depended on: the caller of a rejected promotion disposes of a machine it
|
||||
/// expects to be leg-less, and the reported error names the first field that is
|
||||
/// missing, not an arbitrary one.
|
||||
#[test]
|
||||
fn test_promote_connection_error_order_and_leg_detach() {
|
||||
fn expect_promotion_failure(
|
||||
shape: impl FnOnce(HandshakeSeed) -> HandshakeSeed,
|
||||
expected_reason: &str,
|
||||
) {
|
||||
let mut node = make_node();
|
||||
let link_id = LinkId::new(1);
|
||||
let identity = seed_completed_connection_with(&mut node, link_id, 1000, shape);
|
||||
|
||||
let err = node
|
||||
.promote_connection(link_id, identity, 2000)
|
||||
.expect_err("promotion must reject an incomplete connection");
|
||||
match err {
|
||||
NodeError::PromotionFailed { reason, .. } => assert_eq!(
|
||||
reason, expected_reason,
|
||||
"promotion named the wrong missing field"
|
||||
),
|
||||
other => panic!("expected PromotionFailed, got {other:?}"),
|
||||
}
|
||||
assert!(
|
||||
node.peer_machines
|
||||
.get(&link_id)
|
||||
.expect("the control machine survives a failed promotion")
|
||||
.leg()
|
||||
.is_none(),
|
||||
"a failed promotion must leave the machine leg-less"
|
||||
);
|
||||
assert_eq!(node.peer_count(), 0);
|
||||
}
|
||||
|
||||
// Each case leaves every later field missing as well, so a promotion that
|
||||
// gathered all of them before validating would name the wrong one.
|
||||
expect_promotion_failure(|seed| seed, "missing our_index");
|
||||
expect_promotion_failure(
|
||||
|seed| seed.with_our_index(SessionIndex::new(7)),
|
||||
"missing their_index",
|
||||
);
|
||||
expect_promotion_failure(
|
||||
|seed| {
|
||||
seed.with_our_index(SessionIndex::new(7))
|
||||
.with_their_index(SessionIndex::new(42))
|
||||
},
|
||||
"missing transport_id",
|
||||
);
|
||||
expect_promotion_failure(
|
||||
|seed| {
|
||||
seed.with_our_index(SessionIndex::new(7))
|
||||
.with_their_index(SessionIndex::new(42))
|
||||
.with_transport_id(TransportId::new(1))
|
||||
},
|
||||
"missing source_addr",
|
||||
);
|
||||
|
||||
// An unknown link and a machine carrying no connection are both
|
||||
// ConnectionNotFound, ahead of every field check.
|
||||
let mut node = make_node();
|
||||
let identity = make_peer_identity();
|
||||
assert!(matches!(
|
||||
node.promote_connection(LinkId::new(9), identity, 2000),
|
||||
Err(NodeError::ConnectionNotFound(_))
|
||||
));
|
||||
|
||||
let link_id = LinkId::new(2);
|
||||
node.peer_machines
|
||||
.insert(link_id, PeerMachine::new_inbound(link_id, 1000));
|
||||
assert!(matches!(
|
||||
node.promote_connection(link_id, identity, 2000),
|
||||
Err(NodeError::ConnectionNotFound(_))
|
||||
));
|
||||
|
||||
// A connection that never ran the handshake fails on the session check,
|
||||
// after the detach and ahead of every field check — the seed below
|
||||
// supplies none of the four fields above.
|
||||
let link_id = LinkId::new(3);
|
||||
node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 1000))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
node.promote_connection(link_id, identity, 2000),
|
||||
Err(NodeError::HandshakeIncomplete(_))
|
||||
));
|
||||
assert!(
|
||||
node.peer_machines
|
||||
.get(&link_id)
|
||||
.expect("the control machine survives a failed promotion")
|
||||
.leg()
|
||||
.is_none(),
|
||||
"an incomplete handshake must still leave the machine leg-less"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_node_promote_connection() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let node_addr = *identity.node_addr();
|
||||
|
||||
node.add_connection(conn).unwrap();
|
||||
assert_eq!(node.connection_count(), 1);
|
||||
assert_eq!(node.peer_count(), 0);
|
||||
|
||||
@@ -467,10 +559,9 @@ fn test_node_cross_connection_resolution() {
|
||||
|
||||
// First connection and promotion (becomes active peer)
|
||||
let link_id1 = LinkId::new(1);
|
||||
let (conn1, identity) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let identity = seed_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let node_addr = *identity.node_addr();
|
||||
|
||||
node.add_connection(conn1).unwrap();
|
||||
node.promote_connection(link_id1, identity, 1500).unwrap();
|
||||
|
||||
assert_eq!(node.peer_count(), 1);
|
||||
@@ -500,8 +591,7 @@ fn test_node_peer_limit() {
|
||||
// Add two peers via promotion
|
||||
for i in 0..2 {
|
||||
let link_id = LinkId::new(i as u64 + 1);
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.add_connection(conn).unwrap();
|
||||
let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.promote_connection(link_id, identity, 2000).unwrap();
|
||||
}
|
||||
|
||||
@@ -509,8 +599,7 @@ fn test_node_peer_limit() {
|
||||
|
||||
// Third should fail
|
||||
let link_id = LinkId::new(3);
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 3000);
|
||||
node.add_connection(conn).unwrap();
|
||||
let identity = seed_completed_connection(&mut node, link_id, transport_id, 3000);
|
||||
|
||||
let result = node.promote_connection(link_id, identity, 4000);
|
||||
assert!(matches!(result, Err(NodeError::MaxPeersExceeded { .. })));
|
||||
@@ -558,22 +647,19 @@ fn test_node_sendable_peers() {
|
||||
|
||||
// Add a healthy peer
|
||||
let link_id1 = LinkId::new(1);
|
||||
let (conn1, identity1) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let identity1 = seed_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let node_addr1 = *identity1.node_addr();
|
||||
node.add_connection(conn1).unwrap();
|
||||
node.promote_connection(link_id1, identity1, 2000).unwrap();
|
||||
|
||||
// Add another peer and mark it stale (still sendable)
|
||||
let link_id2 = LinkId::new(2);
|
||||
let (conn2, identity2) = make_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
node.add_connection(conn2).unwrap();
|
||||
let identity2 = seed_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
node.promote_connection(link_id2, identity2, 2000).unwrap();
|
||||
|
||||
// Add a third peer and mark it disconnected (not sendable)
|
||||
let link_id3 = LinkId::new(3);
|
||||
let (conn3, identity3) = make_completed_connection(&mut node, link_id3, transport_id, 1000);
|
||||
let identity3 = seed_completed_connection(&mut node, link_id3, transport_id, 1000);
|
||||
let node_addr3 = *identity3.node_addr();
|
||||
node.add_connection(conn3).unwrap();
|
||||
node.promote_connection(link_id3, identity3, 2000).unwrap();
|
||||
node.get_peer_mut(&node_addr3).unwrap().mark_disconnected();
|
||||
|
||||
@@ -708,20 +794,25 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
|
||||
// This simulates A having sent msg1 to B before B was running.
|
||||
let pending_link_id = LinkId::new(1);
|
||||
let pending_time_ms = 1000;
|
||||
let mut pending_conn =
|
||||
PeerConnection::outbound(pending_link_id, peer_b_identity, pending_time_ms);
|
||||
let pending_index = node.index_allocator.allocate().unwrap();
|
||||
let pending_addr = TransportAddr::from_string("10.0.0.2:2121");
|
||||
node.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(pending_link_id, peer_b_identity, pending_time_ms)
|
||||
.with_our_index(pending_index)
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(pending_addr.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let our_keypair = node.identity().keypair();
|
||||
let _msg1 = pending_conn
|
||||
.start_handshake(our_keypair, node.startup_epoch(), pending_time_ms)
|
||||
let startup_epoch = node.startup_epoch();
|
||||
let _msg1 = node
|
||||
.peer_machines
|
||||
.get_mut(&pending_link_id)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair, startup_epoch, pending_time_ms)
|
||||
.unwrap();
|
||||
|
||||
let pending_index = node.index_allocator.allocate().unwrap();
|
||||
pending_conn.set_our_index(pending_index);
|
||||
pending_conn.set_transport_id(transport_id);
|
||||
let pending_addr = TransportAddr::from_string("10.0.0.2:2121");
|
||||
pending_conn.set_source_addr(pending_addr.clone());
|
||||
|
||||
let pending_link = Link::connectionless(
|
||||
pending_link_id,
|
||||
transport_id,
|
||||
@@ -732,7 +823,6 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
|
||||
node.links.insert(pending_link_id, pending_link);
|
||||
node.addr_to_link
|
||||
.insert((transport_id, pending_addr.clone()), pending_link_id);
|
||||
node.add_connection(pending_conn).unwrap();
|
||||
node.pending_outbound
|
||||
.insert((transport_id, pending_index.as_u32()), pending_link_id);
|
||||
|
||||
@@ -747,16 +837,27 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
|
||||
let completing_link_id = LinkId::new(2);
|
||||
let completing_time_ms = 2000;
|
||||
|
||||
let mut completing_conn =
|
||||
PeerConnection::outbound(completing_link_id, peer_b_identity, completing_time_ms);
|
||||
let completing_index = node.index_allocator.allocate().unwrap();
|
||||
node.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(completing_link_id, peer_b_identity, completing_time_ms)
|
||||
.with_our_index(completing_index)
|
||||
.with_their_index(SessionIndex::new(99))
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(TransportAddr::from_string("10.0.0.2:4001")),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let our_keypair = node.identity().keypair();
|
||||
let msg1 = completing_conn
|
||||
.start_handshake(our_keypair, node.startup_epoch(), completing_time_ms)
|
||||
let startup_epoch = node.startup_epoch();
|
||||
let msg1 = node
|
||||
.peer_machines
|
||||
.get_mut(&completing_link_id)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair, startup_epoch, completing_time_ms)
|
||||
.unwrap();
|
||||
|
||||
// B responds
|
||||
let mut resp_conn = PeerConnection::inbound(LinkId::new(999), completing_time_ms);
|
||||
let mut resp_conn = inbound_leg(LinkId::new(999), completing_time_ms);
|
||||
let peer_keypair = peer_b_full.keypair();
|
||||
let mut resp_epoch = [0u8; 8];
|
||||
rand::Rng::fill_bytes(&mut rand::rng(), &mut resp_epoch);
|
||||
@@ -764,21 +865,16 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
|
||||
.receive_handshake_init(peer_keypair, resp_epoch, &msg1, None, completing_time_ms)
|
||||
.unwrap();
|
||||
|
||||
let (msg3, _neg) = completing_conn
|
||||
let (msg3, _neg) = node
|
||||
.peer_machines
|
||||
.get_mut(&completing_link_id)
|
||||
.unwrap()
|
||||
.complete_handshake(&msg2, None, completing_time_ms)
|
||||
.unwrap();
|
||||
resp_conn
|
||||
.complete_handshake_msg3(&msg3, completing_time_ms)
|
||||
.unwrap();
|
||||
|
||||
let completing_index = node.index_allocator.allocate().unwrap();
|
||||
completing_conn.set_our_index(completing_index);
|
||||
completing_conn.set_their_index(SessionIndex::new(99));
|
||||
completing_conn.set_transport_id(transport_id);
|
||||
completing_conn.set_source_addr(TransportAddr::from_string("10.0.0.2:4001"));
|
||||
|
||||
node.add_connection(completing_conn).unwrap();
|
||||
|
||||
// Now 2 connections, 1 link (pending has link, completing doesn't yet need one for this test)
|
||||
assert_eq!(node.connection_count(), 2);
|
||||
assert_eq!(node.index_allocator.count(), 2);
|
||||
@@ -1042,9 +1138,8 @@ fn test_schedule_retry_skips_connected_peer() {
|
||||
|
||||
// Promote a peer so it's in the peers map
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let node_addr = *identity.node_addr();
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, identity, 2000).unwrap();
|
||||
assert_eq!(node.peer_count(), 1);
|
||||
|
||||
@@ -1061,10 +1156,9 @@ async fn test_try_peer_addresses_skips_connected_peer() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let peer_config = crate::config::PeerConfig::new(peer_identity.npub(), "udp", "127.0.0.1:9");
|
||||
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, peer_identity, 2000)
|
||||
.unwrap();
|
||||
let link_count = node.link_count();
|
||||
@@ -1091,8 +1185,8 @@ async fn test_try_peer_addresses_skips_connecting_peer() {
|
||||
let mut node = make_node();
|
||||
let peer_identity = make_peer_identity();
|
||||
let peer_config = crate::config::PeerConfig::new(peer_identity.npub(), "udp", "127.0.0.1:9");
|
||||
let pending = PeerConnection::outbound(LinkId::new(1), peer_identity, 1000);
|
||||
node.add_connection(pending).unwrap();
|
||||
node.seed_handshake_machine(HandshakeSeed::outbound(LinkId::new(1), peer_identity, 1000))
|
||||
.unwrap();
|
||||
|
||||
node.try_peer_addresses(&peer_config, peer_identity, true)
|
||||
.await
|
||||
@@ -1256,7 +1350,7 @@ async fn update_peers_races_new_alternative_without_dropping_active_peer() {
|
||||
assert_eq!(
|
||||
node.connections()
|
||||
.next()
|
||||
.and_then(|conn| conn.source_addr()),
|
||||
.and_then(|(_, machine)| machine.conn_source_addr()),
|
||||
Some(&new_addr)
|
||||
);
|
||||
let active = node.get_peer(&peer_node_addr).unwrap();
|
||||
@@ -1273,8 +1367,7 @@ async fn test_nostr_traversal_failure_skips_connected_peer() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.add_connection(conn).unwrap();
|
||||
let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.promote_connection(link_id, peer_identity, 2000)
|
||||
.unwrap();
|
||||
|
||||
@@ -1307,8 +1400,7 @@ async fn test_nostr_traversal_established_skips_connected_peer() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.add_connection(conn).unwrap();
|
||||
let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.promote_connection(link_id, peer_identity, 2000)
|
||||
.unwrap();
|
||||
let link_count = node.link_count();
|
||||
@@ -1528,7 +1620,7 @@ fn test_promote_clears_retry_pending() {
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let node_addr = *identity.node_addr();
|
||||
|
||||
// Simulate a retry entry existing for this peer
|
||||
@@ -1538,7 +1630,6 @@ fn test_promote_clears_retry_pending() {
|
||||
);
|
||||
assert_eq!(node.peering.reconciler.retry_pending.len(), 1);
|
||||
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, identity, 2000).unwrap();
|
||||
|
||||
assert!(
|
||||
@@ -2006,15 +2097,25 @@ async fn drive_xx_handshake(
|
||||
|
||||
// node_a initiates the outbound handshake.
|
||||
let link_id_a = node_a.allocate_link_id();
|
||||
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
|
||||
let our_index_a = node_a.index_allocator.allocate().unwrap();
|
||||
let our_keypair_a = node_a.identity().keypair();
|
||||
let noise_msg1 = conn_a
|
||||
.start_handshake(our_keypair_a, node_a.startup_epoch(), 1000)
|
||||
// Mirror the production dial path: the seam seeds the outbound leg's
|
||||
// control machine, which owns the handshake crypto.
|
||||
node_a
|
||||
.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id_a, peer_b_identity, 1000)
|
||||
.with_our_index(our_index_a)
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(remote_addr_b.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let our_keypair_a = node_a.identity().keypair();
|
||||
let startup_epoch_a = node_a.startup_epoch();
|
||||
let noise_msg1 = node_a
|
||||
.peer_machines
|
||||
.get_mut(&link_id_a)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair_a, startup_epoch_a, 1000)
|
||||
.unwrap();
|
||||
conn_a.set_our_index(our_index_a);
|
||||
conn_a.set_transport_id(transport_id);
|
||||
conn_a.set_source_addr(remote_addr_b.clone());
|
||||
let wire_msg1 = build_msg1(our_index_a, &noise_msg1);
|
||||
|
||||
let link_a = Link::connectionless(
|
||||
@@ -2025,9 +2126,6 @@ async fn drive_xx_handshake(
|
||||
Duration::from_millis(100),
|
||||
);
|
||||
node_a.links.insert(link_id_a, link_a);
|
||||
// Mirror the production dial path: the seam seeds the outbound leg's
|
||||
// control machine and embeds the connection on it.
|
||||
node_a.add_connection(conn_a).unwrap();
|
||||
node_a
|
||||
.pending_outbound
|
||||
.insert((transport_id, our_index_a.as_u32()), link_id_a);
|
||||
@@ -2276,3 +2374,332 @@ async fn start_skips_system_tun_when_app_owned() {
|
||||
|
||||
node.stop().await.unwrap();
|
||||
}
|
||||
|
||||
/// A connection whose handshake failed is retained with BOTH Noise handles
|
||||
/// empty, and the stale-connection sweep depends on that: presence of the
|
||||
/// pending connection — not presence of a handle — is what marks a machine as
|
||||
/// handshake-phase. If presence were ever derived from the handles, every
|
||||
/// failed connection would become invisible to the sweep and leak forever,
|
||||
/// holding a peering-budget slot and a wrong `connection_count` permanently.
|
||||
#[test]
|
||||
fn test_failed_connection_is_retained_and_reaped() {
|
||||
use crate::proto::fmp::LifecycleView;
|
||||
|
||||
let mut node = make_node();
|
||||
let link_id = LinkId::new(1);
|
||||
let peer_identity = make_peer_identity();
|
||||
|
||||
node.seed_handshake_machine(HandshakeSeed::outbound(link_id, peer_identity, 1000))
|
||||
.unwrap();
|
||||
let our_keypair = node.identity().keypair();
|
||||
let startup_epoch = node.startup_epoch();
|
||||
node.peer_machines
|
||||
.get_mut(&link_id)
|
||||
.unwrap()
|
||||
.start_handshake(our_keypair, startup_epoch, 1000)
|
||||
.unwrap();
|
||||
|
||||
// The send of that stored initiation fails.
|
||||
let machine = node.peer_machines.get_mut(&link_id).unwrap();
|
||||
machine.mark_failed();
|
||||
machine.mark_send_failed();
|
||||
|
||||
// Both handles are now empty — the initiation handle was dropped and no
|
||||
// session was ever reached — yet the connection is deliberately retained.
|
||||
let leg = node.peer_machines.get(&link_id).unwrap().leg().unwrap();
|
||||
assert!(
|
||||
leg.noise_handshake.is_none() && leg.noise_session.is_none(),
|
||||
"a failed connection holds neither handle"
|
||||
);
|
||||
|
||||
// (a) it still counts while it waits for the sweep
|
||||
assert_eq!(
|
||||
node.connection_count(),
|
||||
1,
|
||||
"a failed connection stays counted until it is reaped"
|
||||
);
|
||||
|
||||
// (b) the sweep yields it
|
||||
let stale = node.stale_connections(2000, 30_000);
|
||||
assert_eq!(
|
||||
stale.len(),
|
||||
1,
|
||||
"the sweep must see a failed connection despite its empty handles"
|
||||
);
|
||||
assert_eq!(stale[0].link, link_id);
|
||||
|
||||
// (c) reaping it clears the carrier
|
||||
node.remove_peer_machine(link_id);
|
||||
assert_eq!(node.connection_count(), 0);
|
||||
}
|
||||
|
||||
/// Handshake-phase membership is decided by whether a crypto carrier is
|
||||
/// ATTACHED, never by whether either Noise handle inside it is populated.
|
||||
///
|
||||
/// The distinction is the whole reason the carrier is a struct rather than a
|
||||
/// pair of bare handle fields. A carrier legitimately sits attached and empty:
|
||||
/// `mark_failed` drops the initiation handle and deliberately keeps the
|
||||
/// carrier so the sweep can reclaim it, `take_session` empties the other, and
|
||||
/// every handshake begins with both handles unset. If presence were derived
|
||||
/// from the handles, every failed handshake would vanish from the sweep,
|
||||
/// the count, and the peering budget at once — a permanent leak that no
|
||||
/// existing test would notice.
|
||||
///
|
||||
/// This drives the empty-carrier shape past every presence predicate on
|
||||
/// `Node` and asserts each one reports "present", then detaches and asserts
|
||||
/// each reports "absent".
|
||||
#[test]
|
||||
fn handshake_presence_tracks_the_carrier_not_the_noise_handles() {
|
||||
use crate::proto::fmp::LifecycleView;
|
||||
|
||||
let mut node = make_node();
|
||||
let link_id = LinkId::new(31);
|
||||
let transport_id = TransportId::new(9);
|
||||
let peer_identity = make_peer_identity();
|
||||
let peer_addr = TransportAddr::from_string("10.0.0.9:9999");
|
||||
|
||||
node.seed_handshake_machine(
|
||||
HandshakeSeed::outbound(link_id, peer_identity, 1000)
|
||||
.with_transport_id(transport_id)
|
||||
.with_source_addr(peer_addr.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// A freshly seeded carrier holds neither handle — the construction window.
|
||||
let leg = node.peer_machines.get(&link_id).unwrap().leg().unwrap();
|
||||
assert!(
|
||||
leg.noise_handshake.is_none() && leg.noise_session.is_none(),
|
||||
"the seeded carrier must start with both handles empty"
|
||||
);
|
||||
|
||||
// Every presence predicate must see it, handles or not.
|
||||
let assert_present = |node: &Node, when: &str| {
|
||||
assert_eq!(node.connection_count(), 1, "connection_count: {when}");
|
||||
assert_eq!(node.connections().count(), 1, "connections(): {when}");
|
||||
assert!(node.has_pending_leg(&link_id), "has_pending_leg: {when}");
|
||||
assert_eq!(
|
||||
node.stale_connections(1_000_000, 30_000).len(),
|
||||
1,
|
||||
"stale_connections: {when}"
|
||||
);
|
||||
assert!(
|
||||
node.is_connecting_to_peer_on_path(peer_identity.node_addr(), transport_id, &peer_addr),
|
||||
"is_connecting_to_peer_on_path: {when}"
|
||||
);
|
||||
// The last two predicates sit inline in functions with no callable
|
||||
// seam, so these MIRROR them rather than exercising them: the shape is
|
||||
// pinned here, but a mutation at the production site would not fail
|
||||
// this test. Both sites read `machine.leg().is_some()` verbatim.
|
||||
assert!(
|
||||
node.peer_machines.values().any(|machine| {
|
||||
machine.leg().is_some() && machine.conn_transport_id() == Some(transport_id)
|
||||
}),
|
||||
"transport-in-use: {when}"
|
||||
);
|
||||
// The complement of the rekey-msg2 discriminator: a machine carrying
|
||||
// a pending handshake marks a fresh establish, so `handle_msg2` must
|
||||
// NOT take its rekey-completion branch.
|
||||
assert!(
|
||||
node.peer_machines
|
||||
.get(&link_id)
|
||||
.is_some_and(|machine| machine.leg().is_some()),
|
||||
"rekey-msg2 discriminator: {when}"
|
||||
);
|
||||
// Fires the live-carrier coherence assertion; a machine that had gone
|
||||
// invisible would panic here rather than fail an assert_eq above.
|
||||
node.debug_assert_peer_maps_coherent();
|
||||
};
|
||||
|
||||
assert_present(&node, "freshly seeded, both handles empty");
|
||||
|
||||
// Drive to the failed shape: the initiation handle is dropped and the
|
||||
// carrier is deliberately retained for the sweep.
|
||||
let our_keypair = node.identity().keypair();
|
||||
let startup_epoch = node.startup_epoch();
|
||||
let machine = node.peer_machines.get_mut(&link_id).unwrap();
|
||||
machine
|
||||
.start_handshake(our_keypair, startup_epoch, 1000)
|
||||
.unwrap();
|
||||
assert!(
|
||||
machine.leg().unwrap().noise_handshake.is_some(),
|
||||
"start_handshake arms the initiation handle"
|
||||
);
|
||||
machine.mark_failed();
|
||||
machine.mark_send_failed();
|
||||
|
||||
let leg = node.peer_machines.get(&link_id).unwrap().leg().unwrap();
|
||||
assert!(
|
||||
leg.noise_handshake.is_none() && leg.noise_session.is_none(),
|
||||
"a failed handshake holds neither handle"
|
||||
);
|
||||
assert_present(&node, "failed, both handles empty");
|
||||
|
||||
// Detaching the carrier — and only that — ends handshake-phase membership.
|
||||
node.peer_machines.get_mut(&link_id).unwrap().take_leg();
|
||||
assert_eq!(node.connection_count(), 0, "connection_count after detach");
|
||||
assert_eq!(node.connections().count(), 0, "connections() after detach");
|
||||
assert!(
|
||||
!node.has_pending_leg(&link_id),
|
||||
"has_pending_leg after detach"
|
||||
);
|
||||
assert_eq!(
|
||||
node.stale_connections(1_000_000, 30_000).len(),
|
||||
0,
|
||||
"stale_connections after detach"
|
||||
);
|
||||
assert!(
|
||||
!node.is_connecting_to_peer_on_path(peer_identity.node_addr(), transport_id, &peer_addr),
|
||||
"is_connecting_to_peer_on_path after detach"
|
||||
);
|
||||
}
|
||||
|
||||
// The master-line `inbound_msg1_records_the_learned_identity_on_the_carrier`
|
||||
// pins the identity learn onto the surviving carrier at msg1. That is an IK
|
||||
// property: XX msg1 is ephemeral-only and learns nothing, so the responder has
|
||||
// no identity to record there. The learn happens in `complete_handshake` (msg2,
|
||||
// initiator) and `complete_handshake_msg3` (msg3, responder); the carrier guard
|
||||
// belongs at those two points on this line.
|
||||
|
||||
/// A msg1 that fails Noise processing must leave no trace in the registry.
|
||||
/// The control machine is built above the crypto so it can drive the
|
||||
/// handshake, but it stays a local until a promote tail inserts it — a
|
||||
/// rejected msg1 drops it.
|
||||
#[tokio::test]
|
||||
async fn test_rejected_msg1_leaves_no_registry_trace() {
|
||||
let mut node = make_node();
|
||||
|
||||
// Well-formed framing, garbage Noise payload: processing fails.
|
||||
let wire_msg1 = crate::proto::fmp::wire::build_msg1(
|
||||
SessionIndex::new(7),
|
||||
&[0u8; crate::noise::HANDSHAKE_MSG1_SIZE],
|
||||
);
|
||||
let packet = ReceivedPacket::with_timestamp(
|
||||
TransportId::new(1),
|
||||
TransportAddr::from_string("127.0.0.1:5000"),
|
||||
wire_msg1,
|
||||
1000,
|
||||
);
|
||||
|
||||
node.handle_msg1(packet).await;
|
||||
|
||||
assert!(
|
||||
node.peer_machines.is_empty(),
|
||||
"a rejected msg1 must leave no control machine behind"
|
||||
);
|
||||
assert_eq!(node.connection_count(), 0);
|
||||
assert_eq!(node.peer_count(), 0);
|
||||
assert_eq!(node.link_count(), 0);
|
||||
assert!(
|
||||
node.peers_by_index.is_empty(),
|
||||
"a rejected msg1 must allocate no session index"
|
||||
);
|
||||
assert_eq!(
|
||||
node.stats().handshake.bad_state,
|
||||
1,
|
||||
"the rejection is attributed to the handshake state-machine counter"
|
||||
);
|
||||
}
|
||||
|
||||
/// The outbound path registers its control machine at dial, before msg1 is
|
||||
/// prepared, so a preparation failure has to unwind that registration rather
|
||||
/// than drop a local.
|
||||
#[tokio::test]
|
||||
async fn test_failed_msg1_preparation_unwinds_the_dial_machine() {
|
||||
let mut node = make_node();
|
||||
let link_id = LinkId::new(1);
|
||||
let transport_id = TransportId::new(1);
|
||||
let remote_addr = TransportAddr::from_string("127.0.0.1:5000");
|
||||
let peer_identity = make_peer_identity();
|
||||
|
||||
// Stand in for the dial: the machine exists before msg1 is prepared.
|
||||
node.peer_machines.insert(
|
||||
link_id,
|
||||
PeerMachine::new_outbound(link_id, Some(peer_identity), 1000),
|
||||
);
|
||||
// Force the index allocation inside msg1 preparation to fail.
|
||||
node.index_allocator = crate::utils::index::IndexAllocator::with_max_attempts(0);
|
||||
|
||||
let result = node.prepare_outbound_msg1(link_id, transport_id, &remote_addr, peer_identity);
|
||||
|
||||
assert!(matches!(result, Err(NodeError::IndexAllocationFailed(_))));
|
||||
assert!(
|
||||
!node.peer_machines.contains_key(&link_id),
|
||||
"a failed msg1 preparation must unwind the dial-time machine"
|
||||
);
|
||||
assert_eq!(node.connection_count(), 0);
|
||||
}
|
||||
|
||||
/// The link, direction, and peer address that promotion and the operator view
|
||||
/// read now come from the control machine rather than the pending connection.
|
||||
/// A machine's direction is seeded at construction and must match the side that
|
||||
/// actually opened the link, and its address must be populated by the time
|
||||
/// promotion needs it.
|
||||
///
|
||||
/// This covers the two shapes that seed a carrier independently: the dial and
|
||||
/// an accepted inbound message 1. The cross-connection winner derives both
|
||||
/// values from a carrier one of those two already seeded, so it has nothing
|
||||
/// separate to pin.
|
||||
#[tokio::test]
|
||||
async fn test_machine_carries_link_direction_and_address_on_dial_and_inbound() {
|
||||
// Outbound: the dial builds the machine, msg1 preparation fills it in.
|
||||
let mut node = make_node();
|
||||
let link_id = LinkId::new(1);
|
||||
let transport_id = TransportId::new(1);
|
||||
let remote_addr = TransportAddr::from_string("127.0.0.1:5000");
|
||||
let peer_identity = make_peer_identity();
|
||||
node.peer_machines.insert(
|
||||
link_id,
|
||||
PeerMachine::new_outbound(link_id, Some(peer_identity), 1000),
|
||||
);
|
||||
node.prepare_outbound_msg1(link_id, transport_id, &remote_addr, peer_identity)
|
||||
.unwrap();
|
||||
|
||||
let machine = node.peer_machines.get(&link_id).unwrap();
|
||||
assert_eq!(machine.link_id(), link_id);
|
||||
assert!(machine.conn_is_outbound(), "a dial is outbound");
|
||||
assert!(!machine.conn_is_inbound());
|
||||
assert_eq!(machine.conn_direction(), LinkDirection::Outbound);
|
||||
assert_eq!(
|
||||
machine.conn_source_addr(),
|
||||
Some(&remote_addr),
|
||||
"the dialled address must reach the surviving carrier"
|
||||
);
|
||||
|
||||
// Inbound: msg1 builds the machine from the packet.
|
||||
let mut responder = make_node();
|
||||
let initiator = make_node();
|
||||
let responder_identity = PeerIdentity::from_pubkey_full(responder.identity().pubkey_full());
|
||||
let mut initiator_leg = outbound_leg(LinkId::new(9), responder_identity, 1000);
|
||||
let noise_msg1 = initiator_leg
|
||||
.start_handshake(
|
||||
initiator.identity().keypair(),
|
||||
initiator.startup_epoch(),
|
||||
1000,
|
||||
)
|
||||
.unwrap();
|
||||
let inbound_addr = TransportAddr::from_string("127.0.0.1:6000");
|
||||
let packet = ReceivedPacket::with_timestamp(
|
||||
TransportId::new(1),
|
||||
inbound_addr.clone(),
|
||||
crate::proto::fmp::wire::build_msg1(SessionIndex::new(7), &noise_msg1),
|
||||
1000,
|
||||
);
|
||||
responder.handle_msg1(packet).await;
|
||||
|
||||
// The responder completes at msg1 and promotes, so the machine survives as
|
||||
// the active peer's control machine — the carrier outlives the connection.
|
||||
let (link, machine) = responder
|
||||
.peer_machines
|
||||
.iter()
|
||||
.next()
|
||||
.expect("msg1 leaves a control machine behind");
|
||||
assert!(machine.conn_is_inbound(), "an accepted msg1 is inbound");
|
||||
assert!(!machine.conn_is_outbound());
|
||||
assert_eq!(machine.conn_direction(), LinkDirection::Inbound);
|
||||
assert_eq!(
|
||||
machine.conn_source_addr(),
|
||||
Some(&inbound_addr),
|
||||
"the sender's address must reach the surviving carrier"
|
||||
);
|
||||
assert_eq!(machine.link_id(), *link);
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ impl ActivePeer {
|
||||
|
||||
/// Create from verified identity with existing link stats.
|
||||
///
|
||||
/// Used when promoting from PeerConnection, preserving handshake stats.
|
||||
/// Used when promoting a completed handshake, preserving its link stats.
|
||||
/// For peers with Noise sessions, use `with_session` instead.
|
||||
pub fn with_stats(
|
||||
identity: PeerIdentity,
|
||||
|
||||
@@ -1,669 +0,0 @@
|
||||
//! Peer Connection (Handshake Phase)
|
||||
//!
|
||||
//! Represents an in-progress connection before authentication completes.
|
||||
//! PeerConnection tracks the Noise IK handshake and transitions to
|
||||
//! ActivePeer upon successful authentication. The handshake *phase* (initial /
|
||||
//! sent_msg1 / complete / failed) is no longer tracked here — it lives on the
|
||||
//! per-peer control machine; the leg's crypto methods gate on the presence of
|
||||
//! their Noise handles (`noise_handshake` / `noise_session`) directly.
|
||||
|
||||
use crate::PeerIdentity;
|
||||
use crate::noise::{self, NoiseError, NoiseSession};
|
||||
use crate::proto::fmp::{ConnectionState, NodeProfile};
|
||||
use crate::transport::{LinkDirection, LinkId, TransportAddr, TransportId};
|
||||
use crate::utils::index::SessionIndex;
|
||||
use secp256k1::Keypair;
|
||||
use std::fmt;
|
||||
|
||||
/// A connection in the handshake phase, before authentication completes.
|
||||
///
|
||||
/// For outbound connections, we know the expected peer identity from config.
|
||||
/// For inbound connections, we learn the identity during the Noise handshake.
|
||||
///
|
||||
/// This is the shell holder for the FMP crypto/state split: the pure
|
||||
/// connection bookkeeping lives in [`ConnectionState`] (`proto::fmp::state`),
|
||||
/// and the two Noise crypto handles stay here beside it. Pure public methods
|
||||
/// delegate to `self.state`; the XX transition methods drive the crypto and
|
||||
/// write results back through `self.state`'s setters.
|
||||
pub struct PeerConnection {
|
||||
/// Pure, runtime-agnostic connection bookkeeping.
|
||||
state: ConnectionState,
|
||||
|
||||
/// Noise handshake state (consumes on completion).
|
||||
noise_handshake: Option<noise::HandshakeState>,
|
||||
|
||||
/// Completed Noise session (available after handshake complete).
|
||||
noise_session: Option<NoiseSession>,
|
||||
}
|
||||
|
||||
impl PeerConnection {
|
||||
/// Create a new outbound connection (we are initiating).
|
||||
///
|
||||
/// For outbound, we know who we're trying to reach from configuration.
|
||||
/// The Noise handshake will be initialized when `start_handshake` is called.
|
||||
pub fn outbound(
|
||||
link_id: LinkId,
|
||||
expected_identity: PeerIdentity,
|
||||
current_time_ms: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: ConnectionState::outbound(link_id, expected_identity, current_time_ms),
|
||||
noise_handshake: None,
|
||||
noise_session: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new outbound connection without a pre-known identity.
|
||||
///
|
||||
/// Used for anonymous discovery on shared-media transports (Ethernet,
|
||||
/// BLE) where the beacon doesn't carry identity. The peer's identity is
|
||||
/// learned from XX msg2 during the handshake.
|
||||
pub fn outbound_anonymous(link_id: LinkId, current_time_ms: u64) -> Self {
|
||||
Self {
|
||||
state: ConnectionState::outbound_anonymous(link_id, current_time_ms),
|
||||
noise_handshake: None,
|
||||
noise_session: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new inbound connection (they are initiating).
|
||||
///
|
||||
/// For inbound, we don't know who they are until we decrypt their
|
||||
/// identity from Noise message 1.
|
||||
pub fn inbound(link_id: LinkId, current_time_ms: u64) -> Self {
|
||||
Self {
|
||||
state: ConnectionState::inbound(link_id, current_time_ms),
|
||||
noise_handshake: None,
|
||||
noise_session: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new inbound connection with transport information.
|
||||
///
|
||||
/// Used when processing msg1 where we know the transport and source address.
|
||||
pub fn inbound_with_transport(
|
||||
link_id: LinkId,
|
||||
transport_id: TransportId,
|
||||
source_addr: TransportAddr,
|
||||
current_time_ms: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: ConnectionState::inbound_with_transport(
|
||||
link_id,
|
||||
transport_id,
|
||||
source_addr,
|
||||
current_time_ms,
|
||||
),
|
||||
noise_handshake: None,
|
||||
noise_session: None,
|
||||
}
|
||||
}
|
||||
|
||||
// === Accessors (delegated to the pure ConnectionState) ===
|
||||
|
||||
/// Get the link ID.
|
||||
pub fn link_id(&self) -> LinkId {
|
||||
self.state.link_id()
|
||||
}
|
||||
|
||||
/// Get the connection direction.
|
||||
pub fn direction(&self) -> LinkDirection {
|
||||
self.state.direction()
|
||||
}
|
||||
|
||||
/// Get the expected/learned peer identity, if known.
|
||||
pub fn expected_identity(&self) -> Option<&PeerIdentity> {
|
||||
self.state.expected_identity()
|
||||
}
|
||||
|
||||
/// Check if this is an outbound connection.
|
||||
pub fn is_outbound(&self) -> bool {
|
||||
self.state.is_outbound()
|
||||
}
|
||||
|
||||
/// Check if this is an inbound connection.
|
||||
pub fn is_inbound(&self) -> bool {
|
||||
self.state.is_inbound()
|
||||
}
|
||||
|
||||
/// When the connection started. Retained only to seed a control machine's
|
||||
/// carrier from a pre-built leg (`Node::add_connection`); the operator-facing
|
||||
/// `started_at_ms`/`last_activity_ms` telemetry now reads the machine carrier,
|
||||
/// not the leg.
|
||||
pub fn started_at(&self) -> u64 {
|
||||
self.state.started_at()
|
||||
}
|
||||
|
||||
/// Connection duration so far.
|
||||
pub fn duration(&self, current_time_ms: u64) -> u64 {
|
||||
self.state.duration(current_time_ms)
|
||||
}
|
||||
|
||||
/// Time since last activity.
|
||||
pub fn idle_time(&self, current_time_ms: u64) -> u64 {
|
||||
self.state.idle_time(current_time_ms)
|
||||
}
|
||||
|
||||
// === Index Accessors ===
|
||||
|
||||
/// Get our session index (if set).
|
||||
pub fn our_index(&self) -> Option<SessionIndex> {
|
||||
self.state.our_index()
|
||||
}
|
||||
|
||||
/// Set our session index.
|
||||
pub fn set_our_index(&mut self, index: SessionIndex) {
|
||||
self.state.set_our_index(index);
|
||||
}
|
||||
|
||||
/// Get their session index (if known).
|
||||
pub fn their_index(&self) -> Option<SessionIndex> {
|
||||
self.state.their_index()
|
||||
}
|
||||
|
||||
/// Set their session index.
|
||||
pub fn set_their_index(&mut self, index: SessionIndex) {
|
||||
self.state.set_their_index(index);
|
||||
}
|
||||
|
||||
/// Get the transport ID (if set).
|
||||
pub fn transport_id(&self) -> Option<TransportId> {
|
||||
self.state.transport_id()
|
||||
}
|
||||
|
||||
/// Set the transport ID.
|
||||
pub fn set_transport_id(&mut self, id: TransportId) {
|
||||
self.state.set_transport_id(id);
|
||||
}
|
||||
|
||||
/// Get the source address (if known).
|
||||
pub fn source_addr(&self) -> Option<&TransportAddr> {
|
||||
self.state.source_addr()
|
||||
}
|
||||
|
||||
/// Set the source address.
|
||||
pub fn set_source_addr(&mut self, addr: TransportAddr) {
|
||||
self.state.set_source_addr(addr);
|
||||
}
|
||||
|
||||
// === Epoch Accessors ===
|
||||
|
||||
/// Get the remote peer's startup epoch (available after handshake).
|
||||
pub fn remote_epoch(&self) -> Option<[u8; 8]> {
|
||||
self.state.remote_epoch()
|
||||
}
|
||||
|
||||
// === Negotiation Results ===
|
||||
|
||||
/// Get the peer's negotiated node profile, if learned.
|
||||
pub fn peer_profile(&self) -> Option<NodeProfile> {
|
||||
self.state.peer_profile()
|
||||
}
|
||||
|
||||
/// Record the peer's node profile learned during FMP negotiation.
|
||||
pub fn set_negotiation_results(&mut self, peer_profile: NodeProfile) {
|
||||
self.state.set_negotiation_results(peer_profile);
|
||||
}
|
||||
|
||||
// === Handshake Resend ===
|
||||
|
||||
/// Store the wire-format msg1 bytes for resend and schedule the first resend.
|
||||
pub fn set_handshake_msg1(&mut self, msg1: Vec<u8>, first_resend_at_ms: u64) {
|
||||
self.state.set_handshake_msg1(msg1, first_resend_at_ms);
|
||||
}
|
||||
|
||||
/// Store the wire-format msg2 bytes for resend on duplicate msg1.
|
||||
pub fn set_handshake_msg2(&mut self, msg2: Vec<u8>) {
|
||||
self.state.set_handshake_msg2(msg2);
|
||||
}
|
||||
|
||||
/// Get the stored msg1 bytes (if any).
|
||||
pub fn handshake_msg1(&self) -> Option<&[u8]> {
|
||||
self.state.handshake_msg1()
|
||||
}
|
||||
|
||||
/// Get the stored msg2 bytes (if any).
|
||||
pub fn handshake_msg2(&self) -> Option<&[u8]> {
|
||||
self.state.handshake_msg2()
|
||||
}
|
||||
|
||||
// === Noise Handshake Operations ===
|
||||
|
||||
/// Start the handshake as initiator and generate message 1.
|
||||
///
|
||||
/// For outbound connections only. Returns the Noise XX msg1 bytes.
|
||||
/// XX msg1 is ephemeral-only (33 bytes) — no identity or epoch.
|
||||
pub fn start_handshake(
|
||||
&mut self,
|
||||
our_keypair: Keypair,
|
||||
epoch: [u8; 8],
|
||||
current_time_ms: u64,
|
||||
) -> Result<Vec<u8>, NoiseError> {
|
||||
if self.state.direction() != LinkDirection::Outbound {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "outbound connection".to_string(),
|
||||
got: "inbound connection".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// XX initiator: no remote static needed upfront. The old `!= Initial`
|
||||
// phase guard is dropped — this method creates the Noise handshake
|
||||
// handle, so there is no `take().expect()` to protect, and its Err path
|
||||
// was unreachable in production (one call per fresh outbound leg).
|
||||
let mut hs = noise::HandshakeState::new_initiator(our_keypair);
|
||||
hs.set_local_epoch(epoch);
|
||||
let msg1 = hs.write_message_1()?;
|
||||
|
||||
self.noise_handshake = Some(hs);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok(msg1)
|
||||
}
|
||||
|
||||
/// Initialize responder and process incoming message 1.
|
||||
///
|
||||
/// For inbound connections only. Returns the Noise XX msg2 bytes.
|
||||
/// XX: identity is NOT learned from msg1 (only ephemeral exchange).
|
||||
/// The responder learns the initiator's identity from msg3.
|
||||
/// The handshake remains in ReceivedMsg1 state (not Complete).
|
||||
///
|
||||
/// If `negotiation_payload` is provided, it is encrypted and appended
|
||||
/// to the returned msg2 bytes.
|
||||
pub fn receive_handshake_init(
|
||||
&mut self,
|
||||
our_keypair: Keypair,
|
||||
epoch: [u8; 8],
|
||||
message: &[u8],
|
||||
negotiation_payload: Option<&[u8]>,
|
||||
current_time_ms: u64,
|
||||
) -> Result<Vec<u8>, NoiseError> {
|
||||
if self.state.direction() != LinkDirection::Inbound {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "inbound connection".to_string(),
|
||||
got: "outbound connection".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut hs = noise::HandshakeState::new_responder(our_keypair);
|
||||
hs.set_local_epoch(epoch);
|
||||
|
||||
// Process XX message 1 (ephemeral only — no identity learned)
|
||||
hs.read_message_1(message)?;
|
||||
|
||||
// Generate XX message 2 (sends our static + epoch)
|
||||
let mut msg2 = hs.write_message_2()?;
|
||||
|
||||
// Append encrypted negotiation payload if provided
|
||||
if let Some(payload) = negotiation_payload {
|
||||
let encrypted = hs.encrypt_payload(payload)?;
|
||||
msg2.extend_from_slice(&encrypted);
|
||||
}
|
||||
|
||||
// XX: handshake NOT complete yet — need msg3. Keep the Noise handshake
|
||||
// handle for complete_handshake_msg3(); the phase (formerly
|
||||
// `ReceivedMsg1`) now lives on the control machine, so no phase is set
|
||||
// here. The handle's presence is the leg-local `ReceivedMsg1` signal.
|
||||
self.noise_handshake = Some(hs);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok(msg2)
|
||||
}
|
||||
|
||||
/// Complete the handshake by processing message 2 and generating message 3.
|
||||
///
|
||||
/// For outbound connections only (initiator). Processes the responder's
|
||||
/// msg2 (learning their identity and epoch), then generates msg3.
|
||||
/// Returns the Noise XX msg3 bytes to send.
|
||||
///
|
||||
/// If `negotiation_payload` is provided, it is encrypted and appended
|
||||
/// to the returned msg3 bytes. If the received msg2 contains a negotiation
|
||||
/// payload (bytes beyond the base XX msg2), it is decrypted and returned.
|
||||
pub fn complete_handshake(
|
||||
&mut self,
|
||||
message: &[u8],
|
||||
negotiation_payload: Option<&[u8]>,
|
||||
current_time_ms: u64,
|
||||
) -> Result<(Vec<u8>, Option<Vec<u8>>), NoiseError> {
|
||||
// The leg is at `SentMsg1` iff its Noise handshake handle is present
|
||||
// (set by `start_handshake`, taken here on completion). Gate on the
|
||||
// handle directly now that the phase enum is gone — byte-equivalent to
|
||||
// the old `!= SentMsg1` guard for every reachable transition.
|
||||
if self.noise_handshake.is_none() {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "sent_msg1 state".to_string(),
|
||||
got: "no active handshake".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut hs = self
|
||||
.noise_handshake
|
||||
.take()
|
||||
.expect("noise handshake must exist in SentMsg1 state");
|
||||
|
||||
// Split msg2 into base XX part and optional negotiation
|
||||
let base_size = noise::HANDSHAKE_MSG2_SIZE;
|
||||
let (base_msg2, extra) = if message.len() > base_size {
|
||||
(&message[..base_size], Some(&message[base_size..]))
|
||||
} else {
|
||||
(message, None)
|
||||
};
|
||||
|
||||
// Process XX msg2 (learns responder identity + epoch)
|
||||
hs.read_message_2(base_msg2)?;
|
||||
|
||||
// Decrypt negotiation payload from msg2 if present
|
||||
let received_negotiation = if let Some(encrypted) = extra {
|
||||
Some(hs.decrypt_payload(encrypted)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Learn responder identity from msg2
|
||||
let remote_static = *hs
|
||||
.remote_static()
|
||||
.expect("remote static available after XX msg2");
|
||||
self.state
|
||||
.set_expected_identity(PeerIdentity::from_pubkey_full(remote_static));
|
||||
|
||||
// Capture remote epoch from msg2
|
||||
self.state.set_remote_epoch(hs.remote_epoch());
|
||||
|
||||
// Generate XX msg3
|
||||
let mut msg3 = hs.write_message_3()?;
|
||||
|
||||
// Append encrypted negotiation payload if provided
|
||||
if let Some(payload) = negotiation_payload {
|
||||
let encrypted = hs.encrypt_payload(payload)?;
|
||||
msg3.extend_from_slice(&encrypted);
|
||||
}
|
||||
|
||||
// Handshake complete for initiator
|
||||
let session = hs.into_session()?;
|
||||
self.noise_session = Some(session);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok((msg3, received_negotiation))
|
||||
}
|
||||
|
||||
/// Complete the responder handshake by processing message 3.
|
||||
///
|
||||
/// For inbound connections only (responder). Processes the initiator's
|
||||
/// msg3, learning their identity and epoch.
|
||||
///
|
||||
/// If the msg3 contains a negotiation payload (bytes beyond base XX msg3),
|
||||
/// it is decrypted and returned.
|
||||
pub fn complete_handshake_msg3(
|
||||
&mut self,
|
||||
message: &[u8],
|
||||
current_time_ms: u64,
|
||||
) -> Result<Option<Vec<u8>>, NoiseError> {
|
||||
// The responder leg is at `ReceivedMsg1` iff its Noise handshake handle
|
||||
// is present (set by `receive_handshake_init`, taken here on msg3). Gate
|
||||
// on the handle directly now that the phase enum is gone — byte-equivalent
|
||||
// to the old `!= ReceivedMsg1` guard for every reachable transition, and
|
||||
// it protects the `take().expect()` below.
|
||||
if self.noise_handshake.is_none() {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "received_msg1 state".to_string(),
|
||||
got: "no active handshake".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut hs = self
|
||||
.noise_handshake
|
||||
.take()
|
||||
.expect("noise handshake must exist in ReceivedMsg1 state");
|
||||
|
||||
// Split msg3 into base XX part and optional negotiation
|
||||
let base_size = noise::HANDSHAKE_MSG3_SIZE;
|
||||
let (base_msg3, extra) = if message.len() > base_size {
|
||||
(&message[..base_size], Some(&message[base_size..]))
|
||||
} else {
|
||||
(message, None)
|
||||
};
|
||||
|
||||
// Process XX msg3 (learns initiator identity + epoch)
|
||||
hs.read_message_3(base_msg3)?;
|
||||
|
||||
// Decrypt negotiation payload from msg3 if present
|
||||
let received_negotiation = if let Some(encrypted) = extra {
|
||||
Some(hs.decrypt_payload(encrypted)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Learn initiator identity from msg3
|
||||
let remote_static = *hs
|
||||
.remote_static()
|
||||
.expect("remote static available after XX msg3");
|
||||
self.state
|
||||
.set_expected_identity(PeerIdentity::from_pubkey_full(remote_static));
|
||||
|
||||
// Capture remote epoch from msg3
|
||||
self.state.set_remote_epoch(hs.remote_epoch());
|
||||
|
||||
// Handshake complete for responder. The completion signal is now the
|
||||
// Noise session's presence (the phase enum is gone); no phase is set.
|
||||
let session = hs.into_session()?;
|
||||
self.noise_session = Some(session);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok(received_negotiation)
|
||||
}
|
||||
|
||||
/// Take the completed Noise session.
|
||||
///
|
||||
/// Returns the NoiseSession for use in ActivePeer. Can only be called
|
||||
/// once after handshake completes.
|
||||
pub fn take_session(&mut self) -> Option<NoiseSession> {
|
||||
// The session exists iff the handshake reached `Complete`, so taking it
|
||||
// unconditionally is byte-equivalent to the old `== Complete` gate.
|
||||
self.noise_session.take()
|
||||
}
|
||||
|
||||
/// Check if we have a completed session ready to take.
|
||||
pub fn has_session(&self) -> bool {
|
||||
self.noise_session.is_some()
|
||||
}
|
||||
|
||||
// === State Transitions (for manual control if needed) ===
|
||||
|
||||
/// Drop the shell-owned crypto handshake handle. The failure *state* now
|
||||
/// lives on the control machine (`PeerMachine`); this only releases the
|
||||
/// leg's Noise handle at the identical point it was released before, so a
|
||||
/// subsequent `complete_handshake` on this leg still reports `WrongState`.
|
||||
pub fn mark_failed(&mut self) {
|
||||
self.noise_handshake = None;
|
||||
}
|
||||
|
||||
// === Validation ===
|
||||
|
||||
/// Check if the connection has timed out.
|
||||
pub fn is_timed_out(&self, current_time_ms: u64, timeout_ms: u64) -> bool {
|
||||
self.state.is_timed_out(current_time_ms, timeout_ms)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PeerConnection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PeerConnection")
|
||||
.field("link_id", &self.state.link_id())
|
||||
.field("direction", &self.state.direction())
|
||||
.field("expected_identity", &self.state.expected_identity())
|
||||
.field("has_noise_handshake", &self.noise_handshake.is_some())
|
||||
.field("has_noise_session", &self.noise_session.is_some())
|
||||
.field("our_index", &self.state.our_index())
|
||||
.field("their_index", &self.state.their_index())
|
||||
.field("transport_id", &self.state.transport_id())
|
||||
.field("started_at", &self.state.started_at())
|
||||
.field("last_activity", &self.state.last_activity())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Identity;
|
||||
use rand::Rng;
|
||||
|
||||
fn make_peer_identity() -> PeerIdentity {
|
||||
let identity = Identity::generate();
|
||||
PeerIdentity::from_pubkey(identity.pubkey())
|
||||
}
|
||||
|
||||
fn make_keypair() -> Keypair {
|
||||
let identity = Identity::generate();
|
||||
identity.keypair()
|
||||
}
|
||||
|
||||
fn make_epoch() -> [u8; 8] {
|
||||
let mut epoch = [0u8; 8];
|
||||
rand::rng().fill_bytes(&mut epoch);
|
||||
epoch
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_outbound_connection() {
|
||||
let identity = make_peer_identity();
|
||||
let conn = PeerConnection::outbound(LinkId::new(1), identity, 1000);
|
||||
|
||||
assert!(conn.is_outbound());
|
||||
assert!(!conn.is_inbound());
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.expected_identity().is_some());
|
||||
assert_eq!(conn.started_at(), 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inbound_connection() {
|
||||
let conn = PeerConnection::inbound(LinkId::new(2), 2000);
|
||||
|
||||
assert!(conn.is_inbound());
|
||||
assert!(!conn.is_outbound());
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.expected_identity().is_none());
|
||||
assert_eq!(conn.started_at(), 2000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_handshake_flow() {
|
||||
// Create identities
|
||||
let initiator_identity = Identity::generate();
|
||||
let responder_identity = Identity::generate();
|
||||
|
||||
let initiator_keypair = initiator_identity.keypair();
|
||||
let responder_keypair = responder_identity.keypair();
|
||||
let initiator_epoch = make_epoch();
|
||||
let responder_epoch = make_epoch();
|
||||
|
||||
// Use from_pubkey_full to preserve parity for ECDH
|
||||
let responder_peer_id = PeerIdentity::from_pubkey_full(responder_identity.pubkey_full());
|
||||
|
||||
// Create connections
|
||||
let mut initiator_conn = PeerConnection::outbound(LinkId::new(1), responder_peer_id, 1000);
|
||||
let mut responder_conn = PeerConnection::inbound(LinkId::new(2), 1000);
|
||||
|
||||
// Initiator starts XX handshake
|
||||
let msg1 = initiator_conn
|
||||
.start_handshake(initiator_keypair, initiator_epoch, 1100)
|
||||
.unwrap();
|
||||
// Post-msg1 the initiator holds an in-flight handshake, not yet a session.
|
||||
assert!(!initiator_conn.has_session());
|
||||
|
||||
// Responder processes msg1 and sends msg2 (XX: does NOT complete yet)
|
||||
let msg2 = responder_conn
|
||||
.receive_handshake_init(responder_keypair, responder_epoch, &msg1, None, 1200)
|
||||
.unwrap();
|
||||
// XX: the responder parks awaiting msg3 — it holds an in-flight
|
||||
// handshake, not yet a session (the deleted phase was `ReceivedMsg1`).
|
||||
assert!(!responder_conn.has_session());
|
||||
// Responder does NOT know initiator's identity yet (XX property)
|
||||
assert!(responder_conn.expected_identity().is_none());
|
||||
|
||||
// Initiator processes msg2 and generates msg3
|
||||
let (msg3, _neg) = initiator_conn
|
||||
.complete_handshake(&msg2, None, 1300)
|
||||
.unwrap();
|
||||
// The initiator completes at msg3 generation: it now holds a session.
|
||||
assert!(initiator_conn.has_session());
|
||||
|
||||
// Initiator learned responder's identity from msg2
|
||||
let discovered = initiator_conn.expected_identity().unwrap();
|
||||
assert_eq!(discovered.pubkey(), responder_identity.pubkey());
|
||||
assert_eq!(initiator_conn.remote_epoch(), Some(responder_epoch));
|
||||
|
||||
// Responder processes msg3 (completes handshake): it now holds a session.
|
||||
let _neg = responder_conn.complete_handshake_msg3(&msg3, 1400).unwrap();
|
||||
assert!(responder_conn.has_session());
|
||||
|
||||
// Responder learned initiator's identity from msg3
|
||||
let discovered = responder_conn.expected_identity().unwrap();
|
||||
assert_eq!(discovered.pubkey(), initiator_identity.pubkey());
|
||||
assert_eq!(responder_conn.remote_epoch(), Some(initiator_epoch));
|
||||
|
||||
// Both have sessions
|
||||
assert!(initiator_conn.has_session());
|
||||
assert!(responder_conn.has_session());
|
||||
|
||||
// Take and verify sessions work
|
||||
let mut init_session = initiator_conn.take_session().unwrap();
|
||||
let mut resp_session = responder_conn.take_session().unwrap();
|
||||
|
||||
// Encrypt/decrypt test
|
||||
let plaintext = b"test message";
|
||||
let ciphertext = init_session.encrypt(plaintext).unwrap();
|
||||
let decrypted = resp_session.decrypt(&ciphertext).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connection_timing() {
|
||||
let identity = make_peer_identity();
|
||||
let conn = PeerConnection::outbound(LinkId::new(1), identity, 1000);
|
||||
|
||||
assert_eq!(conn.duration(1500), 500);
|
||||
assert_eq!(conn.idle_time(1500), 500);
|
||||
assert!(!conn.is_timed_out(1500, 1000));
|
||||
assert!(conn.is_timed_out(2500, 1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connection_failure() {
|
||||
// `mark_failed` releases the leg's Noise handshake handle. The failure
|
||||
// *state* now lives on the control machine, but the leg-local effect is
|
||||
// still observable: a completion attempt afterward reports `WrongState`
|
||||
// (the handle-presence gate) and no session is produced.
|
||||
let identity = make_peer_identity();
|
||||
let keypair = make_keypair();
|
||||
let mut conn = PeerConnection::outbound(LinkId::new(1), identity, 1000);
|
||||
conn.start_handshake(keypair, make_epoch(), 1100).unwrap();
|
||||
|
||||
conn.mark_failed();
|
||||
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.complete_handshake(&[0u8; 96], None, 1200).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrong_direction_errors() {
|
||||
let identity = make_peer_identity();
|
||||
let keypair = make_keypair();
|
||||
|
||||
// Outbound can't receive_handshake_init
|
||||
let mut outbound = PeerConnection::outbound(LinkId::new(1), identity, 1000);
|
||||
assert!(
|
||||
outbound
|
||||
.receive_handshake_init(keypair, make_epoch(), &[0u8; 33], None, 1100)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Inbound can't start_handshake
|
||||
let mut inbound = PeerConnection::inbound(LinkId::new(2), 1000);
|
||||
assert!(
|
||||
inbound
|
||||
.start_handshake(keypair, make_epoch(), 1100)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
//!
|
||||
//! The XX cross-connection swap ([`CrossConnect`](InboundDecision::CrossConnect))
|
||||
//! and rekey-responder ([`RekeyRespond`](InboundDecision::RekeyRespond)) arms both
|
||||
//! move a `NoiseSession` between a `PeerConnection` and an `ActivePeer`
|
||||
//! move a `NoiseSession` between a handshake carrier and an `ActivePeer`
|
||||
//! (`take_session`/`replace_session`/`set_pending_session`) — effects that cannot
|
||||
//! ride in a plain-data [`PeerAction`]. The machine owns only the *classification
|
||||
//! dispatch* + the index-plane facts; it emits a plain-data trigger
|
||||
@@ -83,16 +83,17 @@
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::peer::PeerConnection;
|
||||
use crate::noise::{self, NoiseError, NoiseSession};
|
||||
use crate::proto::fmp::{
|
||||
ConnAction, ConnSnapshot, ConnectionState, EstablishSnapshot, Fmp, InboundDecision,
|
||||
InboundReject, NodeProfile, OutboundDecision, OutboundSnapshot, PeerSnapshot, PromotionResult,
|
||||
RekeyCfg, RekeyResendSnapshot, WireOutcome,
|
||||
};
|
||||
use crate::proto::link::LinkMessageType;
|
||||
use crate::transport::{LinkId, LinkStats, TransportAddr, TransportId};
|
||||
use crate::transport::{LinkDirection, LinkId, LinkStats, TransportAddr, TransportId};
|
||||
use crate::utils::index::{IndexAllocator, SessionIndex};
|
||||
use crate::{NodeAddr, PeerIdentity};
|
||||
use secp256k1::Keypair;
|
||||
|
||||
// ============================================================================
|
||||
// Timing placeholders
|
||||
@@ -491,6 +492,55 @@ pub(crate) enum PeerAction {
|
||||
// The machine (control tier)
|
||||
// ============================================================================
|
||||
|
||||
/// The handshake operations are only reachable while a pending connection is
|
||||
/// attached; every path that drives the crypto attaches it first.
|
||||
fn no_pending_connection() -> NoiseError {
|
||||
NoiseError::WrongState {
|
||||
expected: "attached connection".to_string(),
|
||||
got: "no connection".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The handshake-phase Noise crypto, owned by the control machine.
|
||||
///
|
||||
/// PRESENCE OF THIS STRUCT (`PeerMachine::leg().is_some()`) IS THE
|
||||
/// HANDSHAKE-PHASE CARRIER SIGNAL — it is what `Node::connections()`,
|
||||
/// `connection_count()`, the stale-connection sweep, the transport-in-use
|
||||
/// check, and the peering budget all key on. It is attached and detached at
|
||||
/// exactly the points the pending connection was, and its presence is NOT a
|
||||
/// function of whether either handle is populated.
|
||||
///
|
||||
/// A present-but-empty value is legal and load-bearing: `mark_failed` drops
|
||||
/// the initiation handle while deliberately retaining the carrier so the
|
||||
/// sweep can reclaim it, and `take_session` empties the other. Deriving
|
||||
/// presence from handle presence would make every failed handshake invisible
|
||||
/// to the sweep — a permanent leak. See the presence tests in this module.
|
||||
pub(crate) struct HandshakeCrypto {
|
||||
/// Noise handshake state (consumed on completion).
|
||||
pub(crate) noise_handshake: Option<noise::HandshakeState>,
|
||||
/// Completed Noise session (available once the handshake completes).
|
||||
pub(crate) noise_session: Option<NoiseSession>,
|
||||
}
|
||||
|
||||
impl HandshakeCrypto {
|
||||
/// A fresh carrier holding neither handle, as every handshake begins.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
noise_handshake: None,
|
||||
noise_session: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for HandshakeCrypto {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("HandshakeCrypto")
|
||||
.field("has_noise_handshake", &self.noise_handshake.is_some())
|
||||
.field("has_noise_session", &self.noise_session.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-peer control FSM. Holds control-tier lifecycle state only; the
|
||||
/// send-critical state is published as `PeerSendState` and mutated via the
|
||||
/// emitted [`PeerAction`]s.
|
||||
@@ -501,12 +551,13 @@ pub(crate) struct PeerMachine {
|
||||
/// The crystallized peer node address. Known up front for outbound (from the
|
||||
/// dial identity); learned at msg3 for inbound (from [`WireOutcome`]).
|
||||
node_addr: Option<NodeAddr>,
|
||||
/// The pending handshake connection this machine owns while the leg is in
|
||||
/// the handshake window. `None` before the connection is built (the dial
|
||||
/// window) and after promotion consumes it (the machine survives as the
|
||||
/// active peer's control machine). Pure storage — the machine never reads
|
||||
/// or drives it; the shell reaches it through the accessors below.
|
||||
leg: Option<PeerConnection>,
|
||||
/// The handshake-phase Noise crypto this machine owns while it is in the
|
||||
/// handshake window. `None` before the handshake begins (the dial window)
|
||||
/// and after promotion consumes it (the machine survives as the active
|
||||
/// peer's control machine). Its presence — not the state of the handles
|
||||
/// inside it — is what marks this machine as carrying a pending
|
||||
/// handshake; see [`HandshakeCrypto`].
|
||||
leg: Option<HandshakeCrypto>,
|
||||
/// Pure handshake-phase bookkeeping (link/direction/indices/transport/
|
||||
/// stored handshake bytes/epoch). Reused verbatim from the FMP state core.
|
||||
conn: ConnectionState,
|
||||
@@ -624,12 +675,21 @@ impl PeerMachine {
|
||||
/// their timeout).
|
||||
pub(crate) fn inbound_msg2_sent(link: LinkId, our_index: SessionIndex, now: u64) -> Self {
|
||||
let mut machine = Self::new_inbound(link, now);
|
||||
machine.conn.set_our_index(our_index);
|
||||
machine.state = PeerState::Handshaking {
|
||||
link,
|
||||
machine.park_inbound_msg2_sent(our_index);
|
||||
machine
|
||||
}
|
||||
|
||||
/// Park an inbound machine at `Handshaking{SentMsg2}`, recording the index
|
||||
/// it allocated. The msg1-inline path builds its machine above the crypto
|
||||
/// (the Noise step runs on the machine) and so cannot use the birth ctor,
|
||||
/// which needs an index that does not exist until after the crypto; this
|
||||
/// applies the same transition to the machine already in hand.
|
||||
pub(crate) fn park_inbound_msg2_sent(&mut self, our_index: SessionIndex) {
|
||||
self.conn.set_our_index(our_index);
|
||||
self.state = PeerState::Handshaking {
|
||||
link: self.link,
|
||||
phase: HandshakePhase::SentMsg2,
|
||||
};
|
||||
machine
|
||||
}
|
||||
|
||||
/// New machine for an ALREADY-established peer: the post-handshake state a
|
||||
@@ -685,28 +745,312 @@ impl PeerMachine {
|
||||
self.state
|
||||
}
|
||||
|
||||
/// The pending handshake connection, if this leg is still in the
|
||||
/// handshake window.
|
||||
pub(crate) fn leg(&self) -> Option<&PeerConnection> {
|
||||
/// The handshake crypto carrier, if this machine is still in the
|
||||
/// handshake window. Presence answers "is there a pending handshake
|
||||
/// here", independently of whether either handle is populated.
|
||||
pub(crate) fn leg(&self) -> Option<&HandshakeCrypto> {
|
||||
self.leg.as_ref()
|
||||
}
|
||||
|
||||
/// Mutable access to the pending handshake connection.
|
||||
pub(crate) fn leg_mut(&mut self) -> Option<&mut PeerConnection> {
|
||||
self.leg.as_mut()
|
||||
}
|
||||
|
||||
/// Take the pending handshake connection off the machine (promotion and
|
||||
/// Take the handshake crypto carrier off the machine (promotion and
|
||||
/// teardown consume it by value).
|
||||
pub(crate) fn take_leg(&mut self) -> Option<PeerConnection> {
|
||||
pub(crate) fn take_leg(&mut self) -> Option<HandshakeCrypto> {
|
||||
self.leg.take()
|
||||
}
|
||||
|
||||
/// Embed a pending handshake connection on the machine.
|
||||
pub(crate) fn set_leg(&mut self, leg: PeerConnection) {
|
||||
/// Attach a handshake crypto carrier to the machine.
|
||||
pub(crate) fn set_leg(&mut self, leg: HandshakeCrypto) {
|
||||
self.leg = Some(leg);
|
||||
}
|
||||
|
||||
// === Noise handshake operations ===
|
||||
//
|
||||
// Mechanism, not decision: these are called by the shell, are never
|
||||
// reached from `step()`, and no event triggers them. Each drives the
|
||||
// Noise crypto on the pending connection and records the results on both
|
||||
// that connection and the surviving carrier, so a reader of either sees
|
||||
// the same value at the same point.
|
||||
|
||||
/// Start the handshake as initiator and generate message 1.
|
||||
///
|
||||
/// For outbound connections only. Returns the Noise XX msg1 bytes.
|
||||
/// XX msg1 is ephemeral-only (33 bytes) — no identity or epoch.
|
||||
pub(crate) fn start_handshake(
|
||||
&mut self,
|
||||
our_keypair: Keypair,
|
||||
epoch: [u8; 8],
|
||||
current_time_ms: u64,
|
||||
) -> Result<Vec<u8>, NoiseError> {
|
||||
let msg1 = {
|
||||
let direction = self.conn.direction();
|
||||
let leg = self.leg.as_mut().ok_or_else(no_pending_connection)?;
|
||||
|
||||
if direction != LinkDirection::Outbound {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "outbound connection".to_string(),
|
||||
got: "inbound connection".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// XX initiator: no remote static needed upfront, so an anonymous
|
||||
// dial (no expected identity) is a first-class case rather than an
|
||||
// error. The old `!= Initial` phase guard is dropped — this method
|
||||
// creates the Noise handshake handle, so there is no
|
||||
// `take().expect()` to protect, and its Err path was unreachable in
|
||||
// production (one call per fresh outbound handshake).
|
||||
let mut hs = noise::HandshakeState::new_initiator(our_keypair);
|
||||
hs.set_local_epoch(epoch);
|
||||
let msg1 = hs.write_message_1()?;
|
||||
|
||||
leg.noise_handshake = Some(hs);
|
||||
|
||||
msg1
|
||||
};
|
||||
self.conn.touch(current_time_ms);
|
||||
|
||||
Ok(msg1)
|
||||
}
|
||||
|
||||
/// Initialize responder and process incoming message 1.
|
||||
///
|
||||
/// For inbound connections only. Returns the Noise XX msg2 bytes.
|
||||
/// XX: identity is NOT learned from msg1 (only ephemeral exchange).
|
||||
/// The responder learns the initiator's identity from msg3.
|
||||
///
|
||||
/// If `negotiation_payload` is provided, it is encrypted and appended
|
||||
/// to the returned msg2 bytes.
|
||||
pub(crate) fn receive_handshake_init(
|
||||
&mut self,
|
||||
our_keypair: Keypair,
|
||||
epoch: [u8; 8],
|
||||
message: &[u8],
|
||||
negotiation_payload: Option<&[u8]>,
|
||||
current_time_ms: u64,
|
||||
) -> Result<Vec<u8>, NoiseError> {
|
||||
let msg2 = {
|
||||
let direction = self.conn.direction();
|
||||
let leg = self.leg.as_mut().ok_or_else(no_pending_connection)?;
|
||||
|
||||
if direction != LinkDirection::Inbound {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "inbound connection".to_string(),
|
||||
got: "outbound connection".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut hs = noise::HandshakeState::new_responder(our_keypair);
|
||||
hs.set_local_epoch(epoch);
|
||||
|
||||
// Process XX message 1 (ephemeral only — no identity learned)
|
||||
hs.read_message_1(message)?;
|
||||
|
||||
// Generate XX message 2 (sends our static + epoch)
|
||||
let mut msg2 = hs.write_message_2()?;
|
||||
|
||||
// Append encrypted negotiation payload if provided
|
||||
if let Some(payload) = negotiation_payload {
|
||||
let encrypted = hs.encrypt_payload(payload)?;
|
||||
msg2.extend_from_slice(&encrypted);
|
||||
}
|
||||
|
||||
// XX: handshake NOT complete yet — need msg3. Keep the Noise
|
||||
// handshake handle for `complete_handshake_msg3()`; the phase
|
||||
// (formerly `ReceivedMsg1`) lives on this machine, so no phase is
|
||||
// set here. The handle's presence is the crypto-local
|
||||
// `ReceivedMsg1` signal.
|
||||
leg.noise_handshake = Some(hs);
|
||||
|
||||
msg2
|
||||
};
|
||||
self.conn.touch(current_time_ms);
|
||||
|
||||
Ok(msg2)
|
||||
}
|
||||
|
||||
/// Complete the handshake by processing message 2 and generating message 3.
|
||||
///
|
||||
/// For outbound connections only (initiator). Processes the responder's
|
||||
/// msg2 (learning their identity and epoch), then generates msg3.
|
||||
/// Returns the Noise XX msg3 bytes to send.
|
||||
///
|
||||
/// If `negotiation_payload` is provided, it is encrypted and appended
|
||||
/// to the returned msg3 bytes. If the received msg2 contains a negotiation
|
||||
/// payload (bytes beyond the base XX msg2), it is decrypted and returned.
|
||||
pub(crate) fn complete_handshake(
|
||||
&mut self,
|
||||
message: &[u8],
|
||||
negotiation_payload: Option<&[u8]>,
|
||||
current_time_ms: u64,
|
||||
) -> Result<(Vec<u8>, Option<Vec<u8>>), NoiseError> {
|
||||
let (msg3, received_negotiation, learned_identity, remote_epoch) = {
|
||||
let leg = self.leg.as_mut().ok_or_else(no_pending_connection)?;
|
||||
|
||||
// The connection is at `SentMsg1` iff its Noise handshake handle is
|
||||
// present (set by `start_handshake`, taken here on completion).
|
||||
// Gating on the handle directly is byte-equivalent to the old
|
||||
// `!= SentMsg1` guard for every reachable transition.
|
||||
if leg.noise_handshake.is_none() {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "sent_msg1 state".to_string(),
|
||||
got: "no active handshake".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut hs = leg
|
||||
.noise_handshake
|
||||
.take()
|
||||
.expect("noise handshake must exist in SentMsg1 state");
|
||||
|
||||
// Split msg2 into base XX part and optional negotiation
|
||||
let base_size = noise::HANDSHAKE_MSG2_SIZE;
|
||||
let (base_msg2, extra) = if message.len() > base_size {
|
||||
(&message[..base_size], Some(&message[base_size..]))
|
||||
} else {
|
||||
(message, None)
|
||||
};
|
||||
|
||||
// Process XX msg2 (learns responder identity + epoch)
|
||||
hs.read_message_2(base_msg2)?;
|
||||
|
||||
// Decrypt negotiation payload from msg2 if present
|
||||
let received_negotiation = if let Some(encrypted) = extra {
|
||||
Some(hs.decrypt_payload(encrypted)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Learn responder identity from msg2
|
||||
let remote_static = *hs
|
||||
.remote_static()
|
||||
.expect("remote static available after XX msg2");
|
||||
let learned_identity = PeerIdentity::from_pubkey_full(remote_static);
|
||||
|
||||
// Capture remote epoch from msg2
|
||||
let remote_epoch = hs.remote_epoch();
|
||||
|
||||
// Generate XX msg3
|
||||
let mut msg3 = hs.write_message_3()?;
|
||||
|
||||
// Append encrypted negotiation payload if provided
|
||||
if let Some(payload) = negotiation_payload {
|
||||
let encrypted = hs.encrypt_payload(payload)?;
|
||||
msg3.extend_from_slice(&encrypted);
|
||||
}
|
||||
|
||||
// Handshake complete for initiator
|
||||
let session = hs.into_session()?;
|
||||
leg.noise_session = Some(session);
|
||||
|
||||
(msg3, received_negotiation, learned_identity, remote_epoch)
|
||||
};
|
||||
self.conn.set_expected_identity(learned_identity);
|
||||
self.conn.set_remote_epoch(remote_epoch);
|
||||
self.conn.touch(current_time_ms);
|
||||
|
||||
Ok((msg3, received_negotiation))
|
||||
}
|
||||
|
||||
/// Complete the responder handshake by processing message 3.
|
||||
///
|
||||
/// For inbound connections only (responder). Processes the initiator's
|
||||
/// msg3, learning their identity and epoch.
|
||||
///
|
||||
/// If the msg3 contains a negotiation payload (bytes beyond base XX msg3),
|
||||
/// it is decrypted and returned.
|
||||
pub(crate) fn complete_handshake_msg3(
|
||||
&mut self,
|
||||
message: &[u8],
|
||||
current_time_ms: u64,
|
||||
) -> Result<Option<Vec<u8>>, NoiseError> {
|
||||
let (received_negotiation, learned_identity, remote_epoch) = {
|
||||
let leg = self.leg.as_mut().ok_or_else(no_pending_connection)?;
|
||||
|
||||
// The responder is at `ReceivedMsg1` iff its Noise handshake handle
|
||||
// is present (set by `receive_handshake_init`, taken here on msg3).
|
||||
// Gating on the handle directly is byte-equivalent to the old
|
||||
// `!= ReceivedMsg1` guard for every reachable transition, and it
|
||||
// protects the `take().expect()` below.
|
||||
if leg.noise_handshake.is_none() {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "received_msg1 state".to_string(),
|
||||
got: "no active handshake".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut hs = leg
|
||||
.noise_handshake
|
||||
.take()
|
||||
.expect("noise handshake must exist in ReceivedMsg1 state");
|
||||
|
||||
// Split msg3 into base XX part and optional negotiation
|
||||
let base_size = noise::HANDSHAKE_MSG3_SIZE;
|
||||
let (base_msg3, extra) = if message.len() > base_size {
|
||||
(&message[..base_size], Some(&message[base_size..]))
|
||||
} else {
|
||||
(message, None)
|
||||
};
|
||||
|
||||
// Process XX msg3 (learns initiator identity + epoch)
|
||||
hs.read_message_3(base_msg3)?;
|
||||
|
||||
// Decrypt negotiation payload from msg3 if present
|
||||
let received_negotiation = if let Some(encrypted) = extra {
|
||||
Some(hs.decrypt_payload(encrypted)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Learn initiator identity from msg3
|
||||
let remote_static = *hs
|
||||
.remote_static()
|
||||
.expect("remote static available after XX msg3");
|
||||
let learned_identity = PeerIdentity::from_pubkey_full(remote_static);
|
||||
|
||||
// Capture remote epoch from msg3
|
||||
let remote_epoch = hs.remote_epoch();
|
||||
|
||||
// Handshake complete for responder. The completion signal is the
|
||||
// Noise session's presence (the phase enum is gone); no phase is
|
||||
// set.
|
||||
let session = hs.into_session()?;
|
||||
leg.noise_session = Some(session);
|
||||
|
||||
(received_negotiation, learned_identity, remote_epoch)
|
||||
};
|
||||
self.conn.set_expected_identity(learned_identity);
|
||||
self.conn.set_remote_epoch(remote_epoch);
|
||||
self.conn.touch(current_time_ms);
|
||||
|
||||
Ok(received_negotiation)
|
||||
}
|
||||
|
||||
/// Take the completed Noise session.
|
||||
///
|
||||
/// Returns the NoiseSession for use in ActivePeer. Can only be called
|
||||
/// once after the handshake completes.
|
||||
pub(crate) fn take_session(&mut self) -> Option<NoiseSession> {
|
||||
// The session exists iff the handshake reached `Complete`, so taking it
|
||||
// unconditionally is byte-equivalent to the old `== Complete` gate.
|
||||
self.leg.as_mut().and_then(|leg| leg.noise_session.take())
|
||||
}
|
||||
|
||||
/// Check if we have a completed session ready to take.
|
||||
pub(crate) fn has_session(&self) -> bool {
|
||||
self.leg
|
||||
.as_ref()
|
||||
.is_some_and(|leg| leg.noise_session.is_some())
|
||||
}
|
||||
|
||||
/// Drop the crypto handshake handle. The failure *state* lives on this
|
||||
/// machine; this only releases the Noise handle at the identical point it
|
||||
/// was released before, so a subsequent `complete_handshake` still reports
|
||||
/// `WrongState`.
|
||||
pub(crate) fn mark_failed(&mut self) {
|
||||
if let Some(leg) = self.leg.as_mut() {
|
||||
leg.noise_handshake = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The session index we allocated for this peer, read from the surviving
|
||||
/// carrier. Populated once the index is allocated on either establish path
|
||||
/// (inbound at msg1, outbound at msg1 preparation). `None` before
|
||||
@@ -775,13 +1119,23 @@ impl PeerMachine {
|
||||
|
||||
/// Expected peer identity of the surviving carrier — the home for the
|
||||
/// operator-visible `expected_peer` now that the leg no longer projects it.
|
||||
/// Outbound carries the dial identity from construction; inbound stays `None`
|
||||
/// in this view (the identity learned mid-handshake never rests here, as the
|
||||
/// leg is consumed by promotion within the same message-handling step).
|
||||
/// Outbound carries the dial identity from construction; inbound records
|
||||
/// the identity discovered in msg1, written here by `receive_handshake_init`
|
||||
/// at the same point it reaches the pending connection. Everything that
|
||||
/// names a peer mid-handshake reads this, including the stale-connection
|
||||
/// sweep's retry address.
|
||||
pub(crate) fn conn_expected_identity(&self) -> Option<&PeerIdentity> {
|
||||
self.conn.expected_identity()
|
||||
}
|
||||
|
||||
/// Remote startup epoch of the surviving carrier, recorded by the
|
||||
/// handshake operations at the message that reveals it (msg1 inbound,
|
||||
/// msg2 outbound). Promotion reads it to seed the active peer and to
|
||||
/// detect a peer restart across a reconnect.
|
||||
pub(crate) fn conn_remote_epoch(&self) -> Option<[u8; 8]> {
|
||||
self.conn.remote_epoch()
|
||||
}
|
||||
|
||||
/// Stored wire-format msg1 of the surviving carrier — the resend source for
|
||||
/// the outbound handshake retransmit, now that the leg no longer carries it.
|
||||
pub(crate) fn conn_handshake_msg1(&self) -> Option<&[u8]> {
|
||||
@@ -806,6 +1160,39 @@ impl PeerMachine {
|
||||
self.conn.set_handshake_msg2(msg2);
|
||||
}
|
||||
|
||||
/// The link this machine controls.
|
||||
pub(crate) fn link_id(&self) -> LinkId {
|
||||
self.link
|
||||
}
|
||||
|
||||
/// Which side opened this connection, read from the surviving carrier.
|
||||
/// Seeded at construction: an outbound machine carries an outbound
|
||||
/// connection state and an inbound machine an inbound one.
|
||||
pub(crate) fn conn_direction(&self) -> LinkDirection {
|
||||
self.conn.direction()
|
||||
}
|
||||
|
||||
/// Whether we opened this connection.
|
||||
pub(crate) fn conn_is_outbound(&self) -> bool {
|
||||
self.conn.is_outbound()
|
||||
}
|
||||
|
||||
/// Whether the peer opened this connection.
|
||||
pub(crate) fn conn_is_inbound(&self) -> bool {
|
||||
self.conn.is_inbound()
|
||||
}
|
||||
|
||||
/// The peer's address on this link, read from the surviving carrier.
|
||||
/// Written at the same three points the connection's own copy was: the
|
||||
/// inbound seed, the outbound dial, and message-2 completion.
|
||||
pub(crate) fn conn_source_addr(&self) -> Option<&TransportAddr> {
|
||||
self.conn.source_addr()
|
||||
}
|
||||
|
||||
pub(crate) fn set_conn_source_addr(&mut self, addr: TransportAddr) {
|
||||
self.conn.set_source_addr(addr);
|
||||
}
|
||||
|
||||
/// Peer session index of the surviving carrier — the source for the
|
||||
/// promotion hand-off now that the leg no longer projects it.
|
||||
pub(crate) fn conn_their_index(&self) -> Option<SessionIndex> {
|
||||
@@ -830,23 +1217,19 @@ impl PeerMachine {
|
||||
self.conn.peer_profile()
|
||||
}
|
||||
|
||||
/// Record the negotiated peer profile on the surviving carrier, mirroring
|
||||
/// the leg's `set_negotiation_results` write.
|
||||
/// Record the negotiated peer profile on the surviving carrier.
|
||||
pub(crate) fn set_conn_peer_profile(&mut self, profile: NodeProfile) {
|
||||
self.conn.set_negotiation_results(profile);
|
||||
}
|
||||
|
||||
/// Record the peer session index on the surviving carrier. Seeds the
|
||||
/// carrier from a pre-built leg (`Node::add_connection`) so the promotion
|
||||
/// hand-off matches the establish paths that write it on the machine.
|
||||
/// Record the peer session index on the surviving carrier, so the
|
||||
/// promotion hand-off reads it from the machine.
|
||||
pub(crate) fn set_conn_their_index(&mut self, index: SessionIndex) {
|
||||
self.conn.set_their_index(index);
|
||||
}
|
||||
|
||||
/// Record the transport ID on the surviving carrier. Populated on the
|
||||
/// inbound establish path (the leg seeds it at msg1, but the machine's
|
||||
/// carrier is only written on the outbound dial) and when seeding the
|
||||
/// carrier from a pre-built leg (`Node::add_connection`).
|
||||
/// Record the transport ID on the surviving carrier. Written on the
|
||||
/// inbound establish path at msg1 and on the outbound dial.
|
||||
pub(crate) fn set_conn_transport_id(&mut self, id: TransportId) {
|
||||
self.conn.set_transport_id(id);
|
||||
}
|
||||
@@ -1060,11 +1443,9 @@ impl PeerMachine {
|
||||
/// (`is_handshaking_sent_msg1`) survives until the sweep, and no timer
|
||||
/// actions are emitted.
|
||||
fn on_handshake_send_failed(&mut self) -> Vec<PeerAction> {
|
||||
if let Some(leg) = self.leg.as_mut() {
|
||||
// Drop the leg's Noise handshake handle at the identical point as
|
||||
// before; the failure *state* is recorded on the machine.
|
||||
leg.mark_failed();
|
||||
}
|
||||
// Drop the Noise handshake handle at the identical point as before;
|
||||
// the failure *state* is recorded on the machine.
|
||||
self.mark_failed();
|
||||
self.send_failed = true;
|
||||
Vec::new()
|
||||
}
|
||||
@@ -3082,7 +3463,7 @@ mod tests {
|
||||
100,
|
||||
&mut alloc,
|
||||
);
|
||||
m.set_leg(PeerConnection::outbound(LinkId::new(1), peer, 100));
|
||||
m.set_leg(HandshakeCrypto::new());
|
||||
assert!(m.is_handshaking_sent_msg1());
|
||||
assert!(!m.is_failed());
|
||||
assert_eq!(m.displayed_handshake_state(), "sent_msg1");
|
||||
@@ -3400,4 +3781,194 @@ mod tests {
|
||||
assert_eq!(m.state(), PeerState::Active { addr });
|
||||
assert_eq!(alloc.count(), 0);
|
||||
}
|
||||
|
||||
// ---- Moved from the handshake leg's own test module --------------------
|
||||
// These exercise the Noise handshake operations, which now live on the
|
||||
// control machine. Constructed as a machine with a leg attached; the
|
||||
// assertions are unchanged.
|
||||
|
||||
fn make_peer_identity() -> PeerIdentity {
|
||||
let identity = Identity::generate();
|
||||
PeerIdentity::from_pubkey(identity.pubkey())
|
||||
}
|
||||
|
||||
fn make_keypair() -> Keypair {
|
||||
let identity = Identity::generate();
|
||||
identity.keypair()
|
||||
}
|
||||
|
||||
fn make_epoch() -> [u8; 8] {
|
||||
let mut epoch = [0u8; 8];
|
||||
rand::Rng::fill_bytes(&mut rand::rng(), &mut epoch);
|
||||
epoch
|
||||
}
|
||||
|
||||
fn outbound_leg(
|
||||
link_id: LinkId,
|
||||
expected_identity: PeerIdentity,
|
||||
current_time_ms: u64,
|
||||
) -> PeerMachine {
|
||||
let mut machine =
|
||||
PeerMachine::new_outbound(link_id, Some(expected_identity), current_time_ms);
|
||||
machine.set_leg(HandshakeCrypto::new());
|
||||
machine
|
||||
}
|
||||
|
||||
fn inbound_leg(link_id: LinkId, current_time_ms: u64) -> PeerMachine {
|
||||
let mut machine = PeerMachine::new_inbound(link_id, current_time_ms);
|
||||
machine.set_leg(HandshakeCrypto::new());
|
||||
machine
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_outbound_connection() {
|
||||
let identity = make_peer_identity();
|
||||
let conn = outbound_leg(LinkId::new(1), identity, 1000);
|
||||
|
||||
assert!(conn.conn_is_outbound());
|
||||
assert!(!conn.conn_is_inbound());
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.conn_expected_identity().is_some());
|
||||
assert_eq!(conn.conn_started_at(), 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inbound_connection() {
|
||||
let conn = inbound_leg(LinkId::new(2), 2000);
|
||||
|
||||
assert!(conn.conn_is_inbound());
|
||||
assert!(!conn.conn_is_outbound());
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.conn_expected_identity().is_none());
|
||||
assert_eq!(conn.conn_started_at(), 2000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_handshake_flow() {
|
||||
// Create identities
|
||||
let initiator_identity = Identity::generate();
|
||||
let responder_identity = Identity::generate();
|
||||
|
||||
let initiator_keypair = initiator_identity.keypair();
|
||||
let responder_keypair = responder_identity.keypair();
|
||||
let initiator_epoch = make_epoch();
|
||||
let responder_epoch = make_epoch();
|
||||
|
||||
// Use from_pubkey_full to preserve parity for ECDH
|
||||
let responder_peer_id = PeerIdentity::from_pubkey_full(responder_identity.pubkey_full());
|
||||
|
||||
// Create connections
|
||||
let mut initiator_conn = outbound_leg(LinkId::new(1), responder_peer_id, 1000);
|
||||
let mut responder_conn = inbound_leg(LinkId::new(2), 1000);
|
||||
|
||||
// Initiator starts XX handshake
|
||||
let msg1 = initiator_conn
|
||||
.start_handshake(initiator_keypair, initiator_epoch, 1100)
|
||||
.unwrap();
|
||||
// Post-msg1 the initiator holds an in-flight handshake, not yet a session.
|
||||
assert!(!initiator_conn.has_session());
|
||||
|
||||
// Responder processes msg1 and sends msg2 (XX: does NOT complete yet)
|
||||
let msg2 = responder_conn
|
||||
.receive_handshake_init(responder_keypair, responder_epoch, &msg1, None, 1200)
|
||||
.unwrap();
|
||||
// XX: the responder parks awaiting msg3 — it holds an in-flight
|
||||
// handshake, not yet a session (the deleted phase was `ReceivedMsg1`).
|
||||
assert!(!responder_conn.has_session());
|
||||
// Responder does NOT know initiator's identity yet (XX property)
|
||||
assert!(responder_conn.conn_expected_identity().is_none());
|
||||
|
||||
// Initiator processes msg2 and generates msg3
|
||||
let (msg3, _neg) = initiator_conn
|
||||
.complete_handshake(&msg2, None, 1300)
|
||||
.unwrap();
|
||||
// The initiator completes at msg3 generation: it now holds a session.
|
||||
assert!(initiator_conn.has_session());
|
||||
|
||||
// Initiator learned responder's identity from msg2
|
||||
let discovered = initiator_conn.conn_expected_identity().unwrap();
|
||||
assert_eq!(discovered.pubkey(), responder_identity.pubkey());
|
||||
assert_eq!(initiator_conn.conn_remote_epoch(), Some(responder_epoch));
|
||||
|
||||
// Responder processes msg3 (completes handshake): it now holds a session.
|
||||
let _neg = responder_conn.complete_handshake_msg3(&msg3, 1400).unwrap();
|
||||
assert!(responder_conn.has_session());
|
||||
|
||||
// Responder learned initiator's identity from msg3
|
||||
let discovered = responder_conn.conn_expected_identity().unwrap();
|
||||
assert_eq!(discovered.pubkey(), initiator_identity.pubkey());
|
||||
assert_eq!(responder_conn.conn_remote_epoch(), Some(initiator_epoch));
|
||||
|
||||
// Both have sessions
|
||||
assert!(initiator_conn.has_session());
|
||||
assert!(responder_conn.has_session());
|
||||
|
||||
// Take and verify sessions work
|
||||
let mut init_session = initiator_conn.take_session().unwrap();
|
||||
let mut resp_session = responder_conn.take_session().unwrap();
|
||||
|
||||
// Encrypt/decrypt test
|
||||
let plaintext = b"test message";
|
||||
let ciphertext = init_session.encrypt(plaintext).unwrap();
|
||||
let decrypted = resp_session.decrypt(&ciphertext).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connection_failure() {
|
||||
// `mark_failed` releases the leg's Noise handshake handle. The failure
|
||||
// *state* now lives on the control machine, but the leg-local effect is
|
||||
// still observable: a completion attempt afterward reports `WrongState`
|
||||
// (the handle-presence gate) and no session is produced.
|
||||
let identity = make_peer_identity();
|
||||
let keypair = make_keypair();
|
||||
let mut conn = outbound_leg(LinkId::new(1), identity, 1000);
|
||||
conn.start_handshake(keypair, make_epoch(), 1100).unwrap();
|
||||
|
||||
conn.mark_failed();
|
||||
|
||||
assert!(!conn.has_session());
|
||||
assert!(conn.complete_handshake(&[0u8; 96], None, 1200).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrong_direction_errors() {
|
||||
let identity = make_peer_identity();
|
||||
let keypair = make_keypair();
|
||||
|
||||
// Outbound can't receive_handshake_init
|
||||
let mut outbound = outbound_leg(LinkId::new(1), identity, 1000);
|
||||
assert!(
|
||||
outbound
|
||||
.receive_handshake_init(keypair, make_epoch(), &[0u8; 33], None, 1100)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Inbound can't start_handshake
|
||||
let mut inbound = inbound_leg(LinkId::new(2), 1000);
|
||||
assert!(
|
||||
inbound
|
||||
.start_handshake(keypair, make_epoch(), 1100)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// T-SANSIO: the action vocabulary must stay plain, comparable data.
|
||||
///
|
||||
/// `PeerAction` is the sans-IO boundary between the pure reducer and its
|
||||
/// driver. Requiring `Clone + Eq` is a compile-time statement that no variant
|
||||
/// may carry a runtime handle (a socket, a `JoinHandle`, a channel sender),
|
||||
/// since none of those are `Clone + Eq`. If a future variant smuggles one in,
|
||||
/// this bound stops compiling.
|
||||
#[cfg(test)]
|
||||
mod action_contract {
|
||||
use super::PeerAction;
|
||||
|
||||
fn assert_clone_eq<T: Clone + Eq>() {}
|
||||
|
||||
#[test]
|
||||
fn peer_action_is_plain_comparable_data() {
|
||||
assert_clone_eq::<PeerAction>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
//! Peer Management
|
||||
//!
|
||||
//! Two-phase peer lifecycle:
|
||||
//! 1. **PeerConnection** - Handshake phase, before identity is verified
|
||||
//! 1. **PeerMachine** with a handshake carrier attached - handshake phase,
|
||||
//! before identity is verified
|
||||
//! 2. **ActivePeer** - Authenticated phase, after successful Noise handshake
|
||||
|
||||
mod active;
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) mod connected_udp;
|
||||
mod connection;
|
||||
pub(crate) mod machine;
|
||||
|
||||
pub use active::{ActivePeer, ConnectivityState};
|
||||
pub use connection::PeerConnection;
|
||||
|
||||
use crate::NodeAddr;
|
||||
use crate::transport::LinkId;
|
||||
|
||||
@@ -199,10 +199,12 @@ pub(crate) struct RekeyCfg {
|
||||
///
|
||||
/// On XX the responder learns the initiator's identity and startup epoch only
|
||||
/// once `msg3` completes the Noise handshake (unlike IK, which learns them at
|
||||
/// `msg1`). The shell-side Noise step (`complete_handshake_msg3`) yields these;
|
||||
/// the core reads them to classify. The Noise bytes themselves never reach the
|
||||
/// core — the fresh session extraction stays a shell effect. Only the two facts
|
||||
/// the classification depends on travel here: the peer's node address (for the
|
||||
/// `msg1`). The shell-side Noise step (`complete_handshake_msg3`) runs on the
|
||||
/// control machine and reads **no** `Node` registry state — the essential
|
||||
/// invariant of this decomposition — and yields these; the core reads them to
|
||||
/// classify. The Noise bytes themselves never reach the core — the fresh
|
||||
/// session extraction stays a shell effect. Only the two facts the
|
||||
/// classification depends on travel here: the peer's node address (for the
|
||||
/// smaller-NodeAddr tie-breaks) and the peer's captured startup epoch (for
|
||||
/// restart detection).
|
||||
pub(crate) struct WireOutcome {
|
||||
@@ -424,7 +426,7 @@ pub(crate) enum InboundReject {
|
||||
/// cross-connection and the tie-break decides whether we swap our session to the
|
||||
/// (winning) outbound one or keep our existing inbound session. The rekey-msg2
|
||||
/// completion path is handled by a separate shell driver (it mutates
|
||||
/// `ActivePeer`, not a `PeerConnection`) and never reaches this decision.
|
||||
/// `ActivePeer`, not a pending handshake) and never reaches this decision.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum OutboundDecision {
|
||||
/// No existing peer for this identity: promote the completed outbound
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
//! decision logic (version agreement, profile validation).
|
||||
//! - `limits.rs` — the pure connection-retry backoff math.
|
||||
//! - `state.rs` — [`ConnectionState`], the pure handshake-phase connection
|
||||
//! bookkeeping (owned by the shell `PeerConnection` beside its Noise crypto
|
||||
//! handles), plus [`Fmp`], the (stateless) lifecycle anchor owned by `Node`.
|
||||
//! bookkeeping (owned by the per-peer control machine beside its Noise
|
||||
//! crypto carrier), plus [`Fmp`], the (stateless) lifecycle anchor owned by
|
||||
//! `Node`.
|
||||
//! The handshake phase itself lives on the per-peer control machine.
|
||||
//! - `wire.rs` — the FMP wire codec: XX handshake message types, disconnect
|
||||
//! reasons, the orderly disconnect message, and the negotiation payload.
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
//!
|
||||
//! [`ConnectionState`] owns every **pure** field of the handshake-phase
|
||||
//! connection. The Noise crypto handles (`noise::HandshakeState`,
|
||||
//! `NoiseSession`) stay shell-owned in
|
||||
//! [`PeerConnection`](crate::peer::PeerConnection), which holds a
|
||||
//! `ConnectionState` alongside them and drives the two halves side by side. The
|
||||
//! shell's XX transition methods drive the Noise objects, then write learned
|
||||
//! results back through the pure setters here (`set_expected_identity`,
|
||||
//! `NoiseSession`) stay shell-owned in the per-peer control machine's
|
||||
//! handshake carrier, which the machine drives alongside its own
|
||||
//! `ConnectionState`. The machine's transition methods drive the Noise
|
||||
//! objects, then write learned results back through the pure setters here
|
||||
//! (`set_expected_identity`,
|
||||
//! `set_remote_epoch`, `touch`).
|
||||
//!
|
||||
//! This state is `no_std`+`alloc`-clean with respect to transport: the
|
||||
@@ -36,10 +36,9 @@ use crate::utils::index::SessionIndex;
|
||||
/// Pure, runtime-agnostic bookkeeping for a connection in the handshake phase.
|
||||
///
|
||||
/// Owns every non-crypto field of the handshake-phase connection. The Noise
|
||||
/// crypto handles live beside it in the shell
|
||||
/// [`PeerConnection`](crate::peer::PeerConnection); this struct is written only
|
||||
/// as plain data — the shell extracts learned identity/epoch out of the crypto
|
||||
/// objects and sets them here through the setters.
|
||||
/// crypto handles live beside it on the per-peer control machine; this struct
|
||||
/// is written only as plain data — the machine extracts learned identity and
|
||||
/// epoch out of the crypto objects and sets them here through the setters.
|
||||
#[derive(Debug)]
|
||||
pub struct ConnectionState {
|
||||
// === Link Reference ===
|
||||
@@ -181,6 +180,7 @@ impl ConnectionState {
|
||||
/// Create the pure state for a new inbound connection with transport info.
|
||||
///
|
||||
/// Used when processing msg1 where we know the transport and source address.
|
||||
#[cfg(test)]
|
||||
pub fn inbound_with_transport(
|
||||
link_id: LinkId,
|
||||
transport_id: TransportId,
|
||||
@@ -244,6 +244,7 @@ impl ConnectionState {
|
||||
}
|
||||
|
||||
/// Connection duration so far.
|
||||
#[cfg(test)]
|
||||
pub fn duration(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.started_at)
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ impl TcpTransport {
|
||||
/// node-wide `node.limits.max_connections` > built-in default. This is a
|
||||
/// per-transport *raw-accept* ceiling; the true node-wide peer budget is
|
||||
/// still enforced downstream by the handshake-phase `max_connections`
|
||||
/// admission check (`Node::add_connection`), so deriving this ceiling
|
||||
/// admission check, so deriving this ceiling
|
||||
/// from `max_connections` does not let multiple transports exceed the
|
||||
/// node-wide total — it only stops the transport from rejecting inbound
|
||||
/// below the configured node budget.
|
||||
|
||||
Reference in New Issue
Block a user