mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Introduce a typed RejectReason enum and a NodeStats::record_reject dispatch so every receive-path rejection-and-return site bumps a machine-readable per-subsystem counter while keeping its operator-facing log line. The top-level variants mirror the existing NodeStats subsystem split (Tree, Bloom, Discovery, Forwarding) and add Handshake, Session, Mmp, and Transport categories; HandshakeStats, SessionStats, and MmpStats are new sub-stats. Wired clusters: tree and MMP outbound sign-failure; the FSP session unknown-session and state-machine cluster; the Noise IK handshake state-machine cluster (msg1/msg2); and the decode / crypto / cap / semantic tail across bloom, discovery, forwarding, mmp, and tree. The TreeStats::ancestry_invalid counter, present since the scaffold but never incremented, is now bumped from the validate_semantics ancestry rejection. Several handshake, MMP, tree, and discovery paths that previously had no counter at all are now counted, including the send_lookup_response no-route drop (DiscoveryStats::resp_no_route). Existing direct counters at the bloom / discovery / forwarding sites are retained alongside the new dispatch while the rollout is in progress (the bloom_poison tests expect the transitional +2 delta); a later change collapses the duplicate increment.
980 lines
33 KiB
Rust
980 lines
33 KiB
Rust
//! Node-level statistics for routing, forwarding, and discovery operations.
|
|
//!
|
|
//! Unlike `EthernetStats` (which uses `AtomicU64` + `Arc` for cross-task
|
|
//! sharing), these counters use plain `u64` because `Node` handlers run
|
|
//! on a single `&mut self` context. A `snapshot()` method produces a
|
|
//! copyable struct for control socket queries.
|
|
|
|
use serde::Serialize;
|
|
|
|
use crate::node::reject::{
|
|
BloomReject, DiscoveryReject, ForwardingReject, HandshakeReject, MmpReject, RejectReason,
|
|
SessionReject, TransportReject, TreeReject,
|
|
};
|
|
|
|
/// Forwarding statistics — packets and bytes for each outcome.
|
|
#[derive(Default)]
|
|
pub struct ForwardingStats {
|
|
pub received_packets: u64,
|
|
pub received_bytes: u64,
|
|
pub decode_error_packets: u64,
|
|
pub decode_error_bytes: u64,
|
|
pub ttl_exhausted_packets: u64,
|
|
pub ttl_exhausted_bytes: u64,
|
|
pub delivered_packets: u64,
|
|
pub delivered_bytes: u64,
|
|
pub forwarded_packets: u64,
|
|
pub forwarded_bytes: u64,
|
|
pub drop_no_route_packets: u64,
|
|
pub drop_no_route_bytes: u64,
|
|
pub drop_mtu_exceeded_packets: u64,
|
|
pub drop_mtu_exceeded_bytes: u64,
|
|
pub drop_send_error_packets: u64,
|
|
pub drop_send_error_bytes: u64,
|
|
pub originated_packets: u64,
|
|
pub originated_bytes: u64,
|
|
}
|
|
|
|
impl ForwardingStats {
|
|
pub fn record_received(&mut self, bytes: usize) {
|
|
self.received_packets += 1;
|
|
self.received_bytes += bytes as u64;
|
|
}
|
|
|
|
pub fn record_decode_error(&mut self, bytes: usize) {
|
|
self.decode_error_packets += 1;
|
|
self.decode_error_bytes += bytes as u64;
|
|
}
|
|
|
|
pub fn record_ttl_exhausted(&mut self, bytes: usize) {
|
|
self.ttl_exhausted_packets += 1;
|
|
self.ttl_exhausted_bytes += bytes as u64;
|
|
}
|
|
|
|
pub fn record_delivered(&mut self, bytes: usize) {
|
|
self.delivered_packets += 1;
|
|
self.delivered_bytes += bytes as u64;
|
|
}
|
|
|
|
pub fn record_forwarded(&mut self, bytes: usize) {
|
|
self.forwarded_packets += 1;
|
|
self.forwarded_bytes += bytes as u64;
|
|
}
|
|
|
|
pub fn record_drop_no_route(&mut self, bytes: usize) {
|
|
self.drop_no_route_packets += 1;
|
|
self.drop_no_route_bytes += bytes as u64;
|
|
}
|
|
|
|
pub fn record_drop_mtu_exceeded(&mut self, bytes: usize) {
|
|
self.drop_mtu_exceeded_packets += 1;
|
|
self.drop_mtu_exceeded_bytes += bytes as u64;
|
|
}
|
|
|
|
pub fn record_drop_send_error(&mut self, bytes: usize) {
|
|
self.drop_send_error_packets += 1;
|
|
self.drop_send_error_bytes += bytes as u64;
|
|
}
|
|
|
|
pub fn record_originated(&mut self, bytes: usize) {
|
|
self.originated_packets += 1;
|
|
self.originated_bytes += bytes as u64;
|
|
}
|
|
|
|
/// Dispatch a typed forwarding rejection to its packet counter.
|
|
///
|
|
/// The byte-counted side of each outcome is recorded by the
|
|
/// existing `record_*` methods at the call site (which know the
|
|
/// payload size); `record_reject` only bumps the packet count and
|
|
/// is paired with the byte-aware call at the call site while the
|
|
/// typed-rejection rollout is in progress. A later change may
|
|
/// collapse the two calls into a single typed entry point.
|
|
pub(super) fn record_reject(&mut self, reason: ForwardingReject) {
|
|
match reason {
|
|
ForwardingReject::DecodeError => self.decode_error_packets += 1,
|
|
ForwardingReject::TtlExhausted => self.ttl_exhausted_packets += 1,
|
|
ForwardingReject::NoRoute => self.drop_no_route_packets += 1,
|
|
ForwardingReject::MtuExceeded => self.drop_mtu_exceeded_packets += 1,
|
|
ForwardingReject::SendError => self.drop_send_error_packets += 1,
|
|
}
|
|
}
|
|
|
|
pub fn snapshot(&self) -> ForwardingStatsSnapshot {
|
|
ForwardingStatsSnapshot {
|
|
received_packets: self.received_packets,
|
|
received_bytes: self.received_bytes,
|
|
decode_error_packets: self.decode_error_packets,
|
|
decode_error_bytes: self.decode_error_bytes,
|
|
ttl_exhausted_packets: self.ttl_exhausted_packets,
|
|
ttl_exhausted_bytes: self.ttl_exhausted_bytes,
|
|
delivered_packets: self.delivered_packets,
|
|
delivered_bytes: self.delivered_bytes,
|
|
forwarded_packets: self.forwarded_packets,
|
|
forwarded_bytes: self.forwarded_bytes,
|
|
drop_no_route_packets: self.drop_no_route_packets,
|
|
drop_no_route_bytes: self.drop_no_route_bytes,
|
|
drop_mtu_exceeded_packets: self.drop_mtu_exceeded_packets,
|
|
drop_mtu_exceeded_bytes: self.drop_mtu_exceeded_bytes,
|
|
drop_send_error_packets: self.drop_send_error_packets,
|
|
drop_send_error_bytes: self.drop_send_error_bytes,
|
|
originated_packets: self.originated_packets,
|
|
originated_bytes: self.originated_bytes,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Discovery statistics — packet counts for request and response handling.
|
|
#[derive(Default)]
|
|
pub struct DiscoveryStats {
|
|
// Request counters
|
|
pub req_received: u64,
|
|
pub req_decode_error: u64,
|
|
pub req_duplicate: u64,
|
|
pub req_target_is_us: u64,
|
|
pub req_forwarded: u64,
|
|
pub req_ttl_exhausted: u64,
|
|
pub req_initiated: u64,
|
|
pub req_deduplicated: u64,
|
|
pub req_backoff_suppressed: u64,
|
|
pub req_forward_rate_limited: u64,
|
|
pub req_bloom_miss: u64,
|
|
pub req_no_tree_peer: u64,
|
|
pub req_fallback_forwarded: u64,
|
|
// Response counters
|
|
pub resp_received: u64,
|
|
pub resp_decode_error: u64,
|
|
pub resp_forwarded: u64,
|
|
pub resp_identity_miss: u64,
|
|
pub resp_proof_failed: u64,
|
|
pub resp_no_route: u64,
|
|
pub resp_accepted: u64,
|
|
pub resp_timed_out: u64,
|
|
}
|
|
|
|
impl DiscoveryStats {
|
|
pub(super) fn record_reject(&mut self, reason: DiscoveryReject) {
|
|
match reason {
|
|
DiscoveryReject::ReqDecodeError => self.req_decode_error += 1,
|
|
DiscoveryReject::ReqDuplicate => self.req_duplicate += 1,
|
|
DiscoveryReject::ReqTtlExhausted => self.req_ttl_exhausted += 1,
|
|
DiscoveryReject::RespDecodeError => self.resp_decode_error += 1,
|
|
DiscoveryReject::RespIdentityMiss => self.resp_identity_miss += 1,
|
|
DiscoveryReject::RespProofFailed => self.resp_proof_failed += 1,
|
|
DiscoveryReject::RespNoRoute => self.resp_no_route += 1,
|
|
}
|
|
}
|
|
|
|
pub fn snapshot(&self) -> DiscoveryStatsSnapshot {
|
|
DiscoveryStatsSnapshot {
|
|
req_received: self.req_received,
|
|
req_decode_error: self.req_decode_error,
|
|
req_duplicate: self.req_duplicate,
|
|
req_target_is_us: self.req_target_is_us,
|
|
req_forwarded: self.req_forwarded,
|
|
req_ttl_exhausted: self.req_ttl_exhausted,
|
|
req_initiated: self.req_initiated,
|
|
req_deduplicated: self.req_deduplicated,
|
|
req_backoff_suppressed: self.req_backoff_suppressed,
|
|
req_forward_rate_limited: self.req_forward_rate_limited,
|
|
req_bloom_miss: self.req_bloom_miss,
|
|
req_no_tree_peer: self.req_no_tree_peer,
|
|
req_fallback_forwarded: self.req_fallback_forwarded,
|
|
resp_received: self.resp_received,
|
|
resp_decode_error: self.resp_decode_error,
|
|
resp_forwarded: self.resp_forwarded,
|
|
resp_identity_miss: self.resp_identity_miss,
|
|
resp_proof_failed: self.resp_proof_failed,
|
|
resp_no_route: self.resp_no_route,
|
|
resp_accepted: self.resp_accepted,
|
|
resp_timed_out: self.resp_timed_out,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Spanning tree statistics — announce handling and parent tracking.
|
|
#[derive(Default)]
|
|
pub struct TreeStats {
|
|
// Inbound announce handling
|
|
pub received: u64,
|
|
pub decode_error: u64,
|
|
pub unknown_peer: u64,
|
|
pub addr_mismatch: u64,
|
|
pub sig_failed: u64,
|
|
pub stale: u64,
|
|
pub ancestry_invalid: u64,
|
|
pub accepted: u64,
|
|
pub parent_switched: u64,
|
|
pub loop_detected: u64,
|
|
pub ancestry_changed: u64,
|
|
// Outbound announce sending
|
|
pub sent: u64,
|
|
pub rate_limited: u64,
|
|
pub send_failed: u64,
|
|
pub outbound_sign_failed: u64,
|
|
// Cumulative events
|
|
pub parent_switches: u64,
|
|
pub parent_losses: u64,
|
|
pub flap_dampened: u64,
|
|
}
|
|
|
|
impl TreeStats {
|
|
pub fn snapshot(&self) -> TreeStatsSnapshot {
|
|
TreeStatsSnapshot {
|
|
received: self.received,
|
|
decode_error: self.decode_error,
|
|
unknown_peer: self.unknown_peer,
|
|
addr_mismatch: self.addr_mismatch,
|
|
sig_failed: self.sig_failed,
|
|
stale: self.stale,
|
|
ancestry_invalid: self.ancestry_invalid,
|
|
accepted: self.accepted,
|
|
parent_switched: self.parent_switched,
|
|
loop_detected: self.loop_detected,
|
|
ancestry_changed: self.ancestry_changed,
|
|
sent: self.sent,
|
|
rate_limited: self.rate_limited,
|
|
send_failed: self.send_failed,
|
|
outbound_sign_failed: self.outbound_sign_failed,
|
|
parent_switches: self.parent_switches,
|
|
parent_losses: self.parent_losses,
|
|
flap_dampened: self.flap_dampened,
|
|
}
|
|
}
|
|
|
|
pub(super) fn record_reject(&mut self, reason: TreeReject) {
|
|
match reason {
|
|
TreeReject::AncestryInvalid => self.ancestry_invalid += 1,
|
|
TreeReject::OutboundSignFailed => self.outbound_sign_failed += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Bloom filter statistics — filter announce handling.
|
|
#[derive(Default)]
|
|
pub struct BloomStats {
|
|
// Inbound announce handling
|
|
pub received: u64,
|
|
pub decode_error: u64,
|
|
pub invalid: u64,
|
|
pub non_v1: u64,
|
|
pub unknown_peer: u64,
|
|
pub stale: u64,
|
|
pub fill_exceeded: u64,
|
|
pub accepted: u64,
|
|
// Outbound announce sending
|
|
pub sent: u64,
|
|
pub debounce_suppressed: u64,
|
|
pub send_failed: u64,
|
|
}
|
|
|
|
impl BloomStats {
|
|
pub(super) fn record_reject(&mut self, reason: BloomReject) {
|
|
match reason {
|
|
BloomReject::DecodeError => self.decode_error += 1,
|
|
BloomReject::Invalid => self.invalid += 1,
|
|
BloomReject::NonV1 => self.non_v1 += 1,
|
|
BloomReject::UnknownPeer => self.unknown_peer += 1,
|
|
BloomReject::Stale => self.stale += 1,
|
|
BloomReject::FillExceeded => self.fill_exceeded += 1,
|
|
}
|
|
}
|
|
|
|
pub fn snapshot(&self) -> BloomStatsSnapshot {
|
|
BloomStatsSnapshot {
|
|
received: self.received,
|
|
decode_error: self.decode_error,
|
|
invalid: self.invalid,
|
|
non_v1: self.non_v1,
|
|
unknown_peer: self.unknown_peer,
|
|
stale: self.stale,
|
|
fill_exceeded: self.fill_exceeded,
|
|
accepted: self.accepted,
|
|
sent: self.sent,
|
|
debounce_suppressed: self.debounce_suppressed,
|
|
send_failed: self.send_failed,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// FSP session statistics — receive-path silent-rejection counters.
|
|
///
|
|
/// Covers the unknown-session and state-machine-mismatch rejection
|
|
/// sites in `handlers/session.rs`. Each counter increments once per
|
|
/// dropped inbound message; the WARN/DEBUG log line at the site is
|
|
/// preserved alongside the counter bump for operator visibility.
|
|
#[derive(Default)]
|
|
pub struct SessionStats {
|
|
/// Inbound session-layer message arrived for a peer address with no
|
|
/// matching `SessionEntry`. Aggregates across encrypted data,
|
|
/// SessionAck, SessionMsg3, SessionReceiverReport, and
|
|
/// PathMtuNotification.
|
|
pub unknown_session: u64,
|
|
/// Inbound session-layer message arrived for a `SessionEntry` whose
|
|
/// state is incompatible with the message type (encrypted data
|
|
/// before Established; SessionAck outside Initiating; SessionMsg3
|
|
/// outside AwaitingMsg3).
|
|
pub bad_state: u64,
|
|
}
|
|
|
|
impl SessionStats {
|
|
pub fn snapshot(&self) -> SessionStatsSnapshot {
|
|
SessionStatsSnapshot {
|
|
unknown_session: self.unknown_session,
|
|
bad_state: self.bad_state,
|
|
}
|
|
}
|
|
|
|
pub(super) fn record_reject(&mut self, reason: SessionReject) {
|
|
match reason {
|
|
SessionReject::UnknownSession => self.unknown_session += 1,
|
|
SessionReject::BadState => self.bad_state += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Noise-handshake statistics — receive-path silent-rejection counters.
|
|
///
|
|
/// Covers the state-machine and lookup-miss rejection sites in
|
|
/// `handlers/handshake.rs` across msg1, msg2, and (on the XX side) msg3.
|
|
/// Each counter increments once per dropped inbound message; the
|
|
/// WARN/DEBUG log line at the site is preserved alongside the counter
|
|
/// bump for operator visibility.
|
|
#[derive(Default)]
|
|
pub struct HandshakeStats {
|
|
/// Handshake state-machine rejection: header parse failed, Noise
|
|
/// crypto step failed, identity could not be learned, index allocator
|
|
/// returned an error, msg2/msg3 send failed, promote_connection
|
|
/// returned an error, ACL gate rejected the peer, or the admission
|
|
/// gate fired (max_peers / accept_connections).
|
|
pub bad_state: u64,
|
|
/// Inbound handshake message arrived but no matching connection was
|
|
/// found by the receiver_idx (or addr) lookup: msg2 for an unknown
|
|
/// pending-outbound index, duplicate msg1 with no stored msg2 to
|
|
/// resend, msg3 for an unknown pending-inbound index without a
|
|
/// matching rekey-responder slot.
|
|
pub unknown_connection: u64,
|
|
}
|
|
|
|
impl HandshakeStats {
|
|
pub fn snapshot(&self) -> HandshakeStatsSnapshot {
|
|
HandshakeStatsSnapshot {
|
|
bad_state: self.bad_state,
|
|
unknown_connection: self.unknown_connection,
|
|
}
|
|
}
|
|
|
|
pub(super) fn record_reject(&mut self, reason: HandshakeReject) {
|
|
match reason {
|
|
HandshakeReject::BadState => self.bad_state += 1,
|
|
HandshakeReject::UnknownConnection => self.unknown_connection += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// MMP link-layer rejection statistics.
|
|
///
|
|
/// Covers the receive-path silent-rejection sites in
|
|
/// `src/node/handlers/mmp.rs::handle_sender_report` and
|
|
/// `handle_receiver_report`. Each counter increments once per
|
|
/// dropped inbound report; the WARN/DEBUG log line at the site is
|
|
/// preserved alongside the counter bump.
|
|
#[derive(Default)]
|
|
pub struct MmpStats {
|
|
/// `SenderReport::decode` or `ReceiverReport::decode` returned
|
|
/// an error. Aggregated across the two report types.
|
|
pub decode_error: u64,
|
|
/// SenderReport or ReceiverReport arrived from a peer with no
|
|
/// `ActivePeer` record on this node.
|
|
pub unknown_peer: u64,
|
|
}
|
|
|
|
impl MmpStats {
|
|
pub fn snapshot(&self) -> MmpStatsSnapshot {
|
|
MmpStatsSnapshot {
|
|
decode_error: self.decode_error,
|
|
unknown_peer: self.unknown_peer,
|
|
}
|
|
}
|
|
|
|
pub(super) fn record_reject(&mut self, reason: MmpReject) {
|
|
match reason {
|
|
MmpReject::DecodeError => self.decode_error += 1,
|
|
MmpReject::UnknownPeer => self.unknown_peer += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Transport-layer rejection statistics aggregated at the node level.
|
|
///
|
|
/// Per-transport modules (`transport/tcp/stats.rs`, `transport/tor/stats.rs`)
|
|
/// keep their own `connections_accepted` / `connections_rejected` /
|
|
/// `pool_inbound` / `pool_outbound` counters at the transport layer.
|
|
/// `TransportStats` here collects node-level visibility for any future
|
|
/// admission-rejection paths that the node code itself decides to
|
|
/// register via `record_reject(RejectReason::Transport(...))`.
|
|
///
|
|
/// The `inbound_cap_exceeded` counter is the typed-dispatch parity
|
|
/// counterpart of the per-transport `connections_rejected` counter,
|
|
/// which lives in the accept-loop task with no `NodeStats` access.
|
|
/// Currently this node-side counter stays at zero; it exists so the
|
|
/// typed-rejection enum stays the canonical entry point and so a
|
|
/// future transport-to-node bridge (event or sampling) has a
|
|
/// well-known destination.
|
|
#[derive(Default)]
|
|
pub struct TransportStats {
|
|
/// Reserved for node-side inbound-cap-exceeded admission rejection
|
|
/// dispatch. Per-transport accept-loop cap rejections are tracked
|
|
/// on the transport-level stats (`TcpStats::connections_rejected`,
|
|
/// `TorStats::connections_rejected`) directly.
|
|
pub inbound_cap_exceeded: u64,
|
|
}
|
|
|
|
impl TransportStats {
|
|
pub fn snapshot(&self) -> TransportStatsSnapshot {
|
|
TransportStatsSnapshot {
|
|
inbound_cap_exceeded: self.inbound_cap_exceeded,
|
|
}
|
|
}
|
|
|
|
pub(super) fn record_reject(&mut self, reason: TransportReject) {
|
|
match reason {
|
|
TransportReject::InboundCapExceeded => self.inbound_cap_exceeded += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Error signal statistics — counts of each error signal type received.
|
|
#[derive(Default)]
|
|
pub struct ErrorSignalStats {
|
|
pub coords_required: u64,
|
|
pub path_broken: u64,
|
|
pub mtu_exceeded: u64,
|
|
}
|
|
|
|
impl ErrorSignalStats {
|
|
pub fn snapshot(&self) -> ErrorSignalStatsSnapshot {
|
|
ErrorSignalStatsSnapshot {
|
|
coords_required: self.coords_required,
|
|
path_broken: self.path_broken,
|
|
mtu_exceeded: self.mtu_exceeded,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Congestion event statistics — ECN CE tracking and detection triggers.
|
|
#[derive(Default)]
|
|
pub struct CongestionStats {
|
|
/// Packets forwarded with the CE flag set (incoming CE or locally detected).
|
|
pub ce_forwarded: u64,
|
|
/// CE-flagged packets received at this node as final destination.
|
|
pub ce_received: u64,
|
|
/// Number of times detect_congestion() returned true.
|
|
pub congestion_detected: u64,
|
|
/// Rising-edge transport kernel drop events (not-dropping → dropping).
|
|
pub kernel_drop_events: u64,
|
|
}
|
|
|
|
impl CongestionStats {
|
|
pub fn record_ce_forwarded(&mut self) {
|
|
self.ce_forwarded += 1;
|
|
}
|
|
|
|
pub fn record_ce_received(&mut self) {
|
|
self.ce_received += 1;
|
|
}
|
|
|
|
pub fn record_congestion_detected(&mut self) {
|
|
self.congestion_detected += 1;
|
|
}
|
|
|
|
pub fn record_kernel_drop_event(&mut self) {
|
|
self.kernel_drop_events += 1;
|
|
}
|
|
|
|
pub fn snapshot(&self) -> CongestionStatsSnapshot {
|
|
CongestionStatsSnapshot {
|
|
ce_forwarded: self.ce_forwarded,
|
|
ce_received: self.ce_received,
|
|
congestion_detected: self.congestion_detected,
|
|
kernel_drop_events: self.kernel_drop_events,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Aggregate node statistics.
|
|
#[derive(Default)]
|
|
pub struct NodeStats {
|
|
pub forwarding: ForwardingStats,
|
|
pub discovery: DiscoveryStats,
|
|
pub tree: TreeStats,
|
|
pub bloom: BloomStats,
|
|
pub session: SessionStats,
|
|
pub handshake: HandshakeStats,
|
|
pub mmp: MmpStats,
|
|
pub transport: TransportStats,
|
|
pub errors: ErrorSignalStats,
|
|
pub congestion: CongestionStats,
|
|
}
|
|
|
|
impl NodeStats {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn snapshot(&self) -> NodeStatsSnapshot {
|
|
NodeStatsSnapshot {
|
|
forwarding: self.forwarding.snapshot(),
|
|
discovery: self.discovery.snapshot(),
|
|
tree: self.tree.snapshot(),
|
|
bloom: self.bloom.snapshot(),
|
|
session: self.session.snapshot(),
|
|
handshake: self.handshake.snapshot(),
|
|
mmp: self.mmp.snapshot(),
|
|
transport: self.transport.snapshot(),
|
|
errors: self.errors.snapshot(),
|
|
congestion: self.congestion.snapshot(),
|
|
}
|
|
}
|
|
|
|
/// Record a typed rejection from a silent-rejection site.
|
|
///
|
|
/// Dispatches to the appropriate sub-stats `record_reject` based on
|
|
/// the [`RejectReason`] top-level variant. Sub-enums that have not
|
|
/// yet had any variants populated still use `match r {}` to keep
|
|
/// the dispatch arm exhaustive without dead-code complaints.
|
|
pub fn record_reject(&mut self, reason: RejectReason) {
|
|
match reason {
|
|
RejectReason::Tree(r) => self.tree.record_reject(r),
|
|
RejectReason::Bloom(r) => self.bloom.record_reject(r),
|
|
RejectReason::Discovery(r) => self.discovery.record_reject(r),
|
|
RejectReason::Session(r) => self.session.record_reject(r),
|
|
RejectReason::Handshake(r) => self.handshake.record_reject(r),
|
|
RejectReason::Forwarding(r) => self.forwarding.record_reject(r),
|
|
RejectReason::Transport(r) => self.transport.record_reject(r),
|
|
RejectReason::Mmp(r) => self.mmp.record_reject(r),
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Snapshot types (copyable, serializable) ---
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct ForwardingStatsSnapshot {
|
|
pub received_packets: u64,
|
|
pub received_bytes: u64,
|
|
pub decode_error_packets: u64,
|
|
pub decode_error_bytes: u64,
|
|
pub ttl_exhausted_packets: u64,
|
|
pub ttl_exhausted_bytes: u64,
|
|
pub delivered_packets: u64,
|
|
pub delivered_bytes: u64,
|
|
pub forwarded_packets: u64,
|
|
pub forwarded_bytes: u64,
|
|
pub drop_no_route_packets: u64,
|
|
pub drop_no_route_bytes: u64,
|
|
pub drop_mtu_exceeded_packets: u64,
|
|
pub drop_mtu_exceeded_bytes: u64,
|
|
pub drop_send_error_packets: u64,
|
|
pub drop_send_error_bytes: u64,
|
|
pub originated_packets: u64,
|
|
pub originated_bytes: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct DiscoveryStatsSnapshot {
|
|
pub req_received: u64,
|
|
pub req_decode_error: u64,
|
|
pub req_duplicate: u64,
|
|
pub req_target_is_us: u64,
|
|
pub req_forwarded: u64,
|
|
pub req_ttl_exhausted: u64,
|
|
pub req_initiated: u64,
|
|
pub req_deduplicated: u64,
|
|
pub req_backoff_suppressed: u64,
|
|
pub req_forward_rate_limited: u64,
|
|
pub req_bloom_miss: u64,
|
|
pub req_no_tree_peer: u64,
|
|
pub req_fallback_forwarded: u64,
|
|
pub resp_received: u64,
|
|
pub resp_decode_error: u64,
|
|
pub resp_forwarded: u64,
|
|
pub resp_identity_miss: u64,
|
|
pub resp_proof_failed: u64,
|
|
pub resp_no_route: u64,
|
|
pub resp_accepted: u64,
|
|
pub resp_timed_out: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct TreeStatsSnapshot {
|
|
pub received: u64,
|
|
pub decode_error: u64,
|
|
pub unknown_peer: u64,
|
|
pub addr_mismatch: u64,
|
|
pub sig_failed: u64,
|
|
pub stale: u64,
|
|
pub ancestry_invalid: u64,
|
|
pub accepted: u64,
|
|
pub parent_switched: u64,
|
|
pub loop_detected: u64,
|
|
pub ancestry_changed: u64,
|
|
pub sent: u64,
|
|
pub rate_limited: u64,
|
|
pub send_failed: u64,
|
|
pub outbound_sign_failed: u64,
|
|
pub parent_switches: u64,
|
|
pub parent_losses: u64,
|
|
pub flap_dampened: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct BloomStatsSnapshot {
|
|
pub received: u64,
|
|
pub decode_error: u64,
|
|
pub invalid: u64,
|
|
pub non_v1: u64,
|
|
pub unknown_peer: u64,
|
|
pub stale: u64,
|
|
pub fill_exceeded: u64,
|
|
pub accepted: u64,
|
|
pub sent: u64,
|
|
pub debounce_suppressed: u64,
|
|
pub send_failed: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct SessionStatsSnapshot {
|
|
pub unknown_session: u64,
|
|
pub bad_state: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct HandshakeStatsSnapshot {
|
|
pub bad_state: u64,
|
|
pub unknown_connection: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct MmpStatsSnapshot {
|
|
pub decode_error: u64,
|
|
pub unknown_peer: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct TransportStatsSnapshot {
|
|
pub inbound_cap_exceeded: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct ErrorSignalStatsSnapshot {
|
|
pub coords_required: u64,
|
|
pub path_broken: u64,
|
|
pub mtu_exceeded: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct CongestionStatsSnapshot {
|
|
pub ce_forwarded: u64,
|
|
pub ce_received: u64,
|
|
pub congestion_detected: u64,
|
|
pub kernel_drop_events: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct NodeStatsSnapshot {
|
|
pub forwarding: ForwardingStatsSnapshot,
|
|
pub discovery: DiscoveryStatsSnapshot,
|
|
pub tree: TreeStatsSnapshot,
|
|
pub bloom: BloomStatsSnapshot,
|
|
pub session: SessionStatsSnapshot,
|
|
pub handshake: HandshakeStatsSnapshot,
|
|
pub mmp: MmpStatsSnapshot,
|
|
pub transport: TransportStatsSnapshot,
|
|
pub errors: ErrorSignalStatsSnapshot,
|
|
pub congestion: CongestionStatsSnapshot,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn tree_stats_record_reject_ancestry_invalid() {
|
|
let mut stats = TreeStats::default();
|
|
stats.record_reject(TreeReject::AncestryInvalid);
|
|
stats.record_reject(TreeReject::AncestryInvalid);
|
|
assert_eq!(stats.ancestry_invalid, 2);
|
|
assert_eq!(stats.outbound_sign_failed, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn tree_stats_record_reject_outbound_sign_failed() {
|
|
let mut stats = TreeStats::default();
|
|
stats.record_reject(TreeReject::OutboundSignFailed);
|
|
stats.record_reject(TreeReject::OutboundSignFailed);
|
|
assert_eq!(stats.outbound_sign_failed, 2);
|
|
assert_eq!(stats.ancestry_invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_to_tree() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
|
|
assert_eq!(stats.tree.outbound_sign_failed, 1);
|
|
assert_eq!(stats.tree.ancestry_invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn session_stats_record_reject_unknown_session() {
|
|
let mut stats = SessionStats::default();
|
|
stats.record_reject(SessionReject::UnknownSession);
|
|
stats.record_reject(SessionReject::UnknownSession);
|
|
assert_eq!(stats.unknown_session, 2);
|
|
assert_eq!(stats.bad_state, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn session_stats_record_reject_bad_state() {
|
|
let mut stats = SessionStats::default();
|
|
stats.record_reject(SessionReject::BadState);
|
|
stats.record_reject(SessionReject::BadState);
|
|
assert_eq!(stats.bad_state, 2);
|
|
assert_eq!(stats.unknown_session, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_to_session() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Session(SessionReject::UnknownSession));
|
|
stats.record_reject(RejectReason::Session(SessionReject::BadState));
|
|
assert_eq!(stats.session.unknown_session, 1);
|
|
assert_eq!(stats.session.bad_state, 1);
|
|
assert_eq!(stats.tree.ancestry_invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn handshake_stats_record_reject_bad_state() {
|
|
let mut stats = HandshakeStats::default();
|
|
stats.record_reject(HandshakeReject::BadState);
|
|
stats.record_reject(HandshakeReject::BadState);
|
|
stats.record_reject(HandshakeReject::BadState);
|
|
assert_eq!(stats.bad_state, 3);
|
|
assert_eq!(stats.unknown_connection, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn handshake_stats_record_reject_unknown_connection() {
|
|
let mut stats = HandshakeStats::default();
|
|
stats.record_reject(HandshakeReject::UnknownConnection);
|
|
stats.record_reject(HandshakeReject::UnknownConnection);
|
|
assert_eq!(stats.unknown_connection, 2);
|
|
assert_eq!(stats.bad_state, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_to_handshake() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
|
|
stats.record_reject(RejectReason::Handshake(HandshakeReject::UnknownConnection));
|
|
stats.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
|
|
assert_eq!(stats.handshake.bad_state, 2);
|
|
assert_eq!(stats.handshake.unknown_connection, 1);
|
|
assert_eq!(stats.session.unknown_session, 0);
|
|
assert_eq!(stats.tree.ancestry_invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn bloom_stats_record_reject_decode_error() {
|
|
let mut s = BloomStats::default();
|
|
s.record_reject(BloomReject::DecodeError);
|
|
s.record_reject(BloomReject::DecodeError);
|
|
assert_eq!(s.decode_error, 2);
|
|
assert_eq!(s.invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn bloom_stats_record_reject_invalid() {
|
|
let mut s = BloomStats::default();
|
|
s.record_reject(BloomReject::Invalid);
|
|
assert_eq!(s.invalid, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn bloom_stats_record_reject_non_v1() {
|
|
let mut s = BloomStats::default();
|
|
s.record_reject(BloomReject::NonV1);
|
|
assert_eq!(s.non_v1, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn bloom_stats_record_reject_unknown_peer() {
|
|
let mut s = BloomStats::default();
|
|
s.record_reject(BloomReject::UnknownPeer);
|
|
assert_eq!(s.unknown_peer, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn bloom_stats_record_reject_stale() {
|
|
let mut s = BloomStats::default();
|
|
s.record_reject(BloomReject::Stale);
|
|
assert_eq!(s.stale, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn bloom_stats_record_reject_fill_exceeded() {
|
|
let mut s = BloomStats::default();
|
|
s.record_reject(BloomReject::FillExceeded);
|
|
assert_eq!(s.fill_exceeded, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_to_bloom() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Bloom(BloomReject::DecodeError));
|
|
stats.record_reject(RejectReason::Bloom(BloomReject::Stale));
|
|
assert_eq!(stats.bloom.decode_error, 1);
|
|
assert_eq!(stats.bloom.stale, 1);
|
|
assert_eq!(stats.tree.ancestry_invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn discovery_stats_record_reject_req_decode_error() {
|
|
let mut s = DiscoveryStats::default();
|
|
s.record_reject(DiscoveryReject::ReqDecodeError);
|
|
assert_eq!(s.req_decode_error, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn discovery_stats_record_reject_req_duplicate() {
|
|
let mut s = DiscoveryStats::default();
|
|
s.record_reject(DiscoveryReject::ReqDuplicate);
|
|
assert_eq!(s.req_duplicate, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn discovery_stats_record_reject_req_ttl_exhausted() {
|
|
let mut s = DiscoveryStats::default();
|
|
s.record_reject(DiscoveryReject::ReqTtlExhausted);
|
|
assert_eq!(s.req_ttl_exhausted, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn discovery_stats_record_reject_resp_decode_error() {
|
|
let mut s = DiscoveryStats::default();
|
|
s.record_reject(DiscoveryReject::RespDecodeError);
|
|
assert_eq!(s.resp_decode_error, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn discovery_stats_record_reject_resp_identity_miss() {
|
|
let mut s = DiscoveryStats::default();
|
|
s.record_reject(DiscoveryReject::RespIdentityMiss);
|
|
assert_eq!(s.resp_identity_miss, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn discovery_stats_record_reject_resp_proof_failed() {
|
|
let mut s = DiscoveryStats::default();
|
|
s.record_reject(DiscoveryReject::RespProofFailed);
|
|
assert_eq!(s.resp_proof_failed, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_to_discovery() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Discovery(DiscoveryReject::ReqDecodeError));
|
|
stats.record_reject(RejectReason::Discovery(DiscoveryReject::RespProofFailed));
|
|
assert_eq!(stats.discovery.req_decode_error, 1);
|
|
assert_eq!(stats.discovery.resp_proof_failed, 1);
|
|
assert_eq!(stats.tree.ancestry_invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn forwarding_stats_record_reject_decode_error() {
|
|
let mut s = ForwardingStats::default();
|
|
s.record_reject(ForwardingReject::DecodeError);
|
|
assert_eq!(s.decode_error_packets, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn forwarding_stats_record_reject_ttl_exhausted() {
|
|
let mut s = ForwardingStats::default();
|
|
s.record_reject(ForwardingReject::TtlExhausted);
|
|
assert_eq!(s.ttl_exhausted_packets, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn forwarding_stats_record_reject_no_route() {
|
|
let mut s = ForwardingStats::default();
|
|
s.record_reject(ForwardingReject::NoRoute);
|
|
assert_eq!(s.drop_no_route_packets, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn forwarding_stats_record_reject_mtu_exceeded() {
|
|
let mut s = ForwardingStats::default();
|
|
s.record_reject(ForwardingReject::MtuExceeded);
|
|
assert_eq!(s.drop_mtu_exceeded_packets, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn forwarding_stats_record_reject_send_error() {
|
|
let mut s = ForwardingStats::default();
|
|
s.record_reject(ForwardingReject::SendError);
|
|
assert_eq!(s.drop_send_error_packets, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_to_forwarding() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Forwarding(ForwardingReject::NoRoute));
|
|
stats.record_reject(RejectReason::Forwarding(ForwardingReject::MtuExceeded));
|
|
assert_eq!(stats.forwarding.drop_no_route_packets, 1);
|
|
assert_eq!(stats.forwarding.drop_mtu_exceeded_packets, 1);
|
|
assert_eq!(stats.tree.ancestry_invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn mmp_stats_record_reject_decode_error() {
|
|
let mut s = MmpStats::default();
|
|
s.record_reject(MmpReject::DecodeError);
|
|
s.record_reject(MmpReject::DecodeError);
|
|
assert_eq!(s.decode_error, 2);
|
|
assert_eq!(s.unknown_peer, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn mmp_stats_record_reject_unknown_peer() {
|
|
let mut s = MmpStats::default();
|
|
s.record_reject(MmpReject::UnknownPeer);
|
|
assert_eq!(s.unknown_peer, 1);
|
|
assert_eq!(s.decode_error, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_to_mmp() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Mmp(MmpReject::DecodeError));
|
|
stats.record_reject(RejectReason::Mmp(MmpReject::UnknownPeer));
|
|
assert_eq!(stats.mmp.decode_error, 1);
|
|
assert_eq!(stats.mmp.unknown_peer, 1);
|
|
assert_eq!(stats.tree.ancestry_invalid, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn transport_stats_record_reject_inbound_cap_exceeded() {
|
|
let mut s = TransportStats::default();
|
|
s.record_reject(TransportReject::InboundCapExceeded);
|
|
s.record_reject(TransportReject::InboundCapExceeded);
|
|
assert_eq!(s.inbound_cap_exceeded, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_to_transport() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Transport(TransportReject::InboundCapExceeded));
|
|
assert_eq!(stats.transport.inbound_cap_exceeded, 1);
|
|
assert_eq!(stats.tree.ancestry_invalid, 0);
|
|
}
|
|
}
|