mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-06 19:43:10 +00:00
The forwarding handler resolves a next hop only for datagrams the routing core can forward, which keeps the coordinate-cache touch in that resolution scoped to genuine forwards. Its TTL test (ttl > 1) restated the core's drop (decrement, then drop at zero) in a different form, and no test could see the two disagree: reverting the pre-check to its older ttl != 0 form passed every unit test. ttl_after_hop now holds the rule. The routing core drops on it, SessionDatagram::can_forward uses it, and a new SessionDatagramRef::can_forward is what the handler calls. A unit test pins the function, a routing test checks the core and can_forward agree for every TTL, and a handler test checks that a last-hop transit datagram does not refresh the destination's cached coordinates while a ttl=2 one does.
559 lines
18 KiB
Rust
559 lines
18 KiB
Rust
//! Tests for the sans-IO routing decision core.
|
|
|
|
use super::util::{MockPeer, MockRoutingView, make_coords, make_datagram_ref, make_next_hop};
|
|
use crate::proto::link::{SessionDatagramRef, ttl_after_hop};
|
|
use crate::proto::routing::RoutingSignalType;
|
|
use crate::proto::routing::{
|
|
DropReason, LimitVerdict, RouteAction, RouteOutcome, Router, RoutingView, select_best_candidate,
|
|
};
|
|
use crate::testutil::make_node_addr;
|
|
use crate::{NodeAddr, TreeCoordinate};
|
|
|
|
/// Decode a forwarded byte buffer (which carries the leading msg_type byte)
|
|
/// back into a borrowed view so tests can inspect the re-encoded header.
|
|
fn decode_forward(bytes: &[u8]) -> SessionDatagramRef<'_> {
|
|
SessionDatagramRef::decode(&bytes[1..]).expect("forwarded datagram re-decodes")
|
|
}
|
|
|
|
fn choose_candidate(
|
|
rv: &MockRoutingView,
|
|
dest: &NodeAddr,
|
|
dest_coords: &TreeCoordinate,
|
|
my_coords: &TreeCoordinate,
|
|
) -> Option<NodeAddr> {
|
|
select_best_candidate(rv, dest, dest_coords, my_coords)
|
|
}
|
|
|
|
fn mock_peer(
|
|
addr: u8,
|
|
dest: NodeAddr,
|
|
may_reach: bool,
|
|
link_cost: f64,
|
|
coords: Option<&[u8]>,
|
|
) -> MockPeer {
|
|
MockPeer {
|
|
addr: make_node_addr(addr),
|
|
reach: may_reach.then_some(dest).into_iter().collect(),
|
|
link_cost,
|
|
coords: coords.map(make_coords),
|
|
}
|
|
}
|
|
|
|
/// A transit datagram that arrived already exhausted is dropped and charged
|
|
/// to `TtlExhausted`. A next hop is supplied so the drop is evidence of the
|
|
/// TTL gate rather than of an absent route.
|
|
#[test]
|
|
fn ttl_zero_drops_as_exhausted() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let dg = make_datagram_ref(0, make_node_addr(0x20));
|
|
let rv = MockRoutingView::new(false);
|
|
let out = router.route(
|
|
&dg,
|
|
&my_addr,
|
|
false,
|
|
Some(make_next_hop(make_node_addr(0x30), 1400)),
|
|
&rv,
|
|
);
|
|
assert!(matches!(
|
|
out,
|
|
RouteOutcome::Drop {
|
|
reason: DropReason::TtlExhausted
|
|
}
|
|
));
|
|
}
|
|
|
|
/// Acceptance: a datagram addressed to this node with ttl=0 is delivered
|
|
/// locally. The TTL governs forwarding, not delivery to the addressed host,
|
|
/// so the gate sits after the local-delivery test — and `TtlExhausted` is
|
|
/// not charged for a delivered datagram.
|
|
#[test]
|
|
fn ttl_zero_to_self_delivers_local() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let dg = make_datagram_ref(0, my_addr);
|
|
let rv = MockRoutingView::new(false);
|
|
let out = router.route(&dg, &my_addr, false, None, &rv);
|
|
assert!(
|
|
matches!(out, RouteOutcome::DeliverLocal),
|
|
"ttl=0 addressed to this node must still be delivered locally"
|
|
);
|
|
}
|
|
|
|
/// Acceptance: a transit datagram arriving with ttl=1 would leave with ttl=0,
|
|
/// so it is dropped here rather than transmitted. A next hop is supplied, so
|
|
/// a `Forward` outcome would mean it had been put on the wire at ttl=0.
|
|
#[test]
|
|
fn ttl_one_transit_drops_before_forwarding() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let dg = make_datagram_ref(1, make_node_addr(0x20));
|
|
let rv = MockRoutingView::new(false);
|
|
let out = router.route(
|
|
&dg,
|
|
&my_addr,
|
|
false,
|
|
Some(make_next_hop(make_node_addr(0x30), 1400)),
|
|
&rv,
|
|
);
|
|
assert!(
|
|
matches!(
|
|
out,
|
|
RouteOutcome::Drop {
|
|
reason: DropReason::TtlExhausted
|
|
}
|
|
),
|
|
"transit ttl=1 must be dropped as TTL-exhausted, not forwarded at ttl=0"
|
|
);
|
|
}
|
|
|
|
/// Acceptance: the other side of the same boundary — a transit datagram
|
|
/// arriving with ttl=2 clears the gate and leaves with ttl=1.
|
|
#[test]
|
|
fn ttl_two_transit_forwards_at_one() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let nh_addr = make_node_addr(0x30);
|
|
let dg = make_datagram_ref(2, make_node_addr(0x20));
|
|
let rv = MockRoutingView::new(false);
|
|
let out = router.route(
|
|
&dg,
|
|
&my_addr,
|
|
false,
|
|
Some(make_next_hop(nh_addr, 1400)),
|
|
&rv,
|
|
);
|
|
match out {
|
|
RouteOutcome::Forward { bytes, .. } => {
|
|
assert_eq!(
|
|
decode_forward(&bytes).ttl,
|
|
1,
|
|
"transit ttl=2 must leave with ttl=1"
|
|
);
|
|
}
|
|
_ => panic!("expected Forward"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn destination_is_self_delivers_local() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let dg = make_datagram_ref(5, my_addr);
|
|
let rv = MockRoutingView::new(false);
|
|
// A next hop is irrelevant for local delivery; the shell would pass None.
|
|
let out = router.route(&dg, &my_addr, false, None, &rv);
|
|
assert!(matches!(out, RouteOutcome::DeliverLocal));
|
|
}
|
|
|
|
#[test]
|
|
fn transit_without_next_hop_is_no_route() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let dg = make_datagram_ref(5, make_node_addr(0x20));
|
|
let rv = MockRoutingView::new(false);
|
|
let out = router.route(&dg, &my_addr, false, None, &rv);
|
|
assert!(matches!(out, RouteOutcome::NoRoute));
|
|
}
|
|
|
|
#[test]
|
|
fn forward_decrements_ttl_and_folds_link_mtu() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let nh_addr = make_node_addr(0x30);
|
|
let dg = make_datagram_ref(5, make_node_addr(0x20));
|
|
let rv = MockRoutingView::new(false);
|
|
let out = router.route(
|
|
&dg,
|
|
&my_addr,
|
|
false,
|
|
Some(make_next_hop(nh_addr, 1400)),
|
|
&rv,
|
|
);
|
|
match out {
|
|
RouteOutcome::Forward {
|
|
next_hop,
|
|
bytes,
|
|
outgoing_ce,
|
|
} => {
|
|
assert_eq!(next_hop, nh_addr);
|
|
assert!(!outgoing_ce);
|
|
let decoded = decode_forward(&bytes);
|
|
assert_eq!(decoded.ttl, 4, "TTL decremented once");
|
|
assert_eq!(decoded.path_mtu, 1400, "link MTU is the smaller bound");
|
|
}
|
|
_ => panic!("expected Forward"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn forward_keeps_smaller_datagram_path_mtu() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let nh_addr = make_node_addr(0x30);
|
|
let mut dg = make_datagram_ref(5, make_node_addr(0x20));
|
|
dg.path_mtu = 900; // datagram already bounded below the link MTU
|
|
let rv = MockRoutingView::new(false);
|
|
let out = router.route(
|
|
&dg,
|
|
&my_addr,
|
|
false,
|
|
Some(make_next_hop(nh_addr, 1400)),
|
|
&rv,
|
|
);
|
|
match out {
|
|
RouteOutcome::Forward { bytes, .. } => {
|
|
let decoded = decode_forward(&bytes);
|
|
assert_eq!(decoded.path_mtu, 900, "datagram MTU is the smaller bound");
|
|
}
|
|
_ => panic!("expected Forward"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn outgoing_ce_set_by_incoming_ce() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let nh_addr = make_node_addr(0x30);
|
|
let dg = make_datagram_ref(5, make_node_addr(0x20));
|
|
let rv = MockRoutingView::new(false); // not locally congested
|
|
let out = router.route(&dg, &my_addr, true, Some(make_next_hop(nh_addr, 1400)), &rv);
|
|
match out {
|
|
RouteOutcome::Forward { outgoing_ce, .. } => assert!(outgoing_ce),
|
|
_ => panic!("expected Forward"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn outgoing_ce_set_by_local_congestion() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let nh_addr = make_node_addr(0x30);
|
|
let dg = make_datagram_ref(5, make_node_addr(0x20));
|
|
let rv = MockRoutingView::new(true); // locally congested
|
|
let out = router.route(
|
|
&dg,
|
|
&my_addr,
|
|
false,
|
|
Some(make_next_hop(nh_addr, 1400)),
|
|
&rv,
|
|
);
|
|
match out {
|
|
RouteOutcome::Forward { outgoing_ce, .. } => assert!(outgoing_ce),
|
|
_ => panic!("expected Forward"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn outgoing_ce_clear_when_neither_signal() {
|
|
let mut router = Router::new();
|
|
let my_addr = make_node_addr(0x10);
|
|
let nh_addr = make_node_addr(0x30);
|
|
let dg = make_datagram_ref(5, make_node_addr(0x20));
|
|
let rv = MockRoutingView::new(false);
|
|
let out = router.route(
|
|
&dg,
|
|
&my_addr,
|
|
false,
|
|
Some(make_next_hop(nh_addr, 1400)),
|
|
&rv,
|
|
);
|
|
match out {
|
|
RouteOutcome::Forward { outgoing_ce, .. } => assert!(!outgoing_ce),
|
|
_ => panic!("expected Forward"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cached_coords_reads_the_view_table() {
|
|
let target = make_node_addr(0x40);
|
|
let rv = MockRoutingView {
|
|
congested: false,
|
|
coords: vec![(target, TreeCoordinate::root(target))],
|
|
peers: Vec::new(),
|
|
};
|
|
assert!(rv.cached_coords(&target, 0).is_some());
|
|
assert!(rv.cached_coords(&make_node_addr(0x41), 0).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_selection_is_independent_of_peer_enumeration_order() {
|
|
let dest = make_node_addr(0x50);
|
|
let root = 0x00;
|
|
let my_coords = make_coords(&[0x10, root]);
|
|
let dest_coords = make_coords(&[0x50, root]);
|
|
let lower_addr = mock_peer(0x20, dest, true, 1.0, Some(&[root]));
|
|
let higher_addr = mock_peer(0x30, dest, true, 1.0, Some(&[root]));
|
|
|
|
let forward = MockRoutingView {
|
|
peers: vec![lower_addr.clone(), higher_addr.clone()],
|
|
..MockRoutingView::new(false)
|
|
};
|
|
let reverse = MockRoutingView {
|
|
peers: vec![higher_addr, lower_addr],
|
|
..MockRoutingView::new(false)
|
|
};
|
|
|
|
assert_eq!(
|
|
choose_candidate(&forward, &dest, &dest_coords, &my_coords),
|
|
Some(make_node_addr(0x20))
|
|
);
|
|
assert_eq!(
|
|
choose_candidate(&reverse, &dest, &dest_coords, &my_coords),
|
|
Some(make_node_addr(0x20))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_selection_filters_bloom_and_missing_coords() {
|
|
let dest = make_node_addr(0x50);
|
|
let root = 0x00;
|
|
let my_coords = make_coords(&[0x10, root]);
|
|
let dest_coords = make_coords(&[0x50, root]);
|
|
let eligible = make_node_addr(0x60);
|
|
let rv = MockRoutingView {
|
|
peers: vec![
|
|
mock_peer(0x02, dest, true, 0.0, None),
|
|
mock_peer(0x03, dest, false, 0.0, Some(&[0x50, root])),
|
|
mock_peer(0x60, dest, true, 10.0, Some(&[root])),
|
|
],
|
|
..MockRoutingView::new(false)
|
|
};
|
|
|
|
assert_eq!(
|
|
choose_candidate(&rv, &dest, &dest_coords, &my_coords),
|
|
Some(eligible)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_must_be_strictly_closer_than_self() {
|
|
let dest = make_node_addr(0x50);
|
|
let root = 0x00;
|
|
let my_coords = make_coords(&[0x10, root]);
|
|
let dest_coords = make_coords(&[0x50, root]);
|
|
let rv = MockRoutingView {
|
|
peers: vec![
|
|
// A sibling is exactly as far from dest as this node.
|
|
mock_peer(0x20, dest, true, 1.0, Some(&[0x20, root])),
|
|
// This descendant of a sibling is farther from dest.
|
|
mock_peer(0x21, dest, true, 0.5, Some(&[0x21, 0x20, root])),
|
|
],
|
|
..MockRoutingView::new(false)
|
|
};
|
|
|
|
assert_eq!(choose_candidate(&rv, &dest, &dest_coords, &my_coords), None);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_ordering_is_cost_then_distance_then_address() {
|
|
let dest = make_node_addr(0x50);
|
|
let root = 0x00;
|
|
let my_coords = make_coords(&[0x10, 0x11, root]);
|
|
let dest_coords = make_coords(&[0x50, root]);
|
|
let rv = MockRoutingView {
|
|
peers: vec![
|
|
// Lowest address loses because distance precedes address.
|
|
mock_peer(0x01, dest, true, 1.0, Some(&[root])),
|
|
// Closest peer loses because cost is the primary key.
|
|
mock_peer(0x02, dest, true, 1.0, Some(&[0x50, root])),
|
|
mock_peer(0x04, dest, true, 0.5, Some(&[root])),
|
|
// Same cost and distance: lower address wins.
|
|
mock_peer(0x03, dest, true, 0.5, Some(&[root])),
|
|
],
|
|
..MockRoutingView::new(false)
|
|
};
|
|
|
|
assert_eq!(
|
|
choose_candidate(&rv, &dest, &dest_coords, &my_coords),
|
|
Some(make_node_addr(0x03))
|
|
);
|
|
}
|
|
|
|
/// Extract the error-PDU msg_type byte from an encoded routing-error action.
|
|
/// The action bytes are a SessionDatagram (leading link msg_type byte, then
|
|
/// the header); its payload is the error PDU, whose msg_type sits at offset 4
|
|
/// after the 4-byte FSP prefix.
|
|
fn error_pdu_type(action: &RouteAction) -> u8 {
|
|
let RouteAction::SendError { bytes, .. } = action;
|
|
let dg = SessionDatagramRef::decode(&bytes[1..]).expect("error datagram re-decodes");
|
|
dg.payload[4]
|
|
}
|
|
|
|
#[test]
|
|
fn synth_uses_pathbroken_when_coords_cached() {
|
|
let mut router = Router::new();
|
|
let dest = make_node_addr(0x20);
|
|
let source = make_node_addr(0x21);
|
|
let my_addr = make_node_addr(0x10);
|
|
let rv = MockRoutingView {
|
|
congested: false,
|
|
coords: vec![(dest, TreeCoordinate::root(dest))],
|
|
peers: Vec::new(),
|
|
};
|
|
let action = router
|
|
.synth_routing_error(&dest, &source, &my_addr, &rv, 0, 64)
|
|
.action
|
|
.expect("gate passes on first call");
|
|
let RouteAction::SendError { toward, .. } = &action;
|
|
assert_eq!(
|
|
*toward, source,
|
|
"error routes back toward the failed source"
|
|
);
|
|
assert_eq!(
|
|
error_pdu_type(&action),
|
|
RoutingSignalType::PathBroken.to_byte(),
|
|
"cached coords select PathBroken",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn synth_uses_coords_required_when_not_cached() {
|
|
let mut router = Router::new();
|
|
let dest = make_node_addr(0x20);
|
|
let source = make_node_addr(0x21);
|
|
let my_addr = make_node_addr(0x10);
|
|
let rv = MockRoutingView::new(false); // empty coord table
|
|
let action = router
|
|
.synth_routing_error(&dest, &source, &my_addr, &rv, 0, 64)
|
|
.action
|
|
.expect("gate passes on first call");
|
|
assert_eq!(
|
|
error_pdu_type(&action),
|
|
RoutingSignalType::CoordsRequired.to_byte(),
|
|
"absent coords select CoordsRequired",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn synth_rate_limit_gate_suppresses_second_call() {
|
|
let mut router = Router::new();
|
|
let dest = make_node_addr(0x20);
|
|
let source = make_node_addr(0x21);
|
|
let my_addr = make_node_addr(0x10);
|
|
let rv = MockRoutingView::new(false);
|
|
// First call for this destination passes the gate.
|
|
let first = router.synth_routing_error(&dest, &source, &my_addr, &rv, 0, 64);
|
|
assert!(first.action.is_some());
|
|
assert_eq!(first.verdict, LimitVerdict::Admit);
|
|
// An immediate second call for the same destination is within the
|
|
// rate-limit window and is suppressed (no sleeps needed — the two calls
|
|
// are microseconds apart, well under the 100 ms interval).
|
|
let second = router.synth_routing_error(&dest, &source, &my_addr, &rv, 0, 64);
|
|
assert!(second.action.is_none());
|
|
assert_eq!(
|
|
second.verdict,
|
|
LimitVerdict::Suppress,
|
|
"the shell counts emit_over_dest_interval off this verdict"
|
|
);
|
|
// A different destination is independent and still allowed.
|
|
let other = make_node_addr(0x22);
|
|
let third = router.synth_routing_error(&other, &source, &my_addr, &rv, 0, 64);
|
|
assert!(third.action.is_some());
|
|
assert_eq!(third.verdict, LimitVerdict::Admit);
|
|
}
|
|
|
|
/// Extract the bottleneck MTU (trailing u16 LE) from an MtuExceeded action's
|
|
/// PDU. Layout after the outer link byte + SessionDatagram header: FSP prefix
|
|
/// (4) + msg_type (1) + flags (1) + dest_addr (16) + reporter (16) + mtu (2).
|
|
fn mtu_exceeded_bottleneck(action: &RouteAction) -> u16 {
|
|
let RouteAction::SendError { bytes, .. } = action;
|
|
let dg = SessionDatagramRef::decode(&bytes[1..]).expect("error datagram re-decodes");
|
|
let p = dg.payload;
|
|
u16::from_le_bytes([p[38], p[39]])
|
|
}
|
|
|
|
#[test]
|
|
fn synth_mtu_exceeded_carries_bottleneck_and_targets_source() {
|
|
let mut router = Router::new();
|
|
let dest = make_node_addr(0x20);
|
|
let source = make_node_addr(0x21);
|
|
let my_addr = make_node_addr(0x10);
|
|
let action = router
|
|
.synth_mtu_exceeded(&dest, &source, &my_addr, 1280, 0, 64)
|
|
.action
|
|
.expect("gate passes on first call");
|
|
let RouteAction::SendError { toward, .. } = &action;
|
|
assert_eq!(
|
|
*toward, source,
|
|
"signal routes back toward the failed source"
|
|
);
|
|
assert_eq!(
|
|
error_pdu_type(&action),
|
|
RoutingSignalType::MtuExceeded.to_byte(),
|
|
"PDU is an MtuExceeded signal",
|
|
);
|
|
assert_eq!(
|
|
mtu_exceeded_bottleneck(&action),
|
|
1280,
|
|
"bottleneck MTU is carried verbatim",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn synth_mtu_exceeded_rate_limit_gate_suppresses_second_call() {
|
|
let mut router = Router::new();
|
|
let dest = make_node_addr(0x20);
|
|
let source = make_node_addr(0x21);
|
|
let my_addr = make_node_addr(0x10);
|
|
// First call for this destination passes the gate.
|
|
let first = router.synth_mtu_exceeded(&dest, &source, &my_addr, 1280, 0, 64);
|
|
assert!(first.action.is_some());
|
|
assert_eq!(first.verdict, LimitVerdict::Admit);
|
|
// Immediate second call for the same destination is suppressed.
|
|
let second = router.synth_mtu_exceeded(&dest, &source, &my_addr, 1280, 0, 64);
|
|
assert!(second.action.is_none());
|
|
assert_eq!(
|
|
second.verdict,
|
|
LimitVerdict::Suppress,
|
|
"the shell counts emit_over_dest_interval off this verdict"
|
|
);
|
|
// A different destination is independent and still allowed.
|
|
let other = make_node_addr(0x22);
|
|
let third = router.synth_mtu_exceeded(&other, &source, &my_addr, 1280, 0, 64);
|
|
assert!(third.action.is_some());
|
|
assert_eq!(third.verdict, LimitVerdict::Admit);
|
|
}
|
|
|
|
/// The routing core's hop-limit drop and the shell's `can_forward` pre-check
|
|
/// agree for every TTL: a transit datagram with a next hop is dropped as
|
|
/// TTL-exhausted exactly when `can_forward` is false, and otherwise leaves
|
|
/// with the TTL `ttl_after_hop` gives.
|
|
#[test]
|
|
fn route_drops_for_hop_limit_exactly_when_can_forward_is_false() {
|
|
let my_addr = make_node_addr(0x10);
|
|
let nh_addr = make_node_addr(0x30);
|
|
let rv = MockRoutingView::new(false);
|
|
for ttl in 0..=u8::MAX {
|
|
let mut router = Router::new();
|
|
let dg = make_datagram_ref(ttl, make_node_addr(0x20));
|
|
let out = router.route(
|
|
&dg,
|
|
&my_addr,
|
|
false,
|
|
Some(make_next_hop(nh_addr, 1400)),
|
|
&rv,
|
|
);
|
|
match out {
|
|
RouteOutcome::Drop {
|
|
reason: DropReason::TtlExhausted,
|
|
} => assert!(
|
|
!dg.can_forward(),
|
|
"ttl={ttl}: the core dropped a datagram the pre-check would forward"
|
|
),
|
|
RouteOutcome::Forward { bytes, .. } => {
|
|
assert!(
|
|
dg.can_forward(),
|
|
"ttl={ttl}: the core forwarded a datagram the pre-check would not"
|
|
);
|
|
assert_eq!(
|
|
Some(decode_forward(&bytes).ttl),
|
|
ttl_after_hop(ttl),
|
|
"ttl={ttl}: the forwarded TTL must be the shared rule's"
|
|
);
|
|
}
|
|
_ => panic!("ttl={ttl}: expected Drop(TtlExhausted) or Forward"),
|
|
}
|
|
}
|
|
}
|