mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Implement FSP port multiplexing and IPv6 header compression
Breaking wire format change: DataPacket payloads inside the AEAD envelope now carry a 4-byte port header [src_port:2 LE][dst_port:2 LE] before the service payload. The receiver dispatches by destination port. Port multiplexing: - send_session_data() takes src_port/dst_port params, prepends port header - New send_ipv6_packet() compresses IPv6 header and sends on port 256 - Receive path dispatches DataPackets by port: port 256 decompresses IPv6 header from session context and delivers to TUN, unknown ports dropped - Port constants: FSP_PORT_HEADER_SIZE (4 bytes), FSP_PORT_IPV6_SHIM (256) IPv6 header compression: - New ipv6_shim module with compress_ipv6()/decompress_ipv6() pure functions - Strips src/dst addresses (32 bytes) and payload length (2 bytes) from each packet, preserving traffic class, flow label, next header, and hop limit as 6-byte residual fields - Addresses reconstructed from session context on receive side - Net savings: 29 bytes per packet (overhead 106 → 77 bytes) - FIPS_IPV6_OVERHEAD constant (77 bytes), effective_ipv6_mtu() updated - 16 unit tests for round-trip fidelity, field preservation, error cases Documentation: - fips-wire-formats: DataPacket port header, port registry, IPv6 shim format tables, updated encapsulation walkthrough and overhead budget - fips-ipv6-adapter: FIPS_IPV6_OVERHEAD (77 bytes), updated MTU numbers, TUN reader/writer flow with compression steps, impl status - fips-session-layer: port-based service dispatch section, data transfer description, impl status - fips-intro: IPv6 adapter as port 256 service, node architecture updated - fips-mesh-operation: packet size summary with compressed overhead - DataPacket doc updated with port header and dispatch model - session_wire.rs module doc: DataPacket Port Multiplexing section
This commit is contained in:
@@ -155,18 +155,21 @@ demultiplexing. FSP provides a datagram service to applications above.
|
||||
|
||||
See [fips-session-layer.md](fips-session-layer.md) for the FSP specification.
|
||||
|
||||
**IPv6 adaptation layer**: Sits above FSP and adapts the FIPS datagram
|
||||
service for unmodified IPv6 applications. Provides DNS resolution (npub →
|
||||
fd00::/8 address), identity cache management, MTU enforcement, and a TUN
|
||||
interface. This is the primary way existing applications use the FIPS mesh.
|
||||
**IPv6 adaptation layer**: Sits above FSP as a service on port 256, adapting
|
||||
the FIPS datagram service for unmodified IPv6 applications. Provides DNS
|
||||
resolution (npub → fd00::/8 address), identity cache management, IPv6 header
|
||||
compression, MTU enforcement, and a TUN interface. This is the primary way
|
||||
existing applications use the FIPS mesh.
|
||||
|
||||
See [fips-ipv6-adapter.md](fips-ipv6-adapter.md) for the IPv6 adapter.
|
||||
|
||||
### Node Architecture
|
||||
|
||||
Two application interfaces sit at the top of the stack: a native datagram
|
||||
API addressed by npub, and an IPv6 TUN adapter that maps npubs to `fd00::/8`
|
||||
addresses so unmodified IP applications can use the network transparently.
|
||||
Application services sit at the top of the stack, dispatched by FSP port
|
||||
number: the IPv6 TUN adapter (port 256) maps npubs to `fd00::/8` addresses
|
||||
with header compression so unmodified IP applications can use the network
|
||||
transparently, while the native datagram API addresses destinations directly
|
||||
by npub.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -146,35 +146,41 @@ wrapping.
|
||||
| FSP header | 12 bytes | 4-byte prefix + 8-byte counter (used as AEAD AAD) |
|
||||
| FSP inner header | 6 bytes | 4-byte timestamp + 1-byte msg_type + 1-byte inner_flags (inside AEAD) |
|
||||
| Session AEAD tag | 16 bytes | ChaCha20-Poly1305 tag on session-encrypted payload |
|
||||
| **Data path total** | **106 bytes** | `FIPS_OVERHEAD` constant |
|
||||
| **Protocol envelope** | **106 bytes** | `FIPS_OVERHEAD` constant |
|
||||
| Port header | 4 bytes | src_port + dst_port (DataPacket service dispatch) |
|
||||
| IPv6 compression | −33 bytes | 40-byte IPv6 header → 7-byte format + residual |
|
||||
| **IPv6 data path total** | **77 bytes** | `FIPS_IPV6_OVERHEAD` constant |
|
||||
|
||||
Coordinate piggybacking (CP flag) adds variable overhead: `2 + entries × 16`
|
||||
per coordinate, with both src and dst coords sent. The send path skips the
|
||||
CP flag if adding coords would exceed the transport MTU.
|
||||
|
||||
The `FIPS_OVERHEAD` constant (106 bytes) represents the fixed data path
|
||||
overhead and is used for MTU calculations.
|
||||
The `FIPS_OVERHEAD` constant (106 bytes) represents the base protocol
|
||||
envelope overhead (link encryption + routing + session encryption). For IPv6
|
||||
traffic, FSP port multiplexing adds 4 bytes (port header) while IPv6 header
|
||||
compression saves 33 bytes (40-byte header → 7-byte format + residual),
|
||||
yielding a net `FIPS_IPV6_OVERHEAD` of 77 bytes.
|
||||
|
||||
### Effective IPv6 MTU
|
||||
|
||||
The effective IPv6 MTU visible to applications is:
|
||||
|
||||
```text
|
||||
effective_ipv6_mtu = transport_mtu - FIPS_OVERHEAD
|
||||
effective_ipv6_mtu = transport_mtu - FIPS_IPV6_OVERHEAD
|
||||
```
|
||||
|
||||
For typical deployments:
|
||||
|
||||
| Transport MTU | Effective IPv6 MTU | Notes |
|
||||
| ------------- | ------------------ | ----- |
|
||||
| 1472 (UDP/Ethernet) | 1366 | Standard deployment |
|
||||
| 1280 (UDP minimum) | 1174 | Below IPv6 minimum |
|
||||
| 1472 (UDP/Ethernet) | 1395 | Standard deployment |
|
||||
| 1280 (UDP minimum) | 1203 | Below IPv6 minimum |
|
||||
|
||||
IPv6 mandates that every link support at least 1280 bytes. The minimum
|
||||
transport path MTU for the IPv6 adapter is therefore:
|
||||
|
||||
```text
|
||||
1280 + 106 = 1386 bytes
|
||||
1280 + 77 = 1357 bytes
|
||||
```
|
||||
|
||||
Transports with smaller MTUs (radio at ~250 bytes, serial at 256 bytes) cannot
|
||||
@@ -249,15 +255,18 @@ The TUN reader receives raw IPv6 packets from applications and processes them:
|
||||
3. Look up identity cache — miss returns ICMPv6 Destination Unreachable
|
||||
4. Retrieve NodeAddr and PublicKey from cache
|
||||
5. Look up or establish FSP session
|
||||
6. Encrypt payload with session keys
|
||||
7. Route through FMP toward destination
|
||||
6. Compress IPv6 header: strip addresses and payload length, build format 0x00
|
||||
payload with residual fields (traffic class, flow label, next header, hop limit)
|
||||
7. Prepend port header (src_port=256, dst_port=256)
|
||||
8. Encrypt with session keys
|
||||
9. Route through FMP toward destination
|
||||
|
||||
### Writer Thread
|
||||
|
||||
A single writer thread services an mpsc queue of outbound packets:
|
||||
|
||||
- Inbound mesh traffic (decrypted session payloads destined for local
|
||||
applications)
|
||||
- Inbound mesh traffic on port 256 (IPv6 header reconstructed from session
|
||||
context + residual fields, then delivered as complete IPv6 packets)
|
||||
- ICMPv6 error responses (Packet Too Big, Destination Unreachable)
|
||||
- TCP MSS-clamped SYN-ACK packets
|
||||
|
||||
@@ -305,6 +314,8 @@ TUN device creation requires `CAP_NET_ADMIN`. Options:
|
||||
| ICMP rate limiting (per-source) | **Implemented** |
|
||||
| TCP MSS clamping (SYN + SYN-ACK) | **Implemented** |
|
||||
| DNS service (.fips domain) | **Implemented** |
|
||||
| Port-based service multiplexing (port 256) | **Implemented** |
|
||||
| IPv6 header compression (format 0x00) | **Implemented** |
|
||||
| Per-destination route MTU (netlink) | Planned |
|
||||
| Transit MTU error signal | **Implemented** |
|
||||
| Path MTU tracking (SessionDatagram field) | **Implemented** |
|
||||
|
||||
@@ -647,8 +647,8 @@ routing decisions but retains its own end-to-end encryption and identity.
|
||||
| LookupResponse | ~400 bytes | Response to discovery | Yes (greedy routed) |
|
||||
| SessionDatagram + SessionSetup | ~232–402 bytes | Session establishment | Yes (routed) |
|
||||
| SessionDatagram + SessionAck | ~170 bytes | Session confirmation | Yes (routed) |
|
||||
| SessionDatagram + Data (minimal) | 106 bytes + payload | Bulk traffic | Yes (routed) |
|
||||
| SessionDatagram + Data (with CP) | 106 + coords + payload | Warmup/recovery | Yes (routed) |
|
||||
| SessionDatagram + Data (minimal) | 77 bytes + IPv6 payload | Bulk IPv6 traffic (compressed) | Yes (routed) |
|
||||
| SessionDatagram + Data (with CP) | 77 + coords + IPv6 payload | Warmup/recovery (compressed) | Yes (routed) |
|
||||
| SessionDatagram + CoordsRequired | 70 bytes | Cache miss error | Yes (routed) |
|
||||
| SessionDatagram + PathBroken | 70+ bytes | Dead-end error | Yes (routed) |
|
||||
| Disconnect | 2 bytes | Link teardown | No (peer-to-peer) |
|
||||
|
||||
@@ -45,6 +45,15 @@ arriving at the TUN are translated to FIPS datagrams and routed through FSP.
|
||||
See [fips-ipv6-adapter.md](fips-ipv6-adapter.md) for the IPv6 adaptation
|
||||
layer.
|
||||
|
||||
### Port-Based Service Dispatch
|
||||
|
||||
FSP DataPackets carry a 4-byte port header (source and destination port) inside
|
||||
the AEAD envelope, enabling multiple services to share a single session. The
|
||||
IPv6 adapter runs on port 256; the native FIPS API and future services
|
||||
(gateways, application protocols) register on other ports. Port dispatch is
|
||||
internal to the FSP layer — services see only their payload, not the port
|
||||
header.
|
||||
|
||||
### What Applications Get
|
||||
|
||||
- **Authenticated datagram delivery**: Each datagram is encrypted and
|
||||
@@ -174,7 +183,9 @@ encrypted message includes:
|
||||
flags (including the CP flag for coordinate cache warming)
|
||||
- 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
|
||||
timestamp, message type, inner flags) followed by DataPacket content: a 4-byte
|
||||
port header (src_port, dst_port) and the service payload. The receiver
|
||||
dispatches by destination port to the registered service handler.
|
||||
|
||||
### Session Idle Timeout
|
||||
|
||||
@@ -539,6 +550,8 @@ MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms go
|
||||
| Simultaneous initiation tie-breaker | **Implemented** |
|
||||
| Flush coord cache on parent change | **Implemented** |
|
||||
| Rekey | **Implemented** |
|
||||
| Port-based service multiplexing | **Implemented** |
|
||||
| IPv6 header compression (shim on port 256) | **Implemented** |
|
||||
| Path MTU tracking (FMP SessionDatagram field) | **Implemented** |
|
||||
| Path MTU notification (end-to-end echo) | **Implemented** |
|
||||
|
||||
|
||||
@@ -488,7 +488,7 @@ body.
|
||||
|
||||
| Type | Message | Description |
|
||||
| ---- | ------- | ----------- |
|
||||
| 0x10 | Data | Application data (IPv6 payload via TUN) |
|
||||
| 0x10 | Data | Port-multiplexed service payload (see DataPacket below) |
|
||||
| 0x11 | SenderReport | MMP sender-side metrics report |
|
||||
| 0x12 | ReceiverReport | MMP receiver-side metrics report |
|
||||
| 0x13 | PathMtuNotification | End-to-end path MTU echo |
|
||||
@@ -580,11 +580,64 @@ Encoded with FSP prefix: ver=0, phase=0x3, flags=0, payload_len.
|
||||
SessionMsg3 does not carry coordinates — both endpoints already have each
|
||||
other's coordinates from SessionSetup (msg1) and SessionAck (msg2).
|
||||
|
||||
### Data (0x10)
|
||||
### Data (0x10) — DataPacket Port Multiplexing
|
||||
|
||||
Application data (typically IPv6 payload). This is the `msg_type` byte
|
||||
inside the encrypted inner header — there is no separate DataPacket struct.
|
||||
The body after the inner header is delivered directly to the TUN interface.
|
||||
DataPacket is the primary application data carrier. The body after the
|
||||
6-byte encrypted inner header contains a 4-byte port header followed by
|
||||
the service payload:
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | src_port | 2 bytes LE | Source service port |
|
||||
| 2 | dst_port | 2 bytes LE | Destination service port |
|
||||
| 4 | payload | variable | Service-specific payload |
|
||||
|
||||
The receiver dispatches by `dst_port` to the registered service handler.
|
||||
|
||||
**Port registry (three tiers):**
|
||||
|
||||
| Range | Purpose |
|
||||
| ----- | ------- |
|
||||
| 0–255 (0x00–0xFF) | Reserved, protocol use |
|
||||
| 256–1023 (0x100–0x3FF) | Reserved, FIPS standard services |
|
||||
| 1024–65535 (0x400–0xFFFF) | Application use |
|
||||
|
||||
**Initial assignment**: Port 256 (0x100) = IPv6 shim.
|
||||
|
||||
#### IPv6 Shim Payload Format (Port 256)
|
||||
|
||||
The IPv6 shim defines its own payload format with a leading format byte:
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | format | 1 byte | Compression format (0x00 = mesh-internal compressed) |
|
||||
| 1 | fields | variable | Format-specific residual fields |
|
||||
|
||||
**Format 0x00 — mesh-internal compressed (default):**
|
||||
|
||||
Strips source and destination IPv6 addresses (32 bytes) and payload length
|
||||
(2 bytes) from each packet. Carries residual fields that cannot be derived
|
||||
from session context:
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | format | 1 byte | 0x00 |
|
||||
| 1 | traffic_class | 1 byte | IPv6 Traffic Class (DSCP + ECN) |
|
||||
| 2 | flow_label | 3 bytes | IPv6 Flow Label (20 bits, big-endian, zero-padded) |
|
||||
| 5 | next_header | 1 byte | IPv6 Next Header (protocol identifier) |
|
||||
| 6 | hop_limit | 1 byte | IPv6 Hop Limit |
|
||||
| 7 | upper_payload | variable | Upper-layer payload (TCP, UDP, ICMPv6, etc.) |
|
||||
|
||||
The receiver reconstructs the full 40-byte IPv6 header from session context
|
||||
(source and destination addresses derived from session npubs, version = 6,
|
||||
payload length from outer packet length) plus the 6 bytes of residual fields,
|
||||
then delivers the complete IPv6 packet to the TUN interface.
|
||||
|
||||
**Format 0x01+**: Reserved for future use (e.g., full-header gateway traffic).
|
||||
|
||||
**Compression savings**: 29 bytes per packet (34 bytes stripped, 7 bytes
|
||||
format + residual added). Net overhead for IPv6 traffic: 77 bytes
|
||||
(`FIPS_IPV6_OVERHEAD`), down from 110 bytes base DataPacket overhead.
|
||||
|
||||
### PathMtuNotification (0x13)
|
||||
|
||||
@@ -695,28 +748,39 @@ A complete picture of how application data is wrapped through each layer.
|
||||
|
||||
### Application Data -> Wire
|
||||
|
||||
Starting with an application sending a 1024-byte payload to a destination:
|
||||
Starting with an IPv6 application sending a 1024-byte TCP payload to a
|
||||
destination (the original IPv6 packet at the TUN is 1064 bytes: 40-byte
|
||||
header + 1024-byte payload):
|
||||
|
||||
```text
|
||||
Layer 4: Application data
|
||||
1024 bytes
|
||||
Layer 5: Application data
|
||||
1024 bytes (TCP payload inside 1064-byte IPv6 packet)
|
||||
|
||||
Layer 4: IPv6 shim compression (port 256)
|
||||
Strip IPv6 addresses (32) + payload length (2), keep residual fields
|
||||
format (1) + residual (6) + upper payload (1024) = 1031 bytes
|
||||
|
||||
Layer 3: Session encryption (FSP)
|
||||
FSP header (12 bytes) + AEAD(inner_hdr (6) + payload (1024)) + AEAD tag (16)
|
||||
= 1058 bytes
|
||||
FSP header (12) + AEAD(inner_hdr (6) + port_hdr (4) + shim (1031)) + tag (16)
|
||||
= 12 + 1041 + 16 = 1069 bytes
|
||||
|
||||
Layer 2: SessionDatagram envelope (FMP routing)
|
||||
msg_type (1) + ttl (1) + path_mtu (2) + src_addr (16) + dest_addr (16) + payload (1058)
|
||||
= 1094 bytes
|
||||
msg_type (1) + ttl (1) + path_mtu (2) + src_addr (16) + dest_addr (16) + payload (1069)
|
||||
= 1105 bytes
|
||||
|
||||
Layer 1: Link encryption (FMP per-hop)
|
||||
outer header (16) + encrypted(inner_hdr (5) + datagram (1094)) + AEAD tag (16)
|
||||
= 1131 bytes
|
||||
outer header (16) + encrypted(inner_hdr (5) + datagram (1105)) + AEAD tag (16)
|
||||
= 1142 bytes
|
||||
|
||||
Layer 0: Transport
|
||||
UDP datagram containing 1131 bytes
|
||||
UDP datagram containing 1142 bytes
|
||||
```
|
||||
|
||||
Total overhead for IPv6 traffic: 1142 − 1064 = 78 bytes per packet. The
|
||||
difference from the `FIPS_IPV6_OVERHEAD` constant (77 bytes) is the 1-byte
|
||||
FMP `msg_type` counted in the link inner header rather than the
|
||||
SessionDatagram body.
|
||||
|
||||
### Overhead Budget
|
||||
|
||||
| Layer | Overhead | Component |
|
||||
@@ -726,7 +790,11 @@ Layer 0: Transport
|
||||
| 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 |
|
||||
| **Data path total** | **106 bytes** | `FIPS_OVERHEAD` constant |
|
||||
| **Protocol envelope** | **106 bytes** | `FIPS_OVERHEAD` constant |
|
||||
| Port header | 4 bytes | src_port + dst_port (DataPacket only) |
|
||||
| **DataPacket total** | **110 bytes** | Base overhead for any port-multiplexed service |
|
||||
| IPv6 compression | −33 bytes | 40-byte IPv6 header → 7-byte format + residual |
|
||||
| **IPv6 data path total** | **77 bytes** | `FIPS_IPV6_OVERHEAD` constant |
|
||||
|
||||
### At Each Transit Node
|
||||
|
||||
@@ -784,8 +852,8 @@ endpoint session keys).
|
||||
| SessionSetup | ~170 bytes | Depth-dependent (XK msg1 = 33 bytes) |
|
||||
| SessionAck | ~190 bytes | Depth-dependent, carries both endpoints' coords (XK msg2 = 57 bytes) |
|
||||
| SessionMsg3 | ~80 bytes | Fixed (XK msg3 = 73 bytes, no coords) |
|
||||
| Data (minimal) | 12 + 6 + payload + 16 bytes | Steady state |
|
||||
| Data (with coords) | 12 + ~130 + 6 + payload + 16 bytes | Warmup/recovery |
|
||||
| Data (minimal) | 12 + 6 + 4 + payload + 16 bytes | Steady state (port header included) |
|
||||
| Data (with coords) | 12 + ~130 + 6 + 4 + payload + 16 bytes | Warmup/recovery (port header included) |
|
||||
| SenderReport | 12 + 6 + 46 + 16 bytes | MMP metrics |
|
||||
| ReceiverReport | 12 + 6 + 66 + 16 bytes | MMP metrics |
|
||||
| PathMtuNotification | 12 + 6 + 2 + 16 bytes | MTU signal |
|
||||
@@ -799,8 +867,9 @@ endpoint session keys).
|
||||
| Scenario | Wire Size | Notes |
|
||||
| -------- | --------- | ----- |
|
||||
| Encrypted frame minimum | 37 bytes | Empty body |
|
||||
| SessionDatagram + Data (minimal) | 37 + 35 + 12 + 6 + payload + 16 | 106 + payload |
|
||||
| SessionDatagram + Data (with coords) | 106 + coords + payload | Coords vary with tree depth |
|
||||
| SessionDatagram + Data (minimal) | 37 + 35 + 12 + 6 + 4 + payload + 16 | 110 + payload (any service) |
|
||||
| SessionDatagram + IPv6 Data (minimal) | 110 + 7 + upper_payload − 34 | 77 + IPv6 payload (compressed) |
|
||||
| SessionDatagram + Data (with coords) | 110 + coords + payload | Coords vary with tree depth |
|
||||
| SessionDatagram + SessionSetup | ~275 bytes | Depth-3, both dirs |
|
||||
| SessionDatagram + CoordsRequired | 37 + 36 + 38 = 111 bytes | Including link overhead |
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ 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_FLAG_K, FSP_HEADER_SIZE, FSP_PHASE_ESTABLISHED, FSP_PHASE_MSG1,
|
||||
FSP_PHASE_MSG2, FSP_PHASE_MSG3,
|
||||
FSP_PHASE_MSG2, FSP_PHASE_MSG3, FSP_PORT_HEADER_SIZE, FSP_PORT_IPV6_SHIM,
|
||||
};
|
||||
use crate::protocol::{coords_wire_size, encode_coords};
|
||||
use crate::upper::icmp::FIPS_OVERHEAD;
|
||||
@@ -271,23 +271,55 @@ impl Node {
|
||||
// Dispatch by msg_type
|
||||
match SessionMessageType::from_byte(msg_type) {
|
||||
Some(SessionMessageType::DataPacket) => {
|
||||
// msg_type 0x10: deliver rest (IPv6 payload) to TUN
|
||||
let mut packet = rest.to_vec();
|
||||
// msg_type 0x10: port-multiplexed service dispatch
|
||||
if rest.len() < FSP_PORT_HEADER_SIZE {
|
||||
debug!(len = rest.len(), "DataPacket too short for port header");
|
||||
return;
|
||||
}
|
||||
let dst_port = u16::from_le_bytes([rest[2], rest[3]]);
|
||||
let service_payload = &rest[FSP_PORT_HEADER_SIZE..];
|
||||
|
||||
match dst_port {
|
||||
FSP_PORT_IPV6_SHIM => {
|
||||
use crate::FipsAddress;
|
||||
let src_ipv6 = FipsAddress::from_node_addr(src_addr).to_ipv6().octets();
|
||||
let dst_ipv6 = FipsAddress::from_node_addr(self.node_addr()).to_ipv6().octets();
|
||||
|
||||
match crate::upper::ipv6_shim::decompress_ipv6(service_payload, src_ipv6, dst_ipv6) {
|
||||
Some(mut packet) => {
|
||||
if ce_flag {
|
||||
mark_ipv6_ecn_ce(&mut packet);
|
||||
self.stats_mut().congestion.record_ce_received();
|
||||
}
|
||||
if let Some(tun_tx) = &self.tun_tx {
|
||||
if let Err(e) = tun_tx.send(packet) {
|
||||
debug!(error = %e, "Failed to deliver decrypted packet to TUN");
|
||||
debug!(error = %e, "Failed to deliver decompressed IPv6 packet to TUN");
|
||||
}
|
||||
} else {
|
||||
trace!(
|
||||
src = %self.peer_display_name(src_addr),
|
||||
"DataPacket decrypted (no TUN interface, plaintext dropped)"
|
||||
"IPv6 shim packet decompressed (no TUN interface)"
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!(
|
||||
src = %self.peer_display_name(src_addr),
|
||||
len = service_payload.len(),
|
||||
"IPv6 shim decompression failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
debug!(
|
||||
src = %self.peer_display_name(src_addr),
|
||||
dst_port,
|
||||
"Unknown FSP service port, dropping DataPacket"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(SessionMessageType::SenderReport) => {
|
||||
self.handle_session_sender_report(src_addr, rest);
|
||||
}
|
||||
@@ -1118,10 +1150,16 @@ impl Node {
|
||||
/// Uses the FSP pipeline: builds a 12-byte cleartext header (used as AAD),
|
||||
/// prepends the 6-byte inner header to the plaintext, encrypts with AAD,
|
||||
/// optionally inserts cleartext coords, and wraps in a SessionDatagram.
|
||||
///
|
||||
/// The `src_port` and `dst_port` identify the service. A 4-byte port header
|
||||
/// `[src_port:2 LE][dst_port:2 LE]` is prepended to `payload` inside the
|
||||
/// AEAD envelope. The receiver dispatches by `dst_port`.
|
||||
pub(in crate::node) async fn send_session_data(
|
||||
&mut self,
|
||||
dest_addr: &NodeAddr,
|
||||
plaintext: &[u8],
|
||||
src_port: u16,
|
||||
dst_port: u16,
|
||||
payload: &[u8],
|
||||
) -> Result<(), NodeError> {
|
||||
let now_ms = Self::now_ms();
|
||||
|
||||
@@ -1140,10 +1178,16 @@ impl Node {
|
||||
});
|
||||
}
|
||||
|
||||
// Build port-prefixed plaintext: [src_port:2 LE][dst_port:2 LE][payload...]
|
||||
let mut port_payload = Vec::with_capacity(FSP_PORT_HEADER_SIZE + payload.len());
|
||||
port_payload.extend_from_slice(&src_port.to_le_bytes());
|
||||
port_payload.extend_from_slice(&dst_port.to_le_bytes());
|
||||
port_payload.extend_from_slice(payload);
|
||||
|
||||
// Build inner plaintext (doesn't depend on counter)
|
||||
let msg_type = SessionMessageType::DataPacket.to_byte(); // 0x10
|
||||
let inner_flags = FspInnerFlags { spin_bit }.to_byte();
|
||||
let inner_plaintext = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, plaintext);
|
||||
let inner_plaintext = fsp_prepend_inner_header(timestamp, msg_type, inner_flags, &port_payload);
|
||||
|
||||
// Determine whether coords fit within transport MTU.
|
||||
// If not, send standalone CoordsWarmup before the data packet.
|
||||
@@ -1151,7 +1195,7 @@ impl Node {
|
||||
let src = self.tree_state.my_coords().clone();
|
||||
let dst = self.get_dest_coords(dest_addr);
|
||||
let coords_size = coords_wire_size(&src) + coords_wire_size(&dst);
|
||||
let total_wire = FIPS_OVERHEAD as usize + coords_size + plaintext.len();
|
||||
let total_wire = FIPS_OVERHEAD as usize + FSP_PORT_HEADER_SIZE + coords_size + payload.len();
|
||||
if total_wire <= self.transport_mtu() as usize {
|
||||
(true, Some(src), Some(dst))
|
||||
} else {
|
||||
@@ -1226,7 +1270,7 @@ impl Node {
|
||||
|
||||
// Re-borrow after send (which borrowed &mut self)
|
||||
if let Some(entry) = self.sessions.get_mut(dest_addr) {
|
||||
entry.record_sent(plaintext.len());
|
||||
entry.record_sent(payload.len());
|
||||
if let Some(mmp) = entry.mmp_mut() {
|
||||
mmp.sender.record_sent(counter, timestamp, ciphertext.len());
|
||||
}
|
||||
@@ -1236,6 +1280,24 @@ impl Node {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send an IPv6 packet through the IPv6 shim (port 256) with header compression.
|
||||
///
|
||||
/// Compresses the IPv6 header (format 0x00), then sends via `send_session_data`
|
||||
/// with `src_port=256, dst_port=256`.
|
||||
pub(in crate::node) async fn send_ipv6_packet(
|
||||
&mut self,
|
||||
dest_addr: &NodeAddr,
|
||||
ipv6_packet: &[u8],
|
||||
) -> Result<(), NodeError> {
|
||||
let compressed = crate::upper::ipv6_shim::compress_ipv6(ipv6_packet)
|
||||
.ok_or_else(|| NodeError::SendFailed {
|
||||
node_addr: *dest_addr,
|
||||
reason: "IPv6 header compression failed".into(),
|
||||
})?;
|
||||
self.send_session_data(dest_addr, FSP_PORT_IPV6_SHIM, FSP_PORT_IPV6_SHIM, &compressed)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a non-data session message (reports, notifications) over an established session.
|
||||
///
|
||||
/// Similar to `send_session_data()` but:
|
||||
@@ -1525,7 +1587,7 @@ impl Node {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Err(e) = self.send_session_data(&dest_addr, &ipv6_packet).await {
|
||||
if let Err(e) = self.send_ipv6_packet(&dest_addr, &ipv6_packet).await {
|
||||
debug!(dest = %self.peer_display_name(&dest_addr), error = %e, "Failed to send TUN packet via session");
|
||||
}
|
||||
return;
|
||||
@@ -1634,7 +1696,7 @@ impl Node {
|
||||
None => return,
|
||||
};
|
||||
for packet in packets {
|
||||
if let Err(e) = self.send_session_data(dest_addr, &packet).await {
|
||||
if let Err(e) = self.send_ipv6_packet(dest_addr, &packet).await {
|
||||
debug!(dest = %self.peer_display_name(dest_addr), error = %e, "Failed to send queued TUN packet");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
//! [ver+phase:1][flags:1][payload_len:2 LE]
|
||||
//! ```
|
||||
//!
|
||||
//! ## DataPacket Port Multiplexing
|
||||
//!
|
||||
//! DataPacket (msg_type 0x10) payloads inside the AEAD envelope carry a 4-byte
|
||||
//! port header for service dispatch:
|
||||
//!
|
||||
//! ```text
|
||||
//! [src_port:2 LE][dst_port:2 LE][service payload...]
|
||||
//! ```
|
||||
//!
|
||||
//! Port 256 (0x100) = IPv6 shim with header compression.
|
||||
//!
|
||||
//! ## Message Classes
|
||||
//!
|
||||
//! | Phase | U Flag | Type | Description |
|
||||
@@ -58,6 +69,14 @@ 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
|
||||
|
||||
// FSP DataPacket port header constants.
|
||||
|
||||
/// Size of the FSP DataPacket port header (src_port + dst_port).
|
||||
pub const FSP_PORT_HEADER_SIZE: usize = 4;
|
||||
|
||||
/// FSP port: IPv6 shim service.
|
||||
pub const FSP_PORT_IPV6_SHIM: u16 = 256;
|
||||
|
||||
// Cleartext flag bit constants (byte 1 of common prefix, phase 0x0 only).
|
||||
|
||||
/// Coords Present — source and destination coordinates follow the header.
|
||||
|
||||
@@ -243,7 +243,7 @@ async fn test_session_direct_peer_data_transfer() {
|
||||
let test_data = b"Hello, FIPS session!";
|
||||
nodes[0]
|
||||
.node
|
||||
.send_session_data(&node1_addr, test_data)
|
||||
.send_session_data(&node1_addr, 0, 0, test_data)
|
||||
.await
|
||||
.expect("send_session_data failed");
|
||||
|
||||
@@ -378,7 +378,7 @@ async fn test_session_3node_forwarded_data() {
|
||||
let test_data = b"End-to-end through transit node B";
|
||||
nodes[0]
|
||||
.node
|
||||
.send_session_data(&node2_addr, test_data)
|
||||
.send_session_data(&node2_addr, 0, 0, test_data)
|
||||
.await
|
||||
.expect("send_session_data failed");
|
||||
|
||||
@@ -438,7 +438,7 @@ async fn test_session_send_data_no_session_fails() {
|
||||
let mut node = make_node();
|
||||
let fake_addr = make_node_addr(0xAA);
|
||||
|
||||
let result = node.send_session_data(&fake_addr, b"test").await;
|
||||
let result = node.send_session_data(&fake_addr, 0, 0, b"test").await;
|
||||
assert!(result.is_err(), "Should fail with no session");
|
||||
}
|
||||
|
||||
@@ -618,11 +618,16 @@ async fn test_session_100_nodes() {
|
||||
let dest_addr = all_info[dst].0;
|
||||
let src_addr = all_info[src].0;
|
||||
|
||||
// Build IPv6 packets with pair index as payload
|
||||
let src_fips = crate::FipsAddress::from_node_addr(&src_addr);
|
||||
let dst_fips = crate::FipsAddress::from_node_addr(&dest_addr);
|
||||
|
||||
// Forward: initiator → responder
|
||||
let fwd_payload = format!("fwd-{}", pair_idx).into_bytes();
|
||||
let fwd_ipv6 = build_ipv6_packet(&src_fips, &dst_fips, &fwd_payload);
|
||||
match nodes[src]
|
||||
.node
|
||||
.send_session_data(&dest_addr, &fwd_payload)
|
||||
.send_ipv6_packet(&dest_addr, &fwd_ipv6)
|
||||
.await
|
||||
{
|
||||
Ok(()) => send_forward_ok += 1,
|
||||
@@ -634,9 +639,10 @@ async fn test_session_100_nodes() {
|
||||
// Reverse: responder → initiator
|
||||
// (Responder should already be Established after XK msg3)
|
||||
let rev_payload = format!("rev-{}", pair_idx).into_bytes();
|
||||
let rev_ipv6 = build_ipv6_packet(&dst_fips, &src_fips, &rev_payload);
|
||||
match nodes[dst]
|
||||
.node
|
||||
.send_session_data(&src_addr, &rev_payload)
|
||||
.send_ipv6_packet(&src_addr, &rev_ipv6)
|
||||
.await
|
||||
{
|
||||
Ok(()) => send_reverse_ok += 1,
|
||||
@@ -671,13 +677,21 @@ async fn test_session_100_nodes() {
|
||||
let fwd_payload = format!("fwd-{}", pair_idx).into_bytes();
|
||||
let rev_payload = format!("rev-{}", pair_idx).into_bytes();
|
||||
|
||||
if delivered_per_node[dst].contains(&fwd_payload) {
|
||||
// After decompression, TUN receives full IPv6 packets.
|
||||
// Check that delivered packet's upper-layer payload matches.
|
||||
let fwd_found = delivered_per_node[dst]
|
||||
.iter()
|
||||
.any(|pkt| pkt.len() >= 40 && pkt[40..] == fwd_payload);
|
||||
if fwd_found {
|
||||
fwd_delivered += 1;
|
||||
} else if fwd_missing.len() < 20 {
|
||||
fwd_missing.push((src, dst));
|
||||
}
|
||||
|
||||
if delivered_per_node[src].contains(&rev_payload) {
|
||||
let rev_found = delivered_per_node[src]
|
||||
.iter()
|
||||
.any(|pkt| pkt.len() >= 40 && pkt[40..] == rev_payload);
|
||||
if rev_found {
|
||||
rev_delivered += 1;
|
||||
} else if rev_missing.len() < 20 {
|
||||
rev_missing.push((src, dst));
|
||||
@@ -1836,7 +1850,7 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
|
||||
// With coords (~66 extra), the wire could exceed B's recv buffer.
|
||||
for _ in 0..5 {
|
||||
let small = build_ipv6_packet(&src_fips, &dst_fips, &[0u8; 10]);
|
||||
nodes[0].node.send_session_data(&node2_addr, &small).await.unwrap();
|
||||
nodes[0].node.send_ipv6_packet(&node2_addr, &small).await.unwrap();
|
||||
}
|
||||
drain_to_quiescence(&mut nodes).await;
|
||||
|
||||
@@ -1855,7 +1869,7 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
|
||||
|
||||
// Send the oversized packet — B should fail to forward and send
|
||||
// MtuExceeded signal back.
|
||||
nodes[0].node.send_session_data(&node2_addr, &ipv6_packet).await.unwrap();
|
||||
nodes[0].node.send_ipv6_packet(&node2_addr, &ipv6_packet).await.unwrap();
|
||||
drain_to_quiescence(&mut nodes).await;
|
||||
|
||||
// Verify PathMtuState was updated on A
|
||||
|
||||
@@ -26,7 +26,8 @@ pub enum SessionMessageType {
|
||||
SessionAck = 0x01,
|
||||
|
||||
// Data and metrics (0x10-0x1F) — encrypted, inner header msg_type
|
||||
/// Encrypted IPv6 datagram payload (TUN delivery).
|
||||
/// Port-multiplexed service payload: `[src_port:2 LE][dst_port:2 LE][service data...]`.
|
||||
/// Port 256 = IPv6 shim (compressed header). Receiver dispatches by dst_port.
|
||||
DataPacket = 0x10,
|
||||
/// MMP sender report (metrics from sender to receiver).
|
||||
SenderReport = 0x11,
|
||||
|
||||
@@ -59,12 +59,13 @@ const ICMPV6_HEADER_LEN: usize = 8;
|
||||
/// Maximum original packet bytes to include in ICMPv6 error.
|
||||
const MAX_ORIGINAL_PACKET: usize = MIN_IPV6_MTU - IPV6_HEADER_LEN - ICMPV6_HEADER_LEN;
|
||||
|
||||
/// FIPS encapsulation overhead for data packets (no piggybacked coordinates).
|
||||
/// FIPS base encapsulation overhead for DataPacket (excluding port payload).
|
||||
///
|
||||
/// This is the fixed overhead for a SessionDatagram carrying an FSP DataPacket,
|
||||
/// used to compute `effective_ipv6_mtu()` and TCP MSS clamping. Coordinate
|
||||
/// piggybacking (CP flag) adds variable overhead on top of this; the send path
|
||||
/// guards against exceeding the transport MTU when coords are included.
|
||||
/// used by the send path's CP-flag guard to check whether piggybacked coords
|
||||
/// fit within the transport MTU. For IPv6 effective MTU calculations, use
|
||||
/// [`FIPS_IPV6_OVERHEAD`] which accounts for port multiplexing and header
|
||||
/// compression.
|
||||
///
|
||||
/// Breakdown (traced through the actual send path):
|
||||
///
|
||||
@@ -89,13 +90,26 @@ const MAX_ORIGINAL_PACKET: usize = MIN_IPV6_MTU - IPV6_HEADER_LEN - ICMPV6_HEADE
|
||||
/// body after msg_type is consumed by the dispatch layer.
|
||||
pub const FIPS_OVERHEAD: u16 = 16 + 16 + 5 + 35 + 12 + 6 + 16; // 106 bytes
|
||||
|
||||
/// FIPS encapsulation overhead for compressed IPv6 shim traffic (port 256).
|
||||
///
|
||||
/// With port multiplexing (4 bytes) and IPv6 header compression (format byte +
|
||||
/// 6 residual bytes, stripping 34 bytes of addresses/version/payload length),
|
||||
/// the net overhead for IPv6 packets is 77 bytes.
|
||||
///
|
||||
/// ```text
|
||||
/// Wire size = FIPS_OVERHEAD(106) + port_header(4) + format(1) + residual(6) + upper_payload
|
||||
/// = 117 + (ipv6_len - 40)
|
||||
/// = ipv6_len + 77
|
||||
/// ```
|
||||
pub const FIPS_IPV6_OVERHEAD: u16 = 77;
|
||||
|
||||
/// Calculate the effective IPv6 MTU for FIPS-encapsulated traffic.
|
||||
///
|
||||
/// Given a transport MTU (e.g., UDP payload size), returns the maximum
|
||||
/// IPv6 packet size (including IPv6 header) that can be transmitted
|
||||
/// through the FIPS mesh after encapsulation overhead.
|
||||
/// through the FIPS mesh after IPv6 header compression.
|
||||
pub fn effective_ipv6_mtu(transport_mtu: u16) -> u16 {
|
||||
transport_mtu.saturating_sub(FIPS_OVERHEAD)
|
||||
transport_mtu.saturating_sub(FIPS_IPV6_OVERHEAD)
|
||||
}
|
||||
|
||||
/// Check if we should send an ICMPv6 error for this packet.
|
||||
|
||||
360
src/upper/ipv6_shim.rs
Normal file
360
src/upper/ipv6_shim.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
//! IPv6 Header Compression for the FIPS IPv6 Shim (FSP Port 256)
|
||||
//!
|
||||
//! Compresses and decompresses IPv6 headers for mesh-internal traffic.
|
||||
//! Source and destination addresses are stripped (derivable from session
|
||||
//! context), along with version and payload length. Residual fields
|
||||
//! (traffic class, flow label, next header, hop limit) are preserved.
|
||||
//!
|
||||
//! ## Compressed Format (format 0x00)
|
||||
//!
|
||||
//! ```text
|
||||
//! [format:1][ver_tc_flow:4][next_header:1][hop_limit:1][upper_layer_payload...]
|
||||
//! ```
|
||||
//!
|
||||
//! The `ver_tc_flow` field stores the original IPv6 bytes 0-3 verbatim
|
||||
//! (including the version nibble). On decompression, the version nibble
|
||||
//! is forced to 6, payload length is computed from the remaining data,
|
||||
//! and source/destination addresses are reconstructed from session context.
|
||||
|
||||
/// Compressed format byte for mesh-internal traffic.
|
||||
pub const IPV6_SHIM_FORMAT_COMPRESSED: u8 = 0x00;
|
||||
|
||||
/// Size of the compressed residual fields (ver_tc_flow + next_header + hop_limit).
|
||||
const IPV6_SHIM_RESIDUAL_SIZE: usize = 6;
|
||||
|
||||
/// IPv6 header size.
|
||||
const IPV6_HEADER_SIZE: usize = 40;
|
||||
|
||||
/// Compress an IPv6 packet for the shim.
|
||||
///
|
||||
/// Strips source/destination addresses (32 bytes) and payload length (2 bytes).
|
||||
/// Preserves traffic class, flow label, next header, and hop limit as residual
|
||||
/// fields.
|
||||
///
|
||||
/// Returns `None` if the packet is not a valid IPv6 packet (too short or wrong
|
||||
/// version).
|
||||
pub fn compress_ipv6(ipv6_packet: &[u8]) -> Option<Vec<u8>> {
|
||||
if ipv6_packet.len() < IPV6_HEADER_SIZE || ipv6_packet[0] >> 4 != 6 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let upper_payload = &ipv6_packet[IPV6_HEADER_SIZE..];
|
||||
let mut out = Vec::with_capacity(1 + IPV6_SHIM_RESIDUAL_SIZE + upper_payload.len());
|
||||
|
||||
// Format byte
|
||||
out.push(IPV6_SHIM_FORMAT_COMPRESSED);
|
||||
|
||||
// Residual: bytes 0-3 of IPv6 header (version + TC + flow label)
|
||||
out.extend_from_slice(&ipv6_packet[0..4]);
|
||||
|
||||
// Residual: next header and hop limit
|
||||
out.push(ipv6_packet[6]); // next_header
|
||||
out.push(ipv6_packet[7]); // hop_limit
|
||||
|
||||
// Upper-layer payload (everything after the 40-byte IPv6 header)
|
||||
out.extend_from_slice(upper_payload);
|
||||
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Decompress a shim payload back to a full IPv6 packet.
|
||||
///
|
||||
/// Reconstructs the full 40-byte IPv6 header from the residual fields and
|
||||
/// session context (source/destination addresses). The payload length field
|
||||
/// is computed from the remaining data length.
|
||||
///
|
||||
/// Returns `None` if the format byte is unrecognized or the payload is too
|
||||
/// short.
|
||||
pub fn decompress_ipv6(
|
||||
shim_payload: &[u8],
|
||||
src_ipv6: [u8; 16],
|
||||
dst_ipv6: [u8; 16],
|
||||
) -> Option<Vec<u8>> {
|
||||
if shim_payload.len() < 1 + IPV6_SHIM_RESIDUAL_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let format = shim_payload[0];
|
||||
if format != IPV6_SHIM_FORMAT_COMPRESSED {
|
||||
return None;
|
||||
}
|
||||
|
||||
let residual = &shim_payload[1..1 + IPV6_SHIM_RESIDUAL_SIZE];
|
||||
let upper_payload = &shim_payload[1 + IPV6_SHIM_RESIDUAL_SIZE..];
|
||||
let upper_len = upper_payload.len();
|
||||
|
||||
let mut ipv6 = Vec::with_capacity(IPV6_HEADER_SIZE + upper_len);
|
||||
|
||||
// Bytes 0-3: restore version nibble to 6
|
||||
ipv6.push((residual[0] & 0x0F) | 0x60);
|
||||
ipv6.extend_from_slice(&residual[1..4]);
|
||||
|
||||
// Bytes 4-5: payload length (big-endian)
|
||||
ipv6.extend_from_slice(&(upper_len as u16).to_be_bytes());
|
||||
|
||||
// Byte 6: next header
|
||||
ipv6.push(residual[4]);
|
||||
|
||||
// Byte 7: hop limit
|
||||
ipv6.push(residual[5]);
|
||||
|
||||
// Bytes 8-23: source address
|
||||
ipv6.extend_from_slice(&src_ipv6);
|
||||
|
||||
// Bytes 24-39: destination address
|
||||
ipv6.extend_from_slice(&dst_ipv6);
|
||||
|
||||
// Upper-layer payload
|
||||
ipv6.extend_from_slice(upper_payload);
|
||||
|
||||
Some(ipv6)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build a minimal valid IPv6 packet with the given fields and payload.
|
||||
fn build_ipv6_packet(
|
||||
traffic_class: u8,
|
||||
flow_label: u32,
|
||||
next_header: u8,
|
||||
hop_limit: u8,
|
||||
src: [u8; 16],
|
||||
dst: [u8; 16],
|
||||
payload: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut pkt = Vec::with_capacity(IPV6_HEADER_SIZE + payload.len());
|
||||
|
||||
// Byte 0: version(4) | TC high nibble(4)
|
||||
pkt.push(0x60 | (traffic_class >> 4));
|
||||
// Byte 1: TC low nibble(4) | flow label high nibble(4)
|
||||
pkt.push((traffic_class << 4) | ((flow_label >> 16) as u8 & 0x0F));
|
||||
// Bytes 2-3: flow label low 16 bits
|
||||
pkt.push((flow_label >> 8) as u8);
|
||||
pkt.push(flow_label as u8);
|
||||
|
||||
// Bytes 4-5: payload length
|
||||
pkt.extend_from_slice(&(payload.len() as u16).to_be_bytes());
|
||||
|
||||
// Byte 6: next header
|
||||
pkt.push(next_header);
|
||||
|
||||
// Byte 7: hop limit
|
||||
pkt.push(hop_limit);
|
||||
|
||||
// Bytes 8-23: source address
|
||||
pkt.extend_from_slice(&src);
|
||||
|
||||
// Bytes 24-39: destination address
|
||||
pkt.extend_from_slice(&dst);
|
||||
|
||||
// Payload
|
||||
pkt.extend_from_slice(payload);
|
||||
|
||||
pkt
|
||||
}
|
||||
|
||||
fn sample_src() -> [u8; 16] {
|
||||
[0xfd, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]
|
||||
}
|
||||
|
||||
fn sample_dst() -> [u8; 16] {
|
||||
[0xfd, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
|
||||
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]
|
||||
}
|
||||
|
||||
// ===== Round-trip fidelity =====
|
||||
|
||||
#[test]
|
||||
fn test_compress_decompress_roundtrip() {
|
||||
let payload = vec![0xAA; 100];
|
||||
let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
assert_eq!(decompressed, pkt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_empty_payload() {
|
||||
let pkt = build_ipv6_packet(0, 0, 59, 1, sample_src(), sample_dst(), &[]);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
assert_eq!(compressed.len(), 1 + IPV6_SHIM_RESIDUAL_SIZE); // format + residual only
|
||||
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
assert_eq!(decompressed, pkt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_large_payload() {
|
||||
let payload = vec![0x55; 1400];
|
||||
let pkt = build_ipv6_packet(0, 0, 6, 128, sample_src(), sample_dst(), &payload);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
assert_eq!(decompressed, pkt);
|
||||
}
|
||||
|
||||
// ===== Field preservation =====
|
||||
|
||||
#[test]
|
||||
fn test_preserves_traffic_class() {
|
||||
// TC = 0xAB (DSCP=0x2A, ECN=0x03)
|
||||
let pkt = build_ipv6_packet(0xAB, 0, 17, 64, sample_src(), sample_dst(), &[1, 2, 3]);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
assert_eq!(decompressed, pkt);
|
||||
// Verify TC is in the right position
|
||||
let tc = ((decompressed[0] & 0x0F) << 4) | (decompressed[1] >> 4);
|
||||
assert_eq!(tc, 0xAB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserves_flow_label() {
|
||||
let pkt = build_ipv6_packet(0, 0xFEDCB, 17, 64, sample_src(), sample_dst(), &[1]);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
assert_eq!(decompressed, pkt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserves_tc_and_flow_label_combined() {
|
||||
// TC=0xFF, flow_label=0xFFFFF (maximum values)
|
||||
let pkt = build_ipv6_packet(0xFF, 0xFFFFF, 17, 64, sample_src(), sample_dst(), &[1]);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
assert_eq!(decompressed, pkt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserves_next_header_tcp() {
|
||||
let pkt = build_ipv6_packet(0, 0, 6, 64, sample_src(), sample_dst(), &[0; 20]);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
assert_eq!(decompressed[6], 6); // TCP
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserves_next_header_icmpv6() {
|
||||
let pkt = build_ipv6_packet(0, 0, 58, 255, sample_src(), sample_dst(), &[0; 8]);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
assert_eq!(decompressed[6], 58); // ICMPv6
|
||||
assert_eq!(decompressed[7], 255); // hop limit
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserves_hop_limit() {
|
||||
for hop_limit in [0, 1, 64, 128, 255] {
|
||||
let pkt = build_ipv6_packet(0, 0, 17, hop_limit, sample_src(), sample_dst(), &[1]);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
assert_eq!(decompressed[7], hop_limit);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Payload length reconstruction =====
|
||||
|
||||
#[test]
|
||||
fn test_payload_length_reconstructed() {
|
||||
let payload = vec![0xBB; 256];
|
||||
let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
|
||||
|
||||
let payload_len = u16::from_be_bytes([decompressed[4], decompressed[5]]);
|
||||
assert_eq!(payload_len, 256);
|
||||
}
|
||||
|
||||
// ===== Compression size savings =====
|
||||
|
||||
#[test]
|
||||
fn test_compression_saves_bytes() {
|
||||
let payload = vec![0; 100];
|
||||
let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
|
||||
// Original: 40 header + 100 payload = 140
|
||||
// Compressed: 1 format + 6 residual + 100 payload = 107
|
||||
// Savings: 33 bytes (version nibble kept in residual, so 34 - 1 = 33)
|
||||
assert_eq!(pkt.len(), 140);
|
||||
assert_eq!(compressed.len(), 107);
|
||||
assert_eq!(pkt.len() - compressed.len(), 33);
|
||||
}
|
||||
|
||||
// ===== Error cases =====
|
||||
|
||||
#[test]
|
||||
fn test_compress_rejects_non_ipv6() {
|
||||
let mut pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &[1]);
|
||||
pkt[0] = 0x40; // version 4 (IPv4)
|
||||
assert!(compress_ipv6(&pkt).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compress_rejects_short_packet() {
|
||||
assert!(compress_ipv6(&[0x60; 39]).is_none());
|
||||
assert!(compress_ipv6(&[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decompress_rejects_unknown_format() {
|
||||
let mut compressed = vec![0x01]; // format 0x01 = unknown
|
||||
compressed.extend_from_slice(&[0; IPV6_SHIM_RESIDUAL_SIZE]);
|
||||
assert!(decompress_ipv6(&compressed, sample_src(), sample_dst()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decompress_rejects_short_payload() {
|
||||
// Needs at least 1 (format) + 6 (residual) = 7 bytes
|
||||
assert!(decompress_ipv6(&[0x00; 6], sample_src(), sample_dst()).is_none());
|
||||
assert!(decompress_ipv6(&[], sample_src(), sample_dst()).is_none());
|
||||
}
|
||||
|
||||
// ===== Address reconstruction =====
|
||||
|
||||
#[test]
|
||||
fn test_addresses_from_context() {
|
||||
let original_src = [0xfd, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA];
|
||||
let original_dst = [0xfd, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB];
|
||||
let pkt = build_ipv6_packet(0, 0, 17, 64, original_src, original_dst, &[1, 2]);
|
||||
|
||||
let compressed = compress_ipv6(&pkt).unwrap();
|
||||
|
||||
// Decompress with different addresses (simulating session context)
|
||||
let context_src = sample_src();
|
||||
let context_dst = sample_dst();
|
||||
let decompressed = decompress_ipv6(&compressed, context_src, context_dst).unwrap();
|
||||
|
||||
// Addresses come from context, not original packet
|
||||
assert_eq!(&decompressed[8..24], &context_src);
|
||||
assert_eq!(&decompressed[24..40], &context_dst);
|
||||
|
||||
// But TC, flow label, next header, hop limit, payload match original
|
||||
assert_eq!(&decompressed[0..4], &pkt[0..4]); // ver+TC+flow
|
||||
assert_eq!(decompressed[6], pkt[6]); // next_header
|
||||
assert_eq!(decompressed[7], pkt[7]); // hop_limit
|
||||
assert_eq!(&decompressed[40..], &pkt[40..]); // payload
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,6 @@ pub mod dns;
|
||||
pub mod hosts;
|
||||
pub mod icmp;
|
||||
pub mod icmp_rate_limit;
|
||||
pub mod ipv6_shim;
|
||||
pub mod tcp_mss;
|
||||
pub mod tun;
|
||||
|
||||
Reference in New Issue
Block a user