use super::*;
use crate::protocol::SessionMsg3;
use crate::{SessionAck, SessionDatagram, SessionSetup};
#[tokio::test]
async fn test_stale_mmp_receiver_reports_do_not_change_route_choice() {
let mut config = Config::new();
config.node.routing.mode = RoutingMode::ReplyLearned;
let mut node = Node::new(config).unwrap();
let transport_id = TransportId::new(1);
let direct_link = LinkId::new(1);
let (direct_conn, direct_id) =
make_completed_connection(&mut node, direct_link, transport_id, 1000);
let dest_addr = *direct_id.node_addr();
node.add_connection(direct_conn).unwrap();
node.promote_connection(direct_link, direct_id, 2000)
.unwrap();
let mesh_link = LinkId::new(2);
let (mesh_conn, mesh_id) = make_completed_connection(&mut node, mesh_link, transport_id, 1000);
let mesh_next_hop = *mesh_id.node_addr();
node.add_connection(mesh_conn).unwrap();
node.promote_connection(mesh_link, mesh_id, 2000).unwrap();
node.learn_reverse_route(dest_addr, mesh_next_hop);
let baseline = ReceiverReport {
highest_counter: 100,
cumulative_packets_recv: 100,
cumulative_bytes_recv: 10_000,
timestamp_echo: 0,
dwell_time: 0,
max_burst_loss: 0,
mean_burst_loss: 0,
jitter: 0,
ecn_ce_count: 0,
owd_trend: 0,
burst_loss_count: 0,
cumulative_reorder_count: 0,
interval_packets_recv: 0,
interval_bytes_recv: 0,
}
.encode();
node.handle_receiver_report(&dest_addr, &baseline[1..])
.await;
assert_eq!(
node.find_next_hop(&dest_addr).map(|peer| *peer.node_addr()),
Some(dest_addr),
"healthy direct should initially hide learned fallback"
);
assert_eq!(
node.dataplane_fmp_link_metrics(&dest_addr, std::time::Instant::now())
.and_then(|metrics| metrics.srtt_ms),
None,
"counter-only baseline must not install a route-changing RTT"
);
let switches_before = node.stats().tree.parent_switches;
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
let duplicate_with_bogus_rtt = ReceiverReport {
highest_counter: 100,
cumulative_packets_recv: 100,
cumulative_bytes_recv: 10_000,
timestamp_echo: 1,
dwell_time: 0,
max_burst_loss: u16::MAX,
mean_burst_loss: u16::MAX,
jitter: u32::MAX,
ecn_ce_count: 0,
owd_trend: i32::MAX,
burst_loss_count: u32::MAX,
cumulative_reorder_count: 0,
interval_packets_recv: 0,
interval_bytes_recv: 0,
}
.encode();
node.handle_receiver_report(&dest_addr, &duplicate_with_bogus_rtt[1..])
.await;
let regressed_with_bogus_goodput = ReceiverReport {
highest_counter: 90,
cumulative_packets_recv: 90,
cumulative_bytes_recv: u64::MAX,
timestamp_echo: 1,
dwell_time: 0,
max_burst_loss: u16::MAX,
mean_burst_loss: u16::MAX,
jitter: u32::MAX,
ecn_ce_count: 0,
owd_trend: i32::MIN,
burst_loss_count: u32::MAX,
cumulative_reorder_count: 0,
interval_packets_recv: u32::MAX,
interval_bytes_recv: u32::MAX,
}
.encode();
node.handle_receiver_report(&dest_addr, ®ressed_with_bogus_goodput[1..])
.await;
assert_eq!(
node.find_next_hop(&dest_addr).map(|peer| *peer.node_addr()),
Some(dest_addr),
"bogus stale MMP metrics must not move payload routing to fallback"
);
assert_eq!(
node.stats().tree.parent_switches,
switches_before,
"ignored stale MMP metrics must not trigger parent reevaluation"
);
let direct_mmp = node
.dataplane_fmp_link_metrics(&dest_addr, std::time::Instant::now())
.expect("direct mmp");
assert_eq!(
direct_mmp.srtt_ms, None,
"ignored stale reports must not install an RTT sample"
);
assert_eq!(
direct_mmp.last_forward_loss_sample, None,
"ignored stale reports must not leave a loss sample behind"
);
assert_eq!(
direct_mmp.goodput_bps, 0.0,
"ignored stale reports must not update goodput"
);
assert!(
(node.dataplane_fmp_link_cost(&dest_addr) - 1.0).abs() < f64::EPSILON,
"ignored stale reports must leave default direct link cost unchanged"
);
}
#[test]
fn test_transit_prefers_adjacent_destination_over_learned_route_back_to_previous_hop() {
let mut config = Config::new();
config.node.routing.mode = RoutingMode::ReplyLearned;
let mut node = Node::new(config).unwrap();
let transport_id = TransportId::new(1);
let previous_link = LinkId::new(1);
let (previous_conn, previous_id) =
make_completed_connection(&mut node, previous_link, transport_id, 1000);
let previous_hop = *previous_id.node_addr();
node.add_connection(previous_conn).unwrap();
node.promote_connection(previous_link, previous_id, 2000)
.unwrap();
let dest_link = LinkId::new(2);
let (dest_conn, dest_id) = make_completed_connection(&mut node, dest_link, transport_id, 1000);
let dest_addr = *dest_id.node_addr();
node.add_connection(dest_conn).unwrap();
node.promote_connection(dest_link, dest_id, 2000).unwrap();
seed_dataplane_fmp_srtt_for_test(&mut node, dest_addr, 90);
seed_dataplane_fmp_srtt_for_test(&mut node, previous_hop, 5);
node.learn_reverse_route(dest_addr, previous_hop);
let source_route = node.find_next_hop(&dest_addr).expect("source fallback");
assert_eq!(
source_route.node_addr(),
&previous_hop,
"source traffic may prefer a much cheaper learned fallback"
);
let transit_route = node
.find_transit_next_hop(&dest_addr, &previous_hop)
.expect("adjacent destination route");
assert_eq!(
transit_route, dest_addr,
"a transit node must deliver to its adjacent healthy destination instead of looping back"
);
}
#[test]
fn test_transit_rejects_declared_destination_equal_to_previous_hop() {
let mut node = make_node();
let transport_id = TransportId::new(1);
let previous_link = LinkId::new(1);
let (previous_conn, previous_id) =
make_completed_connection(&mut node, previous_link, transport_id, 1000);
let previous_hop = *previous_id.node_addr();
node.add_connection(previous_conn).unwrap();
node.promote_connection(previous_link, previous_id, 2000)
.unwrap();
assert_eq!(
node.find_next_hop(&previous_hop)
.map(|peer| *peer.node_addr()),
Some(previous_hop),
"fixture requires a healthy direct route to the declared destination"
);
assert!(
node.find_transit_next_hop(&previous_hop, &previous_hop)
.is_none(),
"a transit packet whose destination equals its incoming hop must not bounce back"
);
}
#[test]
fn test_transit_rejects_learned_route_back_to_previous_hop() {
let mut config = Config::new();
config.node.routing.mode = RoutingMode::ReplyLearned;
let mut node = Node::new(config).unwrap();
let transport_id = TransportId::new(1);
let previous_link = LinkId::new(1);
let (previous_conn, previous_id) =
make_completed_connection(&mut node, previous_link, transport_id, 1000);
let previous_hop = *previous_id.node_addr();
node.add_connection(previous_conn).unwrap();
node.promote_connection(previous_link, previous_id, 2000)
.unwrap();
let dest_addr = make_node_addr(0xDD);
node.learn_reverse_route(dest_addr, previous_hop);
let source_route = node.find_next_hop(&dest_addr).expect("learned route");
assert_eq!(
source_route.node_addr(),
&previous_hop,
"fixture should expose the learned route that would loop on transit"
);
assert!(
node.find_transit_next_hop(&dest_addr, &previous_hop)
.is_none(),
"transit forwarding must not bounce a packet back to the peer it arrived from"
);
}
#[test]
fn test_transit_uses_coordinate_fallback_instead_of_learned_reverse_loop() {
let mut config = Config::new();
config.node.routing.mode = RoutingMode::ReplyLearned;
let mut node = Node::new(config).unwrap();
let transport_id = TransportId::new(1);
let my_addr = *node.node_addr();
let previous_link = LinkId::new(1);
let (previous_conn, previous_id) =
make_completed_connection(&mut node, previous_link, transport_id, 1000);
let previous_hop = *previous_id.node_addr();
node.add_connection(previous_conn).unwrap();
node.promote_connection(previous_link, previous_id, 2000)
.unwrap();
let fallback_link = LinkId::new(2);
let (fallback_conn, fallback_id) =
make_completed_connection(&mut node, fallback_link, transport_id, 1000);
let fallback_hop = *fallback_id.node_addr();
node.add_connection(fallback_conn).unwrap();
node.promote_connection(fallback_link, fallback_id, 2000)
.unwrap();
node.tree_state_mut().update_peer(
ParentDeclaration::new(previous_hop, my_addr, 1, 1000),
TreeCoordinate::from_addrs(vec![previous_hop, my_addr]).unwrap(),
);
node.tree_state_mut().update_peer(
ParentDeclaration::new(fallback_hop, my_addr, 1, 1000),
TreeCoordinate::from_addrs(vec![fallback_hop, my_addr]).unwrap(),
);
let dest = make_node_addr(0xDD);
let dest_coords = TreeCoordinate::from_addrs(vec![dest, fallback_hop, my_addr]).unwrap();
node.coord_cache_mut()
.insert(dest, dest_coords, Node::now_ms());
node.learn_reverse_route(dest, previous_hop);
assert_eq!(
node.find_next_hop(&dest).map(|peer| *peer.node_addr()),
Some(previous_hop),
"fixture must select the learned route before transit excludes its previous hop"
);
assert_eq!(
node.find_transit_next_hop(&dest, &previous_hop),
Some(fallback_hop),
"transit must use the loop-free coordinate route instead of dropping into its learned reverse path"
);
}
#[test]
fn test_transit_escapes_multi_hop_learned_cycle_via_coordinate_progress() {
let mut config = Config::new();
config.node.routing.mode = RoutingMode::ReplyLearned;
let mut node = Node::new(config).unwrap();
let transport_id = TransportId::new(1);
let my_addr = *node.node_addr();
let previous_link = LinkId::new(1);
let (previous_conn, previous_id) =
make_completed_connection(&mut node, previous_link, transport_id, 1000);
let previous_hop = *previous_id.node_addr();
node.add_connection(previous_conn).unwrap();
node.promote_connection(previous_link, previous_id, 2000)
.unwrap();
let cycle_link = LinkId::new(2);
let (cycle_conn, cycle_id) =
make_completed_connection(&mut node, cycle_link, transport_id, 1000);
let cycle_hop = *cycle_id.node_addr();
node.add_connection(cycle_conn).unwrap();
node.promote_connection(cycle_link, cycle_id, 2000).unwrap();
let fallback_link = LinkId::new(3);
let (fallback_conn, fallback_id) =
make_completed_connection(&mut node, fallback_link, transport_id, 1000);
let fallback_hop = *fallback_id.node_addr();
node.add_connection(fallback_conn).unwrap();
node.promote_connection(fallback_link, fallback_id, 2000)
.unwrap();
for peer in [previous_hop, cycle_hop, fallback_hop] {
node.tree_state_mut().update_peer(
ParentDeclaration::new(peer, my_addr, 1, 1000),
TreeCoordinate::from_addrs(vec![peer, my_addr]).unwrap(),
);
}
let dest = make_node_addr(0xDD);
let dest_coords = TreeCoordinate::from_addrs(vec![dest, fallback_hop, my_addr]).unwrap();
node.coord_cache_mut()
.insert(dest, dest_coords, Node::now_ms());
node.learn_reverse_route(dest, cycle_hop);
assert_ne!(
cycle_hop, previous_hop,
"cycle must span more than two hops"
);
assert_eq!(
node.find_next_hop(&dest).map(|peer| *peer.node_addr()),
Some(cycle_hop),
"fixture must select a learned hop that can continue a multi-hop cycle"
);
assert_eq!(
node.find_transit_next_hop(&dest, &previous_hop),
Some(fallback_hop),
"transit must reject a non-progressing learned hop and use the strict coordinate fallback"
);
}
#[test]
fn test_transit_drops_multi_hop_learned_cycle_without_progress_fallback() {
let mut config = Config::new();
config.node.routing.mode = RoutingMode::ReplyLearned;
let mut node = Node::new(config).unwrap();
let transport_id = TransportId::new(1);
let my_addr = *node.node_addr();
let previous_link = LinkId::new(1);
let (previous_conn, previous_id) =
make_completed_connection(&mut node, previous_link, transport_id, 1000);
let previous_hop = *previous_id.node_addr();
node.add_connection(previous_conn).unwrap();
node.promote_connection(previous_link, previous_id, 2000)
.unwrap();
let cycle_link = LinkId::new(2);
let (cycle_conn, cycle_id) =
make_completed_connection(&mut node, cycle_link, transport_id, 1000);
let cycle_hop = *cycle_id.node_addr();
node.add_connection(cycle_conn).unwrap();
node.promote_connection(cycle_link, cycle_id, 2000).unwrap();
for peer in [previous_hop, cycle_hop] {
node.tree_state_mut().update_peer(
ParentDeclaration::new(peer, my_addr, 1, 1000),
TreeCoordinate::from_addrs(vec![peer, my_addr]).unwrap(),
);
}
let dest = make_node_addr(0xDD);
let unavailable_progress_hop = make_node_addr(0xEE);
let dest_coords =
TreeCoordinate::from_addrs(vec![dest, unavailable_progress_hop, my_addr]).unwrap();
node.coord_cache_mut()
.insert(dest, dest_coords, Node::now_ms());
node.learn_reverse_route(dest, cycle_hop);
assert_ne!(cycle_hop, previous_hop);
assert!(
node.find_transit_next_hop(&dest, &previous_hop).is_none(),
"transit must drop rather than continue a multi-hop learned cycle when no strict coordinate fallback exists"
);
}
fn learned_leaf_handshake_fixture() -> (Node, NodeAddr, NodeAddr, NodeAddr, NodeAddr, TreeCoordinate)
{
let mut config = Config::new();
config.node.routing.mode = RoutingMode::ReplyLearned;
let mut node = Node::new(config).unwrap();
let transport_id = TransportId::new(1);
let my_addr = *node.node_addr();
let leaf_link = LinkId::new(1);
let (leaf_conn, leaf_id) = make_completed_connection(&mut node, leaf_link, transport_id, 1000);
let learned_leaf = *leaf_id.node_addr();
node.add_connection(leaf_conn).unwrap();
node.promote_connection(leaf_link, leaf_id, 2000).unwrap();
let progress_link = LinkId::new(2);
let (progress_conn, progress_id) =
make_completed_connection(&mut node, progress_link, transport_id, 1000);
let progress_hop = *progress_id.node_addr();
node.add_connection(progress_conn).unwrap();
node.promote_connection(progress_link, progress_id, 2000)
.unwrap();
for peer in [learned_leaf, progress_hop] {
node.tree_state_mut().update_peer(
ParentDeclaration::new(peer, my_addr, 1, 1000),
TreeCoordinate::from_addrs(vec![peer, my_addr]).unwrap(),
);
}
let dest = make_node_addr(0xDD);
let dest_coords = TreeCoordinate::from_addrs(vec![dest, progress_hop, my_addr]).unwrap();
node.coord_cache_mut()
.insert(dest, dest_coords.clone(), Node::now_ms());
node.learn_reverse_route(dest, learned_leaf);
(node, my_addr, learned_leaf, progress_hop, dest, dest_coords)
}
#[test]
fn test_session_ack_origin_uses_progress_route_instead_of_learned_leaf() {
let (mut node, my_addr, learned_leaf, progress_hop, dest, dest_coords) =
learned_leaf_handshake_fixture();
assert_eq!(
node.find_next_hop(&dest).map(|peer| *peer.node_addr()),
Some(learned_leaf),
"generic application routing should retain the learned path"
);
let ack = SessionAck::new(node.tree_state().my_coords().clone(), dest_coords.clone()).encode();
let mut datagram = SessionDatagram::new(my_addr, dest, ack);
let route = node
.resolve_session_datagram_runtime_route(&mut datagram)
.expect("handshake route");
assert_eq!(
route.next_hop_addr, progress_hop,
"handshake control must not originate through a non-progressing learned leaf"
);
let setup =
SessionSetup::new(node.tree_state().my_coords().clone(), dest_coords.clone()).encode();
let mut setup_datagram = SessionDatagram::new(my_addr, dest, setup);
assert_eq!(
node.resolve_session_datagram_runtime_route(&mut setup_datagram)
.expect("setup route")
.next_hop_addr,
progress_hop,
"msg1 must use strict progress when it carries usable destination coordinates"
);
let msg3 = SessionMsg3::new(vec![0; 73]).encode();
let mut msg3_datagram = SessionDatagram::new(my_addr, dest, msg3);
assert_eq!(
node.resolve_session_datagram_runtime_route(&mut msg3_datagram)
.expect("msg3 route")
.next_hop_addr,
progress_hop,
"msg3 must use the current cached destination coordinates"
);
let mut application_datagram = SessionDatagram::new(my_addr, dest, vec![0, 0, 0, 0]);
let application_route = node
.resolve_session_datagram_runtime_route(&mut application_datagram)
.expect("application route");
assert_eq!(
application_route.next_hop_addr, learned_leaf,
"established application data must retain adaptive learned routing"
);
node.coord_cache_mut().remove(&dest);
let unknown_dest_setup = SessionSetup::new(
node.tree_state().my_coords().clone(),
node.tree_state().my_coords().clone(),
)
.encode();
let mut unknown_dest_datagram = SessionDatagram::new(my_addr, dest, unknown_dest_setup);
let unknown_dest_route = node
.resolve_session_datagram_runtime_route(&mut unknown_dest_datagram)
.expect("unknown-coordinate setup route");
assert_eq!(
unknown_dest_route.next_hop_addr, learned_leaf,
"handshake setup must retain learned fallback when destination coordinates are unknown"
);
}
#[test]
fn test_session_ack_uses_current_cache_when_carried_root_is_stale() {
let (mut node, my_addr, _learned_leaf, progress_hop, dest, _dest_coords) =
learned_leaf_handshake_fixture();
let stale_root = make_node_addr(0xFA);
let stale_dest_coords = TreeCoordinate::from_addrs(vec![dest, progress_hop, stale_root])
.expect("stale-root coordinates");
assert_ne!(
stale_dest_coords.root_id(),
node.tree_state().my_coords().root_id(),
"fixture requires carried coordinates from an obsolete tree root"
);
let ack = SessionAck::new(node.tree_state().my_coords().clone(), stale_dest_coords).encode();
let mut datagram = SessionDatagram::new(my_addr, dest, ack);
let route = node
.resolve_session_datagram_runtime_route(&mut datagram)
.expect("current cached coordinates should recover stale carried coordinates");
assert_eq!(route.next_hop_addr, progress_hop);
}
#[test]
fn test_session_ack_does_not_fall_back_to_learned_route_without_progress_hop() {
let (mut node, my_addr, _learned_leaf, _progress_hop, dest, _dest_coords) =
learned_leaf_handshake_fixture();
let unavailable_progress_hop = make_node_addr(0xEE);
let blocked_dest_coords =
TreeCoordinate::from_addrs(vec![dest, unavailable_progress_hop, *node.node_addr()])
.expect("blocked destination coordinates");
node.coord_cache_mut()
.insert(dest, blocked_dest_coords.clone(), Node::now_ms());
let ack = SessionAck::new(node.tree_state().my_coords().clone(), blocked_dest_coords).encode();
let mut datagram = SessionDatagram::new(my_addr, dest, ack);
let error = node
.resolve_session_datagram_runtime_route(&mut datagram)
.expect_err("usable coordinates without a progress hop must not use learned fallback");
assert!(matches!(error, NodeError::SendFailed { .. }));
}
#[test]
fn test_routing_unknown_destination() {
let mut node = make_node();
let unknown = make_node_addr(99);
assert!(node.find_next_hop(&unknown).is_none());
}
#[test]
fn test_routing_bloom_filter_hit() {
let mut node = make_node();
let transport_id = TransportId::new(1);
let my_addr = *node.node_addr();
let link_id1 = LinkId::new(1);
let (conn1, id1) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
let peer1_addr = *id1.node_addr();
node.add_connection(conn1).unwrap();
node.promote_connection(link_id1, id1, 2000).unwrap();
let link_id2 = LinkId::new(2);
let (conn2, id2) = make_completed_connection(&mut node, link_id2, transport_id, 1000);
let peer2_addr = *id2.node_addr();
node.add_connection(conn2).unwrap();
node.promote_connection(link_id2, id2, 2000).unwrap();
let peer1_coords = TreeCoordinate::from_addrs(vec![peer1_addr, my_addr]).unwrap();
node.tree_state_mut().update_peer(
ParentDeclaration::new(peer1_addr, my_addr, 1, 1000),
peer1_coords,
);
let peer2_coords = TreeCoordinate::from_addrs(vec![peer2_addr, my_addr]).unwrap();
node.tree_state_mut().update_peer(
ParentDeclaration::new(peer2_addr, my_addr, 1, 1000),
peer2_coords,
);
let dest = make_node_addr(99);
let dest_coords = TreeCoordinate::from_addrs(vec![dest, peer1_addr, my_addr]).unwrap();
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
node.coord_cache_mut().insert(dest, dest_coords, now_ms);
let peer1 = node.get_peer_mut(&peer1_addr).unwrap();
let mut filter = BloomFilter::new();
filter.insert(&dest);
peer1.update_filter(filter, 1, 3000);
let result = node.find_next_hop(&dest);
assert!(result.is_some());
assert_eq!(result.unwrap().node_addr(), &peer1_addr);
assert_ne!(result.unwrap().node_addr(), &peer2_addr);
}
#[test]
fn test_routing_bloom_filter_multiple_hits_tiebreak() {
let mut node = make_node();
let transport_id = TransportId::new(1);
let my_addr = *node.node_addr();
let mut peer_addrs = Vec::new();
for i in 1..=3 {
let link_id = LinkId::new(i);
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
let addr = *id.node_addr();
peer_addrs.push(addr);
node.add_connection(conn).unwrap();
node.promote_connection(link_id, id, 2000).unwrap();
}
for &addr in &peer_addrs {
let coords = TreeCoordinate::from_addrs(vec![addr, my_addr]).unwrap();
node.tree_state_mut()
.update_peer(ParentDeclaration::new(addr, my_addr, 1, 1000), coords);
}
let dest = make_node_addr(99);
let dest_coords = TreeCoordinate::from_addrs(vec![dest, peer_addrs[0], my_addr]).unwrap();
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
node.coord_cache_mut().insert(dest, dest_coords, now_ms);
for &addr in &peer_addrs {
let peer = node.get_peer_mut(&addr).unwrap();
let mut filter = BloomFilter::new();
filter.insert(&dest);
peer.update_filter(filter, 1, 3000);
}
let result = node.find_next_hop(&dest);
assert!(result.is_some());
assert_eq!(result.unwrap().node_addr(), &peer_addrs[0]);
}