diff --git a/docs/design/fips-configuration.md b/docs/design/fips-configuration.md index 4b4cfec..130df5e 100644 --- a/docs/design/fips-configuration.md +++ b/docs/design/fips-configuration.md @@ -144,15 +144,38 @@ Controls end-to-end session behavior and packet queuing. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `node.session.default_hop_limit` | u8 | `64` | Default SessionDatagram hop limit | +| `node.session.default_ttl` | u8 | `64` | Default SessionDatagram TTL | | `node.session.pending_packets_per_dest` | usize | `16` | Queue depth per destination during session establishment | | `node.session.pending_max_destinations` | usize | `256` | Max destinations with pending packets | -| `node.session.idle_timeout_secs` | u64 | `90` | Idle session timeout; established sessions with no activity for this duration are removed | +| `node.session.idle_timeout_secs` | u64 | `90` | Idle session timeout; established sessions with no application data for this duration are removed. MMP reports (SenderReport, ReceiverReport, PathMtuNotification) do not count as activity | | `node.session.coords_warmup_packets` | u8 | `5` | Number of initial data packets per session that include the CP flag for transit cache warmup; also the reset count on CoordsRequired receipt | The anti-replay window size (2048 packets) is a compile-time constant and not configurable. +### Link-Layer MMP (`node.mmp.*`) + +Metrics Measurement Protocol for per-peer link measurement. See +[fips-link-layer.md](fips-link-layer.md) for behavioral details. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `node.mmp.mode` | string | `"full"` | Operating mode: `full` (sender + receiver reports), `lightweight` (receiver reports only), or `minimal` (spin bit + CE echo only, no reports) | +| `node.mmp.log_interval_secs` | u64 | `30` | Periodic operator log interval for link metrics | +| `node.mmp.owd_window_size` | usize | `32` | One-way delay trend ring buffer size | + +### Session-Layer MMP (`node.session_mmp.*`) + +Metrics Measurement Protocol for end-to-end session measurement. Configured +independently from link-layer MMP because session reports are routed through +every transit link, consuming bandwidth proportional to path length. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `node.session_mmp.mode` | string | `"full"` | Operating mode: `full`, `lightweight`, or `minimal` | +| `node.session_mmp.log_interval_secs` | u64 | `30` | Periodic operator log interval for session metrics | +| `node.session_mmp.owd_window_size` | usize | `32` | One-way delay trend ring buffer size | + ### Internal Buffers (`node.buffers.*`) Channel sizes affecting throughput and memory. Primarily useful for performance @@ -196,6 +219,8 @@ stale address mappings. |-----------|------|---------|-------------| | `transports.udp.bind_addr` | string | `"0.0.0.0:4000"` | UDP bind address and port | | `transports.udp.mtu` | u16 | `1280` | Transport MTU | +| `transports.udp.recv_buf_size` | usize | `2097152` | UDP socket receive buffer size in bytes (2 MB). Linux kernel doubles the requested value internally. Host `net.core.rmem_max` must be >= this value. | +| `transports.udp.send_buf_size` | usize | `2097152` | UDP socket send buffer size in bytes (2 MB). Host `net.core.wmem_max` must be >= this value. | ## Peers (`peers[]`) @@ -297,11 +322,19 @@ node: bloom: update_debounce_ms: 500 session: - default_hop_limit: 64 + default_ttl: 64 pending_packets_per_dest: 16 pending_max_destinations: 256 idle_timeout_secs: 90 coords_warmup_packets: 5 + mmp: + mode: full # full | lightweight | minimal + log_interval_secs: 30 + owd_window_size: 32 + session_mmp: + mode: full # full | lightweight | minimal + log_interval_secs: 30 + owd_window_size: 32 buffers: packet_channel: 1024 tun_channel: 1024 @@ -322,6 +355,8 @@ transports: udp: bind_addr: "0.0.0.0:4000" mtu: 1280 + recv_buf_size: 2097152 # 2 MB (kernel doubles to 4 MB actual) + send_buf_size: 2097152 # 2 MB peers: [] ``` diff --git a/docs/design/fips-intro.md b/docs/design/fips-intro.md index bb7e87b..7e47027 100644 --- a/docs/design/fips-intro.md +++ b/docs/design/fips-intro.md @@ -58,6 +58,10 @@ alone, the network self-organizes: - **Routing decisions** are made locally at each hop, using only the node's immediate peers and cached coordinate information +Each peer link and end-to-end session actively measures RTT, loss, jitter, and +goodput through a lightweight in-band Metrics Measurement Protocol (MMP), +providing operator visibility and a foundation for future quality-aware routing. + The result is a network that builds itself from the bottom up, heals around failures automatically, and scales without central coordination. Adding a node is as simple as connecting to one existing peer — the network integrates the diff --git a/docs/design/fips-ipv6-adapter.md b/docs/design/fips-ipv6-adapter.md index e4af3a4..3b84be9 100644 --- a/docs/design/fips-ipv6-adapter.md +++ b/docs/design/fips-ipv6-adapter.md @@ -283,7 +283,7 @@ TUN device creation requires `CAP_NET_ADMIN`. Options: | Per-destination route MTU (netlink) | Planned | | Transit MTU error signal | Planned | | Path MTU tracking (SessionDatagram field) | **Implemented** | -| Path MTU notification (end-to-end echo) | Future direction | +| Path MTU notification (end-to-end echo) | **Implemented** | | Endpoint fragmentation/reassembly | Future direction | ## Design Considerations @@ -296,8 +296,10 @@ Two complementary mechanisms support full PMTUD: is implemented at the FLP level. The source sets it to its outbound link MTU minus overhead; each transit node applies `min(current, own_outbound_mtu - overhead)`. The destination receives the - forward-path minimum. A session-layer echo (PathMtuNotification, 2 bytes - inside encryption) to return the value to the source is a future direction. + forward-path minimum. PathMtuNotification is handled at the session layer; + the destination sends the observed forward-path MTU back to the source, + which applies it with decrease-immediate / increase-requires-3-consecutive + hysteresis. 2. **Reactive**: When a transit node cannot forward a packet (MTU exceeded), it sends an error signal back to the source. This handles the in-flight gap diff --git a/docs/design/fips-link-layer.md b/docs/design/fips-link-layer.md index c47ca36..9e5a250 100644 --- a/docs/design/fips-link-layer.md +++ b/docs/design/fips-link-layer.md @@ -311,8 +311,8 @@ FLP defines eight message types carried inside encrypted frames: | 0x30 | LookupRequest | Coordinate discovery — flood toward destination | | 0x31 | LookupResponse | Coordinate discovery — response with coordinates | | 0x00 | SessionDatagram | Encapsulated session-layer payload for forwarding | -| 0x01 | SenderReport | Link statistics — what this node sent (reserved) | -| 0x02 | ReceiverReport | Link statistics — what this node observed (reserved) | +| 0x01 | SenderReport | MMP sender-side metrics report | +| 0x02 | ReceiverReport | MMP receiver-side metrics report | | 0x50 | Disconnect | Orderly link teardown with reason code | Additionally, handshake messages (phase 0x1 msg1, phase 0x2 msg2) are sent @@ -327,6 +327,77 @@ See [fips-mesh-operation.md](fips-mesh-operation.md) for how these messages work together to build and maintain the mesh, and [fips-wire-formats.md](fips-wire-formats.md) for byte-level message layouts. +## Metrics Measurement Protocol (MMP) + +Each active peer link runs an instance of the Metrics Measurement Protocol, +providing per-link quality metrics to the operator and (in future) to the +routing layer for link-cost decisions. + +### Metrics Tracked + +MMP computes the following metrics from the per-frame counter and timestamp +fields in the FLP wire format: + +- **SRTT** — Smoothed round-trip time (Jacobson/RFC 6298, α=1/8). Derived + from timestamp-echo in ReceiverReports with dwell-time compensation. +- **Loss rate** — Bidirectional loss inferred from counter gaps. Tracked as + both instantaneous (per-interval) and long-term EWMA. +- **Jitter** — Interarrival jitter (RFC 3550 algorithm) in microseconds. +- **Goodput** — Bytes per second of payload data (excludes MMP reports). +- **OWD trend** — One-way delay trend (µs/s, signed). Indicates congestion + buildup before loss occurs. +- **ETX** — Expected Transmission Count, computed from bidirectional delivery + ratios. Not yet wired into routing decisions (link cost is currently + constant). +- **Dual EWMA trends** — Short-term (α=1/4) and long-term (α=1/32) trend + indicators for both RTT and loss, enabling change detection. + +### Operating Modes + +MMP supports three modes, configured via `node.mmp.mode`: + +| Mode | Reports Exchanged | Metrics Available | +| ---- | ----------------- | ----------------- | +| **Full** (default) | SenderReport + ReceiverReport | All metrics including RTT, loss, jitter, goodput, OWD trend | +| **Lightweight** | ReceiverReport only | Loss (from counter gaps), jitter, OWD trend. No RTT. | +| **Minimal** | None | Spin bit and CE echo flags only. No computed metrics. | + +### Report Scheduling + +Reports are sent at RTT-adaptive intervals, clamped to [100ms, 2s]. A +cold-start interval of 500ms is used before SRTT converges. The interval +formula is `clamp(2 × SRTT, 100ms, 2000ms)`. + +### Spin Bit and RTT + +The SP (spin bit) flag in the FLP inner header follows the QUIC spin bit +pattern: reflected on receive, toggled on send when the reflected value +matches the last sent value. The spin bit state machine runs for TX +reflection, but **RTT samples from the spin bit are discarded**. In a mesh +protocol where frames are sent irregularly (tree announces, bloom filters, +MMP reports on different timers), inter-frame processing delays inflate spin +bit RTT measurements unpredictably. Timestamp-echo from ReceiverReports +(with dwell-time compensation) is the sole SRTT source. + +### CE Echo + +The CE (Congestion Experienced) echo flag (bit 1 in the FLP flags byte) is +reserved for ECN signaling. The transport trait does not currently expose +ECN marking, so the CE echo flag is never set. One-way delay trend serves +as the sole pre-loss congestion indicator. + +### Operator Logging + +MMP emits periodic link metrics at info level (configurable via +`node.mmp.log_interval_secs`, default 30s): + +```text +MMP link metrics peer=node-b rtt=2.3ms loss=0.2% jitter=0.1ms goodput=76.0MB/s tx_pkts=1234 rx_pkts=5678 +``` + +Teardown logs include final SRTT, loss rate, jitter, ETX, goodput, and +cumulative tx/rx packet and byte counts. + ## Security Properties ### Threat Resistance @@ -379,6 +450,7 @@ an attacker sends invalid packets to elicit responses. | AAD binding on encrypted frames | **Implemented** | | Inner header timestamps | **Implemented** | | Path MTU tracking (SessionDatagram) | **Implemented** | +| Metrics Measurement Protocol (MMP) | **Implemented** | | Rekey with index rotation | Planned | | Allowlist/blocklist | Planned | diff --git a/docs/design/fips-mesh-operation.md b/docs/design/fips-mesh-operation.md index c9a36b0..4943b8f 100644 --- a/docs/design/fips-mesh-operation.md +++ b/docs/design/fips-mesh-operation.md @@ -181,8 +181,9 @@ through to greedy tree routing (functional but suboptimal). When bloom filters identify multiple candidate peers, they are ranked by a composite key: -1. **link_cost** — Per-link quality metric (currently constant; designed for - future ETX or similar metrics) +1. **link_cost** — Per-link quality metric. ETX is computed from bidirectional + delivery ratios in MMP metrics but is not yet wired into `find_next_hop()` + candidate ranking; link cost remains constant in the current implementation. 2. **tree_distance** — Coordinate-based distance to destination through this peer 3. **node_addr** — Deterministic tie-breaker @@ -393,6 +394,10 @@ Both error types are rate-limited at transit nodes: maximum one error per destination per 100ms. This prevents storms during topology changes when many packets to the same destination hit the same routing failure simultaneously. +Error signals (CoordsRequired, PathBroken) are handled asynchronously outside +the packet receive path, allowing the RX loop to continue processing without +blocking on discovery or session repair. + ### Error Routing Limitation Error signals route back to the source using `find_next_hop(src_addr)`. For diff --git a/docs/design/fips-session-layer.md b/docs/design/fips-session-layer.md index 642ea0d..ce3a7de 100644 --- a/docs/design/fips-session-layer.md +++ b/docs/design/fips-session-layer.md @@ -159,6 +159,13 @@ encrypted message includes: Sessions that see no traffic for a configurable duration (default 90s) are torn down. When traffic resumes, a new session is established automatically. +Only DataPacket (message type 0x10) send/receive and session establishment +reset `last_activity`. MMP traffic — SenderReport, ReceiverReport, and +PathMtuNotification — does not reset the idle timer. This means a session +carrying only MMP reports and no application data will still tear down after +`node.session.idle_timeout_secs`. MMP reports continue flowing until the +session is torn down, providing final measurement data for the teardown log. + The idle timeout is deliberately shorter than the coordinate cache TTL (300s). This ordering ensures that when traffic stops and the session tears down, the transit node coordinate caches are still warm when a new session is established. @@ -375,6 +382,71 @@ When traffic stops: the session tears down at 90s. When traffic resumes: DNS re-resolves the identity, a fresh SessionSetup carries coordinates, and transit node caches (still within their 300s TTL) are re-warmed. +## Session-Layer MMP + +Each established session runs its own Metrics Measurement Protocol instance, +providing end-to-end quality metrics independent of the number of hops. + +### Relationship to Link-Layer MMP + +Session-layer MMP uses the same report wire format (SenderReport 0x11, +ReceiverReport 0x12) and identical algorithms as link-layer MMP, but with +two key differences: + +1. **End-to-end routing**: Session reports are encrypted and forwarded through + every transit link. A 3-hop session generates report traffic on all 3 links, + making bandwidth cost proportional to path length. +2. **Independent configuration**: The `node.session_mmp.*` parameters are + separate from `node.mmp.*`, allowing operators to run a lighter mode for + sessions (e.g., Lightweight) while keeping Full mode on links. + +### Metrics + +The same metrics as link-layer MMP: SRTT, loss rate, jitter, goodput, OWD +trend, ETX, and dual EWMA trends. Session MMP additionally tracks observed +path MTU. + +### Report Intervals + +Session-layer report intervals are higher than link-layer to account for +bandwidth cost: clamped to [500ms, 10s] with a cold-start interval of 1s +(vs. link-layer [100ms, 2s] with 500ms cold-start). + +### Path MTU Tracking + +PathMtuNotification (message type 0x13) provides end-to-end path MTU +feedback: + +1. The source sets `path_mtu` in each SessionDatagram envelope to its + outbound link MTU. +2. Each transit node applies `min(current, transport.mtu())` before + forwarding. +3. The destination receives the forward-path minimum and sends a + PathMtuNotification (2-byte body: u16 LE path_mtu) back to the source. +4. The source applies the notification with hysteresis: + - **Decrease**: immediate (take lower value). + - **Increase**: requires 3 consecutive higher-value notifications spanning + at least 2 × notification interval. +5. Notifications are sent on first measurement, on any decrease, and + periodically at `max(10s, 5 × SRTT)`. + +### Idle Timeout Interaction + +MMP reports (SenderReport, ReceiverReport) and PathMtuNotification do **not** +reset the session idle timer. Only application data (DataPacket, type 0x10) +resets `last_activity`. This ensures sessions with no application traffic +tear down after `node.session.idle_timeout_secs` (default 90s), while MMP +continues providing measurement data up to the teardown moment. + +### Operator Logging + +Session metrics are logged at info level (configurable via +`node.session_mmp.log_interval_secs`, default 30s): + +```text +MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms goodput=71.3MB/s mtu=1472 tx_pkts=1234 rx_pkts=5678 +``` + ## Implementation Status | Feature | Status | @@ -384,7 +456,7 @@ node caches (still within their 300s TTL) are re-warmed. | Explicit counter replay protection | **Implemented** | | CP warmup-then-reactive | **Implemented** | | FSP wire format (prefix, AAD, inner header) | **Implemented** | -| Session-layer MMP report types | **Implemented** (wire format) | +| Session-layer MMP | **Implemented** | | Identity cache (LRU-only) | **Implemented** | | Coordinate cache (unified, TTL + refresh) | **Implemented** | | Session idle timeout | **Implemented** | @@ -394,7 +466,7 @@ node caches (still within their 300s TTL) are re-warmed. | Flush coord cache on parent change | **Implemented** | | Rekey | Planned | | Path MTU tracking (FLP SessionDatagram field) | **Implemented** | -| Path MTU notification (end-to-end echo) | Planned | +| Path MTU notification (end-to-end echo) | **Implemented** | ## References diff --git a/docs/design/fips-software-architecture.md b/docs/design/fips-software-architecture.md index 17ed514..5b0a477 100644 --- a/docs/design/fips-software-architecture.md +++ b/docs/design/fips-software-architecture.md @@ -157,6 +157,49 @@ retransmission timers, and acknowledgment tracking — complexity that gossip protocols avoid by sending full state periodically. A lost TreeAnnounce is simply replaced by the next one, which carries the same or newer state. +## Metrics Measurement Protocol + +MMP is instantiated at two independent layers, each with its own +configuration and state: + +- **Link layer**: One `MmpPeerState` per `ActivePeer`. Measures per-hop + quality using the FLP counter and timestamp fields that already exist on + every encrypted frame. No additional message overhead beyond periodic + SenderReport/ReceiverReport exchanges. + +- **Session layer**: One `MmpSessionState` per established `SessionEntry`. + Measures end-to-end quality using the FSP counter and timestamp fields. + Reports are encrypted and forwarded through every transit link. + +Both instantiations use identical algorithms (SRTT, loss, jitter, dual EWMA, +OWD trend) but are configured independently via `node.mmp.*` and +`node.session_mmp.*`. This allows operators to run Full mode on links (low +overhead, single hop) while using Lightweight mode for sessions (reduces +bandwidth cost on transit links). + +### Peer Display Names + +The node maintains a `peer_aliases` map (`HashMap`) populated +from the `peers[].alias` field in configuration. All log output uses +`peer_display_name()` to show human-readable names (e.g., "node-b") instead +of truncated public keys, improving operator experience. + +### Buffer Sizing Chain + +Under high forwarding load, back-pressure propagates through: + +1. **UDP socket receive buffer** (`transports.udp.recv_buf_size`, default 2 MB) — + kernel-level buffer for incoming datagrams. +2. **Packet channel** (`node.buffers.packet_channel`, default 1024) — async + channel from transport receive loop to the node's RX event loop. +3. **Processing** — decryption, routing decision, forwarding. + +If the packet channel fills (RX loop can't keep up), the transport receive +loop blocks, and the kernel receive buffer absorbs bursts. If the kernel +buffer also fills, incoming datagrams are silently dropped +(`RcvbufErrors` in `/proc/net/snmp`). The 2 MB default socket buffer was +chosen to handle ~85 MB/s forwarding throughput without kernel drops. + ## Bounded State Principle FIPS nodes maintain state proportional to O(P × D), where P is the number diff --git a/docs/design/fips-transport-layer.md b/docs/design/fips-transport-layer.md index 552f03f..c10bc4c 100644 --- a/docs/design/fips-transport-layer.md +++ b/docs/design/fips-transport-layer.md @@ -204,6 +204,41 @@ For internet-connected nodes, UDP/IP is the recommended transport: Raw IP with a custom protocol number would be simpler but is blocked by most NAT devices and firewalls, limiting deployment to networks without NAT. +### Socket Buffer Sizing + +The default Linux UDP receive buffer (`net.core.rmem_default`, typically +212 KB) is insufficient for high-throughput forwarding. At ~85 MB/s, a 212 KB +buffer fills in ~2.5 ms; any stall in the async receive loop (decryption, +routing, forwarding overhead) causes the kernel to silently drop incoming +datagrams. + +FIPS uses the `socket2` crate to configure socket buffers at bind time, +before the receive loop starts: + +| Parameter | Default | Description | +| ---------------- | ------- | ------------------------------------ | +| `recv_buf_size` | 2 MB | `SO_RCVBUF` — kernel receive buffer | +| `send_buf_size` | 2 MB | `SO_SNDBUF` — kernel send buffer | + +Linux internally doubles the requested value (to account for kernel +bookkeeping overhead), so requesting 2 MB yields 4 MB actual buffer space. +The kernel silently clamps to `net.core.rmem_max` if the request exceeds it. + +**Host requirement**: `net.core.rmem_max` and `net.core.wmem_max` must be +set to at least the requested buffer size on the host. For Docker containers, +this must be configured on the Docker host (containers share the host kernel). +Verify with: + +```text +sysctl net.core.rmem_max net.core.wmem_max +``` + +Actual buffer sizes are logged at startup: + +```text +UDP transport started local_addr=0.0.0.0:4000 recv_buf=4194304 send_buf=4194304 +``` + ## Discovery Discovery determines that a FIPS-capable endpoint is reachable at a given diff --git a/docs/design/fips-wire-formats.md b/docs/design/fips-wire-formats.md index 05ed4f9..7defc27 100644 --- a/docs/design/fips-wire-formats.md +++ b/docs/design/fips-wire-formats.md @@ -128,8 +128,8 @@ Minimum frame: 37 bytes (empty body) | Type | Message | Description | | ---- | ------- | ----------- | | 0x00 | SessionDatagram | Encapsulated session-layer payload for forwarding | -| 0x01 | SenderReport | MMP sender-side report (reserved) | -| 0x02 | ReceiverReport | MMP receiver-side report (reserved) | +| 0x01 | SenderReport | MMP sender-side metrics report (48 bytes) | +| 0x02 | ReceiverReport | MMP receiver-side metrics report (68 bytes) | | 0x10 | TreeAnnounce | Spanning tree state announcement | | 0x20 | FilterAnnounce | Bloom filter reachability update | | 0x30 | LookupRequest | Coordinate discovery request | @@ -396,6 +396,50 @@ Orderly link teardown with reason code. | 0x07 | Timeout | Keepalive or stale detection timeout | | 0xFF | Other | Unspecified reason | +### SenderReport (0x01) + +Sent by the frame sender to provide interval-based transmission statistics. + +| Offset | Field | Size | Encoding | +| ------ | ----- | ---- | -------- | +| 0 | msg_type | 1 | `0x01` | +| 1 | reserved | 3 | Zero | +| 4 | interval_start_counter | 8 | u64 LE — first counter in this interval | +| 12 | interval_end_counter | 8 | u64 LE — last counter in this interval | +| 20 | interval_start_timestamp | 4 | u32 LE — timestamp at interval start | +| 24 | interval_end_timestamp | 4 | u32 LE — timestamp at interval end | +| 28 | interval_bytes_sent | 4 | u32 LE — payload bytes sent in interval | +| 32 | cumulative_packets_sent | 8 | u64 LE — total packets sent on this link | +| 40 | cumulative_bytes_sent | 8 | u64 LE — total bytes sent on this link | + +**Total: 48 bytes.** + +### ReceiverReport (0x02) + +Sent by the frame receiver to provide loss, jitter, and timing feedback. + +| Offset | Field | Size | Encoding | +| ------ | ----- | ---- | -------- | +| 0 | msg_type | 1 | `0x02` | +| 1 | reserved | 3 | Zero | +| 4 | highest_counter | 8 | u64 LE — highest counter value received | +| 12 | cumulative_packets_recv | 8 | u64 LE — total packets received | +| 20 | cumulative_bytes_recv | 8 | u64 LE — total bytes received | +| 28 | timestamp_echo | 4 | u32 LE — echoed sender timestamp for RTT | +| 32 | dwell_time | 2 | u16 LE — time between receive and echo (ms) | +| 34 | max_burst_loss | 2 | u16 LE — largest loss burst in interval | +| 36 | mean_burst_loss | 2 | u16 LE — mean burst length (u8.8 fixed-point) | +| 38 | reserved | 2 | Zero | +| 40 | jitter | 4 | u32 LE — interarrival jitter (microseconds) | +| 44 | ecn_ce_count | 4 | u32 LE — cumulative ECN-CE marked packets | +| 48 | owd_trend | 4 | i32 LE — one-way delay trend (µs/s, signed) | +| 52 | burst_loss_count | 4 | u32 LE — number of loss bursts in interval | +| 56 | cumulative_reorder_count | 4 | u32 LE — total reordered packets | +| 60 | interval_packets_recv | 4 | u32 LE — packets received in interval | +| 64 | interval_bytes_recv | 4 | u32 LE — bytes received in interval | + +**Total: 68 bytes.** + ## Session-Layer Message Formats Session-layer messages are carried as the payload of a SessionDatagram (0x00). @@ -489,6 +533,11 @@ Message types 0x10-0x13 are carried inside the AEAD ciphertext (dispatched by the `msg_type` field in the encrypted inner header). Types 0x20-0x21 are plaintext error signals (U flag set, no encryption). +Session-layer SenderReport (0x11) and ReceiverReport (0x12) use the same +body format as their link-layer counterparts (0x01 and 0x02). The msg_type +byte in the body matches the link-layer value; dispatch to the correct layer +happens at the session level based on the FSP message type. + ### SessionSetup (phase 0x1) Establishes a session and warms transit coordinate caches. @@ -527,6 +576,16 @@ Application data (typically IPv6 payload). This is the `msg_type` byte inside the encrypted inner header — there is no separate DataPacket struct. The body after the inner header is delivered directly to the TUN interface. +### PathMtuNotification (0x13) + +Sent by the destination to report the observed forward-path MTU. + +| Offset | Field | Size | Encoding | +| ------ | ----- | ---- | -------- | +| 0 | path_mtu | 2 | u16 LE — minimum MTU observed along the forward path | + +**Total body: 2 bytes** (plus FSP common prefix + encrypted header + AEAD tag). + ### CoordsRequired (0x20) Plaintext error signal — transit node lacks coordinates for destination.