mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 09:33:23 +00:00
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:
committed by
Johnathan Corgan
co-authored by
Johnathan Corgan
parent
8d94c0f29c
commit
da0d9d39a0
@@ -11,6 +11,8 @@ use crate::transport::{TransportAddr, TransportId};
|
||||
use crate::{NodeAddr, PeerIdentity};
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
const MAX_RECENT_DISCOVERY_REQUESTS: usize = 4096;
|
||||
|
||||
impl Node {
|
||||
/// Handle an incoming LookupRequest from a peer.
|
||||
///
|
||||
@@ -34,6 +36,7 @@ impl Node {
|
||||
};
|
||||
|
||||
let now_ms = Self::now_ms();
|
||||
self.purge_expired_requests(now_ms);
|
||||
|
||||
// Dedup: drop if we've already seen this request_id.
|
||||
// Also serves as loop protection — tree routing is loop-free,
|
||||
@@ -48,13 +51,21 @@ impl Node {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.recent_requests.len() >= MAX_RECENT_DISCOVERY_REQUESTS {
|
||||
debug!(
|
||||
request_id = request.request_id,
|
||||
from = %self.peer_display_name(from),
|
||||
recent_requests = self.recent_requests.len(),
|
||||
max_recent_requests = MAX_RECENT_DISCOVERY_REQUESTS,
|
||||
"Discovery request dedup cache full, dropping LookupRequest"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Record for reverse-path forwarding and dedup
|
||||
self.recent_requests
|
||||
.insert(request.request_id, RecentRequest::new(*from, now_ms));
|
||||
|
||||
// Lazy purge expired entries
|
||||
self.purge_expired_requests(now_ms);
|
||||
|
||||
// Are we the target?
|
||||
if request.target == *self.node_addr() {
|
||||
self.stats_mut().discovery.req_target_is_us += 1;
|
||||
|
||||
@@ -658,8 +658,15 @@ impl Node {
|
||||
// Complete the rekey handshake on the ActivePeer
|
||||
if let Some(peer) = self.peers.get_mut(&peer_node_addr) {
|
||||
match peer.complete_rekey_msg2(noise_msg2) {
|
||||
Ok(session) => {
|
||||
Ok((session, remote_epoch)) => {
|
||||
let our_index = peer.rekey_our_index().unwrap_or(header.receiver_idx);
|
||||
let remote_epoch_changed = matches!(
|
||||
(peer.remote_epoch(), remote_epoch),
|
||||
(Some(old), Some(new)) if old != new
|
||||
);
|
||||
if remote_epoch.is_some() {
|
||||
peer.set_remote_epoch(remote_epoch);
|
||||
}
|
||||
peer.set_pending_session(session, our_index, header.sender_idx);
|
||||
|
||||
if let Some(transport_id) = peer.transport_id() {
|
||||
@@ -667,6 +674,19 @@ impl Node {
|
||||
.insert((transport_id, our_index.as_u32()), peer_node_addr);
|
||||
}
|
||||
|
||||
if remote_epoch_changed {
|
||||
if self.sessions.remove(&peer_node_addr).is_some() {
|
||||
debug!(
|
||||
peer = %display_name,
|
||||
"Cleared stale FSP session after peer restart during FMP rekey"
|
||||
);
|
||||
}
|
||||
info!(
|
||||
peer = %display_name,
|
||||
"Peer restart detected during FMP rekey, replacing stale endpoint session"
|
||||
);
|
||||
}
|
||||
|
||||
debug!(
|
||||
peer = %display_name,
|
||||
new_our_index = %our_index,
|
||||
@@ -1024,9 +1044,15 @@ impl Node {
|
||||
if let Some(existing_peer) = self.peers.get(&peer_node_addr) {
|
||||
let existing_link_id = existing_peer.link_id();
|
||||
|
||||
// Determine which connection wins
|
||||
let this_wins =
|
||||
cross_connection_winner(self.identity.node_addr(), &peer_node_addr, is_outbound);
|
||||
let remote_epoch_changed = matches!((existing_peer.remote_epoch(), remote_epoch), (Some(old), Some(new)) if old != new);
|
||||
|
||||
// Determine which connection wins. A peer restart (different
|
||||
// startup epoch) is not a normal cross-connection: the old link
|
||||
// and FSP sessions are cryptographically stale, so the freshly
|
||||
// authenticated connection must replace them regardless of the
|
||||
// tie-breaker direction.
|
||||
let this_wins = remote_epoch_changed
|
||||
|| cross_connection_winner(self.identity.node_addr(), &peer_node_addr, is_outbound);
|
||||
|
||||
if this_wins {
|
||||
// This connection wins, replace the existing peer
|
||||
@@ -1052,6 +1078,21 @@ impl Node {
|
||||
let _ = self.index_allocator.free(old_idx);
|
||||
}
|
||||
|
||||
if remote_epoch_changed {
|
||||
if self.sessions.remove(&peer_node_addr).is_some() {
|
||||
debug!(
|
||||
peer = %self.peer_display_name(&peer_node_addr),
|
||||
"Cleared stale FSP session after peer restart during promotion"
|
||||
);
|
||||
}
|
||||
info!(
|
||||
peer = %self.peer_display_name(&peer_node_addr),
|
||||
winner_link = %link_id,
|
||||
loser_link = %loser_link_id,
|
||||
"Peer restart detected during promotion, replacing stale active peer"
|
||||
);
|
||||
}
|
||||
|
||||
self.seed_path_mtu_for_link_peer(&peer_node_addr, transport_id, ¤t_addr);
|
||||
|
||||
let mut new_peer = ActivePeer::with_session(
|
||||
|
||||
Reference in New Issue
Block a user