mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 08:37:02 +00:00
Merge branch 'master' into next
# Conflicts: # src/node/tests/bootstrap.rs
This commit is contained in:
@@ -150,11 +150,11 @@ impl Node {
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let flap_dampened = self.tree_state.set_parent(new_parent, new_seq, timestamp);
|
||||
self.tree_state.recompute_coords();
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
warn!(error = %e, "Failed to sign declaration after first-RTT parent eval");
|
||||
return;
|
||||
}
|
||||
self.tree_state.recompute_coords();
|
||||
self.coord_cache.clear();
|
||||
self.reset_discovery_backoff();
|
||||
self.stats_mut().tree.parent_switched += 1;
|
||||
@@ -174,6 +174,24 @@ impl Node {
|
||||
self.send_tree_announce_to_all().await;
|
||||
let all_peers: Vec<crate::NodeAddr> = self.peers.keys().copied().collect();
|
||||
self.bloom_state.mark_all_updates_needed(all_peers);
|
||||
} else if !self.tree_state.is_root() && self.tree_state.should_be_root() {
|
||||
self.tree_state.become_root();
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
warn!(error = %e, "Failed to sign self-root declaration after first-RTT");
|
||||
return;
|
||||
}
|
||||
self.coord_cache.clear();
|
||||
self.reset_discovery_backoff();
|
||||
self.stats_mut().tree.parent_switched += 1;
|
||||
self.stats_mut().tree.parent_switches += 1;
|
||||
info!(
|
||||
new_root = %self.tree_state.root(),
|
||||
trigger = "first-rtt",
|
||||
"Self-promoted to root after first RTT: smallest visible NodeAddr"
|
||||
);
|
||||
self.send_tree_announce_to_all().await;
|
||||
let all_peers: Vec<crate::NodeAddr> = self.peers.keys().copied().collect();
|
||||
self.bloom_state.mark_all_updates_needed(all_peers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,14 +84,42 @@ impl Node {
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
packet = packet_rx.recv() => {
|
||||
match packet {
|
||||
Some(p) => self.process_packet(p).await,
|
||||
None => break, // channel closed
|
||||
}
|
||||
// Drain remaining ready inbound packets in a tight loop
|
||||
// before yielding back to select! — every yield is a
|
||||
// futex hop on tokio's multi-thread scheduler, and at
|
||||
// line rate the kernel UDP queue typically has several
|
||||
// datagrams available per wake. Caps at a batch
|
||||
// boundary so other branches (tick, control) eventually
|
||||
// get a turn even under sustained load.
|
||||
let mut drained = 0;
|
||||
while drained < 256 {
|
||||
match packet_rx.try_recv() {
|
||||
Ok(p) => {
|
||||
self.process_packet(p).await;
|
||||
drained += 1;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ipv6_packet) = tun_outbound_rx.recv() => {
|
||||
self.handle_tun_outbound(ipv6_packet).await;
|
||||
let mut drained = 0;
|
||||
while drained < 256 {
|
||||
match tun_outbound_rx.try_recv() {
|
||||
Ok(p) => {
|
||||
self.handle_tun_outbound(p).await;
|
||||
drained += 1;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(identity) = dns_identity_rx.recv() => {
|
||||
debug!(
|
||||
|
||||
+49
-8
@@ -73,6 +73,25 @@ impl Node {
|
||||
error = %e,
|
||||
"Failed to initiate peer connection"
|
||||
);
|
||||
// Schedule a retry so transient address-resolution failures
|
||||
// (e.g. cached endpoints stale, NAT rebinds, all addresses
|
||||
// currently unreachable) recover without a daemon restart.
|
||||
if let Ok(peer_identity) = PeerIdentity::from_npub(&peer_config.npub) {
|
||||
self.schedule_retry(*peer_identity.node_addr(), Self::now_ms());
|
||||
}
|
||||
// No-transport failures most often mean the cached overlay
|
||||
// advert is pointing at a dead post-NAT-rebind address. The
|
||||
// advert cache is read-only inside fetch_advert, so retries
|
||||
// would loop on the same dead address until expiry. Force a
|
||||
// re-fetch so the next retry tick picks up fresh endpoints.
|
||||
if matches!(e, crate::node::NodeError::NoTransportForType(_))
|
||||
&& let Some(bootstrap) = self.nostr_discovery.clone()
|
||||
{
|
||||
let npub = peer_config.npub.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = bootstrap.refetch_advert_for_stale_check(&npub).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1867,17 +1886,39 @@ impl Node {
|
||||
self.register_identity(peer_node_addr, peer_identity.pubkey_full());
|
||||
|
||||
let transport_id = self.allocate_transport_id();
|
||||
// Adopted ephemeral UDP transports use UdpConfig::default() when the
|
||||
// bootstrap runtime doesn't pass an override. Default MTU resolves to
|
||||
// 1280 (IPv6 minimum), which is the only value guaranteed to survive
|
||||
// arbitrary NAT-traversal middlebox paths. Inheriting from the named
|
||||
// [transports.udp] config (Option 3 in ISSUE-2026-0013) would track
|
||||
// operator config more closely but risks regressions on hostile paths;
|
||||
// accepted as-is until a concrete use case justifies the change.
|
||||
// Adopted ephemeral UDP transports inherit MTU + socket-buffer sizing
|
||||
// (and accept_connections / advertise flags) from the operator's
|
||||
// configured [transports.udp] when the bootstrap runtime doesn't
|
||||
// pass an explicit override. Lookup tries `transport_name` first
|
||||
// (covers the `Named` multi-listener variant) and falls back to the
|
||||
// unnamed `Single` listener, so single- and named-listener configs
|
||||
// both inherit cleanly.
|
||||
//
|
||||
// Tradeoff: `UdpConfig::default()` sets MTU 1280 (IPv6 minimum), the
|
||||
// only value guaranteed to survive arbitrary middlebox paths.
|
||||
// Inheriting a higher operator-chosen MTU means NAT-traversed flows
|
||||
// initially attempt that MTU and may black-hole on tighter paths
|
||||
// until reactive `MtuExceeded` recovery kicks in. Operators who
|
||||
// raise the primary MTU based on known-clean topology accept that
|
||||
// tradeoff; the silent drop on a too-low default was strictly
|
||||
// worse for the common case where the primary MTU is reachable.
|
||||
//
|
||||
// Bind / external address fields are cleared since the socket is
|
||||
// already bound.
|
||||
let inherited_config = traversal.transport_config.clone().unwrap_or_else(|| {
|
||||
let mut cfg = self
|
||||
.lookup_udp_config(traversal.transport_name.as_deref())
|
||||
.or_else(|| self.lookup_udp_config(None))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
cfg.bind_addr = None;
|
||||
cfg.external_addr = None;
|
||||
cfg
|
||||
});
|
||||
let mut transport = crate::transport::udp::UdpTransport::new(
|
||||
transport_id,
|
||||
traversal.transport_name.clone(),
|
||||
traversal.transport_config.clone().unwrap_or_default(),
|
||||
inherited_config,
|
||||
packet_tx,
|
||||
);
|
||||
|
||||
|
||||
+34
-1
@@ -4,7 +4,7 @@
|
||||
//! automatically retry with exponential backoff. Retry state lives on Node
|
||||
//! (not PeerConnection) because each retry creates a fresh connection.
|
||||
|
||||
use super::Node;
|
||||
use super::{Node, NodeError};
|
||||
use crate::PeerIdentity;
|
||||
use crate::config::PeerConfig;
|
||||
use crate::identity::NodeAddr;
|
||||
@@ -259,6 +259,25 @@ impl Node {
|
||||
|
||||
let peer_config = state.peer_config.clone();
|
||||
|
||||
// Refresh the peer's overlay advert before retrying. The cache is
|
||||
// read-only on hit (see fetch_advert), so every retry without a
|
||||
// refetch dials the same cached endpoint — and the most common
|
||||
// reason a peer ended up in retry_pending is that the cached
|
||||
// endpoint just stopped working (NAT rebind, port change, peer
|
||||
// restart on a different port). Without this refresh the retry
|
||||
// loop dials the same dead address forever.
|
||||
//
|
||||
// refetch_advert_for_stale_check uses the relay's advert as
|
||||
// ground truth: replaces the cache if there's a newer one,
|
||||
// evicts if the relay has nothing, otherwise leaves it. Cheap
|
||||
// (one Filter fetch with 2s timeout) and bounded by the retry
|
||||
// backoff cadence.
|
||||
if let Some(bootstrap) = self.nostr_discovery.clone() {
|
||||
let _ = bootstrap
|
||||
.refetch_advert_for_stale_check(&peer_config.npub)
|
||||
.await;
|
||||
}
|
||||
|
||||
match self.initiate_peer_connection(&peer_config).await {
|
||||
Ok(()) => {
|
||||
// Push retry_after_ms past the handshake timeout window so
|
||||
@@ -282,6 +301,20 @@ impl Node {
|
||||
error = %e,
|
||||
"Retry connection initiation failed"
|
||||
);
|
||||
// No-transport failures usually mean the cached overlay
|
||||
// advert is stale (peer rebound NAT, switched relay, etc.).
|
||||
// The advert cache is read-only inside fetch_advert, so
|
||||
// every retry returns the same dead address until the
|
||||
// entry expires. Force a re-fetch so the next retry tick
|
||||
// picks up fresh endpoints.
|
||||
if matches!(e, NodeError::NoTransportForType(_))
|
||||
&& let Some(bootstrap) = self.nostr_discovery.clone()
|
||||
{
|
||||
let npub = peer_config.npub.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = bootstrap.refetch_advert_for_stale_check(&npub).await;
|
||||
});
|
||||
}
|
||||
// Immediate failure counts as an attempt — schedule next retry
|
||||
// (reconnect flag is preserved on existing retry_pending entry)
|
||||
self.schedule_retry(node_addr, now_ms);
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
use super::*;
|
||||
use crate::EstablishedTraversal;
|
||||
use crate::config::UdpConfig;
|
||||
use crate::config::{TransportInstances, UdpConfig};
|
||||
use crate::node::wire::{PHASE_MSG1, PHASE_MSG2, PHASE_MSG3};
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::utils::index::IndexAllocator;
|
||||
use std::collections::HashMap;
|
||||
use tokio::time::{Duration, timeout, timeout_at};
|
||||
|
||||
#[tokio::test]
|
||||
@@ -284,3 +285,90 @@ async fn test_third_peer_can_handshake_via_adopted_transport_socket() {
|
||||
transport.stop().await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_adopted_udp_inherits_mtu_from_single_primary_config() {
|
||||
let mut node = make_node();
|
||||
node.config.transports.udp = TransportInstances::Single(UdpConfig {
|
||||
mtu: Some(1500),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
node.state = NodeState::Running;
|
||||
|
||||
let peer = make_node();
|
||||
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let handoff = EstablishedTraversal::new(
|
||||
"sess-inherit-single",
|
||||
peer.npub(),
|
||||
"127.0.0.1:9".parse().unwrap(),
|
||||
adopted_socket,
|
||||
);
|
||||
|
||||
let result = node.adopt_established_traversal(handoff).await.unwrap();
|
||||
let adopted = node
|
||||
.get_transport(&result.transport_id)
|
||||
.expect("adopted transport present");
|
||||
assert_eq!(
|
||||
adopted.mtu(),
|
||||
1500,
|
||||
"adopted UDP transport should inherit MTU from the primary [transports.udp] config",
|
||||
);
|
||||
|
||||
for (_, transport) in node.transports.iter_mut() {
|
||||
transport.stop().await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_adopted_udp_inherits_mtu_from_named_primary_config() {
|
||||
let mut node = make_node();
|
||||
let mut named = HashMap::new();
|
||||
named.insert(
|
||||
"primary".to_string(),
|
||||
UdpConfig {
|
||||
mtu: Some(1500),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
named.insert(
|
||||
"secondary".to_string(),
|
||||
UdpConfig {
|
||||
mtu: Some(1280),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
node.config.transports.udp = TransportInstances::Named(named);
|
||||
|
||||
let (packet_tx, packet_rx) = packet_channel(64);
|
||||
node.packet_tx = Some(packet_tx);
|
||||
node.packet_rx = Some(packet_rx);
|
||||
node.state = NodeState::Running;
|
||||
|
||||
let peer = make_node();
|
||||
let adopted_socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let handoff = EstablishedTraversal::new(
|
||||
"sess-inherit-named",
|
||||
peer.npub(),
|
||||
"127.0.0.1:9".parse().unwrap(),
|
||||
adopted_socket,
|
||||
)
|
||||
.with_transport_name("primary");
|
||||
|
||||
let result = node.adopt_established_traversal(handoff).await.unwrap();
|
||||
let adopted = node
|
||||
.get_transport(&result.transport_id)
|
||||
.expect("adopted transport present");
|
||||
assert_eq!(
|
||||
adopted.mtu(),
|
||||
1500,
|
||||
"adopted UDP transport should inherit MTU from the named [transports.udp.<name>] config matching transport_name",
|
||||
);
|
||||
|
||||
for (_, transport) in node.transports.iter_mut() {
|
||||
transport.stop().await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,6 +955,38 @@ fn test_promote_clears_retry_pending() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Initial peer-init failure at startup must enqueue a retry. Otherwise a peer
|
||||
/// whose addresses cannot be dialed at boot (no operational transport for the
|
||||
/// configured transport types, all addresses unreachable, NAT rebind, etc.)
|
||||
/// stays dead forever — pings arrive but cannot be answered until the daemon
|
||||
/// is manually restarted.
|
||||
#[tokio::test]
|
||||
async fn test_initiate_peer_connections_schedules_retry_on_no_transport() {
|
||||
let peer_identity = Identity::generate();
|
||||
let peer_npub = peer_identity.npub();
|
||||
let peer_node_addr = *PeerIdentity::from_npub(&peer_npub).unwrap().node_addr();
|
||||
|
||||
let mut config = Config::new();
|
||||
// udp address but no UDP transport registered on the node — every dial
|
||||
// attempt resolves to NodeError::NoTransportForType.
|
||||
config.peers.push(crate::config::PeerConfig::new(
|
||||
peer_npub,
|
||||
"udp",
|
||||
"10.0.0.2:2121",
|
||||
));
|
||||
|
||||
let mut node = Node::new(config).unwrap();
|
||||
assert!(node.retry_pending.is_empty());
|
||||
|
||||
node.initiate_peer_connections().await;
|
||||
|
||||
assert!(
|
||||
node.retry_pending.contains_key(&peer_node_addr),
|
||||
"startup peer-init failure must enqueue a retry so the peer can recover \
|
||||
without a daemon restart"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// transport_mtu() — ISSUE-2026-0011 regression coverage
|
||||
// ============================================================================
|
||||
|
||||
+43
-3
@@ -247,11 +247,14 @@ impl Node {
|
||||
.unwrap_or(0);
|
||||
|
||||
let flap_dampened = self.tree_state.set_parent(new_parent, new_seq, timestamp);
|
||||
// recompute_coords may demote to self_root if the new path would be
|
||||
// invalid; sign AFTER recompute so the signature covers the final
|
||||
// declaration.
|
||||
self.tree_state.recompute_coords();
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
warn!(error = %e, "Failed to sign declaration after parent switch");
|
||||
return;
|
||||
}
|
||||
self.tree_state.recompute_coords();
|
||||
self.coord_cache.clear();
|
||||
self.reset_discovery_backoff();
|
||||
|
||||
@@ -275,6 +278,25 @@ impl Node {
|
||||
// Tree structure changed — trigger bloom filter exchange with all peers
|
||||
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
||||
self.bloom_state.mark_all_updates_needed(all_peers);
|
||||
} else if !self.tree_state.is_root() && self.tree_state.should_be_root() {
|
||||
// Self is the smallest visible NodeAddr — promote to root rather
|
||||
// than continuing to advertise a stale ancestry rooted elsewhere.
|
||||
self.tree_state.become_root();
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
warn!(error = %e, "Failed to sign self-root declaration");
|
||||
return;
|
||||
}
|
||||
self.coord_cache.clear();
|
||||
self.reset_discovery_backoff();
|
||||
self.stats_mut().tree.parent_switched += 1;
|
||||
self.stats_mut().tree.parent_switches += 1;
|
||||
info!(
|
||||
new_root = %self.tree_state.root(),
|
||||
"Self-promoted to root: smallest visible NodeAddr"
|
||||
);
|
||||
self.send_tree_announce_to_all().await;
|
||||
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
||||
self.bloom_state.mark_all_updates_needed(all_peers);
|
||||
} else if !self.tree_state.is_root()
|
||||
&& *self.tree_state.my_declaration().parent_id() == *from
|
||||
{
|
||||
@@ -328,11 +350,11 @@ impl Node {
|
||||
.unwrap_or(0);
|
||||
|
||||
self.tree_state.set_parent(*from, new_seq, timestamp);
|
||||
self.tree_state.recompute_coords();
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
warn!(error = %e, "Failed to sign declaration after parent update");
|
||||
return;
|
||||
}
|
||||
self.tree_state.recompute_coords();
|
||||
self.coord_cache.clear();
|
||||
self.reset_discovery_backoff();
|
||||
|
||||
@@ -418,11 +440,11 @@ impl Node {
|
||||
.unwrap_or(0);
|
||||
|
||||
let flap_dampened = self.tree_state.set_parent(new_parent, new_seq, timestamp);
|
||||
self.tree_state.recompute_coords();
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
warn!(error = %e, "Failed to sign declaration after periodic parent re-eval");
|
||||
return;
|
||||
}
|
||||
self.tree_state.recompute_coords();
|
||||
self.coord_cache.clear();
|
||||
self.reset_discovery_backoff();
|
||||
|
||||
@@ -444,6 +466,24 @@ impl Node {
|
||||
|
||||
self.send_tree_announce_to_all().await;
|
||||
|
||||
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
||||
self.bloom_state.mark_all_updates_needed(all_peers);
|
||||
} else if !self.tree_state.is_root() && self.tree_state.should_be_root() {
|
||||
self.tree_state.become_root();
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
warn!(error = %e, "Failed to sign self-root declaration in periodic reeval");
|
||||
return;
|
||||
}
|
||||
self.coord_cache.clear();
|
||||
self.reset_discovery_backoff();
|
||||
self.stats_mut().tree.parent_switched += 1;
|
||||
self.stats_mut().tree.parent_switches += 1;
|
||||
info!(
|
||||
new_root = %self.tree_state.root(),
|
||||
trigger = "periodic",
|
||||
"Self-promoted to root in periodic reeval: smallest visible NodeAddr"
|
||||
);
|
||||
self.send_tree_announce_to_all().await;
|
||||
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
||||
self.bloom_state.mark_all_updates_needed(all_peers);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user