mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
FSP wire format revision and session-layer MMP implementation
FSP wire format revision (TASK-2026-0007): Introduce the FIPS Session Protocol (FSP) wire format with a 4-byte common prefix [ver_phase:1][flags:1][payload_len:2 LE] replacing the old 1-byte msg_type dispatch. All session messages share this prefix with phase-based dispatch (Established, Setup, Ack, Unencrypted). - New session_wire.rs: FSP constants, header types, parse/build helpers - SessionMessageType enum: DataPacket (0x10), SenderReport (0x11), ReceiverReport (0x12), PathMtuNotification (0x13) - FspFlags (CP/K/U) and FspInnerFlags (SP) for flag management - SessionSenderReport, SessionReceiverReport, PathMtuNotification message structs with encode/decode - FSP send pipeline: 12-byte header as AAD, 6-byte inner header (timestamp + msg_type + inner_flags), encrypt_with_aad() - FSP receive pipeline: parse header, extract cleartext coords (CP), AEAD decrypt with AAD, strip inner header, msg_type dispatch - Forwarding: transit nodes parse cleartext coords without decryption - Removed DataPacket struct and associated types - SessionEntry: session_start_ms, mark_established(), session_timestamp() - FIPS_OVERHEAD: 144 → 150 bytes (+6 for FSP inner header) - Design docs updated for new wire format Session-layer MMP implementation (TASK-2026-0008): Implement complete session-layer MMP reusing the link-layer algorithm modules (SenderState, ReceiverState, MmpMetrics, SpinBitState) with independent configuration and higher report interval clamps. - SessionMmpConfig: separate config section (node.session_mmp.*) - MmpSessionState: session-specific wrapper with PathMtuState tracking - Session-layer constants (500ms-10s report intervals, 1s cold start) - Parameterized interval methods (new_with_cold_start, update_report_interval_with_bounds) on SenderState/ReceiverState - Bidirectional From conversions between link/session report types - SessionEntry: mmp and is_initiator fields, initialized on Established - send_session_msg() for reports/notifications - Per-message RX recording with spin bit state tracking - Handlers for SenderReport, ReceiverReport, PathMtuNotification - path_mtu threaded from SessionDatagram envelope through to handlers - check_session_mmp_reports() tick handler with collect-then-send pattern - Periodic and teardown operator logging for session metrics - PathMtuState: destination observes incoming MTU on all session messages, source seeded from outbound transport MTU, decrease-immediate / increase-requires-3-consecutive rules Link-layer MMP fix: - Stop feeding spin bit RTT samples into SRTT estimator; inter-frame timing in the mesh is irregular, inflating spin-bit RTT by variable processing delays; timestamp-echo provides accurate RTT 29 files changed, 602 tests pass, 0 clippy warnings.
This commit is contained in:
@@ -148,7 +148,7 @@ Controls end-to-end session behavior and packet queuing.
|
|||||||
| `node.session.pending_packets_per_dest` | usize | `16` | Queue depth per destination during session establishment |
|
| `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.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 activity for this duration are removed |
|
||||||
| `node.session.coords_warmup_packets` | u8 | `5` | Number of initial DataPackets per session that include COORDS_PRESENT for transit cache warmup; also the reset count on CoordsRequired receipt |
|
| `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
|
The anti-replay window size (2048 packets) is a compile-time constant and not
|
||||||
configurable.
|
configurable.
|
||||||
|
|||||||
@@ -121,13 +121,14 @@ after all layers of wrapping.
|
|||||||
| ----- | -------- | ------- |
|
| ----- | -------- | ------- |
|
||||||
| Link encryption | 37 bytes | 16-byte outer header + 5-byte inner header + 16-byte AEAD tag |
|
| Link encryption | 37 bytes | 16-byte outer header + 5-byte inner header + 16-byte AEAD tag |
|
||||||
| SessionDatagram envelope | 36 bytes | type + ttl + path_mtu + src_addr + dest_addr |
|
| SessionDatagram envelope | 36 bytes | type + ttl + path_mtu + src_addr + dest_addr |
|
||||||
| DataPacket header | 12 bytes | type + flags + counter + payload_len |
|
| FSP header | 12 bytes | 4-byte prefix + 8-byte counter (used as AEAD AAD) |
|
||||||
| Session encryption | 16 bytes | ChaCha20-Poly1305 AEAD tag |
|
| FSP inner header | 6 bytes | 4-byte timestamp + 1-byte msg_type + 1-byte inner_flags (inside AEAD) |
|
||||||
| **Minimal total** | **101 bytes** | |
|
| Session AEAD tag | 16 bytes | ChaCha20-Poly1305 tag on session-encrypted payload |
|
||||||
|
| **Minimal total** | **107 bytes** | |
|
||||||
| Coordinates (if present) | ~43 bytes | Depth-dependent, first few packets only |
|
| Coordinates (if present) | ~43 bytes | Depth-dependent, first few packets only |
|
||||||
| **Worst case total** | **144 bytes** | With COORDS_PRESENT for depth-3 paths |
|
| **Worst case total** | **150 bytes** | With CP flag set for depth-3 paths |
|
||||||
|
|
||||||
The `FIPS_OVERHEAD` constant (144 bytes) is used for conservative MTU
|
The `FIPS_OVERHEAD` constant (150 bytes) is used for conservative MTU
|
||||||
calculations.
|
calculations.
|
||||||
|
|
||||||
### Effective IPv6 MTU
|
### Effective IPv6 MTU
|
||||||
@@ -142,14 +143,14 @@ For typical deployments:
|
|||||||
|
|
||||||
| Transport MTU | Effective IPv6 MTU | Notes |
|
| Transport MTU | Effective IPv6 MTU | Notes |
|
||||||
| ------------- | ------------------ | ----- |
|
| ------------- | ------------------ | ----- |
|
||||||
| 1472 (UDP/Ethernet) | 1328 | Standard deployment |
|
| 1472 (UDP/Ethernet) | 1322 | Standard deployment |
|
||||||
| 1280 (UDP minimum) | 1136 | Below IPv6 minimum |
|
| 1280 (UDP minimum) | 1130 | Below IPv6 minimum |
|
||||||
|
|
||||||
IPv6 mandates that every link support at least 1280 bytes. The minimum
|
IPv6 mandates that every link support at least 1280 bytes. The minimum
|
||||||
transport path MTU for the IPv6 adapter is therefore:
|
transport path MTU for the IPv6 adapter is therefore:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
1280 + 144 = 1424 bytes
|
1280 + 150 = 1430 bytes
|
||||||
```
|
```
|
||||||
|
|
||||||
Transports with smaller MTUs (LoRa at ~250 bytes, serial at 256 bytes) cannot
|
Transports with smaller MTUs (LoRa at ~250 bytes, serial at 256 bytes) cannot
|
||||||
@@ -315,13 +316,13 @@ decryption (opens attack surface).
|
|||||||
|
|
||||||
Endpoint-only fragmentation (fragment before session encryption, reassemble
|
Endpoint-only fragmentation (fragment before session encryption, reassemble
|
||||||
after decryption) is a future direction that avoids these objections. Each
|
after decryption) is a future direction that avoids these objections. Each
|
||||||
fragment would be independently encrypted and look like a normal DataPacket
|
fragment would be independently encrypted and look like a normal data packet
|
||||||
to transit nodes.
|
to transit nodes.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [fips-intro.md](fips-intro.md) — Protocol overview and architecture
|
- [fips-intro.md](fips-intro.md) — Protocol overview and architecture
|
||||||
- [fips-session-layer.md](fips-session-layer.md) — FSP (below the adapter)
|
- [fips-session-layer.md](fips-session-layer.md) — FSP (below the adapter)
|
||||||
- [fips-wire-formats.md](fips-wire-formats.md) — DataPacket and SessionDatagram
|
- [fips-wire-formats.md](fips-wire-formats.md) — FSP and SessionDatagram wire
|
||||||
formats
|
formats
|
||||||
- [fips-configuration.md](fips-configuration.md) — TUN configuration parameters
|
- [fips-configuration.md](fips-configuration.md) — TUN configuration parameters
|
||||||
|
|||||||
@@ -212,8 +212,7 @@ be made.
|
|||||||
### Unified Cache
|
### Unified Cache
|
||||||
|
|
||||||
The coordinate cache is a single unified cache. All sources — SessionSetup
|
The coordinate cache is a single unified cache. All sources — SessionSetup
|
||||||
transit, COORDS_PRESENT DataPackets, LookupResponse — write to the same
|
transit, CP-flagged data packets, LookupResponse — write to the same cache.
|
||||||
cache.
|
|
||||||
|
|
||||||
### Population Sources
|
### Population Sources
|
||||||
|
|
||||||
@@ -221,7 +220,7 @@ cache.
|
|||||||
| ------ | ---- | ---- |
|
| ------ | ---- | ---- |
|
||||||
| SessionSetup transit | Session establishment | Both src and dest coordinates |
|
| SessionSetup transit | Session establishment | Both src and dest coordinates |
|
||||||
| SessionAck transit | Session establishment | Responder's coordinates |
|
| SessionAck transit | Session establishment | Responder's coordinates |
|
||||||
| DataPacket with COORDS_PRESENT | Warmup or recovery | Both src and dest coordinates |
|
| CP-flagged data packet | Warmup or recovery | Both src and dest coordinates (cleartext) |
|
||||||
| LookupResponse | Discovery | Target's coordinates |
|
| LookupResponse | Discovery | Target's coordinates |
|
||||||
|
|
||||||
### Eviction
|
### Eviction
|
||||||
@@ -312,7 +311,7 @@ Destination Unreachable.
|
|||||||
## SessionSetup Self-Bootstrapping
|
## SessionSetup Self-Bootstrapping
|
||||||
|
|
||||||
SessionSetup is the mechanism that warms transit node coordinate caches
|
SessionSetup is the mechanism that warms transit node coordinate caches
|
||||||
along a path, enabling subsequent DataPackets to route efficiently.
|
along a path, enabling subsequent data packets to route efficiently.
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|
||||||
@@ -335,18 +334,19 @@ coordinates and warming caches in the other direction.
|
|||||||
### Result
|
### Result
|
||||||
|
|
||||||
After the handshake completes, the entire forward and reverse paths have
|
After the handshake completes, the entire forward and reverse paths have
|
||||||
cached coordinates for both endpoints. Subsequent DataPackets use minimal
|
cached coordinates for both endpoints. Subsequent data packets use minimal
|
||||||
headers (no coordinates) and route efficiently through the warmed caches.
|
headers (no coordinates) and route efficiently through the warmed caches.
|
||||||
|
|
||||||
## COORDS_PRESENT Warmup
|
## CP (Coords Present) Warmup
|
||||||
|
|
||||||
The COORDS_PRESENT flag on DataPackets provides a secondary cache-warming
|
The CP flag in the FSP common prefix provides a secondary cache-warming
|
||||||
mechanism that complements SessionSetup. See
|
mechanism that complements SessionSetup. See
|
||||||
[fips-session-layer.md](fips-session-layer.md) for the warmup-then-reactive
|
[fips-session-layer.md](fips-session-layer.md) for the warmup-then-reactive
|
||||||
strategy.
|
strategy.
|
||||||
|
|
||||||
Transit nodes process DataPackets with COORDS_PRESENT by extracting and
|
Transit nodes parse the CP flag from the FSP header and extract source and
|
||||||
caching both source and destination coordinates — the same operation
|
destination coordinates from the cleartext section between the header and
|
||||||
|
ciphertext — no decryption needed. This is the same caching operation
|
||||||
performed for SessionSetup coordinates.
|
performed for SessionSetup coordinates.
|
||||||
|
|
||||||
## Error Recovery
|
## Error Recovery
|
||||||
@@ -367,8 +367,7 @@ coordinates for the destination. It cannot make a forwarding decision.
|
|||||||
|
|
||||||
**Source recovery**:
|
**Source recovery**:
|
||||||
1. Initiate discovery (LookupRequest flood) for the destination
|
1. Initiate discovery (LookupRequest flood) for the destination
|
||||||
2. Reset COORDS_PRESENT warmup counter — subsequent DataPackets include
|
2. Reset CP warmup counter — subsequent data packets include coordinates
|
||||||
coordinates
|
|
||||||
3. When discovery completes, warmup counter resets again (covers timing gap)
|
3. When discovery completes, warmup counter resets again (covers timing gap)
|
||||||
|
|
||||||
The crypto session remains active throughout — only routing state is
|
The crypto session remains active throughout — only routing state is
|
||||||
@@ -386,7 +385,7 @@ source.
|
|||||||
**Source recovery**:
|
**Source recovery**:
|
||||||
1. Remove stale coordinates from cache
|
1. Remove stale coordinates from cache
|
||||||
2. Initiate discovery for the destination
|
2. Initiate discovery for the destination
|
||||||
3. Reset COORDS_PRESENT warmup counter
|
3. Reset CP warmup counter
|
||||||
|
|
||||||
### Error Signal Rate Limiting
|
### Error Signal Rate Limiting
|
||||||
|
|
||||||
@@ -397,11 +396,11 @@ packets to the same destination hit the same routing failure simultaneously.
|
|||||||
### Error Routing Limitation
|
### Error Routing Limitation
|
||||||
|
|
||||||
Error signals route back to the source using `find_next_hop(src_addr)`. For
|
Error signals route back to the source using `find_next_hop(src_addr)`. For
|
||||||
steady-state DataPackets (after the COORDS_PRESENT warmup window), the
|
steady-state data packets (after the CP warmup window), the
|
||||||
transit node may lack cached coordinates for the source. If so, the error is
|
transit node may lack cached coordinates for the source. If so, the error is
|
||||||
silently dropped.
|
silently dropped.
|
||||||
|
|
||||||
This blind spot is partially addressed by COORDS_PRESENT warmup: transit
|
This blind spot is partially addressed by CP warmup: transit
|
||||||
nodes receive source coordinates during the warmup phase. But after warmup
|
nodes receive source coordinates during the warmup phase. But after warmup
|
||||||
expires and transit caches for the source expire, errors may be lost. The
|
expires and transit caches for the source expire, errors may be lost. The
|
||||||
session idle timeout (90s) limits the window — if traffic stops long enough
|
session idle timeout (90s) limits the window — if traffic stops long enough
|
||||||
@@ -423,8 +422,8 @@ sequence:
|
|||||||
returns the destination's coordinates
|
returns the destination's coordinates
|
||||||
4. **Session establishment**: SessionSetup carries coordinates, warming
|
4. **Session establishment**: SessionSetup carries coordinates, warming
|
||||||
transit caches along the path
|
transit caches along the path
|
||||||
5. **Warmup**: First N DataPackets include COORDS_PRESENT, reinforcing
|
5. **Warmup**: First N data packets include CP flag, reinforcing transit
|
||||||
transit caches
|
caches
|
||||||
|
|
||||||
The first packet to a new destination always triggers this sequence. The
|
The first packet to a new destination always triggers this sequence. The
|
||||||
packet is queued (bounded) until the session is established.
|
packet is queued (bounded) until the session is established.
|
||||||
@@ -435,7 +434,7 @@ After session establishment and warmup:
|
|||||||
|
|
||||||
- Transit nodes have cached coordinates for both endpoints
|
- Transit nodes have cached coordinates for both endpoints
|
||||||
- Bloom filters have converged for the destination
|
- Bloom filters have converged for the destination
|
||||||
- DataPackets use minimal headers (no coordinates)
|
- Data packets use minimal headers (no coordinates)
|
||||||
- Routing decisions are fast: bloom candidate selection + distance ranking
|
- Routing decisions are fast: bloom candidate selection + distance ranking
|
||||||
|
|
||||||
### Steady State
|
### Steady State
|
||||||
@@ -445,7 +444,7 @@ In steady state, the mesh is mostly self-maintaining:
|
|||||||
- TreeAnnounce gossip keeps the spanning tree current
|
- TreeAnnounce gossip keeps the spanning tree current
|
||||||
- FilterAnnounce gossip keeps bloom filters current
|
- FilterAnnounce gossip keeps bloom filters current
|
||||||
- Coordinate caches are refreshed by active routing traffic
|
- Coordinate caches are refreshed by active routing traffic
|
||||||
- Occasional cache misses trigger COORDS_PRESENT or discovery, but these
|
- Occasional cache misses trigger CP warmup or discovery, but these
|
||||||
are rare when traffic is flowing
|
are rare when traffic is flowing
|
||||||
|
|
||||||
### Cache Expiry and Recovery
|
### Cache Expiry and Recovery
|
||||||
@@ -511,8 +510,8 @@ routing decisions but retains its own end-to-end encryption and identity.
|
|||||||
| LookupResponse | ~400 bytes | Response to discovery | Yes (greedy routed) |
|
| LookupResponse | ~400 bytes | Response to discovery | Yes (greedy routed) |
|
||||||
| SessionDatagram + SessionSetup | ~232–402 bytes | Session establishment | Yes (routed) |
|
| SessionDatagram + SessionSetup | ~232–402 bytes | Session establishment | Yes (routed) |
|
||||||
| SessionDatagram + SessionAck | ~122 bytes | Session confirmation | Yes (routed) |
|
| SessionDatagram + SessionAck | ~122 bytes | Session confirmation | Yes (routed) |
|
||||||
| SessionDatagram + DataPacket (minimal) | 40 bytes + payload | Bulk traffic | Yes (routed) |
|
| SessionDatagram + Data (minimal) | ~107 bytes + payload | Bulk traffic | Yes (routed) |
|
||||||
| SessionDatagram + DataPacket (with coords) | ~172 bytes + payload | Warmup/recovery | Yes (routed) |
|
| SessionDatagram + Data (with CP) | ~150 bytes + payload | Warmup/recovery | Yes (routed) |
|
||||||
| SessionDatagram + CoordsRequired | 70 bytes | Cache miss error | Yes (routed) |
|
| SessionDatagram + CoordsRequired | 70 bytes | Cache miss error | Yes (routed) |
|
||||||
| SessionDatagram + PathBroken | 70+ bytes | Dead-end error | Yes (routed) |
|
| SessionDatagram + PathBroken | 70+ bytes | Dead-end error | Yes (routed) |
|
||||||
| Disconnect | 2 bytes | Link teardown | No (peer-to-peer) |
|
| Disconnect | 2 bytes | Link teardown | No (peer-to-peer) |
|
||||||
@@ -550,7 +549,7 @@ recovery).
|
|||||||
| Flush coord cache on parent change | **Implemented** |
|
| Flush coord cache on parent change | **Implemented** |
|
||||||
| LookupRequest/LookupResponse discovery | **Implemented** |
|
| LookupRequest/LookupResponse discovery | **Implemented** |
|
||||||
| SessionSetup self-bootstrapping | **Implemented** |
|
| SessionSetup self-bootstrapping | **Implemented** |
|
||||||
| COORDS_PRESENT warmup-then-reactive | **Implemented** |
|
| CP warmup-then-reactive | **Implemented** |
|
||||||
| CoordsRequired recovery | **Implemented** |
|
| CoordsRequired recovery | **Implemented** |
|
||||||
| PathBroken recovery | **Implemented** |
|
| PathBroken recovery | **Implemented** |
|
||||||
| Error signal rate limiting | **Implemented** |
|
| Error signal rate limiting | **Implemented** |
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ and transmitted after the session is established.
|
|||||||
SessionSetup is self-bootstrapping for routing. It carries the source's and
|
SessionSetup is self-bootstrapping for routing. It carries the source's and
|
||||||
destination's tree coordinates in the clear (not inside the Noise payload).
|
destination's tree coordinates in the clear (not inside the Noise payload).
|
||||||
As the message transits intermediate nodes, each node caches these coordinates,
|
As the message transits intermediate nodes, each node caches these coordinates,
|
||||||
warming the path for subsequent DataPackets that carry only addresses (no
|
warming the path for subsequent data packets that carry only addresses (no
|
||||||
coordinates).
|
coordinates).
|
||||||
|
|
||||||
SessionAck carries the responder's coordinates back along the reverse path,
|
SessionAck carries the responder's coordinates back along the reverse path,
|
||||||
@@ -145,12 +145,14 @@ This ensures exactly one handshake completes.
|
|||||||
|
|
||||||
### Data Transfer
|
### Data Transfer
|
||||||
|
|
||||||
Once established, sessions carry DataPacket messages containing encrypted
|
Once established, sessions carry encrypted data using the FSP pipeline. Each
|
||||||
application data. Each DataPacket includes:
|
encrypted message includes:
|
||||||
|
|
||||||
- An explicit 8-byte counter for replay protection (used as the AEAD nonce)
|
- A 12-byte cleartext header (used as AEAD AAD) containing the counter and
|
||||||
- A flags byte (including COORDS_PRESENT for cache warming)
|
flags (including the CP flag for coordinate cache warming)
|
||||||
- The encrypted payload
|
- Optional cleartext coordinates when the CP flag is set
|
||||||
|
- An AEAD-encrypted payload containing a 6-byte inner header (session-relative
|
||||||
|
timestamp, message type, inner flags) followed by the application data
|
||||||
|
|
||||||
### Session Idle Timeout
|
### Session Idle Timeout
|
||||||
|
|
||||||
@@ -239,9 +241,9 @@ discarded after the handshake.
|
|||||||
## Replay Protection
|
## Replay Protection
|
||||||
|
|
||||||
FSP uses explicit 8-byte counters on the wire for replay protection. Each side
|
FSP uses explicit 8-byte counters on the wire for replay protection. Each side
|
||||||
maintains a monotonically increasing send counter, transmitted with every
|
maintains a monotonically increasing send counter, included in the 12-byte
|
||||||
DataPacket. The receiver maintains a sliding window (2048-entry bitmap)
|
cleartext header of every encrypted message. The receiver maintains a sliding
|
||||||
tracking which counters have been seen.
|
window (2048-entry bitmap) tracking which counters have been seen.
|
||||||
|
|
||||||
This design is critical for operation over unreliable transports. Under UDP
|
This design is critical for operation over unreliable transports. Under UDP
|
||||||
packet loss or reordering, implicit nonce counters (where the receiver
|
packet loss or reordering, implicit nonce counters (where the receiver
|
||||||
@@ -253,34 +255,34 @@ regardless of what packets were lost or reordered.
|
|||||||
The same `ReplayWindow` and `decrypt_with_replay_check()` implementation is
|
The same `ReplayWindow` and `decrypt_with_replay_check()` implementation is
|
||||||
used at both the link and session layers.
|
used at both the link and session layers.
|
||||||
|
|
||||||
## COORDS_PRESENT Warmup Strategy
|
## CP (Coords Present) Warmup Strategy
|
||||||
|
|
||||||
Session establishment (SessionSetup/SessionAck) warms transit node coordinate
|
Session establishment (SessionSetup/SessionAck) warms transit node coordinate
|
||||||
caches along the path. But coordinate caches have a finite TTL (default 300s),
|
caches along the path. But coordinate caches have a finite TTL (default 300s),
|
||||||
and entries may be evicted under memory pressure. When a transit node's cache
|
and entries may be evicted under memory pressure. When a transit node's cache
|
||||||
entry expires, it cannot forward DataPackets (which carry only addresses, not
|
entry expires, it cannot forward data packets (which carry only addresses, not
|
||||||
coordinates) and sends a CoordsRequired error.
|
coordinates) and sends a CoordsRequired error.
|
||||||
|
|
||||||
FSP uses a warmup-then-reactive strategy to keep transit caches populated:
|
FSP uses a warmup-then-reactive strategy to keep transit caches populated:
|
||||||
|
|
||||||
### Warmup Phase
|
### Warmup Phase
|
||||||
|
|
||||||
After session establishment, the first N DataPackets (configurable, default 5)
|
After session establishment, the first N data packets (configurable, default 5)
|
||||||
include both source and destination coordinates via the COORDS_PRESENT flag.
|
include both source and destination coordinates via the CP flag in the FSP
|
||||||
Transit nodes cache these coordinates as packets pass through, reinforcing the
|
common prefix. The coordinates appear in cleartext between the 12-byte header
|
||||||
path established by SessionSetup.
|
and the ciphertext, allowing transit nodes to cache them without decryption.
|
||||||
|
|
||||||
### Steady State
|
### Steady State
|
||||||
|
|
||||||
After the warmup count is reached, FSP clears the COORDS_PRESENT flag and
|
After the warmup count is reached, FSP clears the CP flag and sends minimal
|
||||||
sends minimal DataPackets (4-byte header instead of ~136 bytes with
|
data packets (12-byte header + ciphertext). Transit nodes serve from their
|
||||||
coordinates). Transit nodes serve from their coordinate caches.
|
coordinate caches.
|
||||||
|
|
||||||
### Reactive Recovery
|
### Reactive Recovery
|
||||||
|
|
||||||
When FSP receives a CoordsRequired signal:
|
When FSP receives a CoordsRequired signal:
|
||||||
|
|
||||||
1. The warmup counter resets — subsequent DataPackets include coordinates again
|
1. The warmup counter resets — subsequent data packets include coordinates again
|
||||||
2. A new LookupRequest may be initiated to rediscover the destination's
|
2. A new LookupRequest may be initiated to rediscover the destination's
|
||||||
current coordinates
|
current coordinates
|
||||||
3. When the LookupResponse arrives for an established session, the warmup
|
3. When the LookupResponse arrives for an established session, the warmup
|
||||||
@@ -343,7 +345,7 @@ either fall back to bloom-filter-only routing or signal CoordsRequired.
|
|||||||
|
|
||||||
The coordinate cache is a single unified cache (merged from previously
|
The coordinate cache is a single unified cache (merged from previously
|
||||||
separate coord_cache and route_cache). All coordinate sources — SessionSetup
|
separate coord_cache and route_cache). All coordinate sources — SessionSetup
|
||||||
transit, COORDS_PRESENT DataPackets, LookupResponse — write to the same cache.
|
transit, CP-flagged data packets, LookupResponse — write to the same cache.
|
||||||
|
|
||||||
### Eviction Policy
|
### Eviction Policy
|
||||||
|
|
||||||
@@ -380,7 +382,9 @@ node caches (still within their 300s TTL) are re-warmed.
|
|||||||
| Session establishment (Noise IK) | **Implemented** |
|
| Session establishment (Noise IK) | **Implemented** |
|
||||||
| End-to-end encryption (ChaCha20-Poly1305) | **Implemented** |
|
| End-to-end encryption (ChaCha20-Poly1305) | **Implemented** |
|
||||||
| Explicit counter replay protection | **Implemented** |
|
| Explicit counter replay protection | **Implemented** |
|
||||||
| COORDS_PRESENT warmup-then-reactive | **Implemented** |
|
| CP warmup-then-reactive | **Implemented** |
|
||||||
|
| FSP wire format (prefix, AAD, inner header) | **Implemented** |
|
||||||
|
| Session-layer MMP report types | **Implemented** (wire format) |
|
||||||
| Identity cache (LRU-only) | **Implemented** |
|
| Identity cache (LRU-only) | **Implemented** |
|
||||||
| Coordinate cache (unified, TTL + refresh) | **Implemented** |
|
| Coordinate cache (unified, TTL + refresh) | **Implemented** |
|
||||||
| Session idle timeout | **Implemented** |
|
| Session idle timeout | **Implemented** |
|
||||||
|
|||||||
@@ -396,103 +396,166 @@ Orderly link teardown with reason code.
|
|||||||
| 0x07 | Timeout | Keepalive or stale detection timeout |
|
| 0x07 | Timeout | Keepalive or stale detection timeout |
|
||||||
| 0xFF | Other | Unspecified reason |
|
| 0xFF | Other | Unspecified reason |
|
||||||
|
|
||||||
## Session-Layer Message Types
|
## Session-Layer Message Formats
|
||||||
|
|
||||||
These messages are carried as the payload of a SessionDatagram (0x00).
|
Session-layer messages are carried as the payload of a SessionDatagram (0x00).
|
||||||
|
All FSP messages begin with a **4-byte common prefix** that identifies the
|
||||||
|
protocol version, session lifecycle phase, per-packet flags, and payload length.
|
||||||
|
|
||||||
### SessionSetup (0x00)
|
### FSP Common Prefix (4 bytes)
|
||||||
|
|
||||||
|
| Field | Size | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| version | 4 bits (high) | Protocol version. Currently 0x0 |
|
||||||
|
| phase | 4 bits (low) | Session lifecycle phase (see table) |
|
||||||
|
| flags | 1 byte | Per-packet signal flags (zero during handshake) |
|
||||||
|
| payload_len | 2 bytes LE | Length of payload after phase-specific header |
|
||||||
|
|
||||||
|
### FSP Phase Table
|
||||||
|
|
||||||
|
| Phase | Type | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| 0x0 | Established | Post-handshake encrypted traffic or plaintext error signals |
|
||||||
|
| 0x1 | Handshake msg1 | SessionSetup (Noise IK msg1) |
|
||||||
|
| 0x2 | Handshake msg2 | SessionAck (Noise IK msg2) |
|
||||||
|
|
||||||
|
### FSP Flags (Established Phase Only)
|
||||||
|
|
||||||
|
| Bit | Name | Description |
|
||||||
|
| --- | ---- | ----------- |
|
||||||
|
| 0 | CP (coords present) | Source and destination coordinates follow the header in cleartext |
|
||||||
|
| 1 | K (key epoch) | Selects active key during rekeying |
|
||||||
|
| 2 | U (unencrypted) | Payload is plaintext (error signals) |
|
||||||
|
| 3-7 | — | Reserved (must be zero) |
|
||||||
|
|
||||||
|
Flags must be zero in handshake packets (phase 0x1 and 0x2).
|
||||||
|
|
||||||
|
### FSP Encrypted Message (phase 0x0, U flag clear)
|
||||||
|
|
||||||
|
Post-handshake encrypted data. The 12-byte cleartext header is used as AEAD
|
||||||
|
AAD. Coordinates may appear in cleartext between the header and ciphertext
|
||||||
|
when the CP flag is set.
|
||||||
|
|
||||||
|
**Cleartext header** (12 bytes, used as AEAD AAD):
|
||||||
|
|
||||||
|
| Field | Size | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| common prefix | 4 bytes | ver=0, phase=0, flags, payload_len |
|
||||||
|
| counter | 8 bytes LE | Monotonic nonce, used as AEAD nonce and for replay detection |
|
||||||
|
|
||||||
|
**Optional cleartext coordinates** (when CP flag is set):
|
||||||
|
|
||||||
|
| Field | Size | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| src_coords_count | 2 bytes LE | Number of source coordinate entries |
|
||||||
|
| src_coords | 16 x n bytes | Source's ancestry (NodeAddr, self -> root) |
|
||||||
|
| dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
|
||||||
|
| dest_coords | 16 x m bytes | Destination's ancestry |
|
||||||
|
|
||||||
|
Transit nodes parse the CP flag and extract coordinates without decryption.
|
||||||
|
|
||||||
|
**Encrypted inner header** (6 bytes, first bytes of AEAD plaintext):
|
||||||
|
|
||||||
|
| Field | Size | Description |
|
||||||
|
| ----- | ---- | ----------- |
|
||||||
|
| timestamp | 4 bytes LE | Session-relative milliseconds (u32) |
|
||||||
|
| msg_type | 1 byte | Session-layer message type |
|
||||||
|
| inner_flags | 1 byte | Bit 0: SP (spin bit for RTT measurement) |
|
||||||
|
|
||||||
|
After the inner header, the remaining plaintext is the message-type-specific
|
||||||
|
body.
|
||||||
|
|
||||||
|
**Complete encrypted message**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────────────────────────┬─────────────────┬───────────────────────────┐
|
||||||
|
│ header (12 bytes, used as AAD) │ [coords if CP] │ ciphertext + AEAD tag │
|
||||||
|
│ │ │ (inner_hdr + body) + 16 │
|
||||||
|
└─────────────────────────────────┴─────────────────┴───────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### FSP Session Message Types
|
||||||
|
|
||||||
|
| Type | Message | Description |
|
||||||
|
| ---- | ------- | ----------- |
|
||||||
|
| 0x10 | Data | Application data (IPv6 payload via TUN) |
|
||||||
|
| 0x11 | SenderReport | MMP sender-side metrics report |
|
||||||
|
| 0x12 | ReceiverReport | MMP receiver-side metrics report |
|
||||||
|
| 0x13 | PathMtuNotification | End-to-end path MTU echo |
|
||||||
|
| 0x20 | CoordsRequired | Error: transit node lacks destination coordinates |
|
||||||
|
| 0x21 | PathBroken | Error: greedy routing reached dead end |
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
### SessionSetup (phase 0x1)
|
||||||
|
|
||||||
Establishes a session and warms transit coordinate caches.
|
Establishes a session and warms transit coordinate caches.
|
||||||
|
Encoded with FSP prefix: ver=0, phase=0x1, flags=0, payload_len.
|
||||||
|
|
||||||
|
**Body** (after 4-byte FSP prefix):
|
||||||
|
|
||||||
| Offset | Field | Size | Description |
|
| Offset | Field | Size | Description |
|
||||||
| ------ | ----- | ---- | ----------- |
|
| ------ | ----- | ---- | ----------- |
|
||||||
| 0 | msg_type | 1 byte | 0x00 |
|
| 0 | flags | 1 byte | Bit 0: REQUEST_ACK, Bit 1: BIDIRECTIONAL |
|
||||||
| 1 | flags | 1 byte | Bit 0: REQUEST_ACK, Bit 1: BIDIRECTIONAL |
|
| 1 | src_coords_count | 2 bytes LE | Number of source coordinate entries |
|
||||||
| 2 | src_coords_count | 2 bytes LE | Number of source coordinate entries |
|
| 3 | src_coords | 16 x n bytes | Source's ancestry (NodeAddr, self -> root) |
|
||||||
| 4 | src_coords | 16 x n bytes | Source's ancestry (NodeAddr, self -> root) |
|
|
||||||
| ... | dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
|
| ... | dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
|
||||||
| ... | dest_coords | 16 x m bytes | Destination's ancestry |
|
| ... | dest_coords | 16 x m bytes | Destination's ancestry |
|
||||||
| ... | handshake_len | 2 bytes LE | Noise payload length |
|
| ... | handshake_len | 2 bytes LE | Noise payload length |
|
||||||
| ... | handshake_payload | variable | Noise IK msg1 (82 bytes typical) |
|
| ... | handshake_payload | variable | Noise IK msg1 (82 bytes typical) |
|
||||||
|
|
||||||
**Example** (depth-3 source, depth-4 destination):
|
### SessionAck (phase 0x2)
|
||||||
|
|
||||||
```text
|
|
||||||
SessionDatagram header: 36 bytes
|
|
||||||
SessionSetup payload: 1 + 1 + 2 + 48 + 2 + 64 + 2 + 82 = 202 bytes
|
|
||||||
Total: 238 bytes
|
|
||||||
```
|
|
||||||
|
|
||||||
### SessionAck (0x01)
|
|
||||||
|
|
||||||
Confirms session establishment, completes the Noise handshake.
|
Confirms session establishment, completes the Noise handshake.
|
||||||
|
Encoded with FSP prefix: ver=0, phase=0x2, flags=0, payload_len.
|
||||||
|
|
||||||
|
**Body** (after 4-byte FSP prefix):
|
||||||
|
|
||||||
| Offset | Field | Size | Description |
|
| Offset | Field | Size | Description |
|
||||||
| ------ | ----- | ---- | ----------- |
|
| ------ | ----- | ---- | ----------- |
|
||||||
| 0 | msg_type | 1 byte | 0x01 |
|
| 0 | flags | 1 byte | Reserved |
|
||||||
| 1 | flags | 1 byte | Reserved |
|
| 1 | src_coords_count | 2 bytes LE | Number of coordinate entries |
|
||||||
| 2 | src_coords_count | 2 bytes LE | Number of coordinate entries |
|
| 3 | src_coords | 16 x n bytes | Acknowledger's ancestry (for cache warming) |
|
||||||
| 4 | src_coords | 16 x n bytes | Acknowledger's ancestry (for cache warming) |
|
|
||||||
| ... | handshake_len | 2 bytes LE | Noise payload length |
|
| ... | handshake_len | 2 bytes LE | Noise payload length |
|
||||||
| ... | handshake_payload | variable | Noise IK msg2 (33 bytes typical) |
|
| ... | handshake_payload | variable | Noise IK msg2 (33 bytes typical) |
|
||||||
|
|
||||||
### DataPacket (0x10)
|
### Data (0x10)
|
||||||
|
|
||||||
Encrypted application data with explicit replay protection counter.
|
Application data (typically IPv6 payload). This is the `msg_type` byte
|
||||||
|
inside the encrypted inner header — there is no separate DataPacket struct.
|
||||||
**Minimal header** (COORDS_PRESENT = 0):
|
The body after the inner header is delivered directly to the TUN interface.
|
||||||
|
|
||||||
| Offset | Field | Size | Description |
|
|
||||||
| ------ | ----- | ---- | ----------- |
|
|
||||||
| 0 | msg_type | 1 byte | 0x10 |
|
|
||||||
| 1 | flags | 1 byte | Bit 0: COORDS_PRESENT |
|
|
||||||
| 2 | counter | 8 bytes LE | Session encryption counter / replay nonce |
|
|
||||||
| 10 | payload_length | 2 bytes LE | Length of encrypted payload |
|
|
||||||
| 12 | payload | variable | Encrypted application data + 16-byte AEAD tag |
|
|
||||||
|
|
||||||
**Header size**: 12 bytes (`DATA_HEADER_SIZE`)
|
|
||||||
|
|
||||||
**With coordinates** (COORDS_PRESENT = 1):
|
|
||||||
|
|
||||||
| Offset | Field | Size | Description |
|
|
||||||
| ------ | ----- | ---- | ----------- |
|
|
||||||
| 0 | msg_type | 1 byte | 0x10 |
|
|
||||||
| 1 | flags | 1 byte | 0x01 (COORDS_PRESENT) |
|
|
||||||
| 2 | counter | 8 bytes LE | Session encryption counter |
|
|
||||||
| 10 | payload_length | 2 bytes LE | Length of encrypted payload |
|
|
||||||
| 12 | src_coords_count | 2 bytes LE | Source coordinate entries |
|
|
||||||
| 14 | src_coords | 16 x n bytes | Source's ancestry |
|
|
||||||
| ... | dest_coords_count | 2 bytes LE | Dest coordinate entries |
|
|
||||||
| ... | dest_coords | 16 x m bytes | Destination's ancestry |
|
|
||||||
| ... | payload | variable | Encrypted application data |
|
|
||||||
|
|
||||||
### CoordsRequired (0x20)
|
### CoordsRequired (0x20)
|
||||||
|
|
||||||
Link-layer error signal — transit node lacks coordinates for destination.
|
Plaintext error signal — transit node lacks coordinates for destination.
|
||||||
Plaintext (not end-to-end encrypted), generated by transit nodes.
|
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len.
|
||||||
|
|
||||||
|
**Body** (after 4-byte FSP prefix + 1-byte msg_type):
|
||||||
|
|
||||||
| Offset | Field | Size | Description |
|
| Offset | Field | Size | Description |
|
||||||
| ------ | ----- | ---- | ----------- |
|
| ------ | ----- | ---- | ----------- |
|
||||||
| 0 | msg_type | 1 byte | 0x20 |
|
| 0 | flags | 1 byte | Reserved |
|
||||||
| 1 | flags | 1 byte | Reserved |
|
| 1 | dest_addr | 16 bytes | NodeAddr we couldn't route to |
|
||||||
| 2 | dest_addr | 16 bytes | NodeAddr we couldn't route to |
|
| 17 | reporter | 16 bytes | NodeAddr of reporting router |
|
||||||
| 18 | reporter | 16 bytes | NodeAddr of reporting router |
|
|
||||||
|
|
||||||
**Payload**: 34 bytes. Wrapped in SessionDatagram: 70 bytes total.
|
**Body size**: 33 bytes. Total with prefix + msg_type: 38 bytes.
|
||||||
|
|
||||||
### PathBroken (0x21)
|
### PathBroken (0x21)
|
||||||
|
|
||||||
Link-layer error signal — greedy routing reached a dead end. Plaintext,
|
Plaintext error signal — greedy routing reached a dead end.
|
||||||
generated by transit nodes.
|
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len.
|
||||||
|
|
||||||
|
**Body** (after 4-byte FSP prefix + 1-byte msg_type):
|
||||||
|
|
||||||
| Offset | Field | Size | Description |
|
| Offset | Field | Size | Description |
|
||||||
| ------ | ----- | ---- | ----------- |
|
| ------ | ----- | ---- | ----------- |
|
||||||
| 0 | msg_type | 1 byte | 0x21 |
|
| 0 | flags | 1 byte | Reserved |
|
||||||
| 1 | flags | 1 byte | Reserved |
|
| 1 | dest_addr | 16 bytes | Unreachable NodeAddr |
|
||||||
| 2 | dest_addr | 16 bytes | Unreachable NodeAddr |
|
| 17 | reporter | 16 bytes | NodeAddr of reporting router |
|
||||||
| 18 | reporter | 16 bytes | NodeAddr of reporting router |
|
| 33 | last_coords_count | 2 bytes LE | Number of stale coordinate entries |
|
||||||
| 34 | last_coords_count | 2 bytes LE | Number of stale coordinate entries |
|
| 35 | last_known_coords | 16 x n bytes | Stale coordinates that failed |
|
||||||
| 36 | last_known_coords | 16 x n bytes | Stale coordinates that failed |
|
|
||||||
|
|
||||||
## Encapsulation Walkthrough
|
## Encapsulation Walkthrough
|
||||||
|
|
||||||
@@ -507,19 +570,19 @@ Layer 4: Application data
|
|||||||
1024 bytes
|
1024 bytes
|
||||||
|
|
||||||
Layer 3: Session encryption (FSP)
|
Layer 3: Session encryption (FSP)
|
||||||
DataPacket header (12 bytes) + encrypted payload (1024) + AEAD tag (16)
|
FSP header (12 bytes) + AEAD(inner_hdr (6) + payload (1024)) + AEAD tag (16)
|
||||||
= 1052 bytes
|
= 1058 bytes
|
||||||
|
|
||||||
Layer 2: SessionDatagram envelope (FLP routing)
|
Layer 2: SessionDatagram envelope (FLP routing)
|
||||||
msg_type (1) + ttl (1) + path_mtu (2) + src_addr (16) + dest_addr (16) + payload (1052)
|
msg_type (1) + ttl (1) + path_mtu (2) + src_addr (16) + dest_addr (16) + payload (1058)
|
||||||
= 1088 bytes
|
= 1094 bytes
|
||||||
|
|
||||||
Layer 1: Link encryption (FLP per-hop)
|
Layer 1: Link encryption (FLP per-hop)
|
||||||
outer header (16) + encrypted(inner_hdr (5) + datagram (1088)) + AEAD tag (16)
|
outer header (16) + encrypted(inner_hdr (5) + datagram (1094)) + AEAD tag (16)
|
||||||
= 1125 bytes
|
= 1131 bytes
|
||||||
|
|
||||||
Layer 0: Transport
|
Layer 0: Transport
|
||||||
UDP datagram containing 1125 bytes
|
UDP datagram containing 1131 bytes
|
||||||
```
|
```
|
||||||
|
|
||||||
### Overhead Budget
|
### Overhead Budget
|
||||||
@@ -528,11 +591,12 @@ Layer 0: Transport
|
|||||||
| ----- | -------- | --------- |
|
| ----- | -------- | --------- |
|
||||||
| Link encryption | 37 bytes | 16 outer header (AAD) + 5 inner header + 16 AEAD tag |
|
| Link encryption | 37 bytes | 16 outer header (AAD) + 5 inner header + 16 AEAD tag |
|
||||||
| SessionDatagram | 36 bytes | 1 type + 1 ttl + 2 path_mtu + 16 src + 16 dest |
|
| SessionDatagram | 36 bytes | 1 type + 1 ttl + 2 path_mtu + 16 src + 16 dest |
|
||||||
| DataPacket header | 12 bytes | 1 type + 1 flags + 8 counter + 2 length |
|
| FSP header | 12 bytes | 4 prefix + 8 counter |
|
||||||
|
| FSP inner header | 6 bytes | 4 timestamp + 1 msg_type + 1 inner_flags (inside AEAD) |
|
||||||
| Session AEAD tag | 16 bytes | Poly1305 tag on session-encrypted payload |
|
| Session AEAD tag | 16 bytes | Poly1305 tag on session-encrypted payload |
|
||||||
| **Minimal total** | **101 bytes** | |
|
| **Minimal total** | **107 bytes** | |
|
||||||
| Coordinates (if present) | ~43 bytes | Varies with tree depth |
|
| Coordinates (if present) | ~43 bytes | Varies with tree depth |
|
||||||
| **Worst case** | **144 bytes** | `FIPS_OVERHEAD` constant |
|
| **Worst case** | **150 bytes** | `FIPS_OVERHEAD` constant |
|
||||||
|
|
||||||
### At Each Transit Node
|
### At Each Transit Node
|
||||||
|
|
||||||
@@ -581,20 +645,23 @@ endpoint session keys).
|
|||||||
| ------- | ------------ | ----- |
|
| ------- | ------------ | ----- |
|
||||||
| SessionSetup | ~200 bytes | Depth-dependent |
|
| SessionSetup | ~200 bytes | Depth-dependent |
|
||||||
| SessionAck | ~80 bytes | Depth-dependent |
|
| SessionAck | ~80 bytes | Depth-dependent |
|
||||||
| DataPacket (minimal) | 12 + payload bytes | Steady state |
|
| Data (minimal) | 12 + 6 + payload + 16 bytes | Steady state |
|
||||||
| DataPacket (with coords) | 12 + ~130 + payload bytes | Warmup/recovery |
|
| Data (with coords) | 12 + ~130 + 6 + payload + 16 bytes | Warmup/recovery |
|
||||||
| CoordsRequired | 34 bytes | Fixed |
|
| SenderReport | 12 + 6 + 46 + 16 bytes | MMP metrics |
|
||||||
| PathBroken | 36 + 16n bytes | Includes stale coords |
|
| ReceiverReport | 12 + 6 + 66 + 16 bytes | MMP metrics |
|
||||||
|
| PathMtuNotification | 12 + 6 + 2 + 16 bytes | MTU signal |
|
||||||
|
| CoordsRequired | 38 bytes | Fixed (prefix + msg_type + body) |
|
||||||
|
| PathBroken | 35 + 16n bytes | Includes stale coords |
|
||||||
|
|
||||||
### Complete Packet Sizes (link + session)
|
### Complete Packet Sizes (link + session)
|
||||||
|
|
||||||
| Scenario | Wire Size | Notes |
|
| Scenario | Wire Size | Notes |
|
||||||
| -------- | --------- | ----- |
|
| -------- | --------- | ----- |
|
||||||
| Encrypted frame minimum | 37 bytes | Empty body |
|
| Encrypted frame minimum | 37 bytes | Empty body |
|
||||||
| SessionDatagram + DataPacket (minimal) | 37 + 36 + 12 + payload + 16 | 101 + payload |
|
| SessionDatagram + Data (minimal) | 37 + 36 + 12 + 6 + payload + 16 | 107 + payload |
|
||||||
| SessionDatagram + DataPacket (with coords) | ~144 + payload | Worst case |
|
| SessionDatagram + Data (with coords) | ~150 + payload | Worst case |
|
||||||
| SessionDatagram + SessionSetup | ~275 bytes | Depth-3, both dirs |
|
| SessionDatagram + SessionSetup | ~275 bytes | Depth-3, both dirs |
|
||||||
| SessionDatagram + CoordsRequired | 37 + 36 + 34 = 107 bytes | Including link overhead |
|
| SessionDatagram + CoordsRequired | 37 + 36 + 38 = 111 bytes | Including link overhead |
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ use thiserror::Error;
|
|||||||
|
|
||||||
pub use node::{
|
pub use node::{
|
||||||
BloomConfig, BuffersConfig, CacheConfig, DiscoveryConfig, LimitsConfig, NodeConfig,
|
BloomConfig, BuffersConfig, CacheConfig, DiscoveryConfig, LimitsConfig, NodeConfig,
|
||||||
RateLimitConfig, RetryConfig, SessionConfig, TreeConfig,
|
RateLimitConfig, RetryConfig, SessionConfig, SessionMmpConfig, TreeConfig,
|
||||||
};
|
};
|
||||||
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
|
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
|
||||||
pub use transport::{TransportInstances, TransportsConfig, UdpConfig};
|
pub use transport::{TransportInstances, TransportsConfig, UdpConfig};
|
||||||
|
|||||||
+44
-3
@@ -7,7 +7,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::IdentityConfig;
|
use super::IdentityConfig;
|
||||||
use crate::mmp::MmpConfig;
|
use crate::mmp::{MmpConfig, MmpMode, DEFAULT_LOG_INTERVAL_SECS, DEFAULT_OWD_WINDOW_SIZE};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Node Configuration Subsections
|
// Node Configuration Subsections
|
||||||
@@ -227,7 +227,7 @@ pub struct SessionConfig {
|
|||||||
/// Established sessions with no activity for this duration are removed.
|
/// Established sessions with no activity for this duration are removed.
|
||||||
#[serde(default = "SessionConfig::default_idle_timeout_secs")]
|
#[serde(default = "SessionConfig::default_idle_timeout_secs")]
|
||||||
pub idle_timeout_secs: u64,
|
pub idle_timeout_secs: u64,
|
||||||
/// Number of initial DataPackets per session that include COORDS_PRESENT
|
/// Number of initial data packets per session that include COORDS_PRESENT
|
||||||
/// for transit cache warmup (`node.session.coords_warmup_packets`).
|
/// for transit cache warmup (`node.session.coords_warmup_packets`).
|
||||||
/// Also used as the reset count on CoordsRequired receipt.
|
/// Also used as the reset count on CoordsRequired receipt.
|
||||||
#[serde(default = "SessionConfig::default_coords_warmup_packets")]
|
#[serde(default = "SessionConfig::default_coords_warmup_packets")]
|
||||||
@@ -254,6 +254,42 @@ impl SessionConfig {
|
|||||||
fn default_coords_warmup_packets() -> u8 { 5 }
|
fn default_coords_warmup_packets() -> u8 { 5 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Session-layer Metrics Measurement Protocol (`node.session_mmp.*`).
|
||||||
|
///
|
||||||
|
/// Separate from link-layer `node.mmp.*` to allow independent mode/interval
|
||||||
|
/// configuration per layer. Session reports consume bandwidth on every transit
|
||||||
|
/// link, so operators may want a lighter mode (e.g., Lightweight) for sessions
|
||||||
|
/// while running Full mode on links.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SessionMmpConfig {
|
||||||
|
/// Operating mode (`node.session_mmp.mode`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub mode: MmpMode,
|
||||||
|
|
||||||
|
/// Periodic operator log interval in seconds (`node.session_mmp.log_interval_secs`).
|
||||||
|
#[serde(default = "SessionMmpConfig::default_log_interval_secs")]
|
||||||
|
pub log_interval_secs: u64,
|
||||||
|
|
||||||
|
/// OWD trend ring buffer size (`node.session_mmp.owd_window_size`).
|
||||||
|
#[serde(default = "SessionMmpConfig::default_owd_window_size")]
|
||||||
|
pub owd_window_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SessionMmpConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mode: MmpMode::default(),
|
||||||
|
log_interval_secs: DEFAULT_LOG_INTERVAL_SECS,
|
||||||
|
owd_window_size: DEFAULT_OWD_WINDOW_SIZE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionMmpConfig {
|
||||||
|
fn default_log_interval_secs() -> u64 { DEFAULT_LOG_INTERVAL_SECS }
|
||||||
|
fn default_owd_window_size() -> usize { DEFAULT_OWD_WINDOW_SIZE }
|
||||||
|
}
|
||||||
|
|
||||||
/// Internal buffers (`node.buffers.*`).
|
/// Internal buffers (`node.buffers.*`).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BuffersConfig {
|
pub struct BuffersConfig {
|
||||||
@@ -343,9 +379,13 @@ pub struct NodeConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub buffers: BuffersConfig,
|
pub buffers: BuffersConfig,
|
||||||
|
|
||||||
/// Metrics Measurement Protocol (`node.mmp.*`).
|
/// Metrics Measurement Protocol — link layer (`node.mmp.*`).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mmp: MmpConfig,
|
pub mmp: MmpConfig,
|
||||||
|
|
||||||
|
/// Metrics Measurement Protocol — session layer (`node.session_mmp.*`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub session_mmp: SessionMmpConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NodeConfig {
|
impl Default for NodeConfig {
|
||||||
@@ -365,6 +405,7 @@ impl Default for NodeConfig {
|
|||||||
session: SessionConfig::default(),
|
session: SessionConfig::default(),
|
||||||
buffers: BuffersConfig::default(),
|
buffers: BuffersConfig::default(),
|
||||||
mmp: MmpConfig::default(),
|
mmp: MmpConfig::default(),
|
||||||
|
session_mmp: SessionMmpConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ pub use transport::udp::UdpTransport;
|
|||||||
|
|
||||||
// Re-export protocol types
|
// Re-export protocol types
|
||||||
pub use protocol::{
|
pub use protocol::{
|
||||||
CoordsRequired, DataFlags, DataPacket, FilterAnnounce, HandshakeMessageType, LinkMessageType,
|
CoordsRequired, FilterAnnounce, HandshakeMessageType, LinkMessageType,
|
||||||
LookupRequest, LookupResponse, PathBroken, ProtocolError, SessionAck, SessionDatagram,
|
LookupRequest, LookupResponse, PathBroken, ProtocolError, SessionAck, SessionDatagram,
|
||||||
SessionFlags, SessionMessageType, SessionSetup, TreeAnnounce,
|
SessionFlags, SessionMessageType, SessionSetup, TreeAnnounce,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
use crate::mmp::algorithms::{DualEwma, SrttEstimator, compute_etx};
|
use crate::mmp::algorithms::{DualEwma, SrttEstimator, compute_etx};
|
||||||
use crate::mmp::report::ReceiverReport;
|
use crate::mmp::report::ReceiverReport;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
/// Derived MMP metrics, updated from incoming ReceiverReports.
|
/// Derived MMP metrics, updated from incoming ReceiverReports.
|
||||||
///
|
///
|
||||||
@@ -80,6 +81,14 @@ impl MmpMetrics {
|
|||||||
if our_timestamp_ms > echo_ms + dwell_ms {
|
if our_timestamp_ms > echo_ms + dwell_ms {
|
||||||
let rtt_ms = our_timestamp_ms - echo_ms - dwell_ms;
|
let rtt_ms = our_timestamp_ms - echo_ms - dwell_ms;
|
||||||
let rtt_us = (rtt_ms as i64) * 1000;
|
let rtt_us = (rtt_ms as i64) * 1000;
|
||||||
|
debug!(
|
||||||
|
our_ts = our_timestamp_ms,
|
||||||
|
echo = echo_ms,
|
||||||
|
dwell = dwell_ms,
|
||||||
|
rtt_ms = rtt_ms,
|
||||||
|
srtt_ms = self.srtt.srtt_us() as f64 / 1000.0,
|
||||||
|
"RTT sample from timestamp echo"
|
||||||
|
);
|
||||||
self.srtt.update(rtt_us);
|
self.srtt.update(rtt_us);
|
||||||
self.rtt_trend.update(rtt_us as f64);
|
self.rtt_trend.update(rtt_us as f64);
|
||||||
}
|
}
|
||||||
|
|||||||
+258
-7
@@ -30,6 +30,9 @@ pub use receiver::ReceiverState;
|
|||||||
pub use report::{ReceiverReport, SenderReport};
|
pub use report::{ReceiverReport, SenderReport};
|
||||||
pub use sender::SenderState;
|
pub use sender::SenderState;
|
||||||
|
|
||||||
|
// Session-layer re-exports
|
||||||
|
// MmpSessionState and PathMtuState are defined in this file
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Constants
|
// Constants
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -80,15 +83,29 @@ pub const DEFAULT_OWD_WINDOW_SIZE: usize = 32;
|
|||||||
/// Default operator log interval in seconds.
|
/// Default operator log interval in seconds.
|
||||||
pub const DEFAULT_LOG_INTERVAL_SECS: u64 = 30;
|
pub const DEFAULT_LOG_INTERVAL_SECS: u64 = 30;
|
||||||
|
|
||||||
|
// --- Session-layer timing defaults ---
|
||||||
|
// Session reports are routed end-to-end (bandwidth cost on every transit link),
|
||||||
|
// so intervals are higher than link-layer.
|
||||||
|
|
||||||
|
/// Session-layer minimum report interval.
|
||||||
|
pub const MIN_SESSION_REPORT_INTERVAL_MS: u64 = 500;
|
||||||
|
|
||||||
|
/// Session-layer maximum report interval.
|
||||||
|
pub const MAX_SESSION_REPORT_INTERVAL_MS: u64 = 10_000;
|
||||||
|
|
||||||
|
/// Session-layer cold-start report interval (before SRTT is available).
|
||||||
|
pub const SESSION_COLD_START_INTERVAL_MS: u64 = 1_000;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Operating Mode
|
// Operating Mode
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// MMP operating mode.
|
/// MMP operating mode.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum MmpMode {
|
pub enum MmpMode {
|
||||||
/// Sender + receiver reports at RTT-adaptive intervals. Maximum fidelity.
|
/// Sender + receiver reports at RTT-adaptive intervals. Maximum fidelity.
|
||||||
|
#[default]
|
||||||
Full,
|
Full,
|
||||||
/// Receiver reports only. Loss inferred from counter gaps.
|
/// Receiver reports only. Loss inferred from counter gaps.
|
||||||
Lightweight,
|
Lightweight,
|
||||||
@@ -96,12 +113,6 @@ pub enum MmpMode {
|
|||||||
Minimal,
|
Minimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MmpMode {
|
|
||||||
fn default() -> Self {
|
|
||||||
MmpMode::Full
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for MmpMode {
|
impl fmt::Display for MmpMode {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -205,6 +216,246 @@ impl MmpPeerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Per-Session MMP State (session-layer instantiation)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Combined MMP state for a single end-to-end session.
|
||||||
|
///
|
||||||
|
/// Wraps sender, receiver, metrics, spin bit, and path MTU state.
|
||||||
|
/// One instance per established `SessionEntry`.
|
||||||
|
pub struct MmpSessionState {
|
||||||
|
pub sender: SenderState,
|
||||||
|
pub receiver: ReceiverState,
|
||||||
|
pub metrics: MmpMetrics,
|
||||||
|
pub spin_bit: SpinBitState,
|
||||||
|
mode: MmpMode,
|
||||||
|
log_interval: Duration,
|
||||||
|
last_log_time: Option<Instant>,
|
||||||
|
pub path_mtu: PathMtuState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MmpSessionState {
|
||||||
|
/// Create MMP state for a new session.
|
||||||
|
///
|
||||||
|
/// `is_initiator`: true if this node initiated the Noise handshake
|
||||||
|
/// (determines spin bit role).
|
||||||
|
pub fn new(config: &crate::config::SessionMmpConfig, is_initiator: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
sender: SenderState::new_with_cold_start(SESSION_COLD_START_INTERVAL_MS),
|
||||||
|
receiver: ReceiverState::new_with_cold_start(
|
||||||
|
config.owd_window_size,
|
||||||
|
SESSION_COLD_START_INTERVAL_MS,
|
||||||
|
),
|
||||||
|
metrics: MmpMetrics::new(),
|
||||||
|
spin_bit: SpinBitState::new(is_initiator),
|
||||||
|
mode: config.mode,
|
||||||
|
log_interval: Duration::from_secs(config.log_interval_secs),
|
||||||
|
last_log_time: None,
|
||||||
|
path_mtu: PathMtuState::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current operating mode.
|
||||||
|
pub fn mode(&self) -> MmpMode {
|
||||||
|
self.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if it's time to emit a periodic metrics log.
|
||||||
|
pub fn should_log(&self, now: Instant) -> bool {
|
||||||
|
match self.last_log_time {
|
||||||
|
None => true,
|
||||||
|
Some(last) => now.duration_since(last) >= self.log_interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark that a periodic log was emitted.
|
||||||
|
pub fn mark_logged(&mut self, now: Instant) {
|
||||||
|
self.last_log_time = Some(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for MmpSessionState {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("MmpSessionState")
|
||||||
|
.field("mode", &self.mode)
|
||||||
|
.field("path_mtu", &self.path_mtu.current_mtu())
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Path MTU State (session-layer only)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Path MTU tracking for a single session.
|
||||||
|
///
|
||||||
|
/// Destination side: observes `path_mtu` from incoming SessionDatagram envelopes
|
||||||
|
/// and generates PathMtuNotification messages back to the source.
|
||||||
|
///
|
||||||
|
/// Source side: applies received PathMtuNotification to limit outbound datagram
|
||||||
|
/// size. Decrease is immediate; increase requires 3 consecutive notifications.
|
||||||
|
pub struct PathMtuState {
|
||||||
|
/// Current effective path MTU (what we use for sending).
|
||||||
|
current_mtu: u16,
|
||||||
|
/// Last observed path MTU from incoming datagrams (destination-side).
|
||||||
|
last_observed_mtu: u16,
|
||||||
|
/// Whether the observed MTU has changed since the last notification.
|
||||||
|
observed_changed: bool,
|
||||||
|
/// Last time a PathMtuNotification was sent.
|
||||||
|
last_notification_time: Option<Instant>,
|
||||||
|
/// Notification interval: max(10s, 5 * SRTT). Default 10s.
|
||||||
|
notification_interval: Duration,
|
||||||
|
/// For source-side increase tracking: consecutive higher-value notifications.
|
||||||
|
consecutive_increase_count: u8,
|
||||||
|
/// Time of the first notification in the current increase sequence.
|
||||||
|
first_increase_time: Option<Instant>,
|
||||||
|
/// The MTU value being proposed for increase.
|
||||||
|
pending_increase_mtu: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathMtuState {
|
||||||
|
/// Create path MTU state with no initial measurement.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
current_mtu: u16::MAX,
|
||||||
|
last_observed_mtu: u16::MAX,
|
||||||
|
observed_changed: false,
|
||||||
|
last_notification_time: None,
|
||||||
|
notification_interval: Duration::from_secs(10),
|
||||||
|
consecutive_increase_count: 0,
|
||||||
|
first_increase_time: None,
|
||||||
|
pending_increase_mtu: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current effective path MTU (source-side, for sending).
|
||||||
|
pub fn current_mtu(&self) -> u16 {
|
||||||
|
self.current_mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Last observed incoming path MTU (destination-side).
|
||||||
|
pub fn last_observed_mtu(&self) -> u16 {
|
||||||
|
self.last_observed_mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update notification interval from SRTT: max(10s, 5 * SRTT).
|
||||||
|
pub fn update_interval_from_srtt(&mut self, srtt_ms: f64) {
|
||||||
|
let five_srtt = Duration::from_millis((srtt_ms * 5.0) as u64);
|
||||||
|
self.notification_interval = five_srtt.max(Duration::from_secs(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seed source-side current_mtu from outbound transport MTU.
|
||||||
|
///
|
||||||
|
/// Called on each send. Only decreases (never increases) the current_mtu
|
||||||
|
/// so the destination's PathMtuNotification can still raise it later.
|
||||||
|
/// Ensures current_mtu doesn't stay at u16::MAX before any notification
|
||||||
|
/// arrives from the destination.
|
||||||
|
pub fn seed_source_mtu(&mut self, outbound_mtu: u16) {
|
||||||
|
if outbound_mtu < self.current_mtu {
|
||||||
|
self.current_mtu = outbound_mtu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Destination side ---
|
||||||
|
|
||||||
|
/// Observe the path_mtu from an incoming SessionDatagram envelope.
|
||||||
|
///
|
||||||
|
/// Called on the destination (receiver) side for every session message.
|
||||||
|
pub fn observe_incoming_mtu(&mut self, path_mtu: u16) {
|
||||||
|
if path_mtu != self.last_observed_mtu {
|
||||||
|
self.observed_changed = true;
|
||||||
|
self.last_observed_mtu = path_mtu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a PathMtuNotification should be sent.
|
||||||
|
///
|
||||||
|
/// Send on first measurement, on decrease (immediate), or periodic
|
||||||
|
/// confirmation at the notification interval.
|
||||||
|
pub fn should_send_notification(&self, now: Instant) -> bool {
|
||||||
|
if self.last_observed_mtu == u16::MAX {
|
||||||
|
return false; // No measurement yet
|
||||||
|
}
|
||||||
|
match self.last_notification_time {
|
||||||
|
None => true, // First measurement
|
||||||
|
Some(last) => {
|
||||||
|
// Immediate on decrease
|
||||||
|
if self.observed_changed && self.last_observed_mtu < self.current_mtu {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Periodic confirmation
|
||||||
|
now.duration_since(last) >= self.notification_interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a PathMtuNotification from current state.
|
||||||
|
///
|
||||||
|
/// Returns the path_mtu value to send. Caller handles encoding.
|
||||||
|
pub fn build_notification(&mut self, now: Instant) -> Option<u16> {
|
||||||
|
if self.last_observed_mtu == u16::MAX {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.last_notification_time = Some(now);
|
||||||
|
self.observed_changed = false;
|
||||||
|
Some(self.last_observed_mtu)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Source side ---
|
||||||
|
|
||||||
|
/// Apply a received PathMtuNotification.
|
||||||
|
///
|
||||||
|
/// - Decrease: immediate (take the lower value).
|
||||||
|
/// - Increase: require 3 consecutive notifications with the same higher
|
||||||
|
/// value, spanning at least 2 * notification_interval.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the effective MTU changed.
|
||||||
|
pub fn apply_notification(&mut self, reported_mtu: u16, now: Instant) -> bool {
|
||||||
|
if reported_mtu < self.current_mtu {
|
||||||
|
// Decrease: immediate
|
||||||
|
self.current_mtu = reported_mtu;
|
||||||
|
self.consecutive_increase_count = 0;
|
||||||
|
self.first_increase_time = None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if reported_mtu > self.current_mtu {
|
||||||
|
// Increase: track consecutive notifications
|
||||||
|
if reported_mtu == self.pending_increase_mtu {
|
||||||
|
self.consecutive_increase_count += 1;
|
||||||
|
} else {
|
||||||
|
// Different value: reset sequence
|
||||||
|
self.pending_increase_mtu = reported_mtu;
|
||||||
|
self.consecutive_increase_count = 1;
|
||||||
|
self.first_increase_time = Some(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept increase after 3 consecutive spanning 2 * interval
|
||||||
|
if self.consecutive_increase_count >= 3
|
||||||
|
&& let Some(first_time) = self.first_increase_time
|
||||||
|
{
|
||||||
|
let required = self.notification_interval * 2;
|
||||||
|
if now.duration_since(first_time) >= required {
|
||||||
|
self.current_mtu = reported_mtu;
|
||||||
|
self.consecutive_increase_count = 0;
|
||||||
|
self.first_increase_time = None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No change (equal or increase not yet confirmed)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PathMtuState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Debug for MmpPeerState {
|
impl Debug for MmpPeerState {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("MmpPeerState")
|
f.debug_struct("MmpPeerState")
|
||||||
|
|||||||
+19
-4
@@ -170,6 +170,14 @@ pub struct ReceiverState {
|
|||||||
|
|
||||||
impl ReceiverState {
|
impl ReceiverState {
|
||||||
pub fn new(owd_window_size: usize) -> Self {
|
pub fn new(owd_window_size: usize) -> Self {
|
||||||
|
Self::new_with_cold_start(owd_window_size, DEFAULT_COLD_START_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create with a custom cold-start interval (ms).
|
||||||
|
///
|
||||||
|
/// Used by session-layer MMP which needs a longer initial interval
|
||||||
|
/// since reports consume bandwidth on every transit link.
|
||||||
|
pub fn new_with_cold_start(owd_window_size: usize, cold_start_ms: u64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cumulative_packets_recv: 0,
|
cumulative_packets_recv: 0,
|
||||||
cumulative_bytes_recv: 0,
|
cumulative_bytes_recv: 0,
|
||||||
@@ -185,7 +193,7 @@ impl ReceiverState {
|
|||||||
last_sender_timestamp: 0,
|
last_sender_timestamp: 0,
|
||||||
last_recv_time: None,
|
last_recv_time: None,
|
||||||
last_report_time: None,
|
last_report_time: None,
|
||||||
report_interval: Duration::from_millis(DEFAULT_COLD_START_INTERVAL_MS),
|
report_interval: Duration::from_millis(cold_start_ms),
|
||||||
interval_has_data: false,
|
interval_has_data: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,15 +316,22 @@ impl ReceiverState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the report interval based on SRTT.
|
/// Update the report interval based on SRTT (link-layer defaults).
|
||||||
///
|
///
|
||||||
/// Receiver reports at 1× SRTT, clamped to [MIN, MAX].
|
/// Receiver reports at 1× SRTT, clamped to [MIN, MAX].
|
||||||
pub fn update_report_interval_from_srtt(&mut self, srtt_us: i64) {
|
pub fn update_report_interval_from_srtt(&mut self, srtt_us: i64) {
|
||||||
|
self.update_report_interval_with_bounds(srtt_us, MIN_REPORT_INTERVAL_MS, MAX_REPORT_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the report interval based on SRTT with custom bounds.
|
||||||
|
///
|
||||||
|
/// Used by session-layer MMP which needs higher clamp values since
|
||||||
|
/// each report consumes bandwidth on every transit link.
|
||||||
|
pub fn update_report_interval_with_bounds(&mut self, srtt_us: i64, min_ms: u64, max_ms: u64) {
|
||||||
if srtt_us <= 0 {
|
if srtt_us <= 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let interval_ms = ((srtt_us as u64) / 1000)
|
let interval_ms = ((srtt_us as u64) / 1000).clamp(min_ms, max_ms);
|
||||||
.clamp(MIN_REPORT_INTERVAL_MS, MAX_REPORT_INTERVAL_MS);
|
|
||||||
self.report_interval = Duration::from_millis(interval_ms);
|
self.report_interval = Duration::from_millis(interval_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,82 @@ impl ReceiverReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Conversions between link-layer and session-layer report types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
use crate::protocol::{SessionReceiverReport, SessionSenderReport};
|
||||||
|
|
||||||
|
impl From<&SenderReport> for SessionSenderReport {
|
||||||
|
fn from(r: &SenderReport) -> Self {
|
||||||
|
Self {
|
||||||
|
interval_start_counter: r.interval_start_counter,
|
||||||
|
interval_end_counter: r.interval_end_counter,
|
||||||
|
interval_start_timestamp: r.interval_start_timestamp,
|
||||||
|
interval_end_timestamp: r.interval_end_timestamp,
|
||||||
|
interval_bytes_sent: r.interval_bytes_sent,
|
||||||
|
cumulative_packets_sent: r.cumulative_packets_sent,
|
||||||
|
cumulative_bytes_sent: r.cumulative_bytes_sent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SessionSenderReport> for SenderReport {
|
||||||
|
fn from(r: &SessionSenderReport) -> Self {
|
||||||
|
Self {
|
||||||
|
interval_start_counter: r.interval_start_counter,
|
||||||
|
interval_end_counter: r.interval_end_counter,
|
||||||
|
interval_start_timestamp: r.interval_start_timestamp,
|
||||||
|
interval_end_timestamp: r.interval_end_timestamp,
|
||||||
|
interval_bytes_sent: r.interval_bytes_sent,
|
||||||
|
cumulative_packets_sent: r.cumulative_packets_sent,
|
||||||
|
cumulative_bytes_sent: r.cumulative_bytes_sent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ReceiverReport> for SessionReceiverReport {
|
||||||
|
fn from(r: &ReceiverReport) -> Self {
|
||||||
|
Self {
|
||||||
|
highest_counter: r.highest_counter,
|
||||||
|
cumulative_packets_recv: r.cumulative_packets_recv,
|
||||||
|
cumulative_bytes_recv: r.cumulative_bytes_recv,
|
||||||
|
timestamp_echo: r.timestamp_echo,
|
||||||
|
dwell_time: r.dwell_time,
|
||||||
|
max_burst_loss: r.max_burst_loss,
|
||||||
|
mean_burst_loss: r.mean_burst_loss,
|
||||||
|
jitter: r.jitter,
|
||||||
|
ecn_ce_count: r.ecn_ce_count,
|
||||||
|
owd_trend: r.owd_trend,
|
||||||
|
burst_loss_count: r.burst_loss_count,
|
||||||
|
cumulative_reorder_count: r.cumulative_reorder_count,
|
||||||
|
interval_packets_recv: r.interval_packets_recv,
|
||||||
|
interval_bytes_recv: r.interval_bytes_recv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SessionReceiverReport> for ReceiverReport {
|
||||||
|
fn from(r: &SessionReceiverReport) -> Self {
|
||||||
|
Self {
|
||||||
|
highest_counter: r.highest_counter,
|
||||||
|
cumulative_packets_recv: r.cumulative_packets_recv,
|
||||||
|
cumulative_bytes_recv: r.cumulative_bytes_recv,
|
||||||
|
timestamp_echo: r.timestamp_echo,
|
||||||
|
dwell_time: r.dwell_time,
|
||||||
|
max_burst_loss: r.max_burst_loss,
|
||||||
|
mean_burst_loss: r.mean_burst_loss,
|
||||||
|
jitter: r.jitter,
|
||||||
|
ecn_ce_count: r.ecn_ce_count,
|
||||||
|
owd_trend: r.owd_trend,
|
||||||
|
burst_loss_count: r.burst_loss_count,
|
||||||
|
cumulative_reorder_count: r.cumulative_reorder_count,
|
||||||
|
interval_packets_recv: r.interval_packets_recv,
|
||||||
|
interval_bytes_recv: r.interval_bytes_recv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Tests
|
// Tests
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
+20
-6
@@ -35,6 +35,14 @@ pub struct SenderState {
|
|||||||
|
|
||||||
impl SenderState {
|
impl SenderState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
Self::new_with_cold_start(DEFAULT_COLD_START_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create with a custom cold-start interval (ms).
|
||||||
|
///
|
||||||
|
/// Used by session-layer MMP which needs a longer initial interval
|
||||||
|
/// since reports consume bandwidth on every transit link.
|
||||||
|
pub fn new_with_cold_start(cold_start_ms: u64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cumulative_packets_sent: 0,
|
cumulative_packets_sent: 0,
|
||||||
cumulative_bytes_sent: 0,
|
cumulative_bytes_sent: 0,
|
||||||
@@ -45,7 +53,7 @@ impl SenderState {
|
|||||||
last_timestamp: 0,
|
last_timestamp: 0,
|
||||||
interval_has_data: false,
|
interval_has_data: false,
|
||||||
last_report_time: None,
|
last_report_time: None,
|
||||||
report_interval: Duration::from_millis(DEFAULT_COLD_START_INTERVAL_MS),
|
report_interval: Duration::from_millis(cold_start_ms),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,17 +112,23 @@ impl SenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the report interval based on SRTT.
|
/// Update the report interval based on SRTT (link-layer defaults).
|
||||||
///
|
///
|
||||||
/// Sender reports at 2-5× the receiver report interval. For simplicity,
|
/// Sender reports at 2× SRTT clamped to [MIN, MAX].
|
||||||
/// we use 2× SRTT clamped to [MIN, MAX].
|
|
||||||
pub fn update_report_interval_from_srtt(&mut self, srtt_us: i64) {
|
pub fn update_report_interval_from_srtt(&mut self, srtt_us: i64) {
|
||||||
|
self.update_report_interval_with_bounds(srtt_us, MIN_REPORT_INTERVAL_MS, MAX_REPORT_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the report interval based on SRTT with custom bounds.
|
||||||
|
///
|
||||||
|
/// Used by session-layer MMP which needs higher clamp values since
|
||||||
|
/// each report consumes bandwidth on every transit link.
|
||||||
|
pub fn update_report_interval_with_bounds(&mut self, srtt_us: i64, min_ms: u64, max_ms: u64) {
|
||||||
if srtt_us <= 0 {
|
if srtt_us <= 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let interval_us = (srtt_us * 2) as u64;
|
let interval_us = (srtt_us * 2) as u64;
|
||||||
let interval_ms = (interval_us / 1000)
|
let interval_ms = (interval_us / 1000).clamp(min_ms, max_ms);
|
||||||
.clamp(MIN_REPORT_INTERVAL_MS, MAX_REPORT_INTERVAL_MS);
|
|
||||||
self.report_interval = Duration::from_millis(interval_ms);
|
self.report_interval = Duration::from_millis(interval_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ impl Node {
|
|||||||
// If an established session exists, reset the warmup counter.
|
// If an established session exists, reset the warmup counter.
|
||||||
// Discovery has completed and transit nodes along the response
|
// Discovery has completed and transit nodes along the response
|
||||||
// path now have fresh coords. Reset warmup so the next N
|
// path now have fresh coords. Reset warmup so the next N
|
||||||
// DataPackets include COORDS_PRESENT to re-warm the forward path.
|
// data packets include COORDS_PRESENT to re-warm the forward path.
|
||||||
if let Some(entry) = self.sessions.get_mut(&target)
|
if let Some(entry) = self.sessions.get_mut(&target)
|
||||||
&& entry.is_established()
|
&& entry.is_established()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,10 +100,12 @@ impl Node {
|
|||||||
ce_flag,
|
ce_flag,
|
||||||
now,
|
now,
|
||||||
);
|
);
|
||||||
// Spin bit: feed to spin state, get optional RTT sample
|
// Spin bit: advance state machine for correct TX reflection.
|
||||||
if let Some(rtt) = mmp.spin_bit.rx_observe(sp_flag, header.counter, now) {
|
// RTT samples from spin bit are not used for SRTT because
|
||||||
mmp.metrics.srtt.update(rtt.as_micros() as i64);
|
// inter-frame timing in the mesh is irregular, inflating
|
||||||
}
|
// spin-bit RTT by variable processing delays on both sides.
|
||||||
|
// Timestamp-echo in ReceiverReport provides accurate RTT.
|
||||||
|
let _spin_rtt = mmp.spin_bit.rx_observe(sp_flag, header.counter, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update address for roaming support
|
// Update address for roaming support
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
//! locally, and generates error signals on routing failure.
|
//! locally, and generates error signals on routing failure.
|
||||||
|
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
|
use crate::node::session_wire::{
|
||||||
|
parse_encrypted_coords, FspCommonPrefix, FSP_COMMON_PREFIX_SIZE, FSP_HEADER_SIZE,
|
||||||
|
FSP_PHASE_ESTABLISHED, FSP_PHASE_MSG1, FSP_PHASE_MSG2,
|
||||||
|
};
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
CoordsRequired, DataPacket, PathBroken, SessionAck, SessionDatagram, SessionMessageType,
|
CoordsRequired, PathBroken, SessionAck, SessionDatagram, SessionSetup,
|
||||||
SessionSetup,
|
|
||||||
};
|
};
|
||||||
use crate::NodeAddr;
|
use crate::NodeAddr;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@@ -42,7 +45,7 @@ impl Node {
|
|||||||
|
|
||||||
// Local delivery: dispatch to session layer handlers
|
// Local delivery: dispatch to session layer handlers
|
||||||
if datagram.dest_addr == *self.node_addr() {
|
if datagram.dest_addr == *self.node_addr() {
|
||||||
self.handle_session_payload(&datagram.src_addr, &datagram.payload)
|
self.handle_session_payload(&datagram.src_addr, &datagram.payload, datagram.path_mtu)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -81,28 +84,29 @@ impl Node {
|
|||||||
|
|
||||||
/// Attempt to warm the coordinate cache from session-layer payload headers.
|
/// Attempt to warm the coordinate cache from session-layer payload headers.
|
||||||
///
|
///
|
||||||
/// Transit routers can read the session message type byte and, for
|
/// Transit routers parse the 4-byte FSP common prefix to identify message
|
||||||
/// SessionSetup and SessionAck, extract plaintext coordinate fields.
|
/// type, then extract plaintext coordinate fields from:
|
||||||
/// DataPacket with COORDS_PRESENT has plaintext coords before the
|
/// - SessionSetup (phase 0x1): src_coords + dest_coords
|
||||||
/// encrypted payload. Other types are ignored.
|
/// - SessionAck (phase 0x2): src_coords
|
||||||
|
/// - Encrypted with CP flag (phase 0x0): cleartext coords between header and ciphertext
|
||||||
///
|
///
|
||||||
/// Decode failures are logged and silently ignored — they don't block
|
/// Decode failures are logged and silently ignored — they don't block
|
||||||
/// forwarding.
|
/// forwarding.
|
||||||
fn try_warm_coord_cache(&mut self, datagram: &SessionDatagram) {
|
fn try_warm_coord_cache(&mut self, datagram: &SessionDatagram) {
|
||||||
if datagram.payload.is_empty() {
|
let prefix = match FspCommonPrefix::parse(&datagram.payload) {
|
||||||
return;
|
Some(p) => p,
|
||||||
}
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
let msg_type = datagram.payload[0];
|
let inner = &datagram.payload[FSP_COMMON_PREFIX_SIZE..];
|
||||||
let inner = &datagram.payload[1..];
|
|
||||||
|
|
||||||
let now_ms = std::time::SystemTime::now()
|
let now_ms = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_millis() as u64)
|
.map(|d| d.as_millis() as u64)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
match SessionMessageType::from_byte(msg_type) {
|
match prefix.phase {
|
||||||
Some(SessionMessageType::SessionSetup) => {
|
FSP_PHASE_MSG1 => {
|
||||||
match SessionSetup::decode(inner) {
|
match SessionSetup::decode(inner) {
|
||||||
Ok(setup) => {
|
Ok(setup) => {
|
||||||
self.coord_cache_mut().insert(
|
self.coord_cache_mut().insert(
|
||||||
@@ -126,7 +130,7 @@ impl Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(SessionMessageType::SessionAck) => {
|
FSP_PHASE_MSG2 => {
|
||||||
match SessionAck::decode(inner) {
|
match SessionAck::decode(inner) {
|
||||||
Ok(ack) => {
|
Ok(ack) => {
|
||||||
self.coord_cache_mut().insert(
|
self.coord_cache_mut().insert(
|
||||||
@@ -144,38 +148,41 @@ impl Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(SessionMessageType::DataPacket) => {
|
FSP_PHASE_ESTABLISHED if prefix.has_coords() => {
|
||||||
match DataPacket::decode(inner) {
|
// CP flag set: coords in cleartext between header and ciphertext.
|
||||||
Ok(data) => {
|
// Parse coords from the cleartext section after the 12-byte header.
|
||||||
if data.flags.coords_present {
|
// inner starts after the 4-byte prefix, so we need 8 more bytes
|
||||||
if let Some(src_coords) = data.src_coords {
|
// for the counter (header is 12 total = 4 prefix + 8 counter).
|
||||||
self.coord_cache_mut().insert(
|
let coord_data = &datagram.payload[FSP_HEADER_SIZE..];
|
||||||
datagram.src_addr,
|
match parse_encrypted_coords(coord_data) {
|
||||||
src_coords,
|
Ok((src_coords, dest_coords, _bytes_consumed)) => {
|
||||||
now_ms,
|
if let Some(coords) = src_coords {
|
||||||
);
|
self.coord_cache_mut().insert(
|
||||||
}
|
datagram.src_addr,
|
||||||
if let Some(dest_coords) = data.dest_coords {
|
coords,
|
||||||
self.coord_cache_mut().insert(
|
now_ms,
|
||||||
datagram.dest_addr,
|
|
||||||
dest_coords,
|
|
||||||
now_ms,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
debug!(
|
|
||||||
src = %datagram.src_addr,
|
|
||||||
dest = %datagram.dest_addr,
|
|
||||||
"Cached coords from DataPacket"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(coords) = dest_coords {
|
||||||
|
self.coord_cache_mut().insert(
|
||||||
|
datagram.dest_addr,
|
||||||
|
coords,
|
||||||
|
now_ms,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
debug!(
|
||||||
|
src = %datagram.src_addr,
|
||||||
|
dest = %datagram.dest_addr,
|
||||||
|
"Cached coords from encrypted message"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!(error = %e, "Failed to decode DataPacket for cache warming");
|
debug!(error = %e, "Failed to parse coords for cache warming");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// CoordsRequired, PathBroken, unknown: no coords to cache
|
// Phase 0x0 without CP, error signals, unknown: no coords to cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-8
@@ -5,8 +5,12 @@
|
|||||||
//! and teardown metric logs.
|
//! and teardown metric logs.
|
||||||
|
|
||||||
use crate::mmp::MmpMode;
|
use crate::mmp::MmpMode;
|
||||||
|
use crate::mmp::MmpSessionState;
|
||||||
use crate::mmp::report::{ReceiverReport, SenderReport};
|
use crate::mmp::report::{ReceiverReport, SenderReport};
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
|
use crate::protocol::{
|
||||||
|
PathMtuNotification, SessionMessageType, SessionReceiverReport, SessionSenderReport,
|
||||||
|
};
|
||||||
use crate::NodeAddr;
|
use crate::NodeAddr;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
@@ -139,17 +143,19 @@ impl Node {
|
|||||||
let mode = mmp.mode();
|
let mode = mmp.mode();
|
||||||
|
|
||||||
// Sender reports: Full mode only
|
// Sender reports: Full mode only
|
||||||
if mode == MmpMode::Full && mmp.sender.should_send_report(now) {
|
if mode == MmpMode::Full
|
||||||
if let Some(sr) = mmp.sender.build_report(now) {
|
&& mmp.sender.should_send_report(now)
|
||||||
sender_reports.push((*node_addr, sr.encode()));
|
&& let Some(sr) = mmp.sender.build_report(now)
|
||||||
}
|
{
|
||||||
|
sender_reports.push((*node_addr, sr.encode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receiver reports: Full and Lightweight modes
|
// Receiver reports: Full and Lightweight modes
|
||||||
if mode != MmpMode::Minimal && mmp.receiver.should_send_report(now) {
|
if mode != MmpMode::Minimal
|
||||||
if let Some(rr) = mmp.receiver.build_report(now) {
|
&& mmp.receiver.should_send_report(now)
|
||||||
receiver_reports.push((*node_addr, rr.encode()));
|
&& let Some(rr) = mmp.receiver.build_report(now)
|
||||||
}
|
{
|
||||||
|
receiver_reports.push((*node_addr, rr.encode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Periodic operator logging
|
// Periodic operator logging
|
||||||
@@ -243,4 +249,153 @@ impl Node {
|
|||||||
"MMP link teardown"
|
"MMP link teardown"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Session-layer MMP ===
|
||||||
|
|
||||||
|
/// Check all sessions for pending MMP reports and send them.
|
||||||
|
///
|
||||||
|
/// Called from the tick handler. Also emits periodic session MMP logs.
|
||||||
|
/// Uses the collect-then-send pattern to avoid borrowing conflicts.
|
||||||
|
pub(in crate::node) async fn check_session_mmp_reports(&mut self) {
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
// Collect reports to send: (dest_addr, msg_type, encoded_body)
|
||||||
|
let mut reports: Vec<(NodeAddr, u8, Vec<u8>)> = Vec::new();
|
||||||
|
|
||||||
|
for (dest_addr, entry) in self.sessions.iter_mut() {
|
||||||
|
let Some(mmp) = entry.mmp_mut() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mode = mmp.mode();
|
||||||
|
|
||||||
|
// Sender reports: Full mode only
|
||||||
|
if mode == MmpMode::Full
|
||||||
|
&& mmp.sender.should_send_report(now)
|
||||||
|
&& let Some(sr) = mmp.sender.build_report(now)
|
||||||
|
{
|
||||||
|
let session_sr: SessionSenderReport = SessionSenderReport::from(&sr);
|
||||||
|
reports.push((
|
||||||
|
*dest_addr,
|
||||||
|
SessionMessageType::SenderReport.to_byte(),
|
||||||
|
session_sr.encode(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receiver reports: Full and Lightweight modes
|
||||||
|
if mode != MmpMode::Minimal
|
||||||
|
&& mmp.receiver.should_send_report(now)
|
||||||
|
&& let Some(rr) = mmp.receiver.build_report(now)
|
||||||
|
{
|
||||||
|
let session_rr: SessionReceiverReport = SessionReceiverReport::from(&rr);
|
||||||
|
reports.push((
|
||||||
|
*dest_addr,
|
||||||
|
SessionMessageType::ReceiverReport.to_byte(),
|
||||||
|
session_rr.encode(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathMtu notifications (all modes)
|
||||||
|
if mmp.path_mtu.should_send_notification(now)
|
||||||
|
&& let Some(mtu_value) = mmp.path_mtu.build_notification(now)
|
||||||
|
{
|
||||||
|
let notif = PathMtuNotification::new(mtu_value);
|
||||||
|
reports.push((
|
||||||
|
*dest_addr,
|
||||||
|
SessionMessageType::PathMtuNotification.to_byte(),
|
||||||
|
notif.encode(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodic operator logging
|
||||||
|
if mmp.should_log(now) {
|
||||||
|
Self::log_session_mmp_metrics(dest_addr, mmp);
|
||||||
|
mmp.mark_logged(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send collected reports via session-layer encryption
|
||||||
|
for (dest_addr, msg_type, body) in reports {
|
||||||
|
if let Err(e) = self.send_session_msg(&dest_addr, msg_type, &body).await {
|
||||||
|
debug!(
|
||||||
|
dest = %dest_addr,
|
||||||
|
msg_type,
|
||||||
|
error = %e,
|
||||||
|
"Failed to send session MMP report"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit periodic session MMP metrics at info and debug levels.
|
||||||
|
fn log_session_mmp_metrics(dest_addr: &NodeAddr, mmp: &MmpSessionState) {
|
||||||
|
let m = &mmp.metrics;
|
||||||
|
|
||||||
|
let rtt_str = match m.srtt_ms() {
|
||||||
|
Some(rtt) => format!("{:.1}ms", rtt),
|
||||||
|
None => "n/a".to_string(),
|
||||||
|
};
|
||||||
|
let loss_pct = m.loss_rate() * 100.0;
|
||||||
|
let tx_pkts = mmp.sender.cumulative_packets_sent();
|
||||||
|
let rx_pkts = mmp.receiver.cumulative_packets_recv();
|
||||||
|
let goodput_str = format_throughput(m.goodput_bps());
|
||||||
|
let send_mtu = mmp.path_mtu.current_mtu();
|
||||||
|
let observed_mtu = mmp.path_mtu.last_observed_mtu();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
session = %dest_addr,
|
||||||
|
rtt = %rtt_str,
|
||||||
|
loss = format_args!("{:.1}%", loss_pct),
|
||||||
|
goodput = %goodput_str,
|
||||||
|
send_mtu,
|
||||||
|
observed_mtu,
|
||||||
|
tx_pkts,
|
||||||
|
rx_pkts,
|
||||||
|
"MMP session metrics"
|
||||||
|
);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
session = %dest_addr,
|
||||||
|
jitter_us = mmp.receiver.jitter_us(),
|
||||||
|
rtt_trend = format_args!("{}", if m.rtt_trend.initialized() {
|
||||||
|
format!("short={:.1} long={:.1}", m.rtt_trend.short(), m.rtt_trend.long())
|
||||||
|
} else {
|
||||||
|
"n/a".to_string()
|
||||||
|
}),
|
||||||
|
loss_trend = format_args!("{}", if m.loss_trend.initialized() {
|
||||||
|
format!("short={:.4} long={:.4}", m.loss_trend.short(), m.loss_trend.long())
|
||||||
|
} else {
|
||||||
|
"n/a".to_string()
|
||||||
|
}),
|
||||||
|
delivery_fwd = format_args!("{:.3}", m.delivery_ratio_forward),
|
||||||
|
delivery_rev = format_args!("{:.3}", m.delivery_ratio_reverse),
|
||||||
|
mode = %mmp.mode(),
|
||||||
|
"MMP session metrics (detail)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a teardown log summarizing lifetime session MMP metrics.
|
||||||
|
pub(in crate::node) fn log_session_mmp_teardown(dest_addr: &NodeAddr, mmp: &MmpSessionState) {
|
||||||
|
let m = &mmp.metrics;
|
||||||
|
|
||||||
|
let rtt_str = match m.srtt_ms() {
|
||||||
|
Some(rtt) => format!("{:.1}ms", rtt),
|
||||||
|
None => "n/a".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(
|
||||||
|
session = %dest_addr,
|
||||||
|
rtt = %rtt_str,
|
||||||
|
loss = format_args!("{:.1}%", m.loss_rate() * 100.0),
|
||||||
|
etx = format_args!("{:.2}", m.etx),
|
||||||
|
send_mtu = mmp.path_mtu.current_mtu(),
|
||||||
|
observed_mtu = mmp.path_mtu.last_observed_mtu(),
|
||||||
|
tx_pkts = mmp.sender.cumulative_packets_sent(),
|
||||||
|
tx_bytes = mmp.sender.cumulative_bytes_sent(),
|
||||||
|
rx_pkts = mmp.receiver.cumulative_packets_recv(),
|
||||||
|
rx_bytes = mmp.receiver.cumulative_bytes_recv(),
|
||||||
|
jitter_us = mmp.receiver.jitter_us(),
|
||||||
|
"MMP session teardown"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ impl Node {
|
|||||||
self.check_tree_state().await;
|
self.check_tree_state().await;
|
||||||
self.check_bloom_state().await;
|
self.check_bloom_state().await;
|
||||||
self.check_mmp_reports().await;
|
self.check_mmp_reports().await;
|
||||||
|
self.check_session_mmp_reports().await;
|
||||||
self.purge_stale_lookups(now_ms);
|
self.purge_stale_lookups(now_ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+511
-139
@@ -1,16 +1,24 @@
|
|||||||
//! End-to-end session message handlers.
|
//! End-to-end session message handlers.
|
||||||
//!
|
//!
|
||||||
//! Handles locally-delivered session payloads from SessionDatagram envelopes.
|
//! Handles locally-delivered session payloads from SessionDatagram envelopes.
|
||||||
//! Dispatches based on session message type to specific handlers for
|
//! Dispatches based on FSP common prefix phase to specific handlers for
|
||||||
//! SessionSetup (Noise IK msg1), SessionAck (msg2), DataPacket, and
|
//! SessionSetup (Noise IK msg1), SessionAck (msg2), encrypted data,
|
||||||
//! error signals (CoordsRequired, PathBroken).
|
//! and error signals (CoordsRequired, PathBroken).
|
||||||
|
|
||||||
use crate::node::session::{EndToEndState, SessionEntry};
|
use crate::node::session::{EndToEndState, SessionEntry};
|
||||||
|
use crate::node::session_wire::{
|
||||||
|
build_fsp_header, fsp_prepend_inner_header, fsp_strip_inner_header,
|
||||||
|
parse_encrypted_coords, FspCommonPrefix, FspEncryptedHeader, FSP_COMMON_PREFIX_SIZE,
|
||||||
|
FSP_FLAG_CP, FSP_HEADER_SIZE, FSP_PHASE_ESTABLISHED, FSP_PHASE_MSG1, FSP_PHASE_MSG2,
|
||||||
|
};
|
||||||
|
use crate::protocol::encode_coords;
|
||||||
use crate::node::{Node, NodeError};
|
use crate::node::{Node, NodeError};
|
||||||
use crate::noise::{HandshakeState, HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE};
|
use crate::noise::{HandshakeState, HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE};
|
||||||
|
use crate::mmp::report::ReceiverReport;
|
||||||
|
use crate::mmp::{MAX_SESSION_REPORT_INTERVAL_MS, MIN_SESSION_REPORT_INTERVAL_MS};
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
CoordsRequired, DataPacket, PathBroken, SessionAck, SessionDatagram, SessionMessageType,
|
CoordsRequired, FspInnerFlags, PathBroken, PathMtuNotification, SessionAck, SessionDatagram,
|
||||||
SessionSetup,
|
SessionMessageType, SessionReceiverReport, SessionSenderReport, SessionSetup,
|
||||||
};
|
};
|
||||||
use crate::NodeAddr;
|
use crate::NodeAddr;
|
||||||
use secp256k1::PublicKey;
|
use secp256k1::PublicKey;
|
||||||
@@ -20,42 +28,239 @@ impl Node {
|
|||||||
/// Handle a locally-delivered session datagram payload.
|
/// Handle a locally-delivered session datagram payload.
|
||||||
///
|
///
|
||||||
/// Called from `handle_session_datagram()` when `dest_addr == self.node_addr()`.
|
/// Called from `handle_session_datagram()` when `dest_addr == self.node_addr()`.
|
||||||
/// Dispatches to the appropriate handler based on the session message type byte.
|
/// Dispatches based on the 4-byte FSP common prefix:
|
||||||
|
///
|
||||||
|
/// - Phase 0x1 → SessionSetup (handshake msg1)
|
||||||
|
/// - Phase 0x2 → SessionAck (handshake msg2)
|
||||||
|
/// - Phase 0x0 + U flag → plaintext error signal (CoordsRequired/PathBroken)
|
||||||
|
/// - Phase 0x0 + !U → encrypted session message (data, reports, etc.)
|
||||||
pub(in crate::node) async fn handle_session_payload(
|
pub(in crate::node) async fn handle_session_payload(
|
||||||
&mut self,
|
&mut self,
|
||||||
src_addr: &NodeAddr,
|
src_addr: &NodeAddr,
|
||||||
payload: &[u8],
|
payload: &[u8],
|
||||||
|
path_mtu: u16,
|
||||||
) {
|
) {
|
||||||
if payload.is_empty() {
|
let prefix = match FspCommonPrefix::parse(payload) {
|
||||||
debug!("Empty session payload");
|
Some(p) => p,
|
||||||
return;
|
None => {
|
||||||
}
|
debug!(len = payload.len(), "Session payload too short for FSP prefix");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let msg_type = payload[0];
|
let inner = &payload[FSP_COMMON_PREFIX_SIZE..];
|
||||||
let inner = &payload[1..];
|
|
||||||
|
|
||||||
match SessionMessageType::from_byte(msg_type) {
|
match prefix.phase {
|
||||||
Some(SessionMessageType::SessionSetup) => {
|
FSP_PHASE_MSG1 => {
|
||||||
self.handle_session_setup(src_addr, inner).await;
|
self.handle_session_setup(src_addr, inner).await;
|
||||||
}
|
}
|
||||||
Some(SessionMessageType::SessionAck) => {
|
FSP_PHASE_MSG2 => {
|
||||||
self.handle_session_ack(src_addr, inner).await;
|
self.handle_session_ack(src_addr, inner).await;
|
||||||
}
|
}
|
||||||
Some(SessionMessageType::DataPacket) => {
|
FSP_PHASE_ESTABLISHED if prefix.is_unencrypted() => {
|
||||||
self.handle_data_packet(src_addr, inner).await;
|
// Plaintext error signals: read msg_type from first byte after prefix
|
||||||
|
if inner.is_empty() {
|
||||||
|
debug!("Empty plaintext error signal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let error_type = inner[0];
|
||||||
|
let error_body = &inner[1..];
|
||||||
|
match SessionMessageType::from_byte(error_type) {
|
||||||
|
Some(SessionMessageType::CoordsRequired) => {
|
||||||
|
self.handle_coords_required(error_body).await;
|
||||||
|
}
|
||||||
|
Some(SessionMessageType::PathBroken) => {
|
||||||
|
self.handle_path_broken(error_body).await;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!(error_type, "Unknown plaintext error signal type");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(SessionMessageType::CoordsRequired) => {
|
FSP_PHASE_ESTABLISHED => {
|
||||||
self.handle_coords_required(inner).await;
|
self.handle_encrypted_session_msg(src_addr, payload, path_mtu).await;
|
||||||
}
|
}
|
||||||
Some(SessionMessageType::PathBroken) => {
|
_ => {
|
||||||
self.handle_path_broken(inner).await;
|
debug!(phase = prefix.phase, "Unknown FSP phase");
|
||||||
}
|
|
||||||
None => {
|
|
||||||
debug!(msg_type, "Unknown session message type");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle an encrypted session message (phase 0x0, U flag clear).
|
||||||
|
///
|
||||||
|
/// Full FSP receive pipeline:
|
||||||
|
/// 1. Parse FspEncryptedHeader (12 bytes) → counter, flags, header_bytes
|
||||||
|
/// 2. If CP flag: parse cleartext coords, cache them
|
||||||
|
/// 3. Session lookup with Responding→Established transition
|
||||||
|
/// 4. AEAD decrypt with AAD = header_bytes
|
||||||
|
/// 5. Strip FSP inner header → timestamp, msg_type, inner_flags
|
||||||
|
/// 6. Dispatch by msg_type
|
||||||
|
async fn handle_encrypted_session_msg(&mut self, src_addr: &NodeAddr, payload: &[u8], path_mtu: u16) {
|
||||||
|
// Parse the 12-byte encrypted header (includes the 4-byte prefix)
|
||||||
|
let header = match FspEncryptedHeader::parse(payload) {
|
||||||
|
Some(h) => h,
|
||||||
|
None => {
|
||||||
|
debug!(len = payload.len(), "Encrypted session message too short for FSP header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine where ciphertext starts (after header, optionally after coords)
|
||||||
|
let mut ciphertext_offset = FSP_HEADER_SIZE;
|
||||||
|
|
||||||
|
// If CP flag set, parse cleartext coords between header and ciphertext
|
||||||
|
if header.has_coords() {
|
||||||
|
let coord_data = &payload[FSP_HEADER_SIZE..];
|
||||||
|
match parse_encrypted_coords(coord_data) {
|
||||||
|
Ok((src_coords, dest_coords, bytes_consumed)) => {
|
||||||
|
let now_ms = Self::now_ms();
|
||||||
|
if let Some(coords) = src_coords {
|
||||||
|
self.coord_cache.insert(*src_addr, coords, now_ms);
|
||||||
|
}
|
||||||
|
if let Some(coords) = dest_coords {
|
||||||
|
self.coord_cache.insert(*self.node_addr(), coords, now_ms);
|
||||||
|
}
|
||||||
|
ciphertext_offset += bytes_consumed;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!(error = %e, "Failed to parse coords from encrypted session message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ciphertext = &payload[ciphertext_offset..];
|
||||||
|
|
||||||
|
// Look up session entry, handle Responding→Established transition
|
||||||
|
let mut entry = match self.sessions.remove(src_addr) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => {
|
||||||
|
debug!(src = %src_addr, "Encrypted session message for unknown session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if entry.state().is_responding() {
|
||||||
|
let old_state = entry.take_state();
|
||||||
|
let handshake = match old_state {
|
||||||
|
Some(EndToEndState::Responding(hs)) => hs,
|
||||||
|
_ => {
|
||||||
|
debug!(src = %src_addr, "Unexpected state during Responding transition");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let noise_session = match handshake.into_session() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
debug!(error = %e, "Failed to create session from responding handshake");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
entry.set_state(EndToEndState::Established(noise_session));
|
||||||
|
entry.set_coords_warmup_remaining(self.config.node.session.coords_warmup_packets);
|
||||||
|
entry.mark_established(Self::now_ms());
|
||||||
|
entry.init_mmp(&self.config.node.session_mmp);
|
||||||
|
debug!(src = %src_addr, "Session established (responder, on first encrypted message)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt with AAD = the 12-byte header
|
||||||
|
let session = match entry.state_mut() {
|
||||||
|
EndToEndState::Established(s) => s,
|
||||||
|
_ => {
|
||||||
|
debug!(src = %src_addr, "Encrypted message but session not established");
|
||||||
|
self.sessions.insert(*src_addr, entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let plaintext = match session.decrypt_with_replay_check_and_aad(
|
||||||
|
ciphertext,
|
||||||
|
header.counter,
|
||||||
|
&header.header_bytes,
|
||||||
|
) {
|
||||||
|
Ok(pt) => pt,
|
||||||
|
Err(e) => {
|
||||||
|
debug!(
|
||||||
|
error = %e, src = %src_addr, counter = header.counter,
|
||||||
|
"Session AEAD decryption failed"
|
||||||
|
);
|
||||||
|
self.sessions.insert(*src_addr, entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
entry.touch(Self::now_ms());
|
||||||
|
self.sessions.insert(*src_addr, entry);
|
||||||
|
|
||||||
|
// Strip FSP inner header (6 bytes)
|
||||||
|
let (timestamp, msg_type, inner_flags_byte, rest) = match fsp_strip_inner_header(&plaintext) {
|
||||||
|
Some(parts) => parts,
|
||||||
|
None => {
|
||||||
|
debug!(src = %src_addr, "Decrypted payload too short for FSP inner header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// MMP per-message recording on RX path
|
||||||
|
if let Some(entry) = self.sessions.get_mut(src_addr)
|
||||||
|
&& let Some(mmp) = entry.mmp_mut()
|
||||||
|
{
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
mmp.receiver.record_recv(
|
||||||
|
header.counter, timestamp, plaintext.len(), false, now,
|
||||||
|
);
|
||||||
|
// Spin bit: advance state machine for correct TX reflection.
|
||||||
|
// RTT samples not fed into SRTT — timestamp-echo provides
|
||||||
|
// accurate RTT; spin bit includes variable inter-frame delays.
|
||||||
|
let inner_flags = FspInnerFlags::from_byte(inner_flags_byte);
|
||||||
|
let _spin_rtt = mmp.spin_bit.rx_observe(
|
||||||
|
inner_flags.spin_bit, header.counter, now,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed path_mtu from datagram envelope to MMP path MTU tracking.
|
||||||
|
// Done for ALL session messages, not just DataPackets, so the
|
||||||
|
// destination learns the path MTU even when only reports flow.
|
||||||
|
if let Some(entry) = self.sessions.get_mut(src_addr)
|
||||||
|
&& let Some(mmp) = entry.mmp_mut()
|
||||||
|
{
|
||||||
|
mmp.path_mtu.observe_incoming_mtu(path_mtu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch by msg_type
|
||||||
|
match SessionMessageType::from_byte(msg_type) {
|
||||||
|
Some(SessionMessageType::DataPacket) => {
|
||||||
|
// msg_type 0x10: deliver rest (IPv6 payload) to TUN
|
||||||
|
if let Some(tun_tx) = &self.tun_tx {
|
||||||
|
if let Err(e) = tun_tx.send(rest.to_vec()) {
|
||||||
|
debug!(error = %e, "Failed to deliver decrypted packet to TUN");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
src = %src_addr,
|
||||||
|
"DataPacket decrypted (no TUN interface, plaintext dropped)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(SessionMessageType::SenderReport) => {
|
||||||
|
self.handle_session_sender_report(src_addr, rest);
|
||||||
|
}
|
||||||
|
Some(SessionMessageType::ReceiverReport) => {
|
||||||
|
self.handle_session_receiver_report(src_addr, rest);
|
||||||
|
}
|
||||||
|
Some(SessionMessageType::PathMtuNotification) => {
|
||||||
|
self.handle_session_path_mtu_notification(src_addr, rest);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!(src = %src_addr, msg_type, "Unknown session message type, dropping");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any pending outbound packets (e.g., simultaneous initiation
|
||||||
|
// where responder also had queued outbound packets)
|
||||||
|
self.flush_pending_packets(src_addr).await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle an incoming SessionSetup (Noise IK msg1).
|
/// Handle an incoming SessionSetup (Noise IK msg1).
|
||||||
///
|
///
|
||||||
/// The remote node wants to establish an end-to-end session with us.
|
/// The remote node wants to establish an end-to-end session with us.
|
||||||
@@ -143,18 +348,18 @@ impl Node {
|
|||||||
let our_coords = self.tree_state.my_coords().clone();
|
let our_coords = self.tree_state.my_coords().clone();
|
||||||
let ack = SessionAck::new(our_coords).with_handshake(msg2);
|
let ack = SessionAck::new(our_coords).with_handshake(msg2);
|
||||||
let my_addr = *self.node_addr();
|
let my_addr = *self.node_addr();
|
||||||
let datagram = SessionDatagram::new(my_addr, *src_addr, ack.encode())
|
let mut datagram = SessionDatagram::new(my_addr, *src_addr, ack.encode())
|
||||||
.with_ttl(self.config.node.session.default_ttl);
|
.with_ttl(self.config.node.session.default_ttl);
|
||||||
|
|
||||||
// Route the ack back to the initiator
|
// Route the ack back to the initiator
|
||||||
if let Err(e) = self.send_session_datagram(&datagram).await {
|
if let Err(e) = self.send_session_datagram(&mut datagram).await {
|
||||||
debug!(error = %e, dest = %src_addr, "Failed to send SessionAck");
|
debug!(error = %e, dest = %src_addr, "Failed to send SessionAck");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store session entry in Responding state
|
// Store session entry in Responding state
|
||||||
let now_ms = Self::now_ms();
|
let now_ms = Self::now_ms();
|
||||||
let entry = SessionEntry::new(*src_addr, remote_pubkey, EndToEndState::Responding(handshake), now_ms);
|
let entry = SessionEntry::new(*src_addr, remote_pubkey, EndToEndState::Responding(handshake), now_ms, false);
|
||||||
self.sessions.insert(*src_addr, entry);
|
self.sessions.insert(*src_addr, entry);
|
||||||
|
|
||||||
debug!(src = %src_addr, "SessionSetup processed, SessionAck sent");
|
debug!(src = %src_addr, "SessionSetup processed, SessionAck sent");
|
||||||
@@ -210,13 +415,13 @@ impl Node {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let now_ms = Self::now_ms();
|
||||||
entry.set_state(EndToEndState::Established(session));
|
entry.set_state(EndToEndState::Established(session));
|
||||||
entry.set_coords_warmup_remaining(self.config.node.session.coords_warmup_packets);
|
entry.set_coords_warmup_remaining(self.config.node.session.coords_warmup_packets);
|
||||||
entry.touch(Self::now_ms());
|
entry.mark_established(now_ms);
|
||||||
|
entry.init_mmp(&self.config.node.session_mmp);
|
||||||
|
entry.touch(now_ms);
|
||||||
self.sessions.insert(*src_addr, entry);
|
self.sessions.insert(*src_addr, entry);
|
||||||
|
|
||||||
// Cache the responder's coordinates
|
|
||||||
let now_ms = Self::now_ms();
|
|
||||||
self.coord_cache.insert(*src_addr, ack.src_coords, now_ms);
|
self.coord_cache.insert(*src_addr, ack.src_coords, now_ms);
|
||||||
|
|
||||||
// Flush any queued outbound packets for this destination
|
// Flush any queued outbound packets for this destination
|
||||||
@@ -225,6 +430,136 @@ impl Node {
|
|||||||
debug!(src = %src_addr, "Session established (initiator)");
|
debug!(src = %src_addr, "Session established (initiator)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Session-layer MMP report handlers ===
|
||||||
|
|
||||||
|
/// Handle an incoming session-layer SenderReport (msg_type 0x11).
|
||||||
|
///
|
||||||
|
/// Informational only — the peer is telling us about what they sent.
|
||||||
|
/// Logged but not used for metrics (same pattern as link-layer).
|
||||||
|
fn handle_session_sender_report(&mut self, src_addr: &NodeAddr, body: &[u8]) {
|
||||||
|
let sr = match SessionSenderReport::decode(body) {
|
||||||
|
Ok(sr) => sr,
|
||||||
|
Err(e) => {
|
||||||
|
debug!(src = %src_addr, error = %e, "Malformed SessionSenderReport");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
src = %src_addr,
|
||||||
|
cum_pkts = sr.cumulative_packets_sent,
|
||||||
|
interval_bytes = sr.interval_bytes_sent,
|
||||||
|
"Received SessionSenderReport"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle an incoming session-layer ReceiverReport (msg_type 0x12).
|
||||||
|
///
|
||||||
|
/// The peer is telling us about what they received from us. We feed
|
||||||
|
/// this to our metrics to compute RTT, loss rate, and trend indicators.
|
||||||
|
fn handle_session_receiver_report(&mut self, src_addr: &NodeAddr, body: &[u8]) {
|
||||||
|
let session_rr = match SessionReceiverReport::decode(body) {
|
||||||
|
Ok(rr) => rr,
|
||||||
|
Err(e) => {
|
||||||
|
debug!(src = %src_addr, error = %e, "Malformed SessionReceiverReport");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to link-layer ReceiverReport for MmpMetrics processing
|
||||||
|
let rr: ReceiverReport = ReceiverReport::from(&session_rr);
|
||||||
|
|
||||||
|
let now_ms = Self::now_ms();
|
||||||
|
let entry = match self.sessions.get_mut(src_addr) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => {
|
||||||
|
debug!(src = %src_addr, "SessionReceiverReport for unknown session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let our_timestamp_ms = entry.session_timestamp(now_ms);
|
||||||
|
|
||||||
|
let Some(mmp) = entry.mmp_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
mmp.metrics.process_receiver_report(&rr, our_timestamp_ms, now);
|
||||||
|
|
||||||
|
// Feed SRTT back to sender/receiver report interval tuning (session-layer bounds)
|
||||||
|
if let Some(srtt_ms) = mmp.metrics.srtt_ms() {
|
||||||
|
let srtt_us = (srtt_ms * 1000.0) as i64;
|
||||||
|
mmp.sender.update_report_interval_with_bounds(
|
||||||
|
srtt_us,
|
||||||
|
MIN_SESSION_REPORT_INTERVAL_MS,
|
||||||
|
MAX_SESSION_REPORT_INTERVAL_MS,
|
||||||
|
);
|
||||||
|
mmp.receiver.update_report_interval_with_bounds(
|
||||||
|
srtt_us,
|
||||||
|
MIN_SESSION_REPORT_INTERVAL_MS,
|
||||||
|
MAX_SESSION_REPORT_INTERVAL_MS,
|
||||||
|
);
|
||||||
|
// Also update PathMtu notification interval from SRTT
|
||||||
|
mmp.path_mtu.update_interval_from_srtt(srtt_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update reverse delivery ratio from our own receiver state
|
||||||
|
let our_recv_packets = mmp.receiver.cumulative_packets_recv();
|
||||||
|
let peer_highest = mmp.receiver.highest_counter();
|
||||||
|
if peer_highest > 0 {
|
||||||
|
let reverse_ratio = (our_recv_packets as f64) / (peer_highest as f64);
|
||||||
|
mmp.metrics.set_delivery_ratio_reverse(reverse_ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
src = %src_addr,
|
||||||
|
rtt_ms = ?mmp.metrics.srtt_ms(),
|
||||||
|
loss = format_args!("{:.1}%", mmp.metrics.loss_rate() * 100.0),
|
||||||
|
"Processed SessionReceiverReport"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle an incoming PathMtuNotification (msg_type 0x13).
|
||||||
|
///
|
||||||
|
/// The destination is telling us the path MTU has changed.
|
||||||
|
/// Apply source-side rules (decrease immediate, increase validated).
|
||||||
|
fn handle_session_path_mtu_notification(&mut self, src_addr: &NodeAddr, body: &[u8]) {
|
||||||
|
let notif = match PathMtuNotification::decode(body) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
debug!(src = %src_addr, error = %e, "Malformed PathMtuNotification");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = match self.sessions.get_mut(src_addr) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => {
|
||||||
|
debug!(src = %src_addr, "PathMtuNotification for unknown session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(mmp) = entry.mmp_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let old_mtu = mmp.path_mtu.current_mtu();
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
mmp.path_mtu.apply_notification(notif.path_mtu, now);
|
||||||
|
let new_mtu = mmp.path_mtu.current_mtu();
|
||||||
|
|
||||||
|
if new_mtu != old_mtu {
|
||||||
|
debug!(
|
||||||
|
src = %src_addr,
|
||||||
|
old_mtu,
|
||||||
|
new_mtu,
|
||||||
|
"Path MTU changed via notification"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Complete an initiator-side Noise IK handshake given msg2.
|
/// Complete an initiator-side Noise IK handshake given msg2.
|
||||||
fn complete_initiator_handshake(
|
fn complete_initiator_handshake(
|
||||||
mut handshake: HandshakeState,
|
mut handshake: HandshakeState,
|
||||||
@@ -238,90 +573,6 @@ impl Node {
|
|||||||
.map_err(|e| format!("into_session failed: {}", e))
|
.map_err(|e| format!("into_session failed: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle an incoming DataPacket.
|
|
||||||
///
|
|
||||||
/// Decrypts the payload using the established session key and delivers
|
|
||||||
/// to the TUN interface.
|
|
||||||
async fn handle_data_packet(&mut self, src_addr: &NodeAddr, inner: &[u8]) {
|
|
||||||
let packet = match DataPacket::decode(inner) {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
debug!(error = %e, "Malformed DataPacket");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove entry to take ownership for potential state transition
|
|
||||||
let mut entry = match self.sessions.remove(src_addr) {
|
|
||||||
Some(e) => e,
|
|
||||||
None => {
|
|
||||||
debug!(src = %src_addr, "DataPacket for unknown session");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If in Responding state, transition to Established first
|
|
||||||
// (responder wrote msg2, handshake is complete from our side)
|
|
||||||
if entry.state().is_responding() {
|
|
||||||
let old_state = entry.take_state();
|
|
||||||
let handshake = match old_state {
|
|
||||||
Some(EndToEndState::Responding(hs)) => hs,
|
|
||||||
_ => {
|
|
||||||
debug!(src = %src_addr, "Unexpected state in DataPacket handler");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let noise_session = match handshake.into_session() {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
debug!(error = %e, "Failed to create session from responding handshake");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
entry.set_state(EndToEndState::Established(noise_session));
|
|
||||||
entry.set_coords_warmup_remaining(self.config.node.session.coords_warmup_packets);
|
|
||||||
debug!(src = %src_addr, "Session established (responder, on first data)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt
|
|
||||||
let session = match entry.state_mut() {
|
|
||||||
EndToEndState::Established(s) => s,
|
|
||||||
_ => {
|
|
||||||
debug!(src = %src_addr, "DataPacket but session not established");
|
|
||||||
self.sessions.insert(*src_addr, entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let plaintext = match session.decrypt_with_replay_check(&packet.payload, packet.counter) {
|
|
||||||
Ok(pt) => pt,
|
|
||||||
Err(e) => {
|
|
||||||
debug!(error = %e, src = %src_addr, counter = packet.counter, "Session decryption failed");
|
|
||||||
self.sessions.insert(*src_addr, entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
entry.touch(Self::now_ms());
|
|
||||||
self.sessions.insert(*src_addr, entry);
|
|
||||||
|
|
||||||
// Deliver to TUN
|
|
||||||
if let Some(tun_tx) = &self.tun_tx {
|
|
||||||
if let Err(e) = tun_tx.send(plaintext) {
|
|
||||||
debug!(error = %e, "Failed to deliver decrypted packet to TUN");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!(
|
|
||||||
src = %src_addr,
|
|
||||||
"DataPacket decrypted (no TUN interface, plaintext dropped)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush any pending outbound packets (e.g., simultaneous initiation
|
|
||||||
// where responder also had queued outbound packets)
|
|
||||||
self.flush_pending_packets(src_addr).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a CoordsRequired error signal from a transit router.
|
/// Handle a CoordsRequired error signal from a transit router.
|
||||||
///
|
///
|
||||||
/// The router couldn't route our packet because it lacks cached
|
/// The router couldn't route our packet because it lacks cached
|
||||||
@@ -431,18 +682,18 @@ impl Node {
|
|||||||
|
|
||||||
// Wrap in SessionDatagram
|
// Wrap in SessionDatagram
|
||||||
let my_addr = *self.node_addr();
|
let my_addr = *self.node_addr();
|
||||||
let datagram = SessionDatagram::new(my_addr, dest_addr, setup.encode())
|
let mut datagram = SessionDatagram::new(my_addr, dest_addr, setup.encode())
|
||||||
.with_ttl(self.config.node.session.default_ttl);
|
.with_ttl(self.config.node.session.default_ttl);
|
||||||
|
|
||||||
// Route toward destination
|
// Route toward destination
|
||||||
self.send_session_datagram(&datagram).await?;
|
self.send_session_datagram(&mut datagram).await?;
|
||||||
|
|
||||||
// Register destination identity for TUN → session routing
|
// Register destination identity for TUN → session routing
|
||||||
self.register_identity(dest_addr, dest_pubkey);
|
self.register_identity(dest_addr, dest_pubkey);
|
||||||
|
|
||||||
// Store session entry
|
// Store session entry
|
||||||
let now_ms = Self::now_ms();
|
let now_ms = Self::now_ms();
|
||||||
let entry = SessionEntry::new(dest_addr, dest_pubkey, EndToEndState::Initiating(handshake), now_ms);
|
let entry = SessionEntry::new(dest_addr, dest_pubkey, EndToEndState::Initiating(handshake), now_ms, true);
|
||||||
self.sessions.insert(dest_addr, entry);
|
self.sessions.insert(dest_addr, entry);
|
||||||
|
|
||||||
debug!(dest = %dest_addr, "Session initiation started");
|
debug!(dest = %dest_addr, "Session initiation started");
|
||||||
@@ -451,18 +702,28 @@ impl Node {
|
|||||||
|
|
||||||
/// Send application data over an established session.
|
/// Send application data over an established session.
|
||||||
///
|
///
|
||||||
/// Encrypts the payload with the session key, wraps in DataPacket
|
/// Uses the FSP pipeline: builds a 12-byte cleartext header (used as AAD),
|
||||||
/// and SessionDatagram, routes toward destination.
|
/// prepends the 6-byte inner header to the plaintext, encrypts with AAD,
|
||||||
|
/// optionally inserts cleartext coords, and wraps in a SessionDatagram.
|
||||||
pub(in crate::node) async fn send_session_data(
|
pub(in crate::node) async fn send_session_data(
|
||||||
&mut self,
|
&mut self,
|
||||||
dest_addr: &NodeAddr,
|
dest_addr: &NodeAddr,
|
||||||
plaintext: &[u8],
|
plaintext: &[u8],
|
||||||
) -> Result<(), NodeError> {
|
) -> Result<(), NodeError> {
|
||||||
|
let now_ms = Self::now_ms();
|
||||||
let entry = self.sessions.get_mut(dest_addr).ok_or_else(|| NodeError::SendFailed {
|
let entry = self.sessions.get_mut(dest_addr).ok_or_else(|| NodeError::SendFailed {
|
||||||
node_addr: *dest_addr,
|
node_addr: *dest_addr,
|
||||||
reason: "no session".into(),
|
reason: "no session".into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Check warmup counter and get session timestamp
|
||||||
|
let include_coords = entry.coords_warmup_remaining() > 0;
|
||||||
|
if include_coords {
|
||||||
|
entry.set_coords_warmup_remaining(entry.coords_warmup_remaining() - 1);
|
||||||
|
}
|
||||||
|
let timestamp = entry.session_timestamp(now_ms);
|
||||||
|
let spin_bit = entry.mmp().is_some_and(|m| m.spin_bit.tx_bit());
|
||||||
|
|
||||||
let session = match entry.state_mut() {
|
let session = match entry.state_mut() {
|
||||||
EndToEndState::Established(s) => s,
|
EndToEndState::Established(s) => s,
|
||||||
_ => {
|
_ => {
|
||||||
@@ -476,35 +737,129 @@ impl Node {
|
|||||||
// Get counter before encrypting (encrypt will increment it)
|
// Get counter before encrypting (encrypt will increment it)
|
||||||
let counter = session.current_send_counter();
|
let counter = session.current_send_counter();
|
||||||
|
|
||||||
// Encrypt with session key
|
// FSP inner header: [timestamp:4 LE][msg_type:1][inner_flags:1] + plaintext
|
||||||
let ciphertext = session.encrypt(plaintext).map_err(|e| NodeError::SendFailed {
|
let msg_type = SessionMessageType::DataPacket.to_byte(); // 0x10
|
||||||
node_addr: *dest_addr,
|
let inner_flags = FspInnerFlags { spin_bit }.to_byte();
|
||||||
reason: format!("session encrypt failed: {}", e),
|
let inner_plaintext = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, plaintext);
|
||||||
|
|
||||||
|
// Build FSP flags
|
||||||
|
let flags = if include_coords { FSP_FLAG_CP } else { 0 };
|
||||||
|
|
||||||
|
// Build 12-byte FSP header (used as AAD for AEAD)
|
||||||
|
let payload_len = inner_plaintext.len() as u16;
|
||||||
|
let header = build_fsp_header(counter, flags, payload_len);
|
||||||
|
|
||||||
|
// Encrypt with AAD binding to the FSP header
|
||||||
|
let ciphertext = session.encrypt_with_aad(&inner_plaintext, &header).map_err(|e| {
|
||||||
|
NodeError::SendFailed {
|
||||||
|
node_addr: *dest_addr,
|
||||||
|
reason: format!("session encrypt failed: {}", e),
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Check warmup counter and decrement (while entry is still borrowed)
|
// Assemble: header(12) + [coords] + ciphertext
|
||||||
let include_coords = entry.coords_warmup_remaining() > 0;
|
let mut fsp_payload = Vec::with_capacity(FSP_HEADER_SIZE + ciphertext.len() + 200);
|
||||||
if include_coords {
|
fsp_payload.extend_from_slice(&header);
|
||||||
entry.set_coords_warmup_remaining(entry.coords_warmup_remaining() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build DataPacket with explicit counter for replay protection
|
|
||||||
let mut data_packet = DataPacket::new(counter, ciphertext);
|
|
||||||
if include_coords {
|
if include_coords {
|
||||||
let my_coords = self.tree_state.my_coords().clone();
|
let my_coords = self.tree_state.my_coords().clone();
|
||||||
let dest_coords = self.get_dest_coords(dest_addr);
|
let dest_coords = self.get_dest_coords(dest_addr);
|
||||||
data_packet = data_packet.with_coords(my_coords, dest_coords);
|
encode_coords(&my_coords, &mut fsp_payload);
|
||||||
|
encode_coords(&dest_coords, &mut fsp_payload);
|
||||||
}
|
}
|
||||||
|
fsp_payload.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
let my_addr = *self.node_addr();
|
let my_addr = *self.node_addr();
|
||||||
let datagram = SessionDatagram::new(my_addr, *dest_addr, data_packet.encode())
|
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, fsp_payload)
|
||||||
.with_ttl(self.config.node.session.default_ttl);
|
.with_ttl(self.config.node.session.default_ttl);
|
||||||
|
|
||||||
self.send_session_datagram(&datagram).await?;
|
self.send_session_datagram(&mut datagram).await?;
|
||||||
|
|
||||||
// Re-borrow after send (which borrowed &mut self)
|
// Re-borrow after send (which borrowed &mut self)
|
||||||
if let Some(entry) = self.sessions.get_mut(dest_addr) {
|
if let Some(entry) = self.sessions.get_mut(dest_addr) {
|
||||||
entry.touch(Self::now_ms());
|
if let Some(mmp) = entry.mmp_mut() {
|
||||||
|
mmp.sender.record_sent(counter, timestamp, ciphertext.len());
|
||||||
|
}
|
||||||
|
entry.touch(now_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a non-data session message (reports, notifications) over an established session.
|
||||||
|
///
|
||||||
|
/// Similar to `send_session_data()` but:
|
||||||
|
/// - Takes an explicit `msg_type` byte (0x11, 0x12, 0x13, etc.)
|
||||||
|
/// - Never includes COORDS_PRESENT (reports are lightweight)
|
||||||
|
/// - Reads spin bit from MMP state for the inner header
|
||||||
|
/// - Records the send in MMP sender state
|
||||||
|
pub(in crate::node) async fn send_session_msg(
|
||||||
|
&mut self,
|
||||||
|
dest_addr: &NodeAddr,
|
||||||
|
msg_type: u8,
|
||||||
|
payload: &[u8],
|
||||||
|
) -> Result<(), NodeError> {
|
||||||
|
let now_ms = Self::now_ms();
|
||||||
|
|
||||||
|
// Read spin bit and session timestamp from entry
|
||||||
|
let entry = self.sessions.get(dest_addr).ok_or_else(|| NodeError::SendFailed {
|
||||||
|
node_addr: *dest_addr,
|
||||||
|
reason: "no session".into(),
|
||||||
|
})?;
|
||||||
|
let timestamp = entry.session_timestamp(now_ms);
|
||||||
|
let spin_bit = entry.mmp().is_some_and(|m| m.spin_bit.tx_bit());
|
||||||
|
|
||||||
|
// Build inner flags with spin bit
|
||||||
|
let inner_flags = FspInnerFlags { spin_bit }.to_byte();
|
||||||
|
|
||||||
|
// Get mutable access for encryption
|
||||||
|
let entry = self.sessions.get_mut(dest_addr).ok_or_else(|| NodeError::SendFailed {
|
||||||
|
node_addr: *dest_addr,
|
||||||
|
reason: "no session".into(),
|
||||||
|
})?;
|
||||||
|
let session = match entry.state_mut() {
|
||||||
|
EndToEndState::Established(s) => s,
|
||||||
|
_ => {
|
||||||
|
return Err(NodeError::SendFailed {
|
||||||
|
node_addr: *dest_addr,
|
||||||
|
reason: "session not established".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let counter = session.current_send_counter();
|
||||||
|
|
||||||
|
// FSP inner header + plaintext
|
||||||
|
let inner_plaintext = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, payload);
|
||||||
|
|
||||||
|
// Build 12-byte FSP header (no flags — no CP for reports)
|
||||||
|
let payload_len = inner_plaintext.len() as u16;
|
||||||
|
let header = build_fsp_header(counter, 0, payload_len);
|
||||||
|
|
||||||
|
// Encrypt with AAD
|
||||||
|
let ciphertext = session.encrypt_with_aad(&inner_plaintext, &header).map_err(|e| {
|
||||||
|
NodeError::SendFailed {
|
||||||
|
node_addr: *dest_addr,
|
||||||
|
reason: format!("session encrypt failed: {}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Assemble: header(12) + ciphertext (no coords)
|
||||||
|
let mut fsp_payload = Vec::with_capacity(FSP_HEADER_SIZE + ciphertext.len());
|
||||||
|
fsp_payload.extend_from_slice(&header);
|
||||||
|
fsp_payload.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
|
let my_addr = *self.node_addr();
|
||||||
|
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, fsp_payload)
|
||||||
|
.with_ttl(self.config.node.session.default_ttl);
|
||||||
|
|
||||||
|
self.send_session_datagram(&mut datagram).await?;
|
||||||
|
|
||||||
|
// Record in MMP sender state and touch
|
||||||
|
if let Some(entry) = self.sessions.get_mut(dest_addr) {
|
||||||
|
if let Some(mmp) = entry.mmp_mut() {
|
||||||
|
mmp.sender.record_sent(counter, timestamp, ciphertext.len());
|
||||||
|
}
|
||||||
|
entry.touch(now_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -512,11 +867,11 @@ impl Node {
|
|||||||
|
|
||||||
/// Route and send a SessionDatagram through the mesh.
|
/// Route and send a SessionDatagram through the mesh.
|
||||||
///
|
///
|
||||||
/// Finds the next hop for the destination and sends the datagram
|
/// Finds the next hop for the destination, seeds path_mtu from the
|
||||||
/// as an encrypted link message.
|
/// first-hop transport MTU, and sends as an encrypted link message.
|
||||||
async fn send_session_datagram(
|
async fn send_session_datagram(
|
||||||
&mut self,
|
&mut self,
|
||||||
datagram: &SessionDatagram,
|
datagram: &mut SessionDatagram,
|
||||||
) -> Result<(), NodeError> {
|
) -> Result<(), NodeError> {
|
||||||
let next_hop_addr = match self.find_next_hop(&datagram.dest_addr) {
|
let next_hop_addr = match self.find_next_hop(&datagram.dest_addr) {
|
||||||
Some(peer) => *peer.node_addr(),
|
Some(peer) => *peer.node_addr(),
|
||||||
@@ -528,6 +883,23 @@ impl Node {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Seed path_mtu from the first-hop transport MTU (same as forwarding path)
|
||||||
|
if let Some(peer) = self.peers.get(&next_hop_addr)
|
||||||
|
&& let Some(tid) = peer.transport_id()
|
||||||
|
&& let Some(transport) = self.transports.get(&tid)
|
||||||
|
{
|
||||||
|
datagram.path_mtu = datagram.path_mtu.min(transport.mtu());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source-side: seed our PathMtuState.current_mtu from the outbound
|
||||||
|
// transport MTU so it doesn't stay at u16::MAX until the destination
|
||||||
|
// sends a PathMtuNotification back.
|
||||||
|
if let Some(entry) = self.sessions.get_mut(&datagram.dest_addr)
|
||||||
|
&& let Some(mmp) = entry.mmp_mut()
|
||||||
|
{
|
||||||
|
mmp.path_mtu.seed_source_mtu(datagram.path_mtu);
|
||||||
|
}
|
||||||
|
|
||||||
let encoded = datagram.encode();
|
let encoded = datagram.encode();
|
||||||
self.send_encrypted_link_message(&next_hop_addr, &encoded).await
|
self.send_encrypted_link_message(&next_hop_addr, &encoded).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ impl Node {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for addr in idle {
|
for addr in idle {
|
||||||
|
// Log MMP teardown metrics before removing the session
|
||||||
|
if let Some(entry) = self.sessions.get(&addr)
|
||||||
|
&& let Some(mmp) = entry.mmp()
|
||||||
|
{
|
||||||
|
Self::log_session_mmp_teardown(&addr, mmp);
|
||||||
|
}
|
||||||
self.sessions.remove(&addr);
|
self.sessions.remove(&addr);
|
||||||
self.pending_tun_packets.remove(&addr);
|
self.pending_tun_packets.remove(&addr);
|
||||||
debug!(
|
debug!(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod retry;
|
|||||||
mod rate_limit;
|
mod rate_limit;
|
||||||
mod routing_error_rate_limit;
|
mod routing_error_rate_limit;
|
||||||
pub(crate) mod session;
|
pub(crate) mod session;
|
||||||
|
pub(crate) mod session_wire;
|
||||||
pub(crate) mod wire;
|
pub(crate) mod wire;
|
||||||
mod tree;
|
mod tree;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
+53
-1
@@ -4,6 +4,8 @@
|
|||||||
//! Sessions are established via SessionSetup/SessionAck handshake
|
//! Sessions are established via SessionSetup/SessionAck handshake
|
||||||
//! messages carried inside SessionDatagram envelopes through the mesh.
|
//! messages carried inside SessionDatagram envelopes through the mesh.
|
||||||
|
|
||||||
|
use crate::config::SessionMmpConfig;
|
||||||
|
use crate::mmp::MmpSessionState;
|
||||||
use crate::noise::{HandshakeState, NoiseSession};
|
use crate::noise::{HandshakeState, NoiseSession};
|
||||||
use crate::NodeAddr;
|
use crate::NodeAddr;
|
||||||
use secp256k1::PublicKey;
|
use secp256k1::PublicKey;
|
||||||
@@ -54,10 +56,19 @@ pub(crate) struct SessionEntry {
|
|||||||
created_at: u64,
|
created_at: u64,
|
||||||
/// Last activity timestamp (Unix milliseconds).
|
/// Last activity timestamp (Unix milliseconds).
|
||||||
last_activity: u64,
|
last_activity: u64,
|
||||||
/// Remaining DataPackets that should include COORDS_PRESENT.
|
/// When the session transitioned to Established (Unix milliseconds).
|
||||||
|
/// Used to compute session-relative timestamps for the FSP inner header.
|
||||||
|
/// Set to 0 until the session is established.
|
||||||
|
session_start_ms: u64,
|
||||||
|
/// Remaining data packets that should include COORDS_PRESENT.
|
||||||
/// Initialized from config when session becomes Established;
|
/// Initialized from config when session becomes Established;
|
||||||
/// reset on CoordsRequired receipt.
|
/// reset on CoordsRequired receipt.
|
||||||
coords_warmup_remaining: u8,
|
coords_warmup_remaining: u8,
|
||||||
|
/// Whether this node initiated the Noise IK handshake.
|
||||||
|
/// Used for spin bit role assignment in session-layer MMP.
|
||||||
|
is_initiator: bool,
|
||||||
|
/// Session-layer MMP state. Initialized on Established transition.
|
||||||
|
mmp: Option<MmpSessionState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionEntry {
|
impl SessionEntry {
|
||||||
@@ -67,6 +78,7 @@ impl SessionEntry {
|
|||||||
remote_pubkey: PublicKey,
|
remote_pubkey: PublicKey,
|
||||||
state: EndToEndState,
|
state: EndToEndState,
|
||||||
now_ms: u64,
|
now_ms: u64,
|
||||||
|
is_initiator: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
remote_addr,
|
remote_addr,
|
||||||
@@ -74,7 +86,10 @@ impl SessionEntry {
|
|||||||
state: Some(state),
|
state: Some(state),
|
||||||
created_at: now_ms,
|
created_at: now_ms,
|
||||||
last_activity: now_ms,
|
last_activity: now_ms,
|
||||||
|
session_start_ms: 0,
|
||||||
coords_warmup_remaining: 0,
|
coords_warmup_remaining: 0,
|
||||||
|
is_initiator,
|
||||||
|
mmp: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,4 +147,41 @@ impl SessionEntry {
|
|||||||
pub(crate) fn set_coords_warmup_remaining(&mut self, value: u8) {
|
pub(crate) fn set_coords_warmup_remaining(&mut self, value: u8) {
|
||||||
self.coords_warmup_remaining = value;
|
self.coords_warmup_remaining = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark the session as started (transition to Established).
|
||||||
|
///
|
||||||
|
/// Records the current time as the session start for computing
|
||||||
|
/// session-relative timestamps in the FSP inner header.
|
||||||
|
pub(crate) fn mark_established(&mut self, now_ms: u64) {
|
||||||
|
self.session_start_ms = now_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a session-relative timestamp for the FSP inner header.
|
||||||
|
///
|
||||||
|
/// Returns `(now_ms - session_start_ms)` truncated to u32.
|
||||||
|
/// Wraps naturally at ~49.7 days, which is fine for relative timing.
|
||||||
|
pub(crate) fn session_timestamp(&self, now_ms: u64) -> u32 {
|
||||||
|
now_ms.wrapping_sub(self.session_start_ms) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this node initiated the Noise IK handshake.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub(crate) fn is_initiator(&self) -> bool {
|
||||||
|
self.is_initiator
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the session-layer MMP state, if initialized.
|
||||||
|
pub(crate) fn mmp(&self) -> Option<&MmpSessionState> {
|
||||||
|
self.mmp.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to the session-layer MMP state, if initialized.
|
||||||
|
pub(crate) fn mmp_mut(&mut self) -> Option<&mut MmpSessionState> {
|
||||||
|
self.mmp.as_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize session-layer MMP state (called on Established transition).
|
||||||
|
pub(crate) fn init_mmp(&mut self, config: &SessionMmpConfig) {
|
||||||
|
self.mmp = Some(MmpSessionState::new(config, self.is_initiator));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,582 @@
|
|||||||
|
//! FSP Wire Format Parsing and Serialization
|
||||||
|
//!
|
||||||
|
//! Defines the FIPS session-layer wire format (FSP) for packet dispatch.
|
||||||
|
//! All FSP messages begin with a 4-byte common prefix followed by phase-specific
|
||||||
|
//! fields. Encrypted messages use a 12-byte cleartext header as AAD for AEAD,
|
||||||
|
//! and a 6-byte encrypted inner header containing timestamps and message type.
|
||||||
|
//!
|
||||||
|
//! ## Common Prefix (4 bytes)
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! [ver+phase:1][flags:1][payload_len:2 LE]
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Message Classes
|
||||||
|
//!
|
||||||
|
//! | Phase | U Flag | Type | Description |
|
||||||
|
//! |-------|--------|------------------|-----------------------------------|
|
||||||
|
//! | 0x0 | 0 | Encrypted | Post-handshake encrypted data |
|
||||||
|
//! | 0x0 | 1 | Plaintext error | CoordsRequired, PathBroken |
|
||||||
|
//! | 0x1 | - | Handshake msg1 | SessionSetup (Noise IK msg1) |
|
||||||
|
//! | 0x2 | - | Handshake msg2 | SessionAck (Noise IK msg2) |
|
||||||
|
|
||||||
|
use crate::protocol::{ProtocolError, decode_optional_coords};
|
||||||
|
use crate::tree::TreeCoordinate;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Constants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// FSP protocol version (4 high bits of byte 0).
|
||||||
|
pub const FSP_VERSION: u8 = 0;
|
||||||
|
|
||||||
|
/// Phase value for established (encrypted or plaintext error) messages.
|
||||||
|
pub const FSP_PHASE_ESTABLISHED: u8 = 0x0;
|
||||||
|
|
||||||
|
/// Phase value for SessionSetup (Noise IK message 1).
|
||||||
|
pub const FSP_PHASE_MSG1: u8 = 0x1;
|
||||||
|
|
||||||
|
/// Phase value for SessionAck (Noise IK message 2).
|
||||||
|
pub const FSP_PHASE_MSG2: u8 = 0x2;
|
||||||
|
|
||||||
|
/// Size of the common packet prefix (all FSP message types).
|
||||||
|
pub const FSP_COMMON_PREFIX_SIZE: usize = 4;
|
||||||
|
|
||||||
|
/// Size of the full encrypted message header (prefix + counter).
|
||||||
|
pub const FSP_HEADER_SIZE: usize = 12;
|
||||||
|
|
||||||
|
/// Size of the encrypted inner header (timestamp + msg_type + inner_flags).
|
||||||
|
pub const FSP_INNER_HEADER_SIZE: usize = 6;
|
||||||
|
|
||||||
|
/// AEAD authentication tag size (ChaCha20-Poly1305).
|
||||||
|
const TAG_SIZE: usize = 16;
|
||||||
|
|
||||||
|
/// Minimum size for an encrypted FSP message: header + tag (no plaintext).
|
||||||
|
pub const FSP_ENCRYPTED_MIN_SIZE: usize = FSP_HEADER_SIZE + TAG_SIZE; // 28 bytes
|
||||||
|
|
||||||
|
// Cleartext flag bit constants (byte 1 of common prefix, phase 0x0 only).
|
||||||
|
|
||||||
|
/// Coords Present — source and destination coordinates follow the header.
|
||||||
|
pub const FSP_FLAG_CP: u8 = 0x01;
|
||||||
|
|
||||||
|
/// Key Epoch — selects active key during rekeying.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const FSP_FLAG_K: u8 = 0x02;
|
||||||
|
|
||||||
|
/// Unencrypted — payload is plaintext (error signals).
|
||||||
|
pub const FSP_FLAG_U: u8 = 0x04;
|
||||||
|
|
||||||
|
// Inner flag bit constants (byte 5 of decrypted inner header).
|
||||||
|
|
||||||
|
/// Spin bit for end-to-end RTT measurement (inside AEAD).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const FSP_INNER_FLAG_SP: u8 = 0x01;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Common Prefix
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Parsed FSP common packet prefix (first 4 bytes of every FSP message).
|
||||||
|
///
|
||||||
|
/// Wire format:
|
||||||
|
/// ```text
|
||||||
|
/// [ver(4bits)+phase(4bits)][flags:1][payload_len:2 LE]
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FspCommonPrefix {
|
||||||
|
/// Protocol version (high nibble of byte 0).
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub version: u8,
|
||||||
|
/// Session lifecycle phase (low nibble of byte 0).
|
||||||
|
pub phase: u8,
|
||||||
|
/// Per-message signal flags.
|
||||||
|
pub flags: u8,
|
||||||
|
/// Length of payload following the phase-specific header.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub payload_len: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FspCommonPrefix {
|
||||||
|
/// Parse a common prefix from the first 4 bytes of FSP message data.
|
||||||
|
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||||
|
if data.len() < FSP_COMMON_PREFIX_SIZE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = data[0] >> 4;
|
||||||
|
let phase = data[0] & 0x0F;
|
||||||
|
let flags = data[1];
|
||||||
|
let payload_len = u16::from_le_bytes([data[2], data[3]]);
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
version,
|
||||||
|
phase,
|
||||||
|
flags,
|
||||||
|
payload_len,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the Unencrypted flag is set.
|
||||||
|
pub fn is_unencrypted(&self) -> bool {
|
||||||
|
self.flags & FSP_FLAG_U != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the Coords Present flag is set.
|
||||||
|
pub fn has_coords(&self) -> bool {
|
||||||
|
self.flags & FSP_FLAG_CP != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode the ver+phase byte.
|
||||||
|
fn ver_phase_byte(version: u8, phase: u8) -> u8 {
|
||||||
|
(version << 4) | (phase & 0x0F)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Encrypted Message Header
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Parsed FSP encrypted message header (phase 0x0, U flag clear).
|
||||||
|
///
|
||||||
|
/// Wire format (12 bytes):
|
||||||
|
/// ```text
|
||||||
|
/// [ver+phase:1][flags:1][payload_len:2 LE][counter:8 LE]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The full 12-byte header is used as AAD for the AEAD construction.
|
||||||
|
/// No receiver_idx — unlike FLP, FSP is end-to-end (dispatched by src_addr
|
||||||
|
/// from the SessionDatagram envelope, not by index).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FspEncryptedHeader {
|
||||||
|
/// Per-message flags (CP, K).
|
||||||
|
pub flags: u8,
|
||||||
|
/// Length of encrypted payload (excluding AEAD tag).
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub payload_len: u16,
|
||||||
|
/// Monotonic counter used as AEAD nonce.
|
||||||
|
pub counter: u64,
|
||||||
|
/// Raw 12-byte header for use as AEAD AAD.
|
||||||
|
pub header_bytes: [u8; FSP_HEADER_SIZE],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FspEncryptedHeader {
|
||||||
|
/// Parse an encrypted message header from FSP message data.
|
||||||
|
///
|
||||||
|
/// Returns None if the data is too short or has wrong version/phase,
|
||||||
|
/// or if the U flag is set (plaintext messages use a different path).
|
||||||
|
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||||
|
if data.len() < FSP_ENCRYPTED_MIN_SIZE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = data[0] >> 4;
|
||||||
|
let phase = data[0] & 0x0F;
|
||||||
|
|
||||||
|
if version != FSP_VERSION || phase != FSP_PHASE_ESTABLISHED {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let flags = data[1];
|
||||||
|
|
||||||
|
// U flag means plaintext — not an encrypted message
|
||||||
|
if flags & FSP_FLAG_U != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload_len = u16::from_le_bytes([data[2], data[3]]);
|
||||||
|
let counter = u64::from_le_bytes([
|
||||||
|
data[4], data[5], data[6], data[7],
|
||||||
|
data[8], data[9], data[10], data[11],
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mut header_bytes = [0u8; FSP_HEADER_SIZE];
|
||||||
|
header_bytes.copy_from_slice(&data[..FSP_HEADER_SIZE]);
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
flags,
|
||||||
|
payload_len,
|
||||||
|
counter,
|
||||||
|
header_bytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the Coords Present flag is set.
|
||||||
|
pub fn has_coords(&self) -> bool {
|
||||||
|
self.flags & FSP_FLAG_CP != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offset where ciphertext (or coords if CP) begins in the original data.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub fn data_offset(&self) -> usize {
|
||||||
|
FSP_HEADER_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Serialization Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Build the 12-byte cleartext header for an encrypted FSP message.
|
||||||
|
///
|
||||||
|
/// Returns the header bytes for use as AEAD AAD.
|
||||||
|
pub fn build_fsp_header(
|
||||||
|
counter: u64,
|
||||||
|
flags: u8,
|
||||||
|
payload_len: u16,
|
||||||
|
) -> [u8; FSP_HEADER_SIZE] {
|
||||||
|
let mut header = [0u8; FSP_HEADER_SIZE];
|
||||||
|
header[0] = FspCommonPrefix::ver_phase_byte(FSP_VERSION, FSP_PHASE_ESTABLISHED);
|
||||||
|
header[1] = flags;
|
||||||
|
header[2..4].copy_from_slice(&payload_len.to_le_bytes());
|
||||||
|
header[4..12].copy_from_slice(&counter.to_le_bytes());
|
||||||
|
header
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assemble a wire-format encrypted FSP message.
|
||||||
|
///
|
||||||
|
/// Format: `[header:12][ciphertext+tag]`
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub fn build_fsp_encrypted(header: &[u8; FSP_HEADER_SIZE], ciphertext: &[u8]) -> Vec<u8> {
|
||||||
|
let mut packet = Vec::with_capacity(FSP_HEADER_SIZE + ciphertext.len());
|
||||||
|
packet.extend_from_slice(header);
|
||||||
|
packet.extend_from_slice(ciphertext);
|
||||||
|
packet
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a 4-byte common prefix for a handshake message.
|
||||||
|
///
|
||||||
|
/// `phase` should be `FSP_PHASE_MSG1` or `FSP_PHASE_MSG2`.
|
||||||
|
/// Flags are zero during handshake.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub fn build_fsp_handshake_prefix(phase: u8, payload_len: u16) -> [u8; FSP_COMMON_PREFIX_SIZE] {
|
||||||
|
let mut prefix = [0u8; FSP_COMMON_PREFIX_SIZE];
|
||||||
|
prefix[0] = FspCommonPrefix::ver_phase_byte(FSP_VERSION, phase);
|
||||||
|
prefix[1] = 0x00; // flags must be zero during handshake
|
||||||
|
prefix[2..4].copy_from_slice(&payload_len.to_le_bytes());
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a 4-byte common prefix for a plaintext error signal.
|
||||||
|
///
|
||||||
|
/// Sets phase 0x0 and U flag.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
pub fn build_fsp_error_prefix(payload_len: u16) -> [u8; FSP_COMMON_PREFIX_SIZE] {
|
||||||
|
let mut prefix = [0u8; FSP_COMMON_PREFIX_SIZE];
|
||||||
|
prefix[0] = FspCommonPrefix::ver_phase_byte(FSP_VERSION, FSP_PHASE_ESTABLISHED);
|
||||||
|
prefix[1] = FSP_FLAG_U;
|
||||||
|
prefix[2..4].copy_from_slice(&payload_len.to_le_bytes());
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Inner Header Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Prepend the 6-byte FSP inner header to a message payload.
|
||||||
|
///
|
||||||
|
/// Inner header: `[timestamp:4 LE][msg_type:1][inner_flags:1]`
|
||||||
|
///
|
||||||
|
/// The caller provides the message-type-specific payload (e.g., application
|
||||||
|
/// data for msg_type 0x10, report fields for SenderReport). This function
|
||||||
|
/// prepends the inner header.
|
||||||
|
pub fn fsp_prepend_inner_header(
|
||||||
|
timestamp_ms: u32,
|
||||||
|
msg_type: u8,
|
||||||
|
inner_flags: u8,
|
||||||
|
payload: &[u8],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::with_capacity(FSP_INNER_HEADER_SIZE + payload.len());
|
||||||
|
buf.extend_from_slice(×tamp_ms.to_le_bytes());
|
||||||
|
buf.push(msg_type);
|
||||||
|
buf.push(inner_flags);
|
||||||
|
buf.extend_from_slice(payload);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strip the 6-byte FSP inner header from a decrypted payload.
|
||||||
|
///
|
||||||
|
/// Returns `(timestamp, msg_type, inner_flags, &rest)` or None if too short.
|
||||||
|
pub fn fsp_strip_inner_header(plaintext: &[u8]) -> Option<(u32, u8, u8, &[u8])> {
|
||||||
|
if plaintext.len() < FSP_INNER_HEADER_SIZE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let timestamp = u32::from_le_bytes([
|
||||||
|
plaintext[0], plaintext[1], plaintext[2], plaintext[3],
|
||||||
|
]);
|
||||||
|
let msg_type = plaintext[4];
|
||||||
|
let inner_flags = plaintext[5];
|
||||||
|
Some((timestamp, msg_type, inner_flags, &plaintext[FSP_INNER_HEADER_SIZE..]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Coordinate Parsing (for transit nodes and receive path)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Parse source and destination coordinates from the cleartext section
|
||||||
|
/// of an encrypted FSP message when the CP flag is set.
|
||||||
|
///
|
||||||
|
/// Coordinates appear between the 12-byte header and the ciphertext:
|
||||||
|
/// `[src_coords_count:2 LE][src_coords:16×n][dest_coords_count:2 LE][dest_coords:16×m]`
|
||||||
|
///
|
||||||
|
/// Returns `(src_coords, dest_coords, bytes_consumed)`.
|
||||||
|
pub fn parse_encrypted_coords(
|
||||||
|
data: &[u8],
|
||||||
|
) -> Result<(Option<TreeCoordinate>, Option<TreeCoordinate>, usize), ProtocolError> {
|
||||||
|
let (src_coords, src_consumed) = decode_optional_coords(data)?;
|
||||||
|
let (dest_coords, dest_consumed) = decode_optional_coords(&data[src_consumed..])?;
|
||||||
|
Ok((src_coords, dest_coords, src_consumed + dest_consumed))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// ===== Size Constant Tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wire_sizes() {
|
||||||
|
assert_eq!(FSP_COMMON_PREFIX_SIZE, 4);
|
||||||
|
assert_eq!(FSP_HEADER_SIZE, 12);
|
||||||
|
assert_eq!(FSP_INNER_HEADER_SIZE, 6);
|
||||||
|
assert_eq!(FSP_ENCRYPTED_MIN_SIZE, 28); // 12 + 16
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Common Prefix Tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_common_prefix_parse_established() {
|
||||||
|
let data = [0x00, 0x01, 0x40, 0x00]; // ver=0, phase=0, flags=CP, payload_len=64
|
||||||
|
let prefix = FspCommonPrefix::parse(&data).unwrap();
|
||||||
|
assert_eq!(prefix.version, 0);
|
||||||
|
assert_eq!(prefix.phase, FSP_PHASE_ESTABLISHED);
|
||||||
|
assert_eq!(prefix.flags, FSP_FLAG_CP);
|
||||||
|
assert_eq!(prefix.payload_len, 64);
|
||||||
|
assert!(prefix.has_coords());
|
||||||
|
assert!(!prefix.is_unencrypted());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_common_prefix_parse_handshake() {
|
||||||
|
let data = [0x01, 0x00, 0x50, 0x00]; // ver=0, phase=1, flags=0, payload_len=80
|
||||||
|
let prefix = FspCommonPrefix::parse(&data).unwrap();
|
||||||
|
assert_eq!(prefix.version, 0);
|
||||||
|
assert_eq!(prefix.phase, FSP_PHASE_MSG1);
|
||||||
|
assert_eq!(prefix.flags, 0);
|
||||||
|
assert_eq!(prefix.payload_len, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_common_prefix_parse_error_signal() {
|
||||||
|
let data = [0x00, FSP_FLAG_U, 0x22, 0x00]; // ver=0, phase=0, U flag, payload_len=34
|
||||||
|
let prefix = FspCommonPrefix::parse(&data).unwrap();
|
||||||
|
assert_eq!(prefix.phase, FSP_PHASE_ESTABLISHED);
|
||||||
|
assert!(prefix.is_unencrypted());
|
||||||
|
assert_eq!(prefix.payload_len, 34);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_common_prefix_too_short() {
|
||||||
|
assert!(FspCommonPrefix::parse(&[0, 0, 0]).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Encrypted Header Tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypted_header_parse() {
|
||||||
|
let counter = 42u64;
|
||||||
|
let flags = FSP_FLAG_CP;
|
||||||
|
let payload_len = 100u16;
|
||||||
|
let header = build_fsp_header(counter, flags, payload_len);
|
||||||
|
|
||||||
|
// Build a minimal packet: header + 16 bytes of fake ciphertext (tag)
|
||||||
|
let mut packet = Vec::from(header);
|
||||||
|
packet.extend_from_slice(&[0xaa; TAG_SIZE]);
|
||||||
|
|
||||||
|
let parsed = FspEncryptedHeader::parse(&packet).unwrap();
|
||||||
|
assert_eq!(parsed.counter, 42);
|
||||||
|
assert_eq!(parsed.flags, FSP_FLAG_CP);
|
||||||
|
assert_eq!(parsed.payload_len, 100);
|
||||||
|
assert!(parsed.has_coords());
|
||||||
|
assert_eq!(parsed.header_bytes, header);
|
||||||
|
assert_eq!(parsed.data_offset(), FSP_HEADER_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypted_header_too_short() {
|
||||||
|
let packet = vec![0x00; FSP_ENCRYPTED_MIN_SIZE - 1];
|
||||||
|
assert!(FspEncryptedHeader::parse(&packet).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypted_header_wrong_phase() {
|
||||||
|
let mut packet = vec![0x00; FSP_ENCRYPTED_MIN_SIZE];
|
||||||
|
packet[0] = 0x01; // phase 1 (msg1), not established
|
||||||
|
assert!(FspEncryptedHeader::parse(&packet).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypted_header_wrong_version() {
|
||||||
|
let mut packet = vec![0x00; FSP_ENCRYPTED_MIN_SIZE];
|
||||||
|
packet[0] = 0x10; // version 1, phase 0
|
||||||
|
assert!(FspEncryptedHeader::parse(&packet).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypted_header_u_flag_rejected() {
|
||||||
|
let mut packet = vec![0x00; FSP_ENCRYPTED_MIN_SIZE];
|
||||||
|
packet[1] = FSP_FLAG_U; // U flag set → not encrypted
|
||||||
|
assert!(FspEncryptedHeader::parse(&packet).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Build Header Tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_fsp_header() {
|
||||||
|
let header = build_fsp_header(1000, FSP_FLAG_CP, 200);
|
||||||
|
assert_eq!(header[0], 0x00); // ver=0, phase=0
|
||||||
|
assert_eq!(header[1], FSP_FLAG_CP);
|
||||||
|
assert_eq!(u16::from_le_bytes([header[2], header[3]]), 200);
|
||||||
|
assert_eq!(
|
||||||
|
u64::from_le_bytes([
|
||||||
|
header[4], header[5], header[6], header[7],
|
||||||
|
header[8], header[9], header[10], header[11],
|
||||||
|
]),
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_fsp_encrypted() {
|
||||||
|
let header = build_fsp_header(0, 0, 10);
|
||||||
|
let ciphertext = vec![0xCC; 26]; // 10 payload + 16 tag
|
||||||
|
let packet = build_fsp_encrypted(&header, &ciphertext);
|
||||||
|
assert_eq!(packet.len(), FSP_HEADER_SIZE + 26);
|
||||||
|
assert_eq!(&packet[..FSP_HEADER_SIZE], &header);
|
||||||
|
assert_eq!(&packet[FSP_HEADER_SIZE..], &ciphertext[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Handshake Prefix Tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_fsp_handshake_prefix_msg1() {
|
||||||
|
let prefix = build_fsp_handshake_prefix(FSP_PHASE_MSG1, 100);
|
||||||
|
assert_eq!(prefix[0], 0x01); // ver=0, phase=1
|
||||||
|
assert_eq!(prefix[1], 0x00); // flags zero
|
||||||
|
assert_eq!(u16::from_le_bytes([prefix[2], prefix[3]]), 100);
|
||||||
|
|
||||||
|
let parsed = FspCommonPrefix::parse(&prefix).unwrap();
|
||||||
|
assert_eq!(parsed.phase, FSP_PHASE_MSG1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_fsp_handshake_prefix_msg2() {
|
||||||
|
let prefix = build_fsp_handshake_prefix(FSP_PHASE_MSG2, 50);
|
||||||
|
assert_eq!(prefix[0], 0x02); // ver=0, phase=2
|
||||||
|
assert_eq!(prefix[1], 0x00);
|
||||||
|
assert_eq!(u16::from_le_bytes([prefix[2], prefix[3]]), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Error Prefix Tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_fsp_error_prefix() {
|
||||||
|
let prefix = build_fsp_error_prefix(34);
|
||||||
|
assert_eq!(prefix[0], 0x00); // ver=0, phase=0
|
||||||
|
assert_eq!(prefix[1], FSP_FLAG_U);
|
||||||
|
assert_eq!(u16::from_le_bytes([prefix[2], prefix[3]]), 34);
|
||||||
|
|
||||||
|
let parsed = FspCommonPrefix::parse(&prefix).unwrap();
|
||||||
|
assert!(parsed.is_unencrypted());
|
||||||
|
assert_eq!(parsed.phase, FSP_PHASE_ESTABLISHED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Inner Header Tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inner_header_prepend_strip() {
|
||||||
|
let timestamp: u32 = 12345;
|
||||||
|
let msg_type: u8 = 0x10;
|
||||||
|
let inner_flags: u8 = 0x01; // SP bit
|
||||||
|
let payload = vec![0xAA, 0xBB, 0xCC];
|
||||||
|
|
||||||
|
let with_header = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, &payload);
|
||||||
|
assert_eq!(with_header.len(), FSP_INNER_HEADER_SIZE + 3);
|
||||||
|
|
||||||
|
let (ts, mt, flags, rest) = fsp_strip_inner_header(&with_header).unwrap();
|
||||||
|
assert_eq!(ts, 12345);
|
||||||
|
assert_eq!(mt, 0x10);
|
||||||
|
assert_eq!(flags, 0x01);
|
||||||
|
assert_eq!(rest, &payload[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inner_header_empty_payload() {
|
||||||
|
let with_header = fsp_prepend_inner_header(0, 0x13, 0, &[]);
|
||||||
|
assert_eq!(with_header.len(), FSP_INNER_HEADER_SIZE);
|
||||||
|
|
||||||
|
let (ts, mt, flags, rest) = fsp_strip_inner_header(&with_header).unwrap();
|
||||||
|
assert_eq!(ts, 0);
|
||||||
|
assert_eq!(mt, 0x13);
|
||||||
|
assert_eq!(flags, 0);
|
||||||
|
assert!(rest.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inner_header_too_short() {
|
||||||
|
assert!(fsp_strip_inner_header(&[0, 0, 0, 0, 0]).is_none()); // needs 6 bytes
|
||||||
|
assert!(fsp_strip_inner_header(&[]).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Flag Constants Tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_flag_bits_distinct() {
|
||||||
|
// Cleartext flags don't overlap
|
||||||
|
assert_eq!(FSP_FLAG_CP & FSP_FLAG_K, 0);
|
||||||
|
assert_eq!(FSP_FLAG_CP & FSP_FLAG_U, 0);
|
||||||
|
assert_eq!(FSP_FLAG_K & FSP_FLAG_U, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_header_roundtrip() {
|
||||||
|
let counter = 0xDEADBEEF_12345678u64;
|
||||||
|
let flags = FSP_FLAG_CP | FSP_FLAG_K;
|
||||||
|
let payload_len = 1234u16;
|
||||||
|
|
||||||
|
let header = build_fsp_header(counter, flags, payload_len);
|
||||||
|
let ciphertext = vec![0xFF; payload_len as usize + TAG_SIZE];
|
||||||
|
let packet = build_fsp_encrypted(&header, &ciphertext);
|
||||||
|
|
||||||
|
let parsed = FspEncryptedHeader::parse(&packet).unwrap();
|
||||||
|
assert_eq!(parsed.counter, counter);
|
||||||
|
assert_eq!(parsed.flags, flags);
|
||||||
|
assert_eq!(parsed.payload_len, payload_len);
|
||||||
|
assert!(parsed.has_coords());
|
||||||
|
assert_eq!(parsed.header_bytes, header);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_message_types_through_prefix() {
|
||||||
|
// Encrypted (phase 0, no U)
|
||||||
|
let prefix = FspCommonPrefix::parse(&[0x00, 0x00, 0x10, 0x00]).unwrap();
|
||||||
|
assert_eq!(prefix.phase, 0);
|
||||||
|
assert!(!prefix.is_unencrypted());
|
||||||
|
|
||||||
|
// Error signal (phase 0, U set)
|
||||||
|
let prefix = FspCommonPrefix::parse(&[0x00, FSP_FLAG_U, 0x22, 0x00]).unwrap();
|
||||||
|
assert_eq!(prefix.phase, 0);
|
||||||
|
assert!(prefix.is_unencrypted());
|
||||||
|
|
||||||
|
// SessionSetup (phase 1)
|
||||||
|
let prefix = FspCommonPrefix::parse(&[0x01, 0x00, 0x50, 0x00]).unwrap();
|
||||||
|
assert_eq!(prefix.phase, 1);
|
||||||
|
|
||||||
|
// SessionAck (phase 2)
|
||||||
|
let prefix = FspCommonPrefix::parse(&[0x02, 0x00, 0x21, 0x00]).unwrap();
|
||||||
|
assert_eq!(prefix.phase, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
//! multi-hop forwarding through live node topologies.
|
//! multi-hop forwarding through live node topologies.
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::protocol::{DataPacket, SessionAck, SessionDatagram, SessionSetup};
|
use crate::node::session_wire::{build_fsp_header, FSP_FLAG_CP};
|
||||||
|
use crate::protocol::{SessionAck, SessionDatagram, SessionSetup, encode_coords};
|
||||||
use crate::tree::TreeCoordinate;
|
use crate::tree::TreeCoordinate;
|
||||||
use spanning_tree::{
|
use spanning_tree::{
|
||||||
cleanup_nodes, process_available_packets, run_tree_test, verify_tree_convergence,
|
cleanup_nodes, process_available_packets, run_tree_test, verify_tree_convergence,
|
||||||
@@ -184,7 +185,7 @@ async fn test_coord_cache_warming_session_ack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_coord_cache_warming_data_packet_with_coords() {
|
async fn test_coord_cache_warming_encrypted_msg_with_coords() {
|
||||||
let mut node = make_node();
|
let mut node = make_node();
|
||||||
let from = make_node_addr(0xAA);
|
let from = make_node_addr(0xAA);
|
||||||
let src_addr = make_node_addr(0x01);
|
let src_addr = make_node_addr(0x01);
|
||||||
@@ -194,9 +195,13 @@ async fn test_coord_cache_warming_data_packet_with_coords() {
|
|||||||
let src_coords = TreeCoordinate::from_addrs(vec![src_addr, root_addr]).unwrap();
|
let src_coords = TreeCoordinate::from_addrs(vec![src_addr, root_addr]).unwrap();
|
||||||
let dest_coords = TreeCoordinate::from_addrs(vec![dest_addr, root_addr]).unwrap();
|
let dest_coords = TreeCoordinate::from_addrs(vec![dest_addr, root_addr]).unwrap();
|
||||||
|
|
||||||
let data = DataPacket::new(0, vec![1, 2, 3, 4])
|
// Build FSP encrypted message with CP flag: header(12) + coords + fake_ciphertext
|
||||||
.with_coords(src_coords.clone(), dest_coords.clone());
|
let header = build_fsp_header(0, FSP_FLAG_CP, 20);
|
||||||
let data_payload = data.encode();
|
let mut data_payload = Vec::new();
|
||||||
|
data_payload.extend_from_slice(&header);
|
||||||
|
encode_coords(&src_coords, &mut data_payload);
|
||||||
|
encode_coords(&dest_coords, &mut data_payload);
|
||||||
|
data_payload.extend_from_slice(&[0xCC; 36]); // fake ciphertext (20 payload + 16 tag)
|
||||||
|
|
||||||
let dg = SessionDatagram::new(src_addr, dest_addr, data_payload);
|
let dg = SessionDatagram::new(src_addr, dest_addr, data_payload);
|
||||||
let encoded = dg.encode();
|
let encoded = dg.encode();
|
||||||
@@ -213,24 +218,26 @@ async fn test_coord_cache_warming_data_packet_with_coords() {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
node.coord_cache().get(&src_addr, now_ms).is_some(),
|
node.coord_cache().get(&src_addr, now_ms).is_some(),
|
||||||
"src coords not cached from DataPacket"
|
"src coords not cached from encrypted message"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
node.coord_cache().get(&dest_addr, now_ms).is_some(),
|
node.coord_cache().get(&dest_addr, now_ms).is_some(),
|
||||||
"dest coords not cached from DataPacket"
|
"dest coords not cached from encrypted message"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_coord_cache_warming_opaque_data_packet() {
|
async fn test_coord_cache_warming_encrypted_msg_no_coords() {
|
||||||
let mut node = make_node();
|
let mut node = make_node();
|
||||||
let from = make_node_addr(0xAA);
|
let from = make_node_addr(0xAA);
|
||||||
let src_addr = make_node_addr(0x01);
|
let src_addr = make_node_addr(0x01);
|
||||||
let dest_addr = make_node_addr(0x02);
|
let dest_addr = make_node_addr(0x02);
|
||||||
|
|
||||||
// DataPacket without COORDS_PRESENT — no coords to cache
|
// Build FSP encrypted message without CP flag: header(12) + fake_ciphertext
|
||||||
let data = DataPacket::new(0, vec![1, 2, 3, 4]);
|
let header = build_fsp_header(0, 0, 20);
|
||||||
let data_payload = data.encode();
|
let mut data_payload = Vec::new();
|
||||||
|
data_payload.extend_from_slice(&header);
|
||||||
|
data_payload.extend_from_slice(&[0xCC; 36]); // fake ciphertext (20 payload + 16 tag)
|
||||||
|
|
||||||
let dg = SessionDatagram::new(src_addr, dest_addr, data_payload);
|
let dg = SessionDatagram::new(src_addr, dest_addr, data_payload);
|
||||||
let encoded = dg.encode();
|
let encoded = dg.encode();
|
||||||
@@ -244,11 +251,11 @@ async fn test_coord_cache_warming_opaque_data_packet() {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
node.coord_cache().get(&src_addr, now_ms).is_none(),
|
node.coord_cache().get(&src_addr, now_ms).is_none(),
|
||||||
"Should not cache coords from opaque DataPacket"
|
"Should not cache coords from message without CP flag"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
node.coord_cache().get(&dest_addr, now_ms).is_none(),
|
node.coord_cache().get(&dest_addr, now_ms).is_none(),
|
||||||
"Should not cache coords from opaque DataPacket"
|
"Should not cache coords from message without CP flag"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ fn test_session_entry_new_initiating() {
|
|||||||
identity_b.pubkey_full(),
|
identity_b.pubkey_full(),
|
||||||
EndToEndState::Initiating(handshake),
|
EndToEndState::Initiating(handshake),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(entry.state().is_initiating());
|
assert!(entry.state().is_initiating());
|
||||||
@@ -86,6 +87,7 @@ fn test_session_entry_touch() {
|
|||||||
identity_b.pubkey_full(),
|
identity_b.pubkey_full(),
|
||||||
EndToEndState::Initiating(handshake),
|
EndToEndState::Initiating(handshake),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
entry.touch(2000);
|
entry.touch(2000);
|
||||||
@@ -111,6 +113,7 @@ fn test_session_table_operations() {
|
|||||||
identity_b.pubkey_full(),
|
identity_b.pubkey_full(),
|
||||||
EndToEndState::Initiating(handshake),
|
EndToEndState::Initiating(handshake),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
node.sessions.insert(dest_addr, entry);
|
node.sessions.insert(dest_addr, entry);
|
||||||
@@ -223,10 +226,10 @@ async fn test_session_direct_peer_data_transfer() {
|
|||||||
.await
|
.await
|
||||||
.expect("send_session_data failed");
|
.expect("send_session_data failed");
|
||||||
|
|
||||||
// Process packets: DataPacket arrives at Node 1
|
// Process packets: encrypted data arrives at Node 1
|
||||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||||
let count = process_available_packets(&mut nodes).await;
|
let count = process_available_packets(&mut nodes).await;
|
||||||
assert!(count > 0, "Expected DataPacket to arrive");
|
assert!(count > 0, "Expected encrypted data to arrive");
|
||||||
|
|
||||||
// Node 1's session should now be Established (was Responding, transitions on first data)
|
// Node 1's session should now be Established (was Responding, transitions on first data)
|
||||||
assert!(nodes[1]
|
assert!(nodes[1]
|
||||||
@@ -958,7 +961,7 @@ async fn test_tun_outbound_established_session() {
|
|||||||
|
|
||||||
nodes[0].node.handle_tun_outbound(ipv6_packet.clone()).await;
|
nodes[0].node.handle_tun_outbound(ipv6_packet.clone()).await;
|
||||||
|
|
||||||
// Process packets: encrypted DataPacket → Node 1
|
// Process packets: encrypted data → Node 1
|
||||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||||
process_available_packets(&mut nodes).await;
|
process_available_packets(&mut nodes).await;
|
||||||
|
|
||||||
@@ -1176,6 +1179,7 @@ fn test_purge_idle_sessions_removes_expired() {
|
|||||||
remote.pubkey_full(),
|
remote.pubkey_full(),
|
||||||
EndToEndState::Established(session),
|
EndToEndState::Established(session),
|
||||||
1000, // created at t=1000ms
|
1000, // created at t=1000ms
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
node.sessions.insert(remote_addr, entry);
|
node.sessions.insert(remote_addr, entry);
|
||||||
@@ -1201,6 +1205,7 @@ fn test_purge_idle_sessions_keeps_active() {
|
|||||||
remote.pubkey_full(),
|
remote.pubkey_full(),
|
||||||
EndToEndState::Established(session),
|
EndToEndState::Established(session),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Touch at t=80s — recent activity
|
// Touch at t=80s — recent activity
|
||||||
@@ -1232,6 +1237,7 @@ fn test_purge_idle_sessions_ignores_initiating() {
|
|||||||
remote.pubkey_full(),
|
remote.pubkey_full(),
|
||||||
EndToEndState::Initiating(handshake),
|
EndToEndState::Initiating(handshake),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
node.sessions.insert(remote_addr, entry);
|
node.sessions.insert(remote_addr, entry);
|
||||||
@@ -1255,6 +1261,7 @@ fn test_purge_idle_sessions_cleans_pending_packets() {
|
|||||||
remote.pubkey_full(),
|
remote.pubkey_full(),
|
||||||
EndToEndState::Established(session),
|
EndToEndState::Established(session),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
node.sessions.insert(remote_addr, entry);
|
node.sessions.insert(remote_addr, entry);
|
||||||
@@ -1288,6 +1295,7 @@ fn test_purge_idle_sessions_disabled_when_zero() {
|
|||||||
remote.pubkey_full(),
|
remote.pubkey_full(),
|
||||||
EndToEndState::Established(session),
|
EndToEndState::Established(session),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
node.sessions.insert(remote_addr, entry);
|
node.sessions.insert(remote_addr, entry);
|
||||||
@@ -1320,6 +1328,7 @@ fn test_coords_warmup_counter_default_zero_on_new() {
|
|||||||
identity_b.pubkey_full(),
|
identity_b.pubkey_full(),
|
||||||
EndToEndState::Initiating(handshake),
|
EndToEndState::Initiating(handshake),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(entry.coords_warmup_remaining(), 0,
|
assert_eq!(entry.coords_warmup_remaining(), 0,
|
||||||
@@ -1338,6 +1347,7 @@ fn test_coords_warmup_counter_set_and_get() {
|
|||||||
remote.pubkey_full(),
|
remote.pubkey_full(),
|
||||||
EndToEndState::Established(session),
|
EndToEndState::Established(session),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(entry.coords_warmup_remaining(), 0);
|
assert_eq!(entry.coords_warmup_remaining(), 0);
|
||||||
@@ -1361,6 +1371,7 @@ fn test_coords_warmup_counter_decrement() {
|
|||||||
remote.pubkey_full(),
|
remote.pubkey_full(),
|
||||||
EndToEndState::Established(session),
|
EndToEndState::Established(session),
|
||||||
1000,
|
1000,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
entry.set_coords_warmup_remaining(3);
|
entry.set_coords_warmup_remaining(3);
|
||||||
|
|||||||
@@ -272,9 +272,9 @@ impl Disconnect {
|
|||||||
/// | 20 | dest_addr | 16 bytes | Destination node_addr |
|
/// | 20 | dest_addr | 16 bytes | Destination node_addr |
|
||||||
/// | 36 | payload | variable | Session-layer message |
|
/// | 36 | payload | variable | Session-layer message |
|
||||||
///
|
///
|
||||||
/// The payload is either end-to-end encrypted (SessionSetup, SessionAck,
|
/// The payload is either end-to-end encrypted (handshake messages, data,
|
||||||
/// DataPacket) or plaintext link-layer error signals (CoordsRequired,
|
/// reports) or plaintext error signals (CoordsRequired, PathBroken)
|
||||||
/// PathBroken) generated by transit routers.
|
/// generated by transit routers.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SessionDatagram {
|
pub struct SessionDatagram {
|
||||||
/// Source node address (originator of this datagram).
|
/// Source node address (originator of this datagram).
|
||||||
@@ -526,7 +526,7 @@ mod tests {
|
|||||||
fn test_session_datagram_encode_decode() {
|
fn test_session_datagram_encode_decode() {
|
||||||
let src = make_node_addr(0xAA);
|
let src = make_node_addr(0xAA);
|
||||||
let dest = make_node_addr(0xBB);
|
let dest = make_node_addr(0xBB);
|
||||||
let payload = vec![0x10, 0x00, 0x05, 0x00, 1, 2, 3, 4, 5]; // DataPacket payload
|
let payload = vec![0x10, 0x00, 0x05, 0x00, 1, 2, 3, 4, 5]; // session payload
|
||||||
let dg = SessionDatagram::new(src, dest, payload.clone())
|
let dg = SessionDatagram::new(src, dest, payload.clone())
|
||||||
.with_ttl(32);
|
.with_ttl(32);
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -37,10 +37,12 @@ pub use tree::TreeAnnounce;
|
|||||||
pub use filter::FilterAnnounce;
|
pub use filter::FilterAnnounce;
|
||||||
pub use discovery::{LookupRequest, LookupResponse};
|
pub use discovery::{LookupRequest, LookupResponse};
|
||||||
pub use session::{
|
pub use session::{
|
||||||
CoordsRequired, DataFlags, DataPacket, PathBroken, SessionAck, SessionFlags,
|
CoordsRequired, FspFlags, FspInnerFlags, PathBroken, PathMtuNotification, SessionAck,
|
||||||
SessionMessageType, SessionSetup, COORDS_REQUIRED_SIZE, DATA_FLAG_COORDS_PRESENT,
|
SessionFlags, SessionMessageType, SessionReceiverReport, SessionSenderReport, SessionSetup,
|
||||||
DATA_HEADER_SIZE,
|
COORDS_REQUIRED_SIZE, PATH_MTU_NOTIFICATION_SIZE, SESSION_RECEIVER_REPORT_SIZE,
|
||||||
|
SESSION_SENDER_REPORT_SIZE,
|
||||||
};
|
};
|
||||||
|
pub(crate) use session::{decode_optional_coords, encode_coords};
|
||||||
|
|
||||||
/// Protocol version for message compatibility.
|
/// Protocol version for message compatibility.
|
||||||
pub const PROTOCOL_VERSION: u8 = 1;
|
pub const PROTOCOL_VERSION: u8 = 1;
|
||||||
|
|||||||
+617
-358
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -62,13 +62,21 @@ const MAX_ORIGINAL_PACKET: usize = MIN_IPV6_MTU - IPV6_HEADER_LEN - ICMPV6_HEADE
|
|||||||
/// Total FIPS encapsulation overhead (worst case).
|
/// Total FIPS encapsulation overhead (worst case).
|
||||||
///
|
///
|
||||||
/// Breakdown:
|
/// Breakdown:
|
||||||
/// - Noise encryption tag: 16 bytes
|
/// - Noise encryption tag: 16 bytes (FLP link-layer AEAD)
|
||||||
/// - Established frame header: 16 bytes (common prefix + receiver_idx + counter)
|
/// - FLP established frame header: 16 bytes (common prefix + receiver_idx + counter)
|
||||||
/// - Inner header: 5 bytes (4-byte timestamp + 1-byte msg_type)
|
/// - FLP inner header: 5 bytes (4-byte timestamp + 1-byte msg_type)
|
||||||
/// - SessionDatagram fields: 35 bytes (ttl + path_mtu + src_addr + dest_addr)
|
/// - SessionDatagram fields: 35 bytes (ttl + path_mtu + src_addr + dest_addr)
|
||||||
/// - DataPacket header: 12 bytes (msg_type + flags + counter + payload_len)
|
/// - FSP header: 12 bytes (4-byte prefix + 8-byte counter)
|
||||||
|
/// - FSP inner header: 6 bytes (4-byte timestamp + 1-byte msg_type + 1-byte inner_flags)
|
||||||
|
/// - Session AEAD tag: 16 bytes (FSP session-layer AEAD)
|
||||||
/// - Coordinates (worst case): ~60 bytes (2 coords with depth 3 each)
|
/// - Coordinates (worst case): ~60 bytes (2 coords with depth 3 each)
|
||||||
pub const FIPS_OVERHEAD: u16 = 16 + 16 + 5 + 35 + 12 + 60; // 144 bytes
|
///
|
||||||
|
/// The old 12-byte session header is replaced by FSP header (12) + FSP inner
|
||||||
|
/// header (6) + session AEAD tag (16) = 34 bytes, an increase of 22 bytes.
|
||||||
|
/// However, the old overhead already accounted for the session AEAD tag in
|
||||||
|
/// the "Noise encryption tag" line, and the old header included the counter.
|
||||||
|
/// Net change: +6 bytes for FSP inner header.
|
||||||
|
pub const FIPS_OVERHEAD: u16 = 16 + 16 + 5 + 35 + 12 + 6 + 60; // 150 bytes
|
||||||
|
|
||||||
/// Calculate the effective IPv6 MTU for FIPS-encapsulated traffic.
|
/// Calculate the effective IPv6 MTU for FIPS-encapsulated traffic.
|
||||||
///
|
///
|
||||||
|
|||||||
Reference in New Issue
Block a user