Files
fips/src/node/tests/mod.rs
T
Johnathan Corgan 7fe1d75637 peer: delete the pending-connection type, leaving the control machine whole
The per-peer control machine has absorbed every field the pending
connection carried. What remained was a struct holding two Noise
handles beside a duplicate copy of bookkeeping nobody read. Replace it
with a small carrier for the two handles and delete the type.

Presence of that carrier, not the state of the handles inside it, is
what marks a machine as mid-handshake. The distinction is essential
rather than stylistic: a failed handshake drops its initiation handle
and is deliberately retained so the stale sweep can reclaim it, and a
completed one has its session taken before disposal. Deriving presence
from the handles would make both invisible to the sweep, the
connection count, and the peering budget at once, leaking the slot
permanently. A test drives an empty carrier past every presence
predicate and then detaches it, so a future edit cannot quietly couple
the two.

The remote startup epoch now comes from the surviving carrier, which
the handshake operations already wrote at the same two points with the
same value. The paired writes onto the pending connection's own
bookkeeping had no readers left and are gone.

The handshake-phase surface leaves the public API: it was public by
accident rather than design, and the machine behind it is crate
internal. Callers outside the crate that need a view of pending
handshakes go through the operator queries, which are unchanged.

ConnectionState::inbound_with_transport loses its last non-test caller
with the inbound seed and is marked test-only.
2026-07-19 01:18:05 +00:00

185 lines
6.5 KiB
Rust

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;
mod acl;
#[cfg(target_os = "linux")]
mod ble;
mod bloom;
mod bloom_poison;
mod bootstrap;
mod decrypt_failure;
mod disconnect;
mod discovery;
mod establish_chartests;
#[cfg(target_os = "linux")]
mod ethernet;
mod forwarding;
mod handshake;
mod heartbeat;
mod mmp_chartests;
mod routing;
mod session;
mod spanning_tree;
mod tcp;
mod unit;
pub(super) fn make_node() -> Node {
make_node_with(Config::new())
}
/// A test node that reaches `Full` health on `start()`.
///
/// A default [`make_node`] configures no transports, so its `start()` now
/// resolves to `NodeState::Failed` (zero transports up) and
/// returns `NoOperationalTransports`. Lifecycle-state tests that need a running
/// node build one with a single loopback UDP transport (ephemeral port) as the
/// sole configured child — DNS disabled — so bring-up has exactly one
/// configured child and it comes up (`Full`). Mirrors the udp config in
/// `test_node_start_does_not_wait_for_nostr_relay_startup`.
pub(super) fn make_healthy_node() -> Node {
let mut config = Config::new();
config.transports.udp = crate::config::TransportInstances::Single(crate::config::UdpConfig {
bind_addr: Some("127.0.0.1:0".to_string()),
..Default::default()
});
config.dns.enabled = false;
make_node_with(config)
}
/// Build a test node from an explicit `Config`. Immutable state lives solely in
/// the shared `NodeContext`, built once at construction — there is no
/// post-construction field to poke, so set limits/config on the `Config` here.
pub(super) fn make_node_with(config: Config) -> Node {
Node::new(config).unwrap()
}
/// Build a test node with an explicit `max_peers` limit (replaces the removed
/// `set_max_peers` setter; resource limits are immutable post-construction).
pub(super) fn make_node_with_max_peers(max_peers: usize) -> Node {
let mut config = Config::new();
config.node.limits.max_peers = max_peers;
make_node_with(config)
}
/// Build a test node with an explicit `max_links` limit (replaces the removed
/// `set_max_links` setter; resource limits are immutable post-construction).
pub(super) fn make_node_with_max_links(max_links: usize) -> Node {
let mut config = Config::new();
config.node.limits.max_links = max_links;
make_node_with(config)
}
#[allow(dead_code)]
pub(super) fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
pub(super) fn make_peer_identity() -> PeerIdentity {
let identity = Identity::generate();
PeerIdentity::from_pubkey(identity.pubkey())
}
/// 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, 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 IK handshake.
///
/// 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,
) -> 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 leg, where it used to run
/// before the leg was handed over. That reordering is neutral, but not
/// because the handshake leaves the seeded fields alone —
/// `receive_handshake_init` does write `expected_identity`. It is neutral
/// because the only read of `expected_identity` is guarded by `is_outbound`:
/// an inbound leg takes the `new_inbound` arm whether or not the identity has
/// been learned, and an outbound leg never runs that method. The remaining
/// reads (`link_id`, `started_at`, `is_outbound`, `their_index`,
/// `transport_id`) are genuinely 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());
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 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 = 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);
let msg2 = resp_conn
.receive_handshake_init(peer_keypair, resp_epoch, &msg1, current_time_ms)
.unwrap();
// Complete initiator handshake
node.peer_machines
.get_mut(&link_id)
.unwrap()
.complete_handshake(&msg2, current_time_ms)
.unwrap();
peer_identity
}