mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Store node counters in an atomic metric registry read through &self, and introduce a shared NodeContext bundle holding the effectively-immutable fields (config, identity, startup epoch, capability limits). Source the immutable config and identity reads across the receive hot path, the handshake/session/mmp/encrypted state machines, and the discovery, tree, bloom, retry, and lifecycle modules through the context accessors rather than direct field reads. The Node fields and the context are rebuilt in lockstep at every mutation site.
739 lines
29 KiB
Rust
739 lines
29 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;
|
|
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.metrics().discovery.req_received.inc();
|
|
|
|
let request = match LookupRequest::decode(payload) {
|
|
Ok(req) => req,
|
|
Err(e) => {
|
|
self.metrics()
|
|
.discovery
|
|
.record_reject(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.metrics()
|
|
.discovery
|
|
.record_reject(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.metrics()
|
|
.discovery
|
|
.record_reject(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.metrics().discovery.req_target_is_us.inc();
|
|
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.metrics().discovery.req_forward_rate_limited.inc();
|
|
debug!(
|
|
request_id = request.request_id,
|
|
target = %self.peer_display_name(&request.target),
|
|
"Forward rate limited, suppressing LookupRequest"
|
|
);
|
|
return;
|
|
}
|
|
self.metrics().discovery.req_forwarded.inc();
|
|
self.forward_lookup_request(request).await;
|
|
} else {
|
|
self.metrics()
|
|
.discovery
|
|
.record_reject(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.metrics().discovery.resp_received.inc();
|
|
|
|
let mut response = match LookupResponse::decode(payload) {
|
|
Ok(resp) => resp,
|
|
Err(e) => {
|
|
self.metrics()
|
|
.discovery
|
|
.record_reject(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.metrics().discovery.resp_forwarded.inc();
|
|
|
|
// 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.metrics()
|
|
.discovery
|
|
.record_reject(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.metrics()
|
|
.discovery
|
|
.record_reject(DiscoveryReject::RespProofFailed);
|
|
warn!(
|
|
request_id = response.request_id,
|
|
target = %self.peer_display_name(&target),
|
|
"LookupResponse proof verification failed, discarding"
|
|
);
|
|
return;
|
|
}
|
|
|
|
self.metrics().discovery.resp_accepted.inc();
|
|
|
|
// 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.
|
|
let n = self.config().node.session.coords_warmup_packets;
|
|
if let Some(entry) = self.sessions.get_mut(&target)
|
|
&& entry.is_established()
|
|
{
|
|
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.metrics()
|
|
.discovery
|
|
.record_reject(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;
|
|
}
|
|
|
|
// Collect tree peers whose bloom filter contains the target
|
|
let forward_to: Vec<NodeAddr> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(addr, peer)| self.is_tree_peer(addr) && peer.may_reach(&request.target))
|
|
.map(|(addr, _)| *addr)
|
|
.collect();
|
|
|
|
// Fallback: if no tree peer matches, try non-tree bloom-matching peers
|
|
let (forward_to, used_fallback) = if forward_to.is_empty() {
|
|
let fallback: Vec<NodeAddr> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(addr, peer)| !self.is_tree_peer(addr) && peer.may_reach(&request.target))
|
|
.map(|(addr, _)| *addr)
|
|
.collect();
|
|
if fallback.is_empty() {
|
|
self.metrics().discovery.req_no_tree_peer.inc();
|
|
trace!(
|
|
request_id = request.request_id,
|
|
"No eligible peers to forward LookupRequest"
|
|
);
|
|
return;
|
|
}
|
|
(fallback, true)
|
|
} else {
|
|
(forward_to, false)
|
|
};
|
|
|
|
if used_fallback {
|
|
self.metrics().discovery.req_fallback_forwarded.inc();
|
|
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.metrics().discovery.req_initiated.inc();
|
|
|
|
let origin = *self.node_addr();
|
|
let origin_coords = self.tree_state().my_coords().clone();
|
|
let request = LookupRequest::generate(*target, origin, origin_coords, ttl, 0);
|
|
|
|
// Send only to tree peers whose bloom filter contains the target
|
|
let peer_addrs: Vec<NodeAddr> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(addr, peer)| self.is_tree_peer(addr) && peer.may_reach(target))
|
|
.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.metrics().discovery.req_deduplicated.inc();
|
|
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.metrics().discovery.req_backoff_suppressed.inc();
|
|
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.metrics().discovery.req_bloom_miss.inc();
|
|
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.metrics().discovery.resp_timed_out.inc();
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
}
|
|
}
|
|
}
|