mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user