mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
nostr: suppress retraversal of cross-FMP-version peers
Open-discovery NAT traversal succeeds at the UDP layer regardless of what FMP-protocol version the peer speaks. When the daemon discovers a peer running a different FMP version (e.g. a v0/v1 mix during a mid-rollout window, or a misconfigured peer in the same advert namespace), the punch sequence completes, the socket is adopted via `Node::adopt_established_traversal`, and we initiate an FMP handshake. The peer drops our msg1 at its own version-gate and we drop their msg1/msg2 at `Unknown FMP version, dropping`. Neither side advances the handshake. Today the bootstrap transport sits idle until the 31s stale- handshake timeout, drops, and the open-discovery sweep ~30s later fires the full STUN+offer+answer+punch sequence again — every minute, indefinitely, against peers the handshake literally cannot complete with. Add a `Node::bootstrap_transport_npubs` map populated alongside `bootstrap_transports` at adopt time. The rx loop reverse-maps the transport_id → npub on version-mismatch and bumps the discovery layer's `failure_state` to a long structural cooldown via the new `NostrDiscovery::record_protocol_mismatch` API. The next sweep skips the npub for `protocol_mismatch_cooldown_secs` (default 86400 = 24h, separate from the 30-min transient-failure `extended_cooldown_secs`). One-shot WARN per fresh observation. Repeat mismatches inside the cooldown window are silent (the failure_state method returns false when an existing comparable cooldown is already in place). The handshake/transport teardown chain is unchanged — the fix is specifically about preventing the *next* sweep cycle from re-traversing. Cleared on `cleanup_bootstrap_transport_if_unused` and on the adopt-failure rollback path so completed handshakes don't leave stale entries behind. Four new unit tests in `failure_state.rs` cover fresh-entry signaling, repeat-suppression inside the window, streak-pin behavior for `show_peers` rendering, and post-cooldown re-arming.
This commit is contained in:
@@ -160,6 +160,34 @@ impl Node {
|
||||
transport_id = %packet.transport_id,
|
||||
"Unknown FMP version, dropping"
|
||||
);
|
||||
|
||||
// If the packet arrived on an adopted Nostr-NAT bootstrap
|
||||
// transport, the originating peer is necessarily on a
|
||||
// different FMP-protocol version than us — the discovery
|
||||
// sweep would otherwise re-traverse them every cycle even
|
||||
// though no msg1/msg2 exchange can ever succeed. Bump the
|
||||
// discovery-layer cooldown to the long protocol-mismatch
|
||||
// window and emit a single WARN per fresh observation.
|
||||
if self.bootstrap_transports.contains(&packet.transport_id)
|
||||
&& let Some(npub) = self
|
||||
.bootstrap_transport_npubs
|
||||
.get(&packet.transport_id)
|
||||
.cloned()
|
||||
&& let Some(handle) = self.nostr_discovery_handle()
|
||||
{
|
||||
let now_ms = Self::now_ms();
|
||||
let cooldown_secs = handle.protocol_mismatch_cooldown_secs();
|
||||
if handle.record_protocol_mismatch(&npub, now_ms) {
|
||||
warn!(
|
||||
peer_npub = %npub,
|
||||
transport_id = %packet.transport_id,
|
||||
peer_version = prefix.version,
|
||||
our_version = FMP_VERSION,
|
||||
cooldown_secs,
|
||||
"Nostr-discovered peer speaks a different FMP version; suppressing retraversal"
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1852,6 +1852,8 @@ impl Node {
|
||||
crate::transport::TransportHandle::Udp(transport),
|
||||
);
|
||||
self.bootstrap_transports.insert(transport_id);
|
||||
self.bootstrap_transport_npubs
|
||||
.insert(transport_id, traversal.peer_npub.clone());
|
||||
|
||||
let remote_addr = TransportAddr::from_string(&traversal.remote_addr.to_string());
|
||||
if let Err(err) = self
|
||||
@@ -1859,6 +1861,7 @@ impl Node {
|
||||
.await
|
||||
{
|
||||
self.bootstrap_transports.remove(&transport_id);
|
||||
self.bootstrap_transport_npubs.remove(&transport_id);
|
||||
if let Some(mut handle) = self.transports.remove(&transport_id) {
|
||||
let _ = handle.stop().await;
|
||||
}
|
||||
|
||||
@@ -449,6 +449,13 @@ pub struct Node {
|
||||
startup_open_discovery_sweep_done: bool,
|
||||
/// Per-peer UDP transports adopted from NAT traversal handoff.
|
||||
bootstrap_transports: HashSet<TransportId>,
|
||||
/// Originating peer npub (bech32) for each adopted bootstrap
|
||||
/// transport, captured at `adopt_established_traversal` time.
|
||||
/// Populated alongside `bootstrap_transports`; cleared in
|
||||
/// `cleanup_bootstrap_transport_if_unused`. Used by the rx loop to
|
||||
/// route fatal-protocol-mismatch observations back to the
|
||||
/// Nostr-discovery `failure_state` for long cooldown application.
|
||||
bootstrap_transport_npubs: HashMap<TransportId, String>,
|
||||
|
||||
// === Periodic Parent Re-evaluation ===
|
||||
/// Timestamp of last periodic parent re-evaluation (for pacing).
|
||||
@@ -614,6 +621,7 @@ impl Node {
|
||||
nostr_discovery_started_at_ms: None,
|
||||
startup_open_discovery_sweep_done: false,
|
||||
bootstrap_transports: HashSet::new(),
|
||||
bootstrap_transport_npubs: HashMap::new(),
|
||||
last_parent_reeval: None,
|
||||
last_congestion_log: None,
|
||||
estimated_mesh_size: None,
|
||||
@@ -744,6 +752,7 @@ impl Node {
|
||||
nostr_discovery_started_at_ms: None,
|
||||
startup_open_discovery_sweep_done: false,
|
||||
bootstrap_transports: HashSet::new(),
|
||||
bootstrap_transport_npubs: HashMap::new(),
|
||||
last_parent_reeval: None,
|
||||
last_congestion_log: None,
|
||||
estimated_mesh_size: None,
|
||||
@@ -1468,6 +1477,7 @@ impl Node {
|
||||
);
|
||||
|
||||
self.bootstrap_transports.remove(&transport_id);
|
||||
self.bootstrap_transport_npubs.remove(&transport_id);
|
||||
self.transport_drops.remove(&transport_id);
|
||||
self.transports.remove(&transport_id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user