diff --git a/src/node/mod.rs b/src/node/mod.rs index 730ab3b2..23a3d64f 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -19,6 +19,7 @@ mod lifecycle; pub(crate) mod metrics; pub(crate) mod netmon; pub use netmon::NetmonTrigger; +pub(crate) mod path_mtu; mod peer_error_budget; mod peering; mod rate_limit; diff --git a/src/node/path_mtu.rs b/src/node/path_mtu.rs new file mode 100644 index 00000000..dfe1fc01 --- /dev/null +++ b/src/node/path_mtu.rs @@ -0,0 +1,53 @@ +//! Per-destination path MTU state shared with the TCP MSS clamp. +//! +//! Every writer, the release paths and the expiry pass live in the node; +//! the TUN reader and writer threads only read the map. + +use crate::FipsAddress; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +/// One `path_mtu_lookup` entry: the MTU the TCP MSS clamp reads, plus how +/// the entry is released. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PathMtuEntry { + /// Path MTU in bytes. + pub mtu: u16, + /// Unix ms at which a discovery `LookupResponse` supplied this value, or + /// `None` for an entry that some event releases instead of a timer. + /// + /// The discovery carrier is the one with no release path: it writes an + /// entry for a destination this node may never open a session with, and + /// all three callers of `path_mtu_lookup_release` fire on session state. + /// A link MTU this node derived from its own transport, and a value + /// learned inside a session, are both released by an event that says the + /// thing they describe is gone, so they carry no deadline. + pub learned_ms: Option, +} + +impl PathMtuEntry { + /// An entry released by an event rather than a timer: a locally derived + /// link MTU, or a value learned inside a session. + pub fn held(mtu: u16) -> Self { + Self { + mtu, + learned_ms: None, + } + } + + /// A remote party's claim stored at `at_ms` for a destination with no + /// other release path. Expires. + pub fn learned(mtu: u16, at_ms: u64) -> Self { + Self { + mtu, + learned_ms: Some(at_ms), + } + } +} + +/// Read-only handle to the per-destination path MTU map. Populated by +/// the discovery handler on `LookupResponse`; read by the TUN reader +/// (outbound clamp) and writer (inbound clamp) at TCP MSS clamp time. +/// Keyed by [`FipsAddress`] (16 bytes, the IPv6 form of a fips peer +/// address). +pub type PathMtuLookup = Arc>>; diff --git a/src/proto/framing.rs b/src/proto/framing.rs new file mode 100644 index 00000000..9b00b629 --- /dev/null +++ b/src/proto/framing.rs @@ -0,0 +1,37 @@ +//! Encapsulation overhead spanning the FMP and FSP layers. +//! +//! The framing cost of a session datagram is the sum of FMP link framing and +//! FSP session framing, so it belongs to neither layer's wire module alone. + +/// FIPS base encapsulation overhead for DataPacket (excluding port payload). +/// +/// This is the fixed overhead for a SessionDatagram carrying an FSP DataPacket, +/// used by the send path's CP-flag guard to check whether piggybacked coords +/// fit within the transport MTU. For IPv6 effective MTU calculations, use +/// [`FIPS_IPV6_OVERHEAD`] which accounts for port multiplexing and header +/// compression. +/// +/// Breakdown (traced through the actual send path): +/// +/// ```text +/// FMP outer header (cleartext AAD) 16 +/// common prefix (4) + receiver_idx (4) + counter (8) +/// FMP AEAD ciphertext: +/// timestamp (4) + msg_type (1) 5 [FMP inner header] +/// ttl (1) + path_mtu (2) + src (16) + dst (16) 35 [SessionDatagram body] +/// FSP header (4 prefix + 8 counter) 12 [cleartext AAD] +/// FSP AEAD ciphertext: +/// timestamp (4) + msg_type (1) + flags (1) 6 [FSP inner header] +/// +/// Poly1305 tag 16 [FSP AEAD] +/// FMP Poly1305 tag 16 [FMP AEAD] +/// ──── +/// 106 +/// ``` +/// +/// Note: the FMP inner header msg_type byte IS the SessionDatagram msg_type +/// byte (shared, not double-counted). The "35 bytes" is the SessionDatagram +/// body after msg_type is consumed by the dispatch layer. +/// +/// [`FIPS_IPV6_OVERHEAD`]: crate::upper::icmp::FIPS_IPV6_OVERHEAD +pub const FIPS_OVERHEAD: u16 = 16 + 16 + 5 + 35 + 12 + 6 + 16; // 106 bytes diff --git a/src/proto/mmp/limits.rs b/src/proto/mmp/limits.rs index 55ee4172..6417a9b6 100644 --- a/src/proto/mmp/limits.rs +++ b/src/proto/mmp/limits.rs @@ -82,3 +82,19 @@ pub const SESSION_COLD_START_INTERVAL_MS: u64 = 1_000; /// /// [`mss_ceiling`]: crate::upper::icmp::mss_ceiling pub const MIN_ACTIONABLE_PATH_MTU: u16 = 256; + +/// Smallest path MTU this node will act on when the claim arrives on the +/// unauthenticated reactive carrier, `MtuExceeded`. +/// +/// Held equal to [`MIN_ACTIONABLE_PATH_MTU`] so no hop legitimately configured +/// with a small transport MTU loses reactive feedback. It is a separate +/// constant because the two carriers differ in what they prove: the +/// authenticated `PathMtuNotification` and the proof-carrying discovery +/// response come from a party this node has verified, whereas this one comes +/// from whoever could route a datagram here. What keeps a legal-but-forged +/// claim from pinning a session is corroboration against what this node has +/// actually sent, not this floor. Raising it (576 is the value the original +/// path-MTU floor design proposed, and derives an inner IPv6 MTU of 499) +/// bounds the outcome of an uncorroborated claim further, at the cost of +/// ignoring an honest report from any hop configured between the two values. +pub const MIN_REACTIVE_PATH_MTU: u16 = MIN_ACTIONABLE_PATH_MTU; diff --git a/src/proto/mmp/mod.rs b/src/proto/mmp/mod.rs index d5f9a9b8..ded473b3 100644 --- a/src/proto/mmp/mod.rs +++ b/src/proto/mmp/mod.rs @@ -85,6 +85,6 @@ impl fmt::Display for MmpMode { pub use limits::{ COLD_START_SAMPLES, DEFAULT_COLD_START_INTERVAL_MS, DEFAULT_LOG_INTERVAL_SECS, DEFAULT_OWD_WINDOW_SIZE, EWMA_LONG_ALPHA, EWMA_SHORT_ALPHA, MAX_REPORT_INTERVAL_MS, - MAX_SESSION_REPORT_INTERVAL_MS, MIN_ACTIONABLE_PATH_MTU, MIN_REPORT_INTERVAL_MS, - MIN_SESSION_REPORT_INTERVAL_MS, SESSION_COLD_START_INTERVAL_MS, + MAX_SESSION_REPORT_INTERVAL_MS, MIN_ACTIONABLE_PATH_MTU, MIN_REACTIVE_PATH_MTU, + MIN_REPORT_INTERVAL_MS, MIN_SESSION_REPORT_INTERVAL_MS, SESSION_COLD_START_INTERVAL_MS, }; diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 712a9fdf..3997eada 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod bloom; pub(crate) mod codec; pub(crate) mod coord; pub(crate) mod fmp; +pub(crate) mod framing; pub(crate) mod fsp; pub(crate) mod link; pub(crate) mod lookup; diff --git a/src/upper/icmp.rs b/src/upper/icmp.rs index fecfe8e5..080e2534 100644 --- a/src/upper/icmp.rs +++ b/src/upper/icmp.rs @@ -61,34 +61,9 @@ const MAX_ORIGINAL_PACKET: usize = MIN_IPV6_MTU - IPV6_HEADER_LEN - ICMPV6_HEADE /// FIPS base encapsulation overhead for DataPacket (excluding port payload). /// -/// This is the fixed overhead for a SessionDatagram carrying an FSP DataPacket, -/// used by the send path's CP-flag guard to check whether piggybacked coords -/// fit within the transport MTU. For IPv6 effective MTU calculations, use -/// [`FIPS_IPV6_OVERHEAD`] which accounts for port multiplexing and header -/// compression. -/// -/// Breakdown (traced through the actual send path): -/// -/// ```text -/// FMP outer header (cleartext AAD) 16 -/// common prefix (4) + receiver_idx (4) + counter (8) -/// FMP AEAD ciphertext: -/// timestamp (4) + msg_type (1) 5 [FMP inner header] -/// ttl (1) + path_mtu (2) + src (16) + dst (16) 35 [SessionDatagram body] -/// FSP header (4 prefix + 8 counter) 12 [cleartext AAD] -/// FSP AEAD ciphertext: -/// timestamp (4) + msg_type (1) + flags (1) 6 [FSP inner header] -/// -/// Poly1305 tag 16 [FSP AEAD] -/// FMP Poly1305 tag 16 [FMP AEAD] -/// ──── -/// 106 -/// ``` -/// -/// Note: the FMP inner header msg_type byte IS the SessionDatagram msg_type -/// byte (shared, not double-counted). The "35 bytes" is the SessionDatagram -/// body after msg_type is consumed by the dispatch layer. -pub const FIPS_OVERHEAD: u16 = 16 + 16 + 5 + 35 + 12 + 6 + 16; // 106 bytes +/// Re-exported: the value is FMP plus FSP framing and is defined with the +/// protocol layers, in [`crate::proto::framing::FIPS_OVERHEAD`]. +pub use crate::proto::framing::FIPS_OVERHEAD; /// FIPS encapsulation overhead for compressed IPv6 shim traffic (port 256). /// @@ -115,18 +90,10 @@ pub use crate::proto::mmp::MIN_ACTIONABLE_PATH_MTU; /// Smallest path MTU this node will act on when the claim arrives on the /// unauthenticated reactive carrier, `MtuExceeded`. /// -/// Held equal to [`MIN_ACTIONABLE_PATH_MTU`] so no hop legitimately configured -/// with a small transport MTU loses reactive feedback. It is a separate -/// constant because the two carriers differ in what they prove: the -/// authenticated `PathMtuNotification` and the proof-carrying discovery -/// response come from a party this node has verified, whereas this one comes -/// from whoever could route a datagram here. What keeps a legal-but-forged -/// claim from pinning a session is corroboration against what this node has -/// actually sent, not this floor. Raising it (576 is the value the original -/// path-MTU floor design proposed, and derives an inner IPv6 MTU of 499) -/// bounds the outcome of an uncorroborated claim further, at the cost of -/// ignoring an honest report from any hop configured between the two values. -pub const MIN_REACTIVE_PATH_MTU: u16 = MIN_ACTIONABLE_PATH_MTU; +/// Re-exported: a protocol policy decision defined beside +/// [`crate::proto::mmp::MIN_ACTIONABLE_PATH_MTU`], in +/// [`crate::proto::mmp::MIN_REACTIVE_PATH_MTU`]. +pub use crate::proto::mmp::MIN_REACTIVE_PATH_MTU; /// Calculate the effective IPv6 MTU for FIPS-encapsulated traffic. /// diff --git a/src/upper/tun.rs b/src/upper/tun.rs index cf08537d..08349def 100644 --- a/src/upper/tun.rs +++ b/src/upper/tun.rs @@ -14,7 +14,6 @@ use crate::FipsAddress; #[cfg(unix)] use crate::{FipsAddress, TunConfig}; -use std::collections::HashMap; #[cfg(unix)] use std::fs::File; #[cfg(unix)] @@ -25,7 +24,7 @@ use std::io::Write; use std::net::Ipv6Addr; #[cfg(unix)] use std::os::unix::io::{AsRawFd, FromRawFd}; -use std::sync::{Arc, RwLock, mpsc}; +use std::sync::{Arc, mpsc}; use thiserror::Error; #[cfg(unix)] use tracing::error; @@ -35,50 +34,9 @@ use tracing::{error, warn}; #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] use tun::Layer; -/// One `path_mtu_lookup` entry: the MTU the TCP MSS clamp reads, plus how -/// the entry is released. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct PathMtuEntry { - /// Path MTU in bytes. - pub mtu: u16, - /// Unix ms at which a discovery `LookupResponse` supplied this value, or - /// `None` for an entry that some event releases instead of a timer. - /// - /// The discovery carrier is the one with no release path: it writes an - /// entry for a destination this node may never open a session with, and - /// all three callers of `path_mtu_lookup_release` fire on session state. - /// A link MTU this node derived from its own transport, and a value - /// learned inside a session, are both released by an event that says the - /// thing they describe is gone, so they carry no deadline. - pub learned_ms: Option, -} - -impl PathMtuEntry { - /// An entry released by an event rather than a timer: a locally derived - /// link MTU, or a value learned inside a session. - pub fn held(mtu: u16) -> Self { - Self { - mtu, - learned_ms: None, - } - } - - /// A remote party's claim stored at `at_ms` for a destination with no - /// other release path. Expires. - pub fn learned(mtu: u16, at_ms: u64) -> Self { - Self { - mtu, - learned_ms: Some(at_ms), - } - } -} - -/// Read-only handle to the per-destination path MTU map. Populated by -/// the discovery handler on `LookupResponse`; read by the TUN reader -/// (outbound clamp) and writer (inbound clamp) at TCP MSS clamp time. -/// Keyed by [`FipsAddress`] (16 bytes, the IPv6 form of a fips peer -/// address). -pub type PathMtuLookup = Arc>>; +// The path MTU map is node state; re-exported so `crate::upper::tun` paths +// keep resolving. +pub use crate::node::path_mtu::{PathMtuEntry, PathMtuLookup}; /// The node-global TCP MSS ceiling, shared live with the TUN reader and /// writer threads. @@ -1653,6 +1611,8 @@ mod platform { #[cfg(test)] mod tests { use super::*; + use std::collections::HashMap; + use std::sync::RwLock; #[test] fn test_tun_state_display() {