use super::*;
#[test]
fn pending_discovery_lookup_queue_owns_dedup_and_capacity() {
let mut lookups = crate::node::handlers::discovery::PendingDiscoveryLookups::default();
let first = NodeAddr::from_bytes([1u8; 16]);
let second = NodeAddr::from_bytes([2u8; 16]);
assert!(lookups.admission_for(&first, 1).accepted());
assert!(lookups.insert_new(first, 123).is_none());
assert_eq!(lookups.len(), 1);
assert_eq!(
lookups.get(&first).map(|lookup| lookup.initiated_ms),
Some(123)
);
assert!(lookups.admission_for(&first, 1).deduplicated());
assert!(lookups.admission_for(&second, 1).queue_full());
assert!(lookups.remove(&first).is_some());
assert!(lookups.admission_for(&second, 1).accepted());
}
#[tokio::test]
async fn reply_learned_zero_peer_lookup_does_not_backoff_destination() {
let mut node = make_node();
node.config.node.routing.mode = RoutingMode::ReplyLearned;
let target = make_node_addr(0x45);
let baseline_initiated = node.stats().discovery.req_initiated;
node.maybe_initiate_lookup(&target).await;
assert_eq!(
node.stats().discovery.req_initiated,
baseline_initiated + 1,
"the first attempt should still construct one lookup request"
);
assert!(
!node.pending_lookups.contains_key(&target),
"a zero-carrier lookup must not remain pending"
);
assert!(
!node.discovery_backoff.is_suppressed(&target),
"no lookup was put on the wire, so the destination must stay retryable"
);
node.maybe_initiate_lookup(&target).await;
assert_eq!(
node.stats().discovery.req_initiated,
baseline_initiated + 2,
"a later retry should not be suppressed by zero-carrier startup"
);
}
#[tokio::test]
async fn test_check_pending_lookups_default_sequence_unreachable() {
use crate::bloom::BloomFilter;
use crate::node::handlers::discovery::PendingLookup;
use crate::peer::ActivePeer;
use crate::transport::LinkId;
let mut node = make_node();
assert_eq!(
node.config.node.discovery.attempt_timeouts_secs,
vec![1, 2, 4, 8],
"test pins the [1,2,4,8] default; update the test if the default changes"
);
let (tun_tx, tun_rx) = crate::upper::tun::write_channel();
node.tun_tx = Some(tun_tx);
let target_identity = Identity::generate();
let target_addr = *target_identity.node_addr();
let peer_identity_full = Identity::generate();
let peer_addr = *peer_identity_full.node_addr();
let peer_identity = crate::PeerIdentity::from_pubkey(peer_identity_full.pubkey());
let mut peer = ActivePeer::new(peer_identity, LinkId::new(1), 0);
let mut bloom = BloomFilter::new();
bloom.insert(&target_addr);
peer.update_filter(bloom, 1, 0);
node.peers.insert(peer_addr, peer);
let our_addr = *node.node_addr();
let peer_decl = crate::tree::ParentDeclaration::new(peer_addr, our_addr, 1, 0);
let peer_coords = TreeCoordinate::from_addrs(vec![peer_addr, our_addr]).unwrap();
node.tree_state_mut().update_peer(peer_decl, peer_coords);
assert!(node.is_tree_peer(&peer_addr), "peer must be a tree peer");
let mut ipv6_pkt = vec![0u8; 40];
ipv6_pkt[0] = 0x60; ipv6_pkt[6] = 17; ipv6_pkt[7] = 64; ipv6_pkt[8] = 0xfd;
ipv6_pkt[23] = 0x01;
let target_ipv6 = crate::FipsAddress::from_node_addr(&target_addr).to_ipv6();
ipv6_pkt[24..40].copy_from_slice(&target_ipv6.octets());
node.pending_session_traffic
.push_tun_packet(target_addr, ipv6_pkt, usize::MAX, usize::MAX);
node.pending_lookups
.insert(target_addr, PendingLookup::new(0));
let baseline_initiated = node.stats().discovery.req_initiated;
let baseline_timed_out = node.stats().discovery.resp_timed_out;
node.check_pending_lookups(1100).await;
{
let entry = node
.pending_lookups
.get(&target_addr)
.expect("still pending");
assert_eq!(entry.attempt, 2, "after retry #1, attempt should be 2");
assert_eq!(entry.last_sent_ms, 1100);
}
assert_eq!(
node.stats().discovery.req_initiated,
baseline_initiated + 1,
"retry #1 must invoke initiate_lookup exactly once"
);
node.check_pending_lookups(3100).await;
{
let entry = node
.pending_lookups
.get(&target_addr)
.expect("still pending");
assert_eq!(entry.attempt, 3, "after retry #2, attempt should be 3");
assert_eq!(entry.last_sent_ms, 3100);
}
assert_eq!(
node.stats().discovery.req_initiated,
baseline_initiated + 2,
"retry #2 must invoke initiate_lookup exactly once more"
);
node.check_pending_lookups(7100).await;
{
let entry = node
.pending_lookups
.get(&target_addr)
.expect("still pending");
assert_eq!(entry.attempt, 4, "after retry #3, attempt should be 4");
assert_eq!(entry.last_sent_ms, 7100);
}
assert_eq!(
node.stats().discovery.req_initiated,
baseline_initiated + 3,
"retry #3 must invoke initiate_lookup exactly once more"
);
node.check_pending_lookups(15_099).await;
assert!(
node.pending_lookups.contains_key(&target_addr),
"8s window not yet expired: pending_lookup must persist"
);
assert_eq!(
node.stats().discovery.req_initiated,
baseline_initiated + 3,
"no new attempt before final deadline"
);
assert_eq!(
node.stats().discovery.resp_timed_out,
baseline_timed_out,
"no timeout before final deadline"
);
while tun_rx.try_recv_packet().is_ok() {}
node.check_pending_lookups(15_100).await;
assert!(
!node.pending_lookups.contains_key(&target_addr),
"final timeout must remove the pending_lookups entry"
);
assert_eq!(
node.stats().discovery.resp_timed_out,
baseline_timed_out + 1,
"final timeout must increment discovery.resp_timed_out"
);
assert_eq!(
node.stats().discovery.req_initiated,
baseline_initiated + 3,
"the final-timeout step must NOT call initiate_lookup"
);
assert!(
node.pending_session_traffic
.tun_packets_for(&target_addr)
.is_none(),
"queued packets for the unreachable target must be drained"
);
let icmp_frame = tun_rx
.try_recv_packet()
.map(|packet| packet.as_slice().to_vec())
.expect("ICMPv6 Destination Unreachable must be emitted on final timeout");
assert!(
icmp_frame.len() >= 48,
"ICMPv6 frame must be at least IPv6 header (40) + ICMPv6 header (8)"
);
assert_eq!(icmp_frame[0] >> 4, 6, "must be IPv6");
assert_eq!(icmp_frame[6], 58, "next_header must be IPPROTO_ICMPV6 (58)");
assert_eq!(icmp_frame[40], 1, "ICMPv6 type 1 = Destination Unreachable");
let baseline_suppressed = node.stats().discovery.req_backoff_suppressed;
node.maybe_initiate_lookup(&target_addr).await;
assert_eq!(
node.stats().discovery.req_backoff_suppressed,
baseline_suppressed + 1,
"an immediately repeated lookup for the same offline target must be backoff-suppressed"
);
assert_eq!(
node.stats().discovery.req_initiated,
baseline_initiated + 3,
"backoff suppression must not send another fresh LookupRequest"
);
assert!(
!node.pending_lookups.contains_key(&target_addr),
"backoff suppression must not re-open the pending lookup"
);
}
#[tokio::test]
async fn lookup_timeout_preserves_endpoint_data_for_fsp_responder_awaiting_msg3() {
use crate::node::handlers::discovery::PendingLookup;
use crate::node::session::{EndToEndState, SessionEntry};
let mut node = make_node();
let target = Identity::generate();
let target_addr = *target.node_addr();
let responder = crate::noise::HandshakeState::new_xk_responder(node.identity().keypair());
node.sessions.insert(
target_addr,
SessionEntry::new(
target_addr,
node.identity().pubkey_full(),
EndToEndState::AwaitingMsg3(responder),
1_000,
false,
),
);
node.pending_session_traffic
.push_endpoint_data_batch_with_enqueued_at_ms(
target_addr,
vec![
crate::node::EndpointDataPayload::from_service_datagram(
7_370,
7_370,
b"tcp-syn".to_vec(),
)
.expect("pending service datagram"),
],
usize::MAX,
usize::MAX,
1_000,
);
let mut lookup = PendingLookup::new(0);
lookup.attempt = node.config.node.discovery.attempt_timeouts_secs.len() as u8;
node.pending_lookups.insert(target_addr, lookup);
let baseline_timed_out = node.stats().discovery.resp_timed_out;
node.check_pending_lookups(8_000).await;
assert!(
!node.pending_lookups.contains_key(&target_addr),
"the exhausted lookup should stop while the FSP handshake continues"
);
assert_eq!(
node.pending_session_traffic
.endpoint_data_for(&target_addr)
.map(|queue| queue.len()),
Some(1),
"lookup exhaustion must not discard endpoint data owned by an active FSP handshake"
);
assert_eq!(
node.stats().discovery.resp_timed_out,
baseline_timed_out,
"an active authenticated-session handshake is not an unreachable destination"
);
assert!(
!node.discovery_backoff.is_suppressed(&target_addr),
"an active FSP handshake must not poison later discovery"
);
}