node: refresh active peer paths without dropping links

Add Node::update_peers for runtime peer-list refresh. It re-derives the
active peer connections from a new peer configuration, adding newly
configured peers and removing those no longer present, while keeping
links to peers that remain in the set rather than tearing every
connection down. The call returns an UpdatePeersOutcome summarizing the
added, removed, and retained peers.

PeerAddress gains a seen_at_ms recency field (with_seen_at_ms). Active
path selection now sorts address candidates by recency so the most
recently observed address wins when concurrent path probes race.

complete_rekey_msg2 now returns the remote peer's startup epoch
alongside the new Noise session, letting the rekey path detect a peer
restart and clear stale session state. A stale FSP session is cleared
when a peer restart is detected during FMP rekey or cross-connection
promotion, so the session-layer map no longer lingers out of sync with
the freshly promoted peer.

Per-tick work budgets bound the connection churn in a single node tick
(MAX_DISCOVERY_CONNECTS_PER_TICK, MAX_RETRY_CONNECTIONS_PER_TICK,
MAX_PARALLEL_PATH_CANDIDATES_PER_PEER); work beyond a tick's budget is
deferred to the next tick rather than discarded.

Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
This commit is contained in:
Martti Malmi
2026-05-30 00:46:38 +00:00
committed by Johnathan Corgan
co-authored by Johnathan Corgan
parent 8d94c0f29c
commit da0d9d39a0
10 changed files with 872 additions and 67 deletions
+13 -2
View File
@@ -593,6 +593,13 @@ impl ActivePeer {
self.remote_epoch
}
/// Update the remote peer's startup epoch after a successful in-place
/// rekey. Initial handshakes set this through `with_session`, but recovery
/// rekeys also exchange epochs and must keep restart detection current.
pub(crate) fn set_remote_epoch(&mut self, remote_epoch: Option<[u8; 8]>) {
self.remote_epoch = remote_epoch;
}
// === Tree Accessors ===
/// Get the peer's tree coordinates, if known.
@@ -1099,7 +1106,10 @@ impl ActivePeer {
/// Takes the stored handshake state, reads msg2, and returns the
/// completed NoiseSession. Clears the handshake-related fields but
/// leaves rekey_our_index for set_pending_session to use.
pub fn complete_rekey_msg2(&mut self, msg2_bytes: &[u8]) -> Result<NoiseSession, NoiseError> {
pub fn complete_rekey_msg2(
&mut self,
msg2_bytes: &[u8],
) -> Result<(NoiseSession, Option<[u8; 8]>), NoiseError> {
let mut hs = self
.rekey_handshake
.take()
@@ -1109,13 +1119,14 @@ impl ActivePeer {
})?;
hs.read_message_2(msg2_bytes)?;
let remote_epoch = hs.remote_epoch();
let session = hs.into_session()?;
// Clear msg1 resend state
self.rekey_msg1 = None;
self.rekey_msg1_next_resend = 0;
Ok(session)
Ok((session, remote_epoch))
}
/// Check if msg1 needs resending.