node: classify transit forwards by route class; regroup fipstop routing tab

Add six forwarding counters that partition transit-forwarded packets by their
tree relationship to the chosen next hop: tree-up (peer is our ancestor),
tree-down (peer is our descendant and the destination is within its subtree),
tree-down-cross (peer is our descendant but the destination is outside its
subtree), cross-link descend (lateral peer, destination within its subtree),
cross-link ascend (lateral peer, destination outside its subtree), and
direct-peer. The six classes sum to forwarded_packets, asserted by a unit
test. Classification is computed from tree coordinates at the transit
chokepoint, so the error-signal routing callers are excluded.

The two "outside the chosen peer's subtree" classes are both up-and-over
forwards but differ in what they depend on. Tree-down-cross is the
dive-to-tree-child cut-through: we forward down to our own child for a
destination not beneath it, which is only possible because the child
advertised cross-link reach upward to us, beyond its own subtree. Its count
measures how much forwarding depends on that upward advertisement, i.e. what
would change if cross-link advertisements were narrowed to subtree-entry only.
Cross-link ascend, by contrast, uses the node's own lateral cross-link learned
from a peer's split-horizon advertisement, so it does not depend on any upward
advertisement.

Surface the counters through the forwarding stats snapshot (control socket,
show_routing and show_status) and reorganize the fipstop routing tab so its
two columns separate own/endpoint traffic (received, delivered, originated)
from forwarded/transit traffic (the route-class breakdown and drop reasons),
with the tree-down-cross line visually flagged.
This commit is contained in:
Johnathan Corgan
2026-06-18 18:19:55 +00:00
parent 0f1fd18c25
commit 274b09d4ff
10 changed files with 538 additions and 39 deletions

View File

@@ -83,6 +83,35 @@ fn fwd_value(data: &serde_json::Value, pkt_key: &str, byte_key: &str) -> String
format!("{} pkts ({})", pkts, helpers::format_bytes(bytes))
}
/// Read a raw forwarding counter as a u64 (0 if missing), for arithmetic
/// (percentages, derived totals) that the string-returning helpers can't do.
fn fwd_count(data: &serde_json::Value, key: &str) -> u64 {
data.get("forwarding")
.and_then(|f| f.get(key))
.and_then(|v| v.as_u64())
.unwrap_or(0)
}
/// Total mesh egress = locally-originated + transit-forwarded, formatted as
/// "N pkts (B)". There is no single daemon counter for everything this node
/// transmits to peers, so it is derived from its two contributors.
fn mesh_tx_value(data: &serde_json::Value) -> String {
let pkts = fwd_count(data, "originated_packets") + fwd_count(data, "forwarded_packets");
let bytes = fwd_count(data, "originated_bytes") + fwd_count(data, "forwarded_bytes");
format!("{} pkts ({})", pkts, helpers::format_bytes(bytes))
}
/// Format a route-class count as "N (xx.x%)" where the percentage is the class's
/// share of total forwarded (transit) packets. Zero forwarded yields "0.0%".
fn route_class_value(count: u64, total_forwarded: u64) -> String {
let pct = if total_forwarded > 0 {
count as f64 / total_forwarded as f64 * 100.0
} else {
0.0
};
format!("{count} ({pct:.1}%)")
}
/// Build a section: a styled header line followed by the kv pairs rendered
/// through the group helper so the section's values share a left edge.
fn section(title: &str, pairs: &[(&str, String)]) -> Vec<Line<'static>> {
@@ -110,49 +139,39 @@ fn draw_routing_stats(
let err = |key: &str| helpers::nested_u64(data, "error_signals", key);
let cong = |key: &str| helpers::nested_u64(data, "congestion", key);
// Left column: Forwarding + Discovery. Each section's values share a left
// edge via the kv_lines group helper.
// The node is an interface adapter between the local host stack and the
// mesh; the left column reads each side as a Transmitted/Received pair.
//
// Local Stack — traffic crossing the TUN / local-origination boundary:
// Transmitted is what the host injects into the mesh (originated), Received
// is what the mesh hands up to the host (delivered).
let mut left = section(
"Forwarding",
"Local Stack",
&[
(
"Transmitted",
fwd_value(data, "originated_packets", "originated_bytes"),
),
(
"Received",
fwd_value(data, "delivered_packets", "delivered_bytes"),
),
],
);
left.push(Line::from(""));
// Mesh — traffic crossing the peer-link boundary: Transmitted is everything
// this node puts on the wire (originated + forwarded, derived), Received is
// the ingress aggregate from peers (own-delivered + transit + drops).
left.extend(section(
"Mesh",
&[
("Transmitted", mesh_tx_value(data)),
(
"Received",
fwd_value(data, "received_packets", "received_bytes"),
),
(
"Delivered",
fwd_value(data, "delivered_packets", "delivered_bytes"),
),
(
"Forwarded",
fwd_value(data, "forwarded_packets", "forwarded_bytes"),
),
(
"Originated",
fwd_value(data, "originated_packets", "originated_bytes"),
),
(
"Decode Error",
fwd_value(data, "decode_error_packets", "decode_error_bytes"),
),
(
"TTL Exhausted",
fwd_value(data, "ttl_exhausted_packets", "ttl_exhausted_bytes"),
),
(
"No Route",
fwd_value(data, "drop_no_route_packets", "drop_no_route_bytes"),
),
(
"MTU Exceeded",
fwd_value(data, "drop_mtu_exceeded_packets", "drop_mtu_exceeded_bytes"),
),
(
"Send Error",
fwd_value(data, "drop_send_error_packets", "drop_send_error_bytes"),
),
],
);
));
left.push(Line::from(""));
left.extend(section(
"Discovery Requests",
@@ -184,15 +203,89 @@ fn draw_routing_stats(
],
));
// Right column: Error Signals + Congestion
// Right column — "Forwarded" (transit / routed through this node).
// Forwarded total, then the route-class breakdown (a percentage partition
// of the total), then the transit-path drop reasons.
let fwd_total = fwd_count(data, "forwarded_packets");
let mut right = section(
"Forwarded",
&[(
"Forwarded",
fwd_value(data, "forwarded_packets", "forwarded_bytes"),
)],
);
// Blank separator after the Forwarded total, matching the spacing between
// every other section pair; the total and its route-class breakdown read
// as two distinct groups.
right.push(Line::from(""));
// Route-class breakdown: a partition of Forwarded, each line annotated with
// its share of the total. Tree-down cross — the dive-to-tree-child
// cut-through — is the last class; Tree-down + Tree-down cross sum to the
// pre-split tree-down total.
right.extend(section(
"Route Class",
&[
(
"Direct Peer",
route_class_value(fwd_count(data, "route_direct_peer"), fwd_total),
),
(
"Tree-down",
route_class_value(fwd_count(data, "route_tree_down"), fwd_total),
),
(
"Tree-up",
route_class_value(fwd_count(data, "route_tree_up"), fwd_total),
),
(
"Cross-link descend",
route_class_value(fwd_count(data, "route_crosslink_descend"), fwd_total),
),
(
"Cross-link ascend",
route_class_value(fwd_count(data, "route_crosslink_ascend"), fwd_total),
),
(
"Tree-down cross",
route_class_value(fwd_count(data, "route_tree_down_cross"), fwd_total),
),
],
));
right.push(Line::from(""));
right.extend(section(
"Dropped",
&[
(
"No Route",
fwd_value(data, "drop_no_route_packets", "drop_no_route_bytes"),
),
(
"TTL Exhausted",
fwd_value(data, "ttl_exhausted_packets", "ttl_exhausted_bytes"),
),
(
"Decode Error",
fwd_value(data, "decode_error_packets", "decode_error_bytes"),
),
(
"MTU Exceeded",
fwd_value(data, "drop_mtu_exceeded_packets", "drop_mtu_exceeded_bytes"),
),
(
"Send Error",
fwd_value(data, "drop_send_error_packets", "drop_send_error_bytes"),
),
],
));
right.push(Line::from(""));
right.extend(section(
"Error Signals",
&[
("Coords Required", err("coords_required")),
("Path Broken", err("path_broken")),
("MTU Exceeded", err("mtu_exceeded")),
],
);
));
right.push(Line::from(""));
right.extend(section(
"Congestion",

View File

@@ -1134,7 +1134,12 @@ fn routing_focused_pane_scrolls() {
let mut app1 = app_with(Tab::Routing, data);
app1.data.insert(Tab::Cache, json!({}));
app1.focused_pane.insert(Tab::Routing, 2);
app1.scroll_offsets.insert((Tab::Routing, 2), 6);
// Congestion is the last section of the right ("Forwarded") column, below
// the route-class breakdown, Dropped, and Error Signals groups. The left
// column is the taller of the two, so scrolling fully to the bottom would
// over-scroll the right column past Congestion; this offset lands the
// Congestion region inside the short window instead.
app1.scroll_offsets.insert((Tab::Routing, 2), 24);
let buf1 = testkit::render(100, 20, |frame, area| {
super::routing::draw(frame, &app1, area);
});

View File

@@ -53,6 +53,12 @@
"originated_packets": 0,
"received_bytes": 0,
"received_packets": 0,
"route_crosslink_ascend": 0,
"route_crosslink_descend": 0,
"route_direct_peer": 0,
"route_tree_down": 0,
"route_tree_down_cross": 0,
"route_tree_up": 0,
"ttl_exhausted_bytes": 0,
"ttl_exhausted_packets": 0
},

View File

@@ -22,6 +22,12 @@
"originated_packets": 0,
"received_bytes": 0,
"received_packets": 0,
"route_crosslink_ascend": 0,
"route_crosslink_descend": 0,
"route_direct_peer": 0,
"route_tree_down": 0,
"route_tree_down_cross": 0,
"route_tree_up": 0,
"ttl_exhausted_bytes": 0,
"ttl_exhausted_packets": 0
},

View File

@@ -151,6 +151,11 @@ impl Node {
}
} else {
self.metrics().forwarding.record_forwarded(encoded.len());
// Classify this transit forward by route class (partition of
// forwarded_packets). Done here, at the data-plane chokepoint, so
// the error-signal routing callers of find_next_hop are excluded.
let class = self.classify_forward(&datagram.dest_addr, &next_hop_addr);
self.metrics().forwarding.record_route_class(class);
if outgoing_ce {
self.metrics().congestion.ce_forwarded.inc();
}

View File

@@ -81,6 +81,51 @@ pub struct ForwardingMetrics {
pub drop_send_error_bytes: Counter,
pub originated_packets: Counter,
pub originated_bytes: Counter,
pub route_tree_up: Counter,
pub route_tree_down: Counter,
pub route_tree_down_cross: Counter,
pub route_crosslink_descend: Counter,
pub route_crosslink_ascend: Counter,
pub route_direct_peer: Counter,
}
/// Route class of a transit-forwarded packet, classified from tree
/// coordinates at the forwarding decision point. The six variants
/// partition `forwarded_packets` exactly.
///
/// Two variants are up-and-over forwards (destination not in the chosen
/// peer's subtree); they differ in whether they depend on a child
/// advertising cross-link reach *upward* to its parent:
/// - `TreeDownCross`: the chosen peer is our tree descendant, but the
/// destination is *not* in that child's subtree. The forward only fired
/// because the child advertised cross-link reach upward to us, beyond its
/// own subtree. If children advertised only their subtree upward, this
/// forward would route up instead, so its count measures how much
/// forwarding depends on the upward cross-link advertisement — the
/// dive-to-tree-child cut-through.
/// - `CrosslinkAscend`: the chosen peer is lateral (neither ancestor nor
/// descendant) and the destination is not in its subtree. This is a node
/// using its *own* cross-link, learned via the peer's split-horizon
/// advertisement to its neighbors, so it does not depend on any upward
/// advertisement. Tracked alongside `TreeDownCross` as the lateral
/// up-and-over contrast.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouteClass {
/// Chosen peer is our ancestor (tree-up).
TreeUp,
/// Chosen peer is our descendant and dest is in its subtree (canonical
/// tree-down).
TreeDown,
/// Chosen peer is our descendant but dest is *not* in its subtree: the
/// dive-to-tree-child cut-through enabled by upward cross-link
/// advertisement.
TreeDownCross,
/// Chosen peer is lateral and dest is in its subtree (subtree entry).
CrosslinkDescend,
/// Chosen peer is lateral and dest is not in its subtree (up-and-over).
CrosslinkAscend,
/// Chosen peer is the destination itself (degenerate direct hop).
DirectPeer,
}
impl ForwardingMetrics {
@@ -112,6 +157,21 @@ impl ForwardingMetrics {
self.originated_bytes.add(bytes as u64);
}
/// Record the route class of a transit-forwarded packet. The five
/// classes partition `forwarded_packets`, so this is called exactly
/// once per `record_forwarded` (transit chokepoint only).
#[inline]
pub fn record_route_class(&self, class: RouteClass) {
match class {
RouteClass::TreeUp => self.route_tree_up.inc(),
RouteClass::TreeDown => self.route_tree_down.inc(),
RouteClass::TreeDownCross => self.route_tree_down_cross.inc(),
RouteClass::CrosslinkDescend => self.route_crosslink_descend.inc(),
RouteClass::CrosslinkAscend => self.route_crosslink_ascend.inc(),
RouteClass::DirectPeer => self.route_direct_peer.inc(),
}
}
/// Mirror of `ForwardingStats::record_reject_bytes`: route a typed
/// forwarding rejection of `bytes` payload to its packet and byte
/// counters.
@@ -163,6 +223,12 @@ impl ForwardingMetrics {
drop_send_error_bytes: self.drop_send_error_bytes.get(),
originated_packets: self.originated_packets.get(),
originated_bytes: self.originated_bytes.get(),
route_tree_up: self.route_tree_up.get(),
route_tree_down: self.route_tree_down.get(),
route_tree_down_cross: self.route_tree_down_cross.get(),
route_crosslink_descend: self.route_crosslink_descend.get(),
route_crosslink_ascend: self.route_crosslink_ascend.get(),
route_direct_peer: self.route_direct_peer.get(),
}
}
}

View File

@@ -2666,6 +2666,86 @@ impl Node {
self.peers.get(&next_hop_id).filter(|p| p.can_send())
}
/// Classify a transit forward by route class from tree coordinates.
///
/// Called at the transit chokepoint after `find_next_hop` returns a peer,
/// so the six classes partition `forwarded_packets` exactly. The branch
/// that `find_next_hop` took (bloom vs greedy-tree) is *not* the route
/// class: a peer can be selected by either, so the cut-through splits
/// (`TreeDownCross`, `CrosslinkAscend`) are decided here from coordinates,
/// not from which branch fired.
///
/// Inputs: our coords (`tree_state.my_coords`), the chosen peer's coords
/// (`tree_state.peer_coords`), and the destination coords (re-read from the
/// coord cache, which `find_next_hop` just touched). Both the tree-down and
/// cross-link branches split on whether the destination is in the chosen
/// peer's subtree; when the dest coords are unavailable that test defaults
/// to "not in subtree", i.e. the up-and-over variant (`TreeDownCross` for a
/// descendant peer, `CrosslinkAscend` for a lateral one).
pub(crate) fn classify_forward(
&self,
dest: &NodeAddr,
chosen_peer: &NodeAddr,
) -> metrics::RouteClass {
// Degenerate: the next hop is the destination itself (Branch 2).
if chosen_peer == dest {
return metrics::RouteClass::DirectPeer;
}
let my_addr = self.node_addr();
let my_coords = self.tree_state.my_coords();
// Tree-up: the chosen peer is our ancestor.
if my_coords.has_ancestor(chosen_peer) {
return metrics::RouteClass::TreeUp;
}
// Whether the destination is in the chosen peer's subtree. Both the
// tree-down and cross-link splits below turn on this same test, so it
// is computed once. On the live transit path the dest coords are
// always present here: `find_next_hop` looks them up with an early
// return, so a coord-cache miss yields no next hop to classify (the
// caller signals `CoordsRequired` instead of forwarding). The miss
// branch below is therefore defensive — reachable only by direct
// unit-test calls — and defaults the test to "not in subtree", i.e.
// the up-and-over variant of whichever branch fires (TreeDownCross for
// a descendant peer, CrosslinkAscend for a lateral one), matching the
// original cross-link default-to-ascend.
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let dest_in_peer_subtree = self
.coord_cache
.get(dest, now_ms)
.is_some_and(|dest_coords| dest_coords.has_ancestor(chosen_peer));
// Tree-down: the chosen peer is our descendant (we are its ancestor).
// Split by subtree membership: a dest genuinely below the child is the
// canonical tree-down; a dest *not* below it means we only forwarded
// down because the child advertised cross-link reach upward, beyond its
// own subtree — the dive-to-tree-child cut-through (TreeDownCross).
if let Some(peer_coords) = self.tree_state.peer_coords(chosen_peer)
&& peer_coords.has_ancestor(my_addr)
{
return if dest_in_peer_subtree {
metrics::RouteClass::TreeDown
} else {
metrics::RouteClass::TreeDownCross
};
}
// Cross-link (lateral): split by whether the destination is in the
// chosen peer's subtree. Descend = subtree entry; ascend = up-and-over
// via the node's own cross-link (learned from the peer's split-horizon
// advertisement, independent of any upward advertisement).
if dest_in_peer_subtree {
return metrics::RouteClass::CrosslinkDescend;
}
metrics::RouteClass::CrosslinkAscend
}
/// Select the best peer from a set of bloom filter candidates.
///
/// Uses distance from each candidate's tree coordinates to the destination

View File

@@ -229,6 +229,12 @@ pub struct ForwardingStatsSnapshot {
pub drop_send_error_bytes: u64,
pub originated_packets: u64,
pub originated_bytes: u64,
pub route_tree_up: u64,
pub route_tree_down: u64,
pub route_tree_down_cross: u64,
pub route_crosslink_descend: u64,
pub route_crosslink_ascend: u64,
pub route_direct_peer: u64,
}
#[derive(Clone, Debug, Default, Serialize)]

View File

@@ -1110,3 +1110,226 @@ async fn test_routing_source_only_coords_100_nodes() {
cleanup_nodes(&mut nodes).await;
}
// === Route-class classification (transit-forward partition) ===
use crate::node::metrics::{ForwardingMetrics, RouteClass};
/// Current epoch millis, matching the cache-insert idiom used above.
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[test]
fn test_classify_forward_tree_up() {
// my_coords = [me, parent, root]; the chosen peer is our parent (an
// ancestor in our path) → tree-up.
let mut node = make_node();
let me = *node.node_addr();
let parent = make_node_addr(10);
let root = make_node_addr(1);
node.tree_state_mut()
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, parent, root]).unwrap());
// Destination somewhere above us; routed via the parent.
let dest = make_node_addr(50);
node.coord_cache_mut().insert(
dest,
TreeCoordinate::from_addrs(vec![dest, root]).unwrap(),
now_ms(),
);
assert_eq!(
node.classify_forward(&dest, &parent),
RouteClass::TreeUp,
"chosen peer is our ancestor"
);
}
#[test]
fn test_classify_forward_tree_down() {
// Chosen peer is our descendant: its coords name us as an ancestor.
let mut node = make_node();
let me = *node.node_addr();
let root = make_node_addr(1);
node.tree_state_mut()
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
let child = make_node_addr(20);
node.tree_state_mut().update_peer(
ParentDeclaration::new(child, me, 1, 1000),
TreeCoordinate::from_addrs(vec![child, me, root]).unwrap(),
);
// Destination below the child; routed down to it.
let dest = make_node_addr(60);
node.coord_cache_mut().insert(
dest,
TreeCoordinate::from_addrs(vec![dest, child, me, root]).unwrap(),
now_ms(),
);
assert_eq!(
node.classify_forward(&dest, &child),
RouteClass::TreeDown,
"chosen peer is our descendant, dest in its subtree"
);
}
#[test]
fn test_classify_forward_tree_down_cross() {
// Chosen peer is our descendant (a tree child), but the destination is NOT
// in that child's subtree: we are diving down to the child only because it
// advertised cross-link reach upward, beyond its own subtree. This is the
// dive-to-tree-child cut-through.
let mut node = make_node();
let me = *node.node_addr();
let root = make_node_addr(1);
node.tree_state_mut()
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
let child = make_node_addr(20);
node.tree_state_mut().update_peer(
ParentDeclaration::new(child, me, 1, 1000),
TreeCoordinate::from_addrs(vec![child, me, root]).unwrap(),
);
// Destination lives elsewhere (directly under root), NOT under the child;
// reachable from the child only via a cross-link.
let dest = make_node_addr(60);
node.coord_cache_mut().insert(
dest,
TreeCoordinate::from_addrs(vec![dest, root]).unwrap(),
now_ms(),
);
assert_eq!(
node.classify_forward(&dest, &child),
RouteClass::TreeDownCross,
"descendant peer, dest not in its subtree (dive-to-tree-child cut-through)"
);
}
#[test]
fn test_classify_forward_crosslink_descend() {
// Chosen peer is lateral (not in our path, we are not in its path) and the
// destination is inside the peer's subtree → cross-link descend.
let mut node = make_node();
let me = *node.node_addr();
let root = make_node_addr(1);
let sibling_parent = make_node_addr(2);
node.tree_state_mut()
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
let peer = make_node_addr(30);
node.tree_state_mut().update_peer(
ParentDeclaration::new(peer, sibling_parent, 1, 1000),
TreeCoordinate::from_addrs(vec![peer, sibling_parent, root]).unwrap(),
);
// Destination is under the cross-link peer.
let dest = make_node_addr(70);
node.coord_cache_mut().insert(
dest,
TreeCoordinate::from_addrs(vec![dest, peer, sibling_parent, root]).unwrap(),
now_ms(),
);
assert_eq!(
node.classify_forward(&dest, &peer),
RouteClass::CrosslinkDescend,
"lateral peer, dest in its subtree"
);
}
#[test]
fn test_classify_forward_crosslink_ascend() {
// Chosen peer is lateral and the destination is NOT in its subtree → the
// up-and-over case (the Bloom v2 behavior delta).
let mut node = make_node();
let me = *node.node_addr();
let root = make_node_addr(1);
let sibling_parent = make_node_addr(2);
node.tree_state_mut()
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
let peer = make_node_addr(40);
node.tree_state_mut().update_peer(
ParentDeclaration::new(peer, sibling_parent, 1, 1000),
TreeCoordinate::from_addrs(vec![peer, sibling_parent, root]).unwrap(),
);
// Destination lives elsewhere (under root directly), NOT under the peer.
let dest = make_node_addr(80);
node.coord_cache_mut().insert(
dest,
TreeCoordinate::from_addrs(vec![dest, root]).unwrap(),
now_ms(),
);
assert_eq!(
node.classify_forward(&dest, &peer),
RouteClass::CrosslinkAscend,
"lateral peer, dest not in its subtree"
);
}
#[test]
fn test_classify_forward_direct_peer() {
// Degenerate case: the next hop is the destination itself.
let mut node = make_node();
let me = *node.node_addr();
let root = make_node_addr(1);
node.tree_state_mut()
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
let dest = make_node_addr(90);
assert_eq!(
node.classify_forward(&dest, &dest),
RouteClass::DirectPeer,
"next hop is the destination"
);
}
#[test]
fn test_route_class_partition_sums_to_forwarded() {
// The six route classes partition forwarded_packets: bumping
// record_forwarded once per record_route_class keeps the sum of the class
// counters equal to forwarded_packets.
let m = ForwardingMetrics::default();
let classes = [
RouteClass::TreeUp,
RouteClass::TreeUp,
RouteClass::TreeDown,
RouteClass::TreeDownCross,
RouteClass::TreeDownCross,
RouteClass::CrosslinkDescend,
RouteClass::CrosslinkAscend,
RouteClass::CrosslinkAscend,
RouteClass::CrosslinkAscend,
RouteClass::DirectPeer,
];
for &c in &classes {
m.record_forwarded(100);
m.record_route_class(c);
}
let snap = m.snapshot();
let class_sum = snap.route_tree_up
+ snap.route_tree_down
+ snap.route_tree_down_cross
+ snap.route_crosslink_descend
+ snap.route_crosslink_ascend
+ snap.route_direct_peer;
assert_eq!(
class_sum, snap.forwarded_packets,
"route classes must partition forwarded_packets"
);
assert_eq!(snap.route_tree_up, 2);
assert_eq!(snap.route_tree_down_cross, 2);
assert_eq!(snap.route_crosslink_ascend, 3);
assert_eq!(snap.route_direct_peer, 1);
}

View File

@@ -92,6 +92,15 @@ impl TreeState {
&self.my_coords
}
/// Test-only override of this node's coordinates, bypassing the
/// parent/declaration state machine. Lets routing tests place the node at
/// an arbitrary tree position to exercise coordinate-based classification.
#[cfg(test)]
pub(crate) fn set_my_coords_for_test(&mut self, coords: TreeCoordinate) {
self.root = *coords.root_id();
self.my_coords = coords;
}
/// Get the current root.
pub fn root(&self) -> &NodeAddr {
&self.root