mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Bring master's runtime peer-list refresh, opt-in mDNS LAN discovery, and the receive-path reject-reason / reloadable-config refactor into the XX handshake integration branch. The peer-restart-epoch detection and stale-FSP-session teardown authored against Noise IK are re-authored onto the Noise XX rekey path: complete_rekey_msg2 now also surfaces the remote startup epoch, and the stale session is cleared once the XX rekey completes (after msg3 is sent). Connect-budget and path-refresh work is threaded through the XX anonymous-discovery branch.
780 lines
31 KiB
Rust
780 lines
31 KiB
Rust
//! LookupRequest/LookupResponse discovery protocol handlers.
|
|
//!
|
|
//! Handles coordinate discovery via bloom-filter-guided tree routing.
|
|
//! Requests are forwarded only to tree peers (parent + children) whose
|
|
//! bloom filter contains the target. TTL and request_id dedup provide
|
|
//! safety bounds.
|
|
|
|
use crate::node::reject::{DiscoveryReject, RejectReason};
|
|
use crate::node::{Node, RecentRequest};
|
|
use crate::protocol::{LookupRequest, LookupResponse};
|
|
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.
|
|
///
|
|
/// Processing steps:
|
|
/// 1. Decode and validate
|
|
/// 2. Check request_id for duplicates (dedup / reverse-path routing)
|
|
/// 3. Record request for reverse-path forwarding
|
|
/// 4. Lazy purge expired entries
|
|
/// 5. If we're the target, generate and send response
|
|
/// 6. If TTL > 0, forward to tree peers whose bloom filter matches
|
|
pub(in crate::node) async fn handle_lookup_request(&mut self, from: &NodeAddr, payload: &[u8]) {
|
|
self.stats_mut().discovery.req_received += 1;
|
|
|
|
let request = match LookupRequest::decode(payload) {
|
|
Ok(req) => req,
|
|
Err(e) => {
|
|
self.stats_mut().discovery.req_decode_error += 1;
|
|
self.stats_mut()
|
|
.record_reject(RejectReason::Discovery(DiscoveryReject::ReqDecodeError));
|
|
debug!(from = %self.peer_display_name(from), error = %e, "Malformed LookupRequest");
|
|
return;
|
|
}
|
|
};
|
|
|
|
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,
|
|
// but request_id dedup catches edge cases during tree restructuring.
|
|
if self.recent_requests.contains_key(&request.request_id) {
|
|
self.stats_mut().discovery.req_duplicate += 1;
|
|
self.stats_mut()
|
|
.record_reject(RejectReason::Discovery(DiscoveryReject::ReqDuplicate));
|
|
debug!(
|
|
request_id = request.request_id,
|
|
from = %self.peer_display_name(from),
|
|
"Duplicate LookupRequest, dropping"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if self.recent_requests.len() >= MAX_RECENT_DISCOVERY_REQUESTS {
|
|
self.stats_mut()
|
|
.record_reject(RejectReason::Discovery(DiscoveryReject::ReqDedupCacheFull));
|
|
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));
|
|
|
|
// Are we the target?
|
|
if request.target == *self.node_addr() {
|
|
self.stats_mut().discovery.req_target_is_us += 1;
|
|
debug!(
|
|
request_id = request.request_id,
|
|
origin = %self.peer_display_name(&request.origin),
|
|
"We are the lookup target, generating response"
|
|
);
|
|
self.send_lookup_response(&request).await;
|
|
return;
|
|
}
|
|
|
|
// Forward if TTL permits
|
|
if request.can_forward() {
|
|
// Transit-side rate limit: collapse rapid-fire lookups for the
|
|
// same target from misbehaving nodes generating fresh request_ids.
|
|
if !self
|
|
.discovery_forward_limiter
|
|
.should_forward(&request.target)
|
|
{
|
|
self.stats_mut().discovery.req_forward_rate_limited += 1;
|
|
debug!(
|
|
request_id = request.request_id,
|
|
target = %self.peer_display_name(&request.target),
|
|
"Forward rate limited, suppressing LookupRequest"
|
|
);
|
|
return;
|
|
}
|
|
self.stats_mut().discovery.req_forwarded += 1;
|
|
self.forward_lookup_request(request).await;
|
|
} else {
|
|
self.stats_mut().discovery.req_ttl_exhausted += 1;
|
|
self.stats_mut()
|
|
.record_reject(RejectReason::Discovery(DiscoveryReject::ReqTtlExhausted));
|
|
debug!(
|
|
request_id = request.request_id,
|
|
target = %self.peer_display_name(&request.target),
|
|
"LookupRequest TTL exhausted"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Handle an incoming LookupResponse from a peer.
|
|
///
|
|
/// Processing steps:
|
|
/// 1. Decode and validate
|
|
/// 2. Check recent_requests to determine if we originated or are forwarding
|
|
/// 3. If originator: verify proof signature, then cache target_coords and path_mtu in coord_cache
|
|
/// 4. If transit: apply path_mtu min(outgoing_link_mtu), reverse-path forward to from_peer
|
|
pub(in crate::node) async fn handle_lookup_response(
|
|
&mut self,
|
|
from: &NodeAddr,
|
|
payload: &[u8],
|
|
) {
|
|
self.stats_mut().discovery.resp_received += 1;
|
|
|
|
let mut response = match LookupResponse::decode(payload) {
|
|
Ok(resp) => resp,
|
|
Err(e) => {
|
|
self.stats_mut().discovery.resp_decode_error += 1;
|
|
self.stats_mut()
|
|
.record_reject(RejectReason::Discovery(DiscoveryReject::RespDecodeError));
|
|
debug!(from = %self.peer_display_name(from), error = %e, "Malformed LookupResponse");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let now_ms = Self::now_ms();
|
|
|
|
// Check if we forwarded this request (transit node) or originated it
|
|
if let Some(recent) = self.recent_requests.get_mut(&response.request_id) {
|
|
// Already forwarded a response for this request — drop to
|
|
// prevent response routing loops.
|
|
if recent.response_forwarded {
|
|
debug!(
|
|
request_id = response.request_id,
|
|
target = %self.peer_display_name(&response.target),
|
|
"Response already forwarded for this request, dropping"
|
|
);
|
|
return;
|
|
}
|
|
recent.response_forwarded = true;
|
|
|
|
// Transit node: reverse-path forward
|
|
let from_peer = recent.from_peer;
|
|
self.stats_mut().discovery.resp_forwarded += 1;
|
|
|
|
// Apply path_mtu min() from the outgoing link's transport MTU
|
|
self.apply_outgoing_link_mtu_to_response(&mut response, &from_peer);
|
|
|
|
debug!(
|
|
request_id = response.request_id,
|
|
target = %self.peer_display_name(&response.target),
|
|
next_hop = %self.peer_display_name(&from_peer),
|
|
path_mtu = response.path_mtu,
|
|
"Reverse-path forwarding LookupResponse"
|
|
);
|
|
|
|
let encoded = response.encode();
|
|
if let Err(e) = self.send_encrypted_link_message(&from_peer, &encoded).await {
|
|
debug!(
|
|
next_hop = %self.peer_display_name(&from_peer),
|
|
error = %e,
|
|
"Failed to forward LookupResponse"
|
|
);
|
|
}
|
|
} else {
|
|
// We originated this request — verify proof before caching
|
|
let target = response.target;
|
|
let path_mtu = response.path_mtu;
|
|
|
|
// Look up the target's public key from identity_cache
|
|
let mut prefix = [0u8; 15];
|
|
prefix.copy_from_slice(&target.as_bytes()[0..15]);
|
|
let target_pubkey = match self.lookup_by_fips_prefix(&prefix) {
|
|
Some((_addr, pubkey)) => pubkey,
|
|
None => {
|
|
self.stats_mut().discovery.resp_identity_miss += 1;
|
|
self.stats_mut()
|
|
.record_reject(RejectReason::Discovery(DiscoveryReject::RespIdentityMiss));
|
|
warn!(
|
|
request_id = response.request_id,
|
|
target = %self.peer_display_name(&target),
|
|
"identity_cache miss for lookup target, cannot verify proof"
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Verify the proof signature
|
|
let (xonly, _parity) = target_pubkey.x_only_public_key();
|
|
let peer_id = PeerIdentity::from_pubkey(xonly);
|
|
let proof_data =
|
|
LookupResponse::proof_bytes(response.request_id, &target, &response.target_coords);
|
|
if !peer_id.verify(&proof_data, &response.proof) {
|
|
self.stats_mut().discovery.resp_proof_failed += 1;
|
|
self.stats_mut()
|
|
.record_reject(RejectReason::Discovery(DiscoveryReject::RespProofFailed));
|
|
warn!(
|
|
request_id = response.request_id,
|
|
target = %self.peer_display_name(&target),
|
|
"LookupResponse proof verification failed, discarding"
|
|
);
|
|
return;
|
|
}
|
|
|
|
self.stats_mut().discovery.resp_accepted += 1;
|
|
|
|
// Clear backoff on success — target is reachable
|
|
self.discovery_backoff.record_success(&target);
|
|
|
|
info!(
|
|
request_id = response.request_id,
|
|
target = %self.peer_display_name(&target),
|
|
depth = response.target_coords.depth(),
|
|
path_mtu = path_mtu,
|
|
"Discovery succeeded, proof verified, route cached"
|
|
);
|
|
|
|
self.coord_cache
|
|
.insert_with_path_mtu(target, response.target_coords, now_ms, path_mtu);
|
|
|
|
// Mirror path_mtu into the FipsAddress-keyed read-only lookup
|
|
// map used by the TUN reader/writer at TCP MSS clamp time.
|
|
let fips_addr = crate::FipsAddress::from_node_addr(&target);
|
|
match self.path_mtu_lookup.write() {
|
|
Ok(mut map) => {
|
|
let prior = map.insert(fips_addr, path_mtu);
|
|
debug!(
|
|
target = %self.peer_display_name(&target),
|
|
fips_addr = %fips_addr,
|
|
path_mtu = path_mtu,
|
|
prior = ?prior,
|
|
map_len = map.len(),
|
|
"Wrote path_mtu_lookup from discovery LookupResponse"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
warn!(
|
|
target = %self.peer_display_name(&target),
|
|
fips_addr = %fips_addr,
|
|
path_mtu = path_mtu,
|
|
error = %e,
|
|
"path_mtu_lookup write lock poisoned; clamp will not see this update"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Clean up pending lookup tracking
|
|
self.pending_lookups.remove(&target);
|
|
|
|
// If an established session exists, reset the warmup counter.
|
|
if let Some(entry) = self.sessions.get_mut(&target)
|
|
&& entry.is_established()
|
|
{
|
|
let n = self.config.node.session.coords_warmup_packets;
|
|
entry.set_coords_warmup_remaining(n);
|
|
debug!(
|
|
dest = %self.peer_display_name(&target),
|
|
warmup_packets = n,
|
|
"Reset coords warmup after discovery for existing session"
|
|
);
|
|
}
|
|
|
|
// If we have pending TUN packets for this target, retry session
|
|
// initiation. The coord_cache now has coords, so find_next_hop()
|
|
// should succeed.
|
|
if let Some(packets) = self.pending_tun_packets.get(&target) {
|
|
debug!(
|
|
dest = %self.peer_display_name(&target),
|
|
queued_packets = packets.len(),
|
|
"Retrying queued packets after discovery"
|
|
);
|
|
self.retry_session_after_discovery(target).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate and send a LookupResponse when we are the target.
|
|
async fn send_lookup_response(&mut self, request: &LookupRequest) {
|
|
let our_coords = self.tree_state().my_coords().clone();
|
|
|
|
// Sign proof: Identity::sign hashes with SHA-256 internally
|
|
let proof_data =
|
|
LookupResponse::proof_bytes(request.request_id, &request.target, &our_coords);
|
|
let proof = self.identity().sign(&proof_data);
|
|
|
|
let mut response =
|
|
LookupResponse::new(request.request_id, request.target, our_coords, proof);
|
|
|
|
// Route toward origin via reverse path.
|
|
let next_hop_addr = if let Some(recent) = self.recent_requests.get(&request.request_id) {
|
|
recent.from_peer
|
|
} else {
|
|
// Fallback: try greedy tree routing toward origin
|
|
match self.find_next_hop(&request.origin) {
|
|
Some(peer) => *peer.node_addr(),
|
|
None => {
|
|
debug!(
|
|
origin = %self.peer_display_name(&request.origin),
|
|
"Cannot route LookupResponse: no reverse path or tree route to origin"
|
|
);
|
|
self.stats_mut()
|
|
.record_reject(RejectReason::Discovery(DiscoveryReject::RespNoRoute));
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Fold our outgoing-link MTU into path_mtu so the target-edge link
|
|
// appears in the bottleneck calculation. Without this, the response
|
|
// leaves the target with path_mtu = u16::MAX and only intermediate
|
|
// transits min-fold; the target's first reverse-path hop is missed.
|
|
self.apply_outgoing_link_mtu_to_response(&mut response, &next_hop_addr);
|
|
|
|
debug!(
|
|
request_id = request.request_id,
|
|
origin = %self.peer_display_name(&request.origin),
|
|
next_hop = %self.peer_display_name(&next_hop_addr),
|
|
path_mtu = response.path_mtu,
|
|
"Sending LookupResponse"
|
|
);
|
|
|
|
let encoded = response.encode();
|
|
if let Err(e) = self
|
|
.send_encrypted_link_message(&next_hop_addr, &encoded)
|
|
.await
|
|
{
|
|
debug!(
|
|
next_hop = %self.peer_display_name(&next_hop_addr),
|
|
error = %e,
|
|
"Failed to send LookupResponse"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Forward a LookupRequest to eligible peers.
|
|
///
|
|
/// Primary path: tree peers (parent + children) whose bloom filter
|
|
/// contains the target. Restricting to tree peers follows the spanning
|
|
/// tree partition, producing a single directed path.
|
|
///
|
|
/// Fallback: if no tree peer's bloom matches, try non-tree peers whose
|
|
/// bloom contains the target. This recovers from dead ends caused by
|
|
/// stale bloom filters, tree restructuring, or transit node failures.
|
|
async fn forward_lookup_request(&mut self, mut request: LookupRequest) {
|
|
if !request.forward() {
|
|
return;
|
|
}
|
|
|
|
// Leaf nodes don't forward discovery requests
|
|
if self.node_profile == crate::protocol::NodeProfile::Leaf {
|
|
return;
|
|
}
|
|
|
|
// Collect full tree peers whose bloom filter contains the target
|
|
let min_mtu = request.min_mtu;
|
|
let forward_to: Vec<NodeAddr> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(addr, peer)| {
|
|
peer.peer_profile() == crate::protocol::NodeProfile::Full
|
|
&& self.is_tree_peer(addr)
|
|
&& peer.may_reach(&request.target)
|
|
&& self.peer_meets_mtu(peer, min_mtu)
|
|
})
|
|
.map(|(addr, _)| *addr)
|
|
.collect();
|
|
|
|
// Fallback: if no tree peer matches, try non-tree full bloom-matching peers
|
|
let (forward_to, used_fallback) = if forward_to.is_empty() {
|
|
let fallback: Vec<NodeAddr> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(addr, peer)| {
|
|
peer.peer_profile() == crate::protocol::NodeProfile::Full
|
|
&& !self.is_tree_peer(addr)
|
|
&& peer.may_reach(&request.target)
|
|
&& self.peer_meets_mtu(peer, min_mtu)
|
|
})
|
|
.map(|(addr, _)| *addr)
|
|
.collect();
|
|
if fallback.is_empty() {
|
|
self.stats_mut().discovery.req_no_tree_peer += 1;
|
|
trace!(
|
|
request_id = request.request_id,
|
|
"No eligible peers to forward LookupRequest"
|
|
);
|
|
return;
|
|
}
|
|
(fallback, true)
|
|
} else {
|
|
(forward_to, false)
|
|
};
|
|
|
|
if used_fallback {
|
|
self.stats_mut().discovery.req_fallback_forwarded += 1;
|
|
debug!(
|
|
request_id = request.request_id,
|
|
target = %self.peer_display_name(&request.target),
|
|
ttl = request.ttl,
|
|
peer_count = forward_to.len(),
|
|
"Forwarding LookupRequest via non-tree fallback"
|
|
);
|
|
} else {
|
|
debug!(
|
|
request_id = request.request_id,
|
|
target = %self.peer_display_name(&request.target),
|
|
ttl = request.ttl,
|
|
peer_count = forward_to.len(),
|
|
"Forwarding LookupRequest"
|
|
);
|
|
}
|
|
|
|
let encoded = request.encode();
|
|
|
|
for peer_addr in forward_to {
|
|
if let Err(e) = self.send_encrypted_link_message(&peer_addr, &encoded).await {
|
|
debug!(
|
|
peer = %self.peer_display_name(&peer_addr),
|
|
error = %e,
|
|
"Failed to forward LookupRequest to peer"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Initiate a discovery lookup for a target node.
|
|
///
|
|
/// Creates a LookupRequest and sends it to tree peers whose bloom
|
|
/// filters contain the target. Returns the number of peers sent to.
|
|
/// The originator does NOT record the request_id in recent_requests,
|
|
/// so when the response arrives, it's recognized as "our request".
|
|
pub(in crate::node) async fn initiate_lookup(&mut self, target: &NodeAddr, ttl: u8) -> usize {
|
|
self.stats_mut().discovery.req_initiated += 1;
|
|
|
|
let origin = *self.node_addr();
|
|
let min_mtu = self.config.tun.mtu();
|
|
let request = LookupRequest::generate(*target, origin, ttl, min_mtu);
|
|
|
|
// Send only to full tree peers whose bloom filter contains the target
|
|
let peer_addrs: Vec<NodeAddr> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(addr, peer)| {
|
|
peer.peer_profile() == crate::protocol::NodeProfile::Full
|
|
&& self.is_tree_peer(addr)
|
|
&& peer.may_reach(target)
|
|
&& self.peer_meets_mtu(peer, request.min_mtu)
|
|
})
|
|
.map(|(addr, _)| *addr)
|
|
.collect();
|
|
|
|
let peer_count = peer_addrs.len();
|
|
|
|
debug!(
|
|
request_id = request.request_id,
|
|
target = %self.peer_display_name(target),
|
|
ttl = ttl,
|
|
peer_count = peer_count,
|
|
total_peers = self.peers.len(),
|
|
"Discovery lookup initiated"
|
|
);
|
|
|
|
if peer_count == 0 {
|
|
return 0;
|
|
}
|
|
|
|
let encoded = request.encode();
|
|
|
|
for peer_addr in peer_addrs {
|
|
if let Err(e) = self.send_encrypted_link_message(&peer_addr, &encoded).await {
|
|
debug!(
|
|
peer = %self.peer_display_name(&peer_addr),
|
|
error = %e,
|
|
"Failed to send LookupRequest to peer"
|
|
);
|
|
}
|
|
}
|
|
|
|
peer_count
|
|
}
|
|
|
|
/// Initiate a discovery lookup if one is not already pending for this target.
|
|
///
|
|
/// Checks: pending dedup, post-failure backoff (off by default), bloom
|
|
/// filter pre-check. If all pass, sends the first attempt's LookupRequest.
|
|
/// Subsequent attempts (with fresh request_ids) are scheduled by
|
|
/// [`Self::check_pending_lookups`] when each attempt's per-attempt timeout
|
|
/// expires, using the sequence in `node.discovery.attempt_timeouts_secs`.
|
|
pub(in crate::node) async fn maybe_initiate_lookup(&mut self, dest: &NodeAddr) {
|
|
let now_ms = Self::now_ms();
|
|
|
|
// Dedup: any pending lookup means we are already trying.
|
|
if self.pending_lookups.contains_key(dest) {
|
|
self.stats_mut().discovery.req_deduplicated += 1;
|
|
debug!(
|
|
target_node = %self.peer_display_name(dest),
|
|
"Discovery lookup deduplicated, already pending"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Optional post-failure suppression. Defaults are 0/0 (inert);
|
|
// operators can opt in by setting `node.discovery.backoff_*_secs`.
|
|
if self.discovery_backoff.is_suppressed(dest) {
|
|
self.stats_mut().discovery.req_backoff_suppressed += 1;
|
|
debug!(
|
|
target_node = %self.peer_display_name(dest),
|
|
failures = self.discovery_backoff.failure_count(dest),
|
|
"Discovery lookup suppressed by backoff"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Bloom filter pre-check: if no peer's filter contains the target,
|
|
// it's not in the mesh — skip the lookup and record as failure.
|
|
let reachable = self.peers.values().any(|peer| peer.may_reach(dest));
|
|
if !reachable {
|
|
self.stats_mut().discovery.req_bloom_miss += 1;
|
|
self.discovery_backoff.record_failure(dest);
|
|
debug!(
|
|
target_node = %self.peer_display_name(dest),
|
|
"Discovery skipped, target not in any peer bloom filter"
|
|
);
|
|
return;
|
|
}
|
|
|
|
self.pending_lookups
|
|
.insert(*dest, PendingLookup::new(now_ms));
|
|
let ttl = self.config.node.discovery.ttl;
|
|
let sent = self.initiate_lookup(dest, ttl).await;
|
|
|
|
// If no tree peers had the target, fail immediately
|
|
if sent == 0 {
|
|
self.pending_lookups.remove(dest);
|
|
self.discovery_backoff.record_failure(dest);
|
|
debug!(
|
|
target_node = %self.peer_display_name(dest),
|
|
"Discovery failed, no tree peers with bloom match"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Check pending lookups for next-attempt or final timeout.
|
|
///
|
|
/// Called periodically from the tick handler. The lookup state machine
|
|
/// runs through `node.discovery.attempt_timeouts_secs` (default
|
|
/// `[1, 2, 4, 8]`): each entry is the deadline for one attempt. When the
|
|
/// current attempt's deadline elapses:
|
|
/// - If more entries remain: send the next attempt with a fresh
|
|
/// `request_id`.
|
|
/// - Otherwise: declare the destination unreachable, drop queued packets,
|
|
/// and emit ICMPv6 destination-unreachable for each.
|
|
pub(in crate::node) async fn check_pending_lookups(&mut self, now_ms: u64) {
|
|
let timeouts = self.config.node.discovery.attempt_timeouts_secs.clone();
|
|
let max_attempts = timeouts.len() as u8;
|
|
|
|
// Collect targets needing action
|
|
let mut to_retry: Vec<NodeAddr> = Vec::new();
|
|
let mut to_timeout: Vec<NodeAddr> = Vec::new();
|
|
|
|
for (&target, entry) in &self.pending_lookups {
|
|
let attempt_idx = (entry.attempt as usize).saturating_sub(1);
|
|
let attempt_timeout_ms = timeouts.get(attempt_idx).copied().unwrap_or(0) * 1000;
|
|
if now_ms.saturating_sub(entry.last_sent_ms) >= attempt_timeout_ms {
|
|
if entry.attempt >= max_attempts {
|
|
to_timeout.push(target);
|
|
} else {
|
|
to_retry.push(target);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process retries
|
|
for target in to_retry {
|
|
if let Some(entry) = self.pending_lookups.get_mut(&target) {
|
|
entry.attempt += 1;
|
|
entry.last_sent_ms = now_ms;
|
|
let attempt = entry.attempt;
|
|
|
|
let ttl = self.config.node.discovery.ttl;
|
|
let sent = self.initiate_lookup(&target, ttl).await;
|
|
if sent > 0 {
|
|
debug!(
|
|
target_node = %self.peer_display_name(&target),
|
|
attempt = attempt,
|
|
"Discovery retry sent"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process timeouts
|
|
for addr in to_timeout {
|
|
self.stats_mut().discovery.resp_timed_out += 1;
|
|
self.pending_lookups.remove(&addr);
|
|
|
|
// Record failure for optional backoff
|
|
self.discovery_backoff.record_failure(&addr);
|
|
let failures = self.discovery_backoff.failure_count(&addr);
|
|
|
|
let queued = self.pending_tun_packets.remove(&addr);
|
|
let pkt_count = queued.as_ref().map_or(0, |p| p.len());
|
|
info!(
|
|
target_node = %self.peer_display_name(&addr),
|
|
queued_packets = pkt_count,
|
|
failures = failures,
|
|
"Discovery lookup timed out, destination unreachable"
|
|
);
|
|
if let Some(packets) = queued {
|
|
for pkt in &packets {
|
|
self.send_icmpv6_dest_unreachable(pkt);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reset discovery backoff on topology changes.
|
|
pub(in crate::node) fn reset_discovery_backoff(&mut self) {
|
|
if !self.discovery_backoff.is_empty() {
|
|
debug!(
|
|
entries = self.discovery_backoff.entry_count(),
|
|
"Resetting discovery backoff on topology change"
|
|
);
|
|
self.discovery_backoff.reset_all();
|
|
}
|
|
}
|
|
|
|
/// Check if a peer's outgoing link MTU meets the min_mtu requirement.
|
|
///
|
|
/// Returns true if min_mtu is 0 (no requirement) or if the peer's
|
|
/// transport link MTU is >= min_mtu.
|
|
fn peer_meets_mtu(&self, peer: &crate::peer::ActivePeer, min_mtu: u16) -> bool {
|
|
if min_mtu == 0 {
|
|
return true;
|
|
}
|
|
if let Some(tid) = peer.transport_id()
|
|
&& let Some(transport) = self.transports.get(&tid)
|
|
{
|
|
let link_mtu = peer
|
|
.current_addr()
|
|
.map(|addr| transport.link_mtu(addr))
|
|
.unwrap_or_else(|| transport.mtu());
|
|
link_mtu >= min_mtu
|
|
} else {
|
|
// No transport info available — don't prune
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Remove expired entries from the recent_requests cache.
|
|
fn purge_expired_requests(&mut self, current_time_ms: u64) {
|
|
let expiry_ms = self.config.node.discovery.recent_expiry_secs * 1000;
|
|
self.recent_requests
|
|
.retain(|_, entry| !entry.is_expired(current_time_ms, expiry_ms));
|
|
}
|
|
|
|
/// Min-fold our outgoing-link MTU into a LookupResponse's `path_mtu`.
|
|
///
|
|
/// Used at both transit-side reverse-path forward and at the target's
|
|
/// own send_lookup_response. The link MTU we apply is the MTU of the
|
|
/// transport+addr we'll use to deliver the response toward `next_hop`.
|
|
/// No-op when `next_hop` is not a directly-connected peer or its
|
|
/// transport is not registered.
|
|
pub(in crate::node) fn apply_outgoing_link_mtu_to_response(
|
|
&self,
|
|
response: &mut LookupResponse,
|
|
next_hop: &NodeAddr,
|
|
) {
|
|
if let Some(peer) = self.peers.get(next_hop)
|
|
&& let Some(tid) = peer.transport_id()
|
|
&& let Some(transport) = self.transports.get(&tid)
|
|
{
|
|
let link_mtu = if let Some(addr) = peer.current_addr() {
|
|
transport.link_mtu(addr)
|
|
} else {
|
|
transport.mtu()
|
|
};
|
|
response.path_mtu = response.path_mtu.min(link_mtu);
|
|
}
|
|
}
|
|
|
|
/// Seed `path_mtu_lookup` for a directly-connected peer.
|
|
///
|
|
/// Called when an FMP link-layer peer is promoted to active. The seed
|
|
/// value is the local outgoing-link MTU on the peer's transport, which
|
|
/// is the actual link constraint for direct-link traffic. Stored only
|
|
/// when no tighter value exists: discovery's reverse-path bottleneck
|
|
/// or MMP `MtuExceeded` reactive learning take precedence when smaller.
|
|
///
|
|
/// Without this seed, configured/auto-connect peers (which establish
|
|
/// sessions without going through the discovery Lookup flow) leave
|
|
/// `path_mtu_lookup` empty for their FipsAddress, causing
|
|
/// `per_flow_max_mss` to fall back to the global ceiling and the
|
|
/// SYN-time TCP MSS clamp to over-estimate the effective path.
|
|
pub(in crate::node) fn seed_path_mtu_for_link_peer(
|
|
&self,
|
|
peer_addr: &NodeAddr,
|
|
transport_id: TransportId,
|
|
addr: &TransportAddr,
|
|
) {
|
|
let Some(transport) = self.transports.get(&transport_id) else {
|
|
debug!(
|
|
peer = %self.peer_display_name(peer_addr),
|
|
transport_id = %transport_id,
|
|
"seed_path_mtu_for_link_peer: transport not registered, skipping seed"
|
|
);
|
|
return;
|
|
};
|
|
let link_mtu = transport.link_mtu(addr);
|
|
let fips_addr = crate::FipsAddress::from_node_addr(peer_addr);
|
|
let Ok(mut map) = self.path_mtu_lookup.write() else {
|
|
warn!(
|
|
peer = %self.peer_display_name(peer_addr),
|
|
"seed_path_mtu_for_link_peer: path_mtu_lookup write lock poisoned"
|
|
);
|
|
return;
|
|
};
|
|
match map.get(&fips_addr).copied() {
|
|
Some(existing) if existing <= link_mtu => {
|
|
// Keep the tighter learned value; never loosen the clamp.
|
|
debug!(
|
|
peer = %self.peer_display_name(peer_addr),
|
|
fips_addr = %fips_addr,
|
|
link_mtu = link_mtu,
|
|
existing = existing,
|
|
"seed_path_mtu_for_link_peer: keeping tighter existing value"
|
|
);
|
|
}
|
|
other => {
|
|
map.insert(fips_addr, link_mtu);
|
|
debug!(
|
|
peer = %self.peer_display_name(peer_addr),
|
|
fips_addr = %fips_addr,
|
|
link_mtu = link_mtu,
|
|
prior = ?other,
|
|
map_len = map.len(),
|
|
"seed_path_mtu_for_link_peer: wrote link MTU"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tracks a pending discovery lookup with retry state.
|
|
pub struct PendingLookup {
|
|
/// When the lookup was first initiated.
|
|
pub initiated_ms: u64,
|
|
/// When the last attempt was sent.
|
|
pub last_sent_ms: u64,
|
|
/// Current attempt number (1 = initial, 2 = first retry, ...).
|
|
pub attempt: u8,
|
|
}
|
|
|
|
impl PendingLookup {
|
|
pub fn new(now_ms: u64) -> Self {
|
|
Self {
|
|
initiated_ms: now_ms,
|
|
last_sent_ms: now_ms,
|
|
attempt: 1,
|
|
}
|
|
}
|
|
}
|