Merge refactor-sans-io: sans-IO proto cleanup into the next line

Forward-merge the sans-IO cleanup series (bloom fpr, discovery const/RNG-injection,
mmp state decomposition, mmp shell dissolution, STP clock-free classify core, STP
declaration split, shared proto/coord relocation, shared rate limiter/backoff,
typed proto errors dropping thiserror, shared bounds-checked codec reader/writer,
core/alloc import sweep, no_std-shaped filter math) onto the next wire-format line.

Reconciled master structure against the next wire semantics: the mmp role-module
split adopts next slim report format (spin-bit stays dropped, sender/receiver
build next reports), the discovery and fmp codecs keep next TLV and profile
negotiation while moving to the typed error, and the parent-eval handler keeps the
Full/Leaf profile filter under the new ParentEval/is_switch_suppressed seam.
This commit is contained in:
Johnathan Corgan
2026-07-08 17:52:24 +00:00
62 changed files with 2891 additions and 2473 deletions
+2 -2
View File
@@ -87,7 +87,7 @@ impl Node {
// operator to see one clear message, not spam.
let max_fpr = self.config().node.bloom.max_inbound_fpr;
let out_fill = sent_filter.fill_ratio();
let out_fpr = out_fill.powi(sent_filter.hash_count() as i32);
let out_fpr = sent_filter.fpr();
if out_fpr > max_fpr {
let now = std::time::Instant::now();
let should_warn = self
@@ -213,7 +213,7 @@ impl Node {
// to wipe a victim's contribution to aggregation.
let max_fpr = self.config().node.bloom.max_inbound_fpr;
let fill = announce.filter.fill_ratio();
let fpr = fill.powi(announce.filter.hash_count() as i32);
let fpr = announce.filter.fpr();
if fpr > max_fpr {
self.metrics()
.bloom
+8 -4
View File
@@ -7,13 +7,13 @@
use crate::node::Node;
use crate::node::reject::DiscoveryReject;
use crate::proto::discovery::{DiscoveryAction, LookupRequest, LookupResponse};
use crate::proto::discovery::{
DiscoveryAction, LookupRequest, LookupResponse, MAX_RECENT_DISCOVERY_REQUESTS,
};
use crate::transport::{TransportAddr, TransportId};
use crate::{NodeAddr, PeerIdentity};
use tracing::{debug, info, trace, warn};
const MAX_RECENT_DISCOVERY_REQUESTS: usize = 4096;
/// Shell adapter exposing the live routing tables to the sans-IO discovery
/// core's `RoutingView` read seam. Lives in `node` so it can read `Node`'s
/// private `peers` map and call the crate-private tree/bloom predicates.
@@ -500,7 +500,11 @@ impl Node {
let origin = *self.node_addr();
let min_mtu = self.config().tun.mtu();
let request = LookupRequest::generate(*target, origin, ttl, min_mtu);
let request_id = {
use rand::RngExt;
rand::rng().random()
};
let request = LookupRequest::new(request_id, *target, origin, ttl, min_mtu);
// Tree-peer selection restricted to Full peers meeting min_mtu, plus the
// single encode, live in the sans-IO core. The core keeps the tree-only
+2 -2
View File
@@ -256,7 +256,7 @@ impl Node {
};
// MMP per-frame processing and statistics
let now_ms = crate::mmp::mono_ms();
let now_ms = crate::time::mono_ms();
let ce_flag = header.flags & FLAG_CE != 0;
if let Some(peer) = self.peers.get_mut(&node_addr) {
@@ -356,7 +356,7 @@ impl Node {
} else {
return;
};
let now_ms = crate::mmp::mono_ms();
let now_ms = crate::time::mono_ms();
let mut address_changed = false;
if let Some(peer) = self.peers.get_mut(node_addr) {
peer.reset_decrypt_failures();
+14 -8
View File
@@ -13,6 +13,7 @@ use crate::proto::mmp::{
LinkReportKind, LinkReportSnapshot, MmpAction, PeerLivenessSnapshot, ReceiverReport, RrLog,
SenderReport,
};
use crate::proto::stp::ParentEval;
use std::time::{Duration, Instant};
use tracing::{debug, info, trace, warn};
@@ -153,7 +154,7 @@ impl Node {
// Process the report: computes RTT from timestamp echo, updates
// loss rate, goodput rate, jitter trend, and ETX.
let now_ms = crate::mmp::mono_ms();
let now_ms = crate::time::mono_ms();
let (first_rtt, rr_log) =
mmp.metrics
.process_receiver_report(&rr, our_timestamp_ms, now_ms);
@@ -198,12 +199,17 @@ impl Node {
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mono_now_ms = crate::mmp::mono_ms();
let mono_now_ms = crate::time::mono_ms();
let skip = self.non_full_peers();
if let Some(new_parent) =
self.tree_state
.evaluate_parent(&peer_costs, &skip, mono_now_ms)
{
// Compute the flap-dampening / hold-down veto at the edge; a mandatory
// switch bypasses it, a discretionary one is taken only if not suppressed.
let switch_suppressed = self.tree_state.is_switch_suppressed(mono_now_ms);
let new_parent = match self.tree_state.evaluate_parent(&peer_costs, &skip) {
ParentEval::Mandatory(p) => Some(p),
ParentEval::Discretionary(p) if !switch_suppressed => Some(p),
ParentEval::Discretionary(_) | ParentEval::None => None,
};
if let Some(new_parent) = new_parent {
let new_seq = self.tree_state.my_declaration().sequence() + 1;
let flap_dampened =
self.tree_state
@@ -278,7 +284,7 @@ impl Node {
///
/// Called from the tick handler. Also emits periodic operator logs.
pub(in crate::node) async fn check_mmp_reports(&mut self) {
let now_ms = crate::mmp::mono_ms();
let now_ms = crate::time::mono_ms();
// Build one report-gating snapshot per peer, resolving every timing read
// shell-side into a `bool`. `send_sr`/`send_rr` come from the peer's
@@ -427,7 +433,7 @@ impl Node {
// Monotonic ms for the MMP receiver's injected-`u64` liveness clock; the
// Instant `now` is still used for the shell-owned heartbeat timing and
// the session-start fallback (both `ActivePeer` Instants).
let now_ms = crate::mmp::mono_ms();
let now_ms = crate::time::mono_ms();
let heartbeat_interval = Duration::from_secs(self.config().node.heartbeat_interval_secs);
let dead_timeout = Duration::from_secs(self.config().node.link_dead_timeout_secs);
let dead_timeout_ms = dead_timeout.as_millis() as u64;
+5 -5
View File
@@ -321,7 +321,7 @@ impl Node {
if let Some(entry) = self.sessions.get_mut(src_addr)
&& let Some(mmp) = entry.mmp_mut()
{
let now_ms = crate::mmp::mono_ms();
let now_ms = crate::time::mono_ms();
mmp.receiver
.record_recv(header.counter, timestamp, plaintext.len(), ce_flag, now_ms);
let _inner_flags = FspInnerFlags::from_byte(inner_flags_byte);
@@ -1031,7 +1031,7 @@ impl Node {
/// Called from the tick handler. Also emits periodic session MMP logs.
/// Uses the collect-then-send pattern to avoid borrowing conflicts.
pub(in crate::node) async fn check_session_mmp_reports(&mut self) {
let now_ms = crate::mmp::mono_ms();
let now_ms = crate::time::mono_ms();
// Build one report-gating snapshot per session, resolving every timing
// read shell-side into a `bool`. The snapshots own only
@@ -1298,7 +1298,7 @@ impl Node {
let (_first_rtt, rr_log) =
mmp.metrics
.process_receiver_report(&rr, our_timestamp_ms, crate::mmp::mono_ms());
.process_receiver_report(&rr, our_timestamp_ms, crate::time::mono_ms());
// Re-emit the operator trace the core used to log mid-decision.
super::mmp::log_rr_outcome(&rr, our_timestamp_ms, rr_log);
@@ -1368,7 +1368,7 @@ impl Node {
let old_mtu = mmp.path_mtu.current_mtu();
let changed = mmp
.path_mtu
.apply_notification(notif.path_mtu, crate::mmp::mono_ms());
.apply_notification(notif.path_mtu, crate::time::mono_ms());
let new_mtu = mmp.path_mtu.current_mtu();
if !changed {
@@ -1606,7 +1606,7 @@ impl Node {
let old_mtu = mmp.path_mtu.current_mtu();
if mmp
.path_mtu
.apply_notification(msg.mtu, crate::mmp::mono_ms())
.apply_notification(msg.mtu, crate::time::mono_ms())
{
let new_mtu = mmp.path_mtu.current_mtu();
info!(
+1 -1
View File
@@ -667,7 +667,7 @@ impl SessionEntry {
self.rekey_jitter_secs = draw_rekey_jitter();
// Reset MMP counters to avoid metric discontinuity
let now_ms = crate::mmp::mono_ms();
let now_ms = crate::time::mono_ms();
if let Some(mmp) = &mut self.mmp {
mmp.reset_for_rekey(now_ms);
}
+5 -5
View File
@@ -1144,11 +1144,11 @@ async fn test_open_discovery_sweep_queues_eligible_skips_filtered() {
/// (t=1100ms, 3100ms, 7100ms) and unreachable at t=15100ms.
/// 2. **Fresh `initiate_lookup` per attempt** — `req_initiated` counter
/// increments by exactly one on each retry. The actual `request_id`
/// is generated by `LookupRequest::generate(...)` via `rand::random()`
/// inside `initiate_lookup` and is not stored on the originator
/// side, so per-attempt freshness is verified indirectly: each
/// `req_initiated` increment corresponds to one fresh
/// `LookupRequest::generate` call.
/// is drawn via `rand::rng().random()` at the shell inside
/// `initiate_lookup` and passed to `LookupRequest::new(...)`; it is
/// not stored on the originator side, so per-attempt freshness is
/// verified indirectly: each `req_initiated` increment corresponds
/// to one fresh `initiate_lookup` call.
/// 3. **Final-timeout state transitions** — `pending_lookups` entry is
/// removed, `discovery.resp_timed_out` counter ticks, queued packet
/// is drained, and an ICMPv6 Destination Unreachable frame is
+12 -12
View File
@@ -12,12 +12,12 @@
//!
//! Assertions capture what the code does today, surprising or not.
//!
//! Report-generation is probed through the reused `src/mmp/` primitives
//! Report-generation is probed through the reused `proto/mmp/` primitives
//! (`should_send_report` / `should_send_notification`): after a handler tick,
//! a *consumed* interval reads as "not due" (the report was built) while an
//! *ungated* interval still reads as "due" (the report was suppressed by the
//! mode/flag gate). This survives the later refactor because those primitives
//! stay in `src/mmp/` unchanged.
//! stay in `proto/mmp/` unchanged.
//!
//! Two `#[cfg(test)]` production seams are used, both on `ActivePeer`:
//! * `test_init_mmp(mode)` — attach link MMP with a chosen mode to a
@@ -55,7 +55,7 @@ fn arm_link_mmp(node: &mut Node, addr: &NodeAddr) {
let mmp = node.get_peer_mut(addr).unwrap().mmp_mut().unwrap();
mmp.sender.record_sent(1, 100, 500);
mmp.receiver
.record_recv(1, 100, 500, false, crate::mmp::mono_ms());
.record_recv(1, 100, 500, false, crate::time::mono_ms());
}
/// Complete an in-memory Noise XX handshake, returning the initiator session.
@@ -110,7 +110,7 @@ fn arm_session_mmp(node: &mut Node, addr: &NodeAddr) {
let mmp = node.sessions.get_mut(addr).unwrap().mmp_mut().unwrap();
mmp.sender.record_sent(1, 100, 500);
mmp.receiver
.record_recv(1, 100, 500, false, crate::mmp::mono_ms());
.record_recv(1, 100, 500, false, crate::time::mono_ms());
}
// ===========================================================================
@@ -128,7 +128,7 @@ async fn mmp_full_mode_builds_sender_and_receiver_reports() {
node.check_mmp_reports().await;
let mmp = node.get_peer(&addr).unwrap().mmp().unwrap();
let now = crate::mmp::mono_ms();
let now = crate::time::mono_ms();
assert!(
!mmp.sender.should_send_report(now),
"Full mode consumes the sender interval (SenderReport built)"
@@ -150,7 +150,7 @@ async fn mmp_lightweight_mode_builds_receiver_report_only() {
node.check_mmp_reports().await;
let mmp = node.get_peer(&addr).unwrap().mmp().unwrap();
let now = crate::mmp::mono_ms();
let now = crate::time::mono_ms();
assert!(
mmp.sender.should_send_report(now),
"Lightweight mode suppresses the SenderReport (sender interval intact)"
@@ -171,7 +171,7 @@ async fn mmp_minimal_mode_builds_nothing() {
node.check_mmp_reports().await;
let mmp = node.get_peer(&addr).unwrap().mmp().unwrap();
let now = crate::mmp::mono_ms();
let now = crate::time::mono_ms();
assert!(
mmp.sender.should_send_report(now),
"Minimal mode suppresses the SenderReport"
@@ -195,7 +195,7 @@ async fn mmp_should_log_marks_logged_once_per_interval() {
.unwrap()
.mmp()
.unwrap()
.should_log(crate::mmp::mono_ms()),
.should_log(crate::time::mono_ms()),
"a freshly created peer is due for its first operator log"
);
@@ -207,7 +207,7 @@ async fn mmp_should_log_marks_logged_once_per_interval() {
.unwrap()
.mmp()
.unwrap()
.should_log(crate::mmp::mono_ms()),
.should_log(crate::time::mono_ms()),
"after one tick the log is marked and not due again within the interval"
);
}
@@ -227,7 +227,7 @@ async fn session_full_mode_builds_sender_and_receiver_reports() {
node.check_session_mmp_reports().await;
let mmp = node.get_session(&addr).unwrap().mmp().unwrap();
let now = crate::mmp::mono_ms();
let now = crate::time::mono_ms();
assert!(
!mmp.sender.should_send_report(now),
"Full session consumes the sender interval"
@@ -255,7 +255,7 @@ async fn session_minimal_mode_still_sends_path_mtu() {
.path_mtu
.observe_incoming_mtu(1200);
let now_before = crate::mmp::mono_ms();
let now_before = crate::time::mono_ms();
assert!(
node.get_session(&addr)
.unwrap()
@@ -269,7 +269,7 @@ async fn session_minimal_mode_still_sends_path_mtu() {
node.check_session_mmp_reports().await;
let mmp = node.get_session(&addr).unwrap().mmp().unwrap();
let now = crate::mmp::mono_ms();
let now = crate::time::mono_ms();
assert!(
mmp.sender.should_send_report(now),
"Minimal mode suppresses the session SenderReport"
+1 -1
View File
@@ -1909,7 +1909,7 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
let entry = nodes[0].node.get_session_mut(&node1_addr).unwrap();
let mmp = entry.mmp_mut().unwrap();
mmp.path_mtu
.apply_notification(reduced_mtu, crate::mmp::mono_ms());
.apply_notification(reduced_mtu, crate::time::mono_ms());
assert_eq!(mmp.path_mtu.current_mtu(), reduced_mtu);
}
+17 -5
View File
@@ -330,9 +330,18 @@ impl Node {
// Monotonic ms for the flap-dampening / hold-down timers (distinct from
// the wall-clock `now_ms` above used for the peer's tree position). Read
// once and threaded into classify + the state mutators.
let mono_now_ms = crate::mmp::mono_ms();
let mono_now_ms = crate::time::mono_ms();
// Compute the flap-dampening / hold-down veto at the edge; the classify core
// is clock-free and consumes only this pre-computed verdict.
let switch_suppressed = self.tree_state.is_switch_suppressed(mono_now_ms);
match Stp::classify_announce(&self.tree_state, *from, &peer_costs, &skip, mono_now_ms) {
match Stp::classify_announce(
&self.tree_state,
*from,
&peer_costs,
&skip,
switch_suppressed,
) {
TreeDecision::Switch {
new_parent,
new_seq,
@@ -582,9 +591,12 @@ impl Node {
// Monotonic ms for the flap-dampening / hold-down timers, read once and
// threaded into classify + the state mutators.
let mono_now_ms = crate::mmp::mono_ms();
let mono_now_ms = crate::time::mono_ms();
// Compute the flap-dampening / hold-down veto at the edge; the classify core
// is clock-free and consumes only this pre-computed verdict.
let switch_suppressed = self.tree_state.is_switch_suppressed(mono_now_ms);
match Stp::classify_periodic(&self.tree_state, &peer_costs, &skip, mono_now_ms) {
match Stp::classify_periodic(&self.tree_state, &peer_costs, &skip, switch_suppressed) {
TreeDecision::Switch {
new_parent,
new_seq,
@@ -735,7 +747,7 @@ impl Node {
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mono_now_ms = crate::mmp::mono_ms();
let mono_now_ms = crate::time::mono_ms();
// Removal is not a pure classify: `handle_parent_lost` is a &mut mutator
// whose returned `changed` bool IS the decision. Drive it and map the