Skip to main content

fips_core/upper/
ipv6_shim.rs

1//! IPv6 Header Compression for the FIPS IPv6 Shim (FSP Port 256)
2//!
3//! Compresses and decompresses IPv6 headers for mesh-internal traffic.
4//! Source and destination addresses are stripped (derivable from session
5//! context), along with version and payload length. Residual fields
6//! (traffic class, flow label, next header, hop limit) are preserved.
7//!
8//! ## Compressed Format (format 0x00)
9//!
10//! ```text
11//! [format:1][ver_tc_flow:4][next_header:1][hop_limit:1][upper_layer_payload...]
12//! ```
13//!
14//! The `ver_tc_flow` field stores the original IPv6 bytes 0-3 verbatim
15//! (including the version nibble). On decompression, the version nibble
16//! is forced to 6, payload length is computed from the remaining data,
17//! and source/destination addresses are reconstructed from session context.
18
19use crate::transport::PacketBuffer;
20
21/// Compressed format byte for mesh-internal traffic.
22pub const IPV6_SHIM_FORMAT_COMPRESSED: u8 = 0x00;
23
24/// Size of the compressed residual fields (ver_tc_flow + next_header + hop_limit).
25const IPV6_SHIM_RESIDUAL_SIZE: usize = 6;
26
27/// IPv6 header size.
28const IPV6_HEADER_SIZE: usize = 40;
29
30/// Compress an IPv6 packet for the shim.
31///
32/// Strips source/destination addresses (32 bytes) and payload length (2 bytes).
33/// Preserves traffic class, flow label, next header, and hop limit as residual
34/// fields.
35///
36/// Returns `None` if the packet is not a valid IPv6 packet (too short or wrong
37/// version).
38pub fn compress_ipv6(ipv6_packet: &[u8]) -> Option<Vec<u8>> {
39    if ipv6_packet.len() < IPV6_HEADER_SIZE || ipv6_packet[0] >> 4 != 6 {
40        return None;
41    }
42
43    let upper_payload = &ipv6_packet[IPV6_HEADER_SIZE..];
44    let mut out = Vec::with_capacity(1 + IPV6_SHIM_RESIDUAL_SIZE + upper_payload.len());
45
46    // Format byte
47    out.push(IPV6_SHIM_FORMAT_COMPRESSED);
48
49    // Residual: bytes 0-3 of IPv6 header (version + TC + flow label)
50    out.extend_from_slice(&ipv6_packet[0..4]);
51
52    // Residual: next header and hop limit
53    out.push(ipv6_packet[6]); // next_header
54    out.push(ipv6_packet[7]); // hop_limit
55
56    // Upper-layer payload (everything after the 40-byte IPv6 header)
57    out.extend_from_slice(upper_payload);
58
59    Some(out)
60}
61
62/// Compress an IPv6 packet in-place and prepend the FSP DataPacket port header.
63///
64/// On success `packet` is rewritten to:
65///
66/// ```text
67/// [src_port:2][dst_port:2][format:1][ver_tc_flow:4][next_header:1][hop_limit:1][payload...]
68/// ```
69///
70/// This is byte-identical to prefixing [`compress_ipv6`] with the two LE port
71/// fields, but it avoids allocating and copying the whole upper-layer payload on
72/// the TUN outbound hot path.
73pub fn compress_ipv6_with_port_header_in_place(
74    packet: &mut Vec<u8>,
75    src_port: u16,
76    dst_port: u16,
77) -> bool {
78    if packet.len() < IPV6_HEADER_SIZE || packet[0] >> 4 != 6 {
79        return false;
80    }
81
82    let residual_0_3 = [packet[0], packet[1], packet[2], packet[3]];
83    let next_header = packet[6];
84    let hop_limit = packet[7];
85    let upper_payload_len = packet.len() - IPV6_HEADER_SIZE;
86    const PORT_HEADER_SIZE: usize = 4;
87    let compressed_len = PORT_HEADER_SIZE + 1 + IPV6_SHIM_RESIDUAL_SIZE + upper_payload_len;
88
89    packet[0..2].copy_from_slice(&src_port.to_le_bytes());
90    packet[2..4].copy_from_slice(&dst_port.to_le_bytes());
91    packet[4] = IPV6_SHIM_FORMAT_COMPRESSED;
92    packet[5..9].copy_from_slice(&residual_0_3);
93    packet[9] = next_header;
94    packet[10] = hop_limit;
95    packet.copy_within(
96        IPV6_HEADER_SIZE..,
97        PORT_HEADER_SIZE + 1 + IPV6_SHIM_RESIDUAL_SIZE,
98    );
99    packet.truncate(compressed_len);
100    true
101}
102
103/// Decompress a shim payload back to a full IPv6 packet.
104///
105/// Reconstructs the full 40-byte IPv6 header from the residual fields and
106/// session context (source/destination addresses). The payload length field
107/// is computed from the remaining data length.
108///
109/// Returns `None` if the format byte is unrecognized or the payload is too
110/// short.
111pub fn decompress_ipv6(
112    shim_payload: &[u8],
113    src_ipv6: [u8; 16],
114    dst_ipv6: [u8; 16],
115) -> Option<Vec<u8>> {
116    if shim_payload.len() < 1 + IPV6_SHIM_RESIDUAL_SIZE {
117        return None;
118    }
119
120    let format = shim_payload[0];
121    if format != IPV6_SHIM_FORMAT_COMPRESSED {
122        return None;
123    }
124
125    let residual = &shim_payload[1..1 + IPV6_SHIM_RESIDUAL_SIZE];
126    let upper_payload = &shim_payload[1 + IPV6_SHIM_RESIDUAL_SIZE..];
127    let upper_len = upper_payload.len();
128
129    let mut ipv6 = Vec::with_capacity(IPV6_HEADER_SIZE + upper_len);
130
131    // Bytes 0-3: restore version nibble to 6
132    ipv6.push((residual[0] & 0x0F) | 0x60);
133    ipv6.extend_from_slice(&residual[1..4]);
134
135    // Bytes 4-5: payload length (big-endian)
136    ipv6.extend_from_slice(&(upper_len as u16).to_be_bytes());
137
138    // Byte 6: next header
139    ipv6.push(residual[4]);
140
141    // Byte 7: hop limit
142    ipv6.push(residual[5]);
143
144    // Bytes 8-23: source address
145    ipv6.extend_from_slice(&src_ipv6);
146
147    // Bytes 24-39: destination address
148    ipv6.extend_from_slice(&dst_ipv6);
149
150    // Upper-layer payload
151    ipv6.extend_from_slice(upper_payload);
152
153    Some(ipv6)
154}
155
156/// Decompress a shim payload inside an existing packet buffer.
157///
158/// `shim_offset` is relative to the buffer's visible window and points at the
159/// shim format byte. On success the visible buffer becomes a full IPv6 packet.
160pub(crate) fn decompress_ipv6_packet_buffer(
161    packet: &mut PacketBuffer,
162    shim_offset: usize,
163    src_ipv6: [u8; 16],
164    dst_ipv6: [u8; 16],
165) -> bool {
166    let Some(prefix_len) = shim_offset.checked_add(1 + IPV6_SHIM_RESIDUAL_SIZE) else {
167        return false;
168    };
169    if packet.len() < prefix_len {
170        return false;
171    }
172
173    let visible = packet.as_slice();
174    if visible[shim_offset] != IPV6_SHIM_FORMAT_COMPRESSED {
175        return false;
176    }
177    let residual = &visible[shim_offset + 1..prefix_len];
178    let upper_len = visible.len() - prefix_len;
179    let Ok(upper_len) = u16::try_from(upper_len) else {
180        return false;
181    };
182
183    let mut ipv6_header = [0u8; IPV6_HEADER_SIZE];
184    ipv6_header[0] = (residual[0] & 0x0F) | 0x60;
185    ipv6_header[1..4].copy_from_slice(&residual[1..4]);
186    ipv6_header[4..6].copy_from_slice(&upper_len.to_be_bytes());
187    ipv6_header[6] = residual[4];
188    ipv6_header[7] = residual[5];
189    ipv6_header[8..24].copy_from_slice(&src_ipv6);
190    ipv6_header[24..40].copy_from_slice(&dst_ipv6);
191
192    packet.replace_visible_prefix(prefix_len, &ipv6_header)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    /// Build a minimal valid IPv6 packet with the given fields and payload.
200    fn build_ipv6_packet(
201        traffic_class: u8,
202        flow_label: u32,
203        next_header: u8,
204        hop_limit: u8,
205        src: [u8; 16],
206        dst: [u8; 16],
207        payload: &[u8],
208    ) -> Vec<u8> {
209        let mut pkt = Vec::with_capacity(IPV6_HEADER_SIZE + payload.len());
210
211        // Byte 0: version(4) | TC high nibble(4)
212        pkt.push(0x60 | (traffic_class >> 4));
213        // Byte 1: TC low nibble(4) | flow label high nibble(4)
214        pkt.push((traffic_class << 4) | ((flow_label >> 16) as u8 & 0x0F));
215        // Bytes 2-3: flow label low 16 bits
216        pkt.push((flow_label >> 8) as u8);
217        pkt.push(flow_label as u8);
218
219        // Bytes 4-5: payload length
220        pkt.extend_from_slice(&(payload.len() as u16).to_be_bytes());
221
222        // Byte 6: next header
223        pkt.push(next_header);
224
225        // Byte 7: hop limit
226        pkt.push(hop_limit);
227
228        // Bytes 8-23: source address
229        pkt.extend_from_slice(&src);
230
231        // Bytes 24-39: destination address
232        pkt.extend_from_slice(&dst);
233
234        // Payload
235        pkt.extend_from_slice(payload);
236
237        pkt
238    }
239
240    fn sample_src() -> [u8; 16] {
241        [
242            0xfd, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
243            0x0e, 0x0f,
244        ]
245    }
246
247    fn sample_dst() -> [u8; 16] {
248        [
249            0xfd, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
250            0x1e, 0x1f,
251        ]
252    }
253
254    // ===== Round-trip fidelity =====
255
256    #[test]
257    fn test_compress_decompress_roundtrip() {
258        let payload = vec![0xAA; 100];
259        let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
260
261        let compressed = compress_ipv6(&pkt).unwrap();
262        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
263
264        assert_eq!(decompressed, pkt);
265    }
266
267    #[test]
268    fn test_in_place_port_header_compression_matches_allocating_path() {
269        let payload = vec![0x5A; 256];
270        let pkt = build_ipv6_packet(0x24, 0x12345, 6, 32, sample_src(), sample_dst(), &payload);
271        let compressed = compress_ipv6(&pkt).unwrap();
272        let src_port = 0x0100u16;
273        let dst_port = 0x0200u16;
274        let mut expected = Vec::with_capacity(4 + compressed.len());
275        expected.extend_from_slice(&src_port.to_le_bytes());
276        expected.extend_from_slice(&dst_port.to_le_bytes());
277        expected.extend_from_slice(&compressed);
278
279        let mut in_place = pkt.clone();
280        assert!(compress_ipv6_with_port_header_in_place(
281            &mut in_place,
282            src_port,
283            dst_port
284        ));
285
286        assert_eq!(in_place, expected);
287    }
288
289    #[test]
290    fn test_packet_buffer_decompression_matches_allocating_path() {
291        let payload = vec![0x5A; 256];
292        let pkt = build_ipv6_packet(0x24, 0x12345, 6, 32, sample_src(), sample_dst(), &payload);
293        let compressed = compress_ipv6(&pkt).unwrap();
294
295        const HEADROOM: usize = 23;
296        const INNER_HEADER_LEN: usize = 6;
297        const PORT_HEADER_LEN: usize = 4;
298        let mut stored = vec![0xEE; HEADROOM];
299        stored.extend_from_slice(&[0, 0, 0, 0, 0x10, 0]);
300        stored.extend_from_slice(&0x0100u16.to_le_bytes());
301        stored.extend_from_slice(&0x0100u16.to_le_bytes());
302        stored.extend_from_slice(&compressed);
303        let mut packet = PacketBuffer::new(stored);
304        assert!(packet.trim_front(HEADROOM));
305
306        assert!(decompress_ipv6_packet_buffer(
307            &mut packet,
308            INNER_HEADER_LEN + PORT_HEADER_LEN,
309            sample_src(),
310            sample_dst()
311        ));
312
313        assert_eq!(packet.as_slice(), pkt.as_slice());
314    }
315
316    #[test]
317    fn test_roundtrip_empty_payload() {
318        let pkt = build_ipv6_packet(0, 0, 59, 1, sample_src(), sample_dst(), &[]);
319
320        let compressed = compress_ipv6(&pkt).unwrap();
321        assert_eq!(compressed.len(), 1 + IPV6_SHIM_RESIDUAL_SIZE); // format + residual only
322
323        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
324        assert_eq!(decompressed, pkt);
325    }
326
327    #[test]
328    fn test_roundtrip_large_payload() {
329        let payload = vec![0x55; 1400];
330        let pkt = build_ipv6_packet(0, 0, 6, 128, sample_src(), sample_dst(), &payload);
331
332        let compressed = compress_ipv6(&pkt).unwrap();
333        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
334
335        assert_eq!(decompressed, pkt);
336    }
337
338    // ===== Field preservation =====
339
340    #[test]
341    fn test_preserves_traffic_class() {
342        // TC = 0xAB (DSCP=0x2A, ECN=0x03)
343        let pkt = build_ipv6_packet(0xAB, 0, 17, 64, sample_src(), sample_dst(), &[1, 2, 3]);
344
345        let compressed = compress_ipv6(&pkt).unwrap();
346        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
347
348        assert_eq!(decompressed, pkt);
349        // Verify TC is in the right position
350        let tc = ((decompressed[0] & 0x0F) << 4) | (decompressed[1] >> 4);
351        assert_eq!(tc, 0xAB);
352    }
353
354    #[test]
355    fn test_preserves_flow_label() {
356        let pkt = build_ipv6_packet(0, 0xFEDCB, 17, 64, sample_src(), sample_dst(), &[1]);
357
358        let compressed = compress_ipv6(&pkt).unwrap();
359        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
360
361        assert_eq!(decompressed, pkt);
362    }
363
364    #[test]
365    fn test_preserves_tc_and_flow_label_combined() {
366        // TC=0xFF, flow_label=0xFFFFF (maximum values)
367        let pkt = build_ipv6_packet(0xFF, 0xFFFFF, 17, 64, sample_src(), sample_dst(), &[1]);
368
369        let compressed = compress_ipv6(&pkt).unwrap();
370        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
371
372        assert_eq!(decompressed, pkt);
373    }
374
375    #[test]
376    fn test_preserves_next_header_tcp() {
377        let pkt = build_ipv6_packet(0, 0, 6, 64, sample_src(), sample_dst(), &[0; 20]);
378
379        let compressed = compress_ipv6(&pkt).unwrap();
380        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
381
382        assert_eq!(decompressed[6], 6); // TCP
383    }
384
385    #[test]
386    fn test_preserves_next_header_icmpv6() {
387        let pkt = build_ipv6_packet(0, 0, 58, 255, sample_src(), sample_dst(), &[0; 8]);
388
389        let compressed = compress_ipv6(&pkt).unwrap();
390        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
391
392        assert_eq!(decompressed[6], 58); // ICMPv6
393        assert_eq!(decompressed[7], 255); // hop limit
394    }
395
396    #[test]
397    fn test_preserves_hop_limit() {
398        for hop_limit in [0, 1, 64, 128, 255] {
399            let pkt = build_ipv6_packet(0, 0, 17, hop_limit, sample_src(), sample_dst(), &[1]);
400
401            let compressed = compress_ipv6(&pkt).unwrap();
402            let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
403
404            assert_eq!(decompressed[7], hop_limit);
405        }
406    }
407
408    // ===== Payload length reconstruction =====
409
410    #[test]
411    fn test_payload_length_reconstructed() {
412        let payload = vec![0xBB; 256];
413        let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
414
415        let compressed = compress_ipv6(&pkt).unwrap();
416        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
417
418        let payload_len = u16::from_be_bytes([decompressed[4], decompressed[5]]);
419        assert_eq!(payload_len, 256);
420    }
421
422    // ===== Compression size savings =====
423
424    #[test]
425    fn test_compression_saves_bytes() {
426        let payload = vec![0; 100];
427        let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
428
429        let compressed = compress_ipv6(&pkt).unwrap();
430
431        // Original: 40 header + 100 payload = 140
432        // Compressed: 1 format + 6 residual + 100 payload = 107
433        // Savings: 33 bytes (version nibble kept in residual, so 34 - 1 = 33)
434        assert_eq!(pkt.len(), 140);
435        assert_eq!(compressed.len(), 107);
436        assert_eq!(pkt.len() - compressed.len(), 33);
437    }
438
439    // ===== Error cases =====
440
441    #[test]
442    fn test_compress_rejects_non_ipv6() {
443        let mut pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &[1]);
444        pkt[0] = 0x40; // version 4 (IPv4)
445        assert!(compress_ipv6(&pkt).is_none());
446    }
447
448    #[test]
449    fn test_compress_rejects_short_packet() {
450        assert!(compress_ipv6(&[0x60; 39]).is_none());
451        assert!(compress_ipv6(&[]).is_none());
452    }
453
454    #[test]
455    fn test_decompress_rejects_unknown_format() {
456        let mut compressed = vec![0x01]; // format 0x01 = unknown
457        compressed.extend_from_slice(&[0; IPV6_SHIM_RESIDUAL_SIZE]);
458        assert!(decompress_ipv6(&compressed, sample_src(), sample_dst()).is_none());
459    }
460
461    #[test]
462    fn test_decompress_rejects_short_payload() {
463        // Needs at least 1 (format) + 6 (residual) = 7 bytes
464        assert!(decompress_ipv6(&[0x00; 6], sample_src(), sample_dst()).is_none());
465        assert!(decompress_ipv6(&[], sample_src(), sample_dst()).is_none());
466    }
467
468    // ===== Address reconstruction =====
469
470    #[test]
471    fn test_addresses_from_context() {
472        let original_src = [
473            0xfd, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
474            0xAA, 0xAA,
475        ];
476        let original_dst = [
477            0xfd, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
478            0xBB, 0xBB,
479        ];
480        let pkt = build_ipv6_packet(0, 0, 17, 64, original_src, original_dst, &[1, 2]);
481
482        let compressed = compress_ipv6(&pkt).unwrap();
483
484        // Decompress with different addresses (simulating session context)
485        let context_src = sample_src();
486        let context_dst = sample_dst();
487        let decompressed = decompress_ipv6(&compressed, context_src, context_dst).unwrap();
488
489        // Addresses come from context, not original packet
490        assert_eq!(&decompressed[8..24], &context_src);
491        assert_eq!(&decompressed[24..40], &context_dst);
492
493        // But TC, flow label, next header, hop limit, payload match original
494        assert_eq!(&decompressed[0..4], &pkt[0..4]); // ver+TC+flow
495        assert_eq!(decompressed[6], pkt[6]); // next_header
496        assert_eq!(decompressed[7], pkt[7]); // hop_limit
497        assert_eq!(&decompressed[40..], &pkt[40..]); // payload
498    }
499}