mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
A session rekey responder that has sent its SessionAck holds the only handshake the initiator's msg3 can complete: by then the initiator has read the SessionAck and holds the new keys. Two unauthenticated messages could discard that handshake inside the window. The responder arm of the SessionMsg3 handler abandoned it when the msg3 read failed, though nothing authenticates a msg3 before that read beyond a source address the sender chooses. And a setup message naming the peer while the handshake was armed ran the dual-initiation tie-break, which at the larger address abandoned the handshake to answer the setup. Either way the genuine msg3 and its resends found no handshake, the initiator cut over to keys this node never derived, and frames from it stopped decoding until a later rekey completed. The initial-handshake arm had the same flaw: it removed the half-open entry to read msg3 and did not put it back when the read failed, so a garbage msg3 naming the initiator, arriving ahead of the genuine one, left the genuine msg3 to an unknown session. The Noise handshake gains a rolling-back msg3 read, shaped like the msg2 one, and both msg3 arms use it and put the handshake back on a failed read. The rollback matters because the read advances the cipher nonce before it opens the first AEAD, so a handshake put back after a plain read could never read the genuine msg3. The restore leaves the rekey deadline, which runs from the peer's setup, and the half-open entry's activity stamp unchanged, so a spray cannot hold a handshake open. The abandons and drops after a successful read stay: each follows a read that authenticated the sender. A setup message is now dropped, at either address, while a handshake the peer armed awaits its msg3, and counted as rekey_held; the tie-break runs only when this node initiated the armed handshake. The cost is that a genuine retry from a peer that abandoned the rekey this node answered completes one handshake timeout later, once the held handshake expires, as it already did at the smaller address. New tests drive a genuine rekey to the point where the initiator has read the SessionAck, deliver a garbage msg3 or, at the larger-address end, a forged setup, and follow the genuine msg3, the cutover, the msg3 resend budget and the next rekey cycle, asserting the forged setup was held off by the armed handshake. Unit tests check that a forged msg3 leaves a stranger-armed handshake able to read the msg3 that finishes it and a peer-armed one with its deadline unchanged, that a second setup leaves a peer-armed handshake and the completed epoch intact, and that a forged initial msg3 leaves the half-open entry and its activity stamp untouched. Noise tests cover the rolling-back read against a msg3 failing at either AEAD, with a control showing the plain read cannot recover. The retry tests for a responder holding a stale handshake assert rekey_held rather than the tie-break counters. No wire format change.
694 lines
26 KiB
Rust
694 lines
26 KiB
Rust
//! Node-level statistics for the session, handshake, mmp, and transport
|
|
//! families. The forwarding, discovery, tree, bloom, congestion, and error
|
|
//! families have migrated to the atomic
|
|
//! [`MetricsRegistry`](crate::node::metrics::MetricsRegistry).
|
|
//!
|
|
//! 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::{
|
|
HandshakeReject, MmpReject, RejectReason, SessionReject, TransportReject,
|
|
};
|
|
|
|
/// 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,
|
|
/// Inbound XK msg3 whose initiator static key does not derive the
|
|
/// source address the datagram claimed. The half-open session is
|
|
/// dropped and no identity is registered.
|
|
pub addr_mismatch: u64,
|
|
/// Inbound rekey XK msg3 whose initiator static key differs from
|
|
/// the key the session was established with. The rekey is
|
|
/// abandoned and the existing session is left intact.
|
|
pub rekey_key_mismatch: u64,
|
|
/// A setup message naming an already-established peer armed a
|
|
/// responder-side handshake alongside the running session and a
|
|
/// SessionAck was sent. The message carries no authenticator, so this
|
|
/// counts genuine peer restarts and forged setups alike; its rate is
|
|
/// the signal that something is spraying setup messages, which the
|
|
/// per-message DEBUG line cannot carry safely at line rate.
|
|
pub rekey_armed: u64,
|
|
/// A setup message named an established peer while our own rekey of
|
|
/// that session was in flight and our address sorted smaller, so the
|
|
/// tie-break dropped their msg1 and kept us as initiator.
|
|
pub rekey_tiebreak: u64,
|
|
/// A setup message named an established peer while our own rekey of
|
|
/// that session was in flight and our address sorted larger, so we
|
|
/// abandoned our own rekey and answered as responder. A sustained
|
|
/// rate here means local key rotation is being suppressed.
|
|
pub rekey_yielded: u64,
|
|
/// A setup message named an established peer while a handshake that
|
|
/// peer armed was still waiting for its msg3, so the message was dropped
|
|
/// and the handshake kept. A genuine retry lands here when the peer
|
|
/// abandoned a rekey this node answered; a sustained rate means setups
|
|
/// are being sprayed at the session.
|
|
pub rekey_held: u64,
|
|
/// A setup message named an established peer that already holds a
|
|
/// completed rekey awaiting cut-over, so the message was dropped
|
|
/// rather than arming a second handshake.
|
|
pub rekey_pending: u64,
|
|
/// A responder-side handshake armed by a peer's setup message passed
|
|
/// the handshake timeout without a msg3 and was discarded. The
|
|
/// established session is retained.
|
|
pub rekey_expired: u64,
|
|
/// A rekey this node initiated got no readable SessionAck within the
|
|
/// handshake timeout, and its handshake was discarded so the trigger
|
|
/// can retry. The established session is retained. A sustained rate
|
|
/// means setups or acks to that peer are being lost, or the peer holds
|
|
/// a stuck handshake of its own and wins the tie-break.
|
|
pub rekey_unanswered: u64,
|
|
/// A completed rekey session still waiting for the peer's cut-over was
|
|
/// replaced by a newer one, completed from a msg3 carrying the same
|
|
/// authenticated peer key. This drops key material the peer may
|
|
/// already have adopted, so a sustained rate means one side keeps
|
|
/// rekeying while the other never appears on the new epoch.
|
|
pub pending_replaced: u64,
|
|
/// An inbound SessionAck failed the XK msg2 read against a session we
|
|
/// are initiating. The entry is kept and the handshake rolled back,
|
|
/// since the message authenticates nothing; a sustained rate is either
|
|
/// a broken path to the responder or forged acks holding establishment
|
|
/// down.
|
|
pub ack_handshake_failed: u64,
|
|
/// A setup message was refused by the per-link-peer setup limiter,
|
|
/// before any handshake state was created or any ack sent.
|
|
pub setup_rate_limited: u64,
|
|
/// A session would have been created but the table is at
|
|
/// `node.limits.max_sessions`. A sustained rate means either the cap
|
|
/// is sized below what this node legitimately carries, or something is
|
|
/// holding the table full.
|
|
pub table_full: u64,
|
|
/// A session would have been created but unauthenticated half-open
|
|
/// entries already hold their share of the table.
|
|
pub half_open_full: u64,
|
|
}
|
|
|
|
impl SessionStats {
|
|
pub fn snapshot(&self) -> SessionStatsSnapshot {
|
|
SessionStatsSnapshot {
|
|
unknown_session: self.unknown_session,
|
|
bad_state: self.bad_state,
|
|
addr_mismatch: self.addr_mismatch,
|
|
rekey_key_mismatch: self.rekey_key_mismatch,
|
|
rekey_armed: self.rekey_armed,
|
|
rekey_tiebreak: self.rekey_tiebreak,
|
|
rekey_yielded: self.rekey_yielded,
|
|
rekey_held: self.rekey_held,
|
|
rekey_pending: self.rekey_pending,
|
|
rekey_expired: self.rekey_expired,
|
|
rekey_unanswered: self.rekey_unanswered,
|
|
pending_replaced: self.pending_replaced,
|
|
ack_handshake_failed: self.ack_handshake_failed,
|
|
setup_rate_limited: self.setup_rate_limited,
|
|
table_full: self.table_full,
|
|
half_open_full: self.half_open_full,
|
|
}
|
|
}
|
|
|
|
pub(super) fn record_reject(&mut self, reason: SessionReject) {
|
|
match reason {
|
|
SessionReject::UnknownSession => self.unknown_session += 1,
|
|
SessionReject::BadState => self.bad_state += 1,
|
|
SessionReject::AddrMismatch => self.addr_mismatch += 1,
|
|
SessionReject::RekeyKeyMismatch => self.rekey_key_mismatch += 1,
|
|
SessionReject::RekeyTiebreak => self.rekey_tiebreak += 1,
|
|
SessionReject::RekeyYielded => self.rekey_yielded += 1,
|
|
SessionReject::RekeyHeld => self.rekey_held += 1,
|
|
SessionReject::RekeyPending => self.rekey_pending += 1,
|
|
SessionReject::AckHandshakeFailed => self.ack_handshake_failed += 1,
|
|
SessionReject::SetupRateLimited => self.setup_rate_limited += 1,
|
|
SessionReject::TableFull => self.table_full += 1,
|
|
SessionReject::HalfOpenFull => self.half_open_full += 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.
|
|
///
|
|
/// `payload_len_mismatch` is different in kind: it counts a framing
|
|
/// drop the node itself performs at the receive dispatch point, so it
|
|
/// has a live writer and can be non-zero on a running node.
|
|
#[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,
|
|
/// Inbound FMP frames dropped because the payload length declared
|
|
/// in the common prefix did not match the frame the transport
|
|
/// delivered.
|
|
pub payload_len_mismatch: u64,
|
|
}
|
|
|
|
impl TransportStats {
|
|
pub fn snapshot(&self) -> TransportStatsSnapshot {
|
|
TransportStatsSnapshot {
|
|
inbound_cap_exceeded: self.inbound_cap_exceeded,
|
|
payload_len_mismatch: self.payload_len_mismatch,
|
|
}
|
|
}
|
|
|
|
pub(super) fn record_reject(&mut self, reason: TransportReject) {
|
|
match reason {
|
|
TransportReject::InboundCapExceeded => self.inbound_cap_exceeded += 1,
|
|
TransportReject::PayloadLenMismatch => self.payload_len_mismatch += 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Aggregate node statistics.
|
|
///
|
|
/// Holds only the families that have not migrated to the atomic
|
|
/// [`MetricsRegistry`](crate::node::metrics::MetricsRegistry): session,
|
|
/// handshake, mmp, and transport. The forwarding, discovery, tree,
|
|
/// bloom, congestion, and error families are served exclusively from
|
|
/// the registry.
|
|
#[derive(Default)]
|
|
pub struct NodeStats {
|
|
pub session: SessionStats,
|
|
pub handshake: HandshakeStats,
|
|
pub mmp: MmpStats,
|
|
pub transport: TransportStats,
|
|
}
|
|
|
|
impl NodeStats {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Record a typed rejection from a silent-rejection site.
|
|
///
|
|
/// Dispatches to the appropriate sub-stats `record_reject` based on
|
|
/// the [`RejectReason`] top-level variant. Only the families still
|
|
/// stored on `NodeStats` (session, handshake, mmp, transport) are
|
|
/// routed here; the migrated families are recorded directly on the
|
|
/// [`MetricsRegistry`](crate::node::metrics::MetricsRegistry).
|
|
pub fn record_reject(&mut self, reason: RejectReason) {
|
|
match reason {
|
|
RejectReason::Session(r) => self.session.record_reject(r),
|
|
RejectReason::Handshake(r) => self.handshake.record_reject(r),
|
|
RejectReason::Transport(r) => self.transport.record_reject(r),
|
|
RejectReason::Mmp(r) => self.mmp.record_reject(r),
|
|
// The forwarding, discovery, tree, and bloom families are
|
|
// recorded directly on the MetricsRegistry and never reach
|
|
// this NodeStats dispatch.
|
|
RejectReason::Forwarding(_)
|
|
| RejectReason::Discovery(_)
|
|
| RejectReason::Tree(_)
|
|
| RejectReason::Bloom(_) => {
|
|
debug_assert!(false, "migrated reject family must use MetricsRegistry");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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 warm_malformed_packets: u64,
|
|
pub warm_malformed_bytes: u64,
|
|
pub coord_warm_foreign_root: u64,
|
|
pub coord_warm_key_mismatch: u64,
|
|
pub coord_hint_changed: u64,
|
|
pub coord_hint_rejected: 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,
|
|
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)]
|
|
pub struct LookupStatsSnapshot {
|
|
pub req_received: u64,
|
|
pub req_decode_error: u64,
|
|
pub req_duplicate: u64,
|
|
pub req_own_loopback: u64,
|
|
pub req_dedup_cache_full: u64,
|
|
pub req_dedup_evicted: u64,
|
|
pub req_sign_rate_limited: 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_unsolicited: 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 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,
|
|
pub addr_mismatch: u64,
|
|
pub rekey_key_mismatch: u64,
|
|
pub rekey_armed: u64,
|
|
pub rekey_tiebreak: u64,
|
|
pub rekey_yielded: u64,
|
|
pub rekey_held: u64,
|
|
pub rekey_pending: u64,
|
|
pub rekey_expired: u64,
|
|
pub rekey_unanswered: u64,
|
|
pub pending_replaced: u64,
|
|
pub ack_handshake_failed: u64,
|
|
pub setup_rate_limited: u64,
|
|
pub table_full: u64,
|
|
pub half_open_full: 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,
|
|
pub payload_len_mismatch: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct ErrorSignalStatsSnapshot {
|
|
pub coords_required: u64,
|
|
pub path_broken: u64,
|
|
pub mtu_exceeded: u64,
|
|
pub path_mtu_notif_below_floor: u64,
|
|
pub mtu_exceeded_below_floor: u64,
|
|
pub lookup_resp_mtu_below_floor: u64,
|
|
pub unbound_coords: u64,
|
|
pub unbound_broken: u64,
|
|
pub unbound_mtu: u64,
|
|
pub unbound_forged: u64,
|
|
pub emit_over_peer_budget: u64,
|
|
pub emit_over_dest_interval: u64,
|
|
pub emit_limiter_at_capacity: u64,
|
|
pub mtu_exceeded_uncorroborated: 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,
|
|
}
|
|
|
|
/// Native datagram API counters.
|
|
///
|
|
/// Eight of the `drop_*` fields are one per
|
|
/// [`DropReason`](crate::native::link::DropReason) variant, so a variant added
|
|
/// later has nowhere to hide. `drop_oversize` is the ninth and has no reason
|
|
/// because it is refused on the send side, before the registry sees the
|
|
/// datagram.
|
|
///
|
|
/// Present on every platform even though the API builds only on Linux and
|
|
/// FreeBSD, so `show_metrics` carries one schema everywhere and a consumer does
|
|
/// not branch on the host.
|
|
#[derive(Clone, Debug, Default, Serialize)]
|
|
pub struct NativeStatsSnapshot {
|
|
pub flows_opened: u64,
|
|
pub flows_accepted: u64,
|
|
pub flows_closed: u64,
|
|
pub flows_expired: u64,
|
|
pub sent_datagrams: u64,
|
|
pub sent_bytes: u64,
|
|
pub received_datagrams: u64,
|
|
pub received_bytes: u64,
|
|
pub drop_no_port: u64,
|
|
pub drop_backlog_full: u64,
|
|
pub drop_too_many_flows: u64,
|
|
pub drop_pending_queue_full: u64,
|
|
pub drop_flow_queue_full: u64,
|
|
pub drop_arrival_queue_full: u64,
|
|
pub drop_listener_not_reading: u64,
|
|
pub drop_listener_gone: u64,
|
|
pub drop_oversize: u64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[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 session_stats_record_reject_addr_mismatch() {
|
|
let mut stats = SessionStats::default();
|
|
stats.record_reject(SessionReject::AddrMismatch);
|
|
stats.record_reject(SessionReject::AddrMismatch);
|
|
assert_eq!(stats.addr_mismatch, 2);
|
|
assert_eq!(stats.rekey_key_mismatch, 0);
|
|
assert_eq!(stats.unknown_session, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn session_stats_record_reject_rekey_key_mismatch() {
|
|
let mut stats = SessionStats::default();
|
|
stats.record_reject(SessionReject::RekeyKeyMismatch);
|
|
assert_eq!(stats.rekey_key_mismatch, 1);
|
|
assert_eq!(stats.addr_mismatch, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn session_stats_record_reject_separates_the_four_rekey_arming_refusals() {
|
|
let mut stats = SessionStats::default();
|
|
stats.record_reject(SessionReject::RekeyTiebreak);
|
|
stats.record_reject(SessionReject::RekeyYielded);
|
|
stats.record_reject(SessionReject::RekeyYielded);
|
|
stats.record_reject(SessionReject::RekeyHeld);
|
|
stats.record_reject(SessionReject::RekeyHeld);
|
|
stats.record_reject(SessionReject::RekeyHeld);
|
|
stats.record_reject(SessionReject::RekeyPending);
|
|
assert_eq!(stats.rekey_tiebreak, 1);
|
|
assert_eq!(stats.rekey_yielded, 2);
|
|
assert_eq!(stats.rekey_held, 3);
|
|
assert_eq!(stats.rekey_pending, 1);
|
|
assert_eq!(stats.rekey_armed, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn session_stats_snapshot_carries_the_rekey_arming_counters() {
|
|
let mut stats = SessionStats::default();
|
|
stats.record_reject(SessionReject::RekeyTiebreak);
|
|
stats.record_reject(SessionReject::RekeyHeld);
|
|
stats.rekey_armed = 7;
|
|
stats.rekey_expired = 3;
|
|
stats.pending_replaced = 2;
|
|
let snap = stats.snapshot();
|
|
assert_eq!(snap.rekey_tiebreak, 1);
|
|
assert_eq!(snap.rekey_held, 1);
|
|
assert_eq!(snap.rekey_armed, 7);
|
|
assert_eq!(snap.rekey_expired, 3);
|
|
assert_eq!(snap.pending_replaced, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn session_stats_snapshot_carries_identity_binding_counters() {
|
|
let mut stats = SessionStats::default();
|
|
stats.record_reject(SessionReject::AddrMismatch);
|
|
stats.record_reject(SessionReject::RekeyKeyMismatch);
|
|
stats.record_reject(SessionReject::RekeyKeyMismatch);
|
|
let snap = stats.snapshot();
|
|
assert_eq!(snap.addr_mismatch, 1);
|
|
assert_eq!(snap.rekey_key_mismatch, 2);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
/// Records both transport reasons so a swapped or shared arm shows up
|
|
/// as a mis-attributed counter rather than as a plausible total.
|
|
#[test]
|
|
fn transport_stats_record_reject_keeps_the_two_reasons_on_separate_counters() {
|
|
let mut s = TransportStats::default();
|
|
s.record_reject(TransportReject::PayloadLenMismatch);
|
|
s.record_reject(TransportReject::PayloadLenMismatch);
|
|
s.record_reject(TransportReject::InboundCapExceeded);
|
|
assert_eq!(s.payload_len_mismatch, 2);
|
|
assert_eq!(s.inbound_cap_exceeded, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn node_stats_record_reject_dispatches_payload_len_mismatch_to_transport() {
|
|
let mut stats = NodeStats::new();
|
|
stats.record_reject(RejectReason::Transport(TransportReject::PayloadLenMismatch));
|
|
assert_eq!(stats.transport.payload_len_mismatch, 1);
|
|
assert_eq!(stats.transport.inbound_cap_exceeded, 0);
|
|
assert_eq!(stats.handshake.bad_state, 0);
|
|
}
|
|
}
|