mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
The upper module holds the TUN adapter, the .fips DNS responder, ICMPv6 generation, TCP MSS clamping and the IPv6 shim codec: the host-side IPv6 plane. Name the directory for what it is, so host-networking code has one obvious home as more of it is gathered there. This is a pure directory move. No moved file changes, and a `pub use ipv6tun as upper;` alias in lib.rs keeps every crate::upper:: and fips::upper:: path resolving, so no consumer changes either. Tracing targets follow module paths, so fips::upper::* becomes fips::ipv6tun::*. Update the RUST_LOG example in the MTU diagnosis guide and note the renamed targets in the changelog.
369 lines
12 KiB
Rust
369 lines
12 KiB
Rust
//! 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
|
|
}
|
|
}
|