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"
);
}