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
19/// Compressed format byte for mesh-internal traffic.
20pub const IPV6_SHIM_FORMAT_COMPRESSED: u8 = 0x00;
21
22/// Size of the compressed residual fields (ver_tc_flow + next_header + hop_limit).
23const IPV6_SHIM_RESIDUAL_SIZE: usize = 6;
24
25/// IPv6 header size.
26const IPV6_HEADER_SIZE: usize = 40;
27
28/// Compress an IPv6 packet for the shim.
29///
30/// Strips source/destination addresses (32 bytes) and payload length (2 bytes).
31/// Preserves traffic class, flow label, next header, and hop limit as residual
32/// fields.
33///
34/// Returns `None` if the packet is not a valid IPv6 packet (too short or wrong
35/// version).
36pub fn compress_ipv6(ipv6_packet: &[u8]) -> Option<Vec<u8>> {
37    if ipv6_packet.len() < IPV6_HEADER_SIZE || ipv6_packet[0] >> 4 != 6 {
38        return None;
39    }
40
41    let upper_payload = &ipv6_packet[IPV6_HEADER_SIZE..];
42    let mut out = Vec::with_capacity(1 + IPV6_SHIM_RESIDUAL_SIZE + upper_payload.len());
43
44    // Format byte
45    out.push(IPV6_SHIM_FORMAT_COMPRESSED);
46
47    // Residual: bytes 0-3 of IPv6 header (version + TC + flow label)
48    out.extend_from_slice(&ipv6_packet[0..4]);
49
50    // Residual: next header and hop limit
51    out.push(ipv6_packet[6]); // next_header
52    out.push(ipv6_packet[7]); // hop_limit
53
54    // Upper-layer payload (everything after the 40-byte IPv6 header)
55    out.extend_from_slice(upper_payload);
56
57    Some(out)
58}
59
60/// Decompress a shim payload back to a full IPv6 packet.
61///
62/// Reconstructs the full 40-byte IPv6 header from the residual fields and
63/// session context (source/destination addresses). The payload length field
64/// is computed from the remaining data length.
65///
66/// Returns `None` if the format byte is unrecognized or the payload is too
67/// short.
68pub fn decompress_ipv6(
69    shim_payload: &[u8],
70    src_ipv6: [u8; 16],
71    dst_ipv6: [u8; 16],
72) -> Option<Vec<u8>> {
73    if shim_payload.len() < 1 + IPV6_SHIM_RESIDUAL_SIZE {
74        return None;
75    }
76
77    let format = shim_payload[0];
78    if format != IPV6_SHIM_FORMAT_COMPRESSED {
79        return None;
80    }
81
82    let residual = &shim_payload[1..1 + IPV6_SHIM_RESIDUAL_SIZE];
83    let upper_payload = &shim_payload[1 + IPV6_SHIM_RESIDUAL_SIZE..];
84    let upper_len = upper_payload.len();
85
86    let mut ipv6 = Vec::with_capacity(IPV6_HEADER_SIZE + upper_len);
87
88    // Bytes 0-3: restore version nibble to 6
89    ipv6.push((residual[0] & 0x0F) | 0x60);
90    ipv6.extend_from_slice(&residual[1..4]);
91
92    // Bytes 4-5: payload length (big-endian)
93    ipv6.extend_from_slice(&(upper_len as u16).to_be_bytes());
94
95    // Byte 6: next header
96    ipv6.push(residual[4]);
97
98    // Byte 7: hop limit
99    ipv6.push(residual[5]);
100
101    // Bytes 8-23: source address
102    ipv6.extend_from_slice(&src_ipv6);
103
104    // Bytes 24-39: destination address
105    ipv6.extend_from_slice(&dst_ipv6);
106
107    // Upper-layer payload
108    ipv6.extend_from_slice(upper_payload);
109
110    Some(ipv6)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    /// Build a minimal valid IPv6 packet with the given fields and payload.
118    fn build_ipv6_packet(
119        traffic_class: u8,
120        flow_label: u32,
121        next_header: u8,
122        hop_limit: u8,
123        src: [u8; 16],
124        dst: [u8; 16],
125        payload: &[u8],
126    ) -> Vec<u8> {
127        let mut pkt = Vec::with_capacity(IPV6_HEADER_SIZE + payload.len());
128
129        // Byte 0: version(4) | TC high nibble(4)
130        pkt.push(0x60 | (traffic_class >> 4));
131        // Byte 1: TC low nibble(4) | flow label high nibble(4)
132        pkt.push((traffic_class << 4) | ((flow_label >> 16) as u8 & 0x0F));
133        // Bytes 2-3: flow label low 16 bits
134        pkt.push((flow_label >> 8) as u8);
135        pkt.push(flow_label as u8);
136
137        // Bytes 4-5: payload length
138        pkt.extend_from_slice(&(payload.len() as u16).to_be_bytes());
139
140        // Byte 6: next header
141        pkt.push(next_header);
142
143        // Byte 7: hop limit
144        pkt.push(hop_limit);
145
146        // Bytes 8-23: source address
147        pkt.extend_from_slice(&src);
148
149        // Bytes 24-39: destination address
150        pkt.extend_from_slice(&dst);
151
152        // Payload
153        pkt.extend_from_slice(payload);
154
155        pkt
156    }
157
158    fn sample_src() -> [u8; 16] {
159        [
160            0xfd, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
161            0x0e, 0x0f,
162        ]
163    }
164
165    fn sample_dst() -> [u8; 16] {
166        [
167            0xfd, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
168            0x1e, 0x1f,
169        ]
170    }
171
172    // ===== Round-trip fidelity =====
173
174    #[test]
175    fn test_compress_decompress_roundtrip() {
176        let payload = vec![0xAA; 100];
177        let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
178
179        let compressed = compress_ipv6(&pkt).unwrap();
180        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
181
182        assert_eq!(decompressed, pkt);
183    }
184
185    #[test]
186    fn test_roundtrip_empty_payload() {
187        let pkt = build_ipv6_packet(0, 0, 59, 1, sample_src(), sample_dst(), &[]);
188
189        let compressed = compress_ipv6(&pkt).unwrap();
190        assert_eq!(compressed.len(), 1 + IPV6_SHIM_RESIDUAL_SIZE); // format + residual only
191
192        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
193        assert_eq!(decompressed, pkt);
194    }
195
196    #[test]
197    fn test_roundtrip_large_payload() {
198        let payload = vec![0x55; 1400];
199        let pkt = build_ipv6_packet(0, 0, 6, 128, sample_src(), sample_dst(), &payload);
200
201        let compressed = compress_ipv6(&pkt).unwrap();
202        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
203
204        assert_eq!(decompressed, pkt);
205    }
206
207    // ===== Field preservation =====
208
209    #[test]
210    fn test_preserves_traffic_class() {
211        // TC = 0xAB (DSCP=0x2A, ECN=0x03)
212        let pkt = build_ipv6_packet(0xAB, 0, 17, 64, sample_src(), sample_dst(), &[1, 2, 3]);
213
214        let compressed = compress_ipv6(&pkt).unwrap();
215        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
216
217        assert_eq!(decompressed, pkt);
218        // Verify TC is in the right position
219        let tc = ((decompressed[0] & 0x0F) << 4) | (decompressed[1] >> 4);
220        assert_eq!(tc, 0xAB);
221    }
222
223    #[test]
224    fn test_preserves_flow_label() {
225        let pkt = build_ipv6_packet(0, 0xFEDCB, 17, 64, sample_src(), sample_dst(), &[1]);
226
227        let compressed = compress_ipv6(&pkt).unwrap();
228        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
229
230        assert_eq!(decompressed, pkt);
231    }
232
233    #[test]
234    fn test_preserves_tc_and_flow_label_combined() {
235        // TC=0xFF, flow_label=0xFFFFF (maximum values)
236        let pkt = build_ipv6_packet(0xFF, 0xFFFFF, 17, 64, sample_src(), sample_dst(), &[1]);
237
238        let compressed = compress_ipv6(&pkt).unwrap();
239        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
240
241        assert_eq!(decompressed, pkt);
242    }
243
244    #[test]
245    fn test_preserves_next_header_tcp() {
246        let pkt = build_ipv6_packet(0, 0, 6, 64, sample_src(), sample_dst(), &[0; 20]);
247
248        let compressed = compress_ipv6(&pkt).unwrap();
249        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
250
251        assert_eq!(decompressed[6], 6); // TCP
252    }
253
254    #[test]
255    fn test_preserves_next_header_icmpv6() {
256        let pkt = build_ipv6_packet(0, 0, 58, 255, sample_src(), sample_dst(), &[0; 8]);
257
258        let compressed = compress_ipv6(&pkt).unwrap();
259        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
260
261        assert_eq!(decompressed[6], 58); // ICMPv6
262        assert_eq!(decompressed[7], 255); // hop limit
263    }
264
265    #[test]
266    fn test_preserves_hop_limit() {
267        for hop_limit in [0, 1, 64, 128, 255] {
268            let pkt = build_ipv6_packet(0, 0, 17, hop_limit, sample_src(), sample_dst(), &[1]);
269
270            let compressed = compress_ipv6(&pkt).unwrap();
271            let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
272
273            assert_eq!(decompressed[7], hop_limit);
274        }
275    }
276
277    // ===== Payload length reconstruction =====
278
279    #[test]
280    fn test_payload_length_reconstructed() {
281        let payload = vec![0xBB; 256];
282        let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
283
284        let compressed = compress_ipv6(&pkt).unwrap();
285        let decompressed = decompress_ipv6(&compressed, sample_src(), sample_dst()).unwrap();
286
287        let payload_len = u16::from_be_bytes([decompressed[4], decompressed[5]]);
288        assert_eq!(payload_len, 256);
289    }
290
291    // ===== Compression size savings =====
292
293    #[test]
294    fn test_compression_saves_bytes() {
295        let payload = vec![0; 100];
296        let pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &payload);
297
298        let compressed = compress_ipv6(&pkt).unwrap();
299
300        // Original: 40 header + 100 payload = 140
301        // Compressed: 1 format + 6 residual + 100 payload = 107
302        // Savings: 33 bytes (version nibble kept in residual, so 34 - 1 = 33)
303        assert_eq!(pkt.len(), 140);
304        assert_eq!(compressed.len(), 107);
305        assert_eq!(pkt.len() - compressed.len(), 33);
306    }
307
308    // ===== Error cases =====
309
310    #[test]
311    fn test_compress_rejects_non_ipv6() {
312        let mut pkt = build_ipv6_packet(0, 0, 17, 64, sample_src(), sample_dst(), &[1]);
313        pkt[0] = 0x40; // version 4 (IPv4)
314        assert!(compress_ipv6(&pkt).is_none());
315    }
316
317    #[test]
318    fn test_compress_rejects_short_packet() {
319        assert!(compress_ipv6(&[0x60; 39]).is_none());
320        assert!(compress_ipv6(&[]).is_none());
321    }
322
323    #[test]
324    fn test_decompress_rejects_unknown_format() {
325        let mut compressed = vec![0x01]; // format 0x01 = unknown
326        compressed.extend_from_slice(&[0; IPV6_SHIM_RESIDUAL_SIZE]);
327        assert!(decompress_ipv6(&compressed, sample_src(), sample_dst()).is_none());
328    }
329
330    #[test]
331    fn test_decompress_rejects_short_payload() {
332        // Needs at least 1 (format) + 6 (residual) = 7 bytes
333        assert!(decompress_ipv6(&[0x00; 6], sample_src(), sample_dst()).is_none());
334        assert!(decompress_ipv6(&[], sample_src(), sample_dst()).is_none());
335    }
336
337    // ===== Address reconstruction =====
338
339    #[test]
340    fn test_addresses_from_context() {
341        let original_src = [
342            0xfd, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
343            0xAA, 0xAA,
344        ];
345        let original_dst = [
346            0xfd, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
347            0xBB, 0xBB,
348        ];
349        let pkt = build_ipv6_packet(0, 0, 17, 64, original_src, original_dst, &[1, 2]);
350
351        let compressed = compress_ipv6(&pkt).unwrap();
352
353        // Decompress with different addresses (simulating session context)
354        let context_src = sample_src();
355        let context_dst = sample_dst();
356        let decompressed = decompress_ipv6(&compressed, context_src, context_dst).unwrap();
357
358        // Addresses come from context, not original packet
359        assert_eq!(&decompressed[8..24], &context_src);
360        assert_eq!(&decompressed[24..40], &context_dst);
361
362        // But TC, flow label, next header, hop limit, payload match original
363        assert_eq!(&decompressed[0..4], &pkt[0..4]); // ver+TC+flow
364        assert_eq!(decompressed[6], pkt[6]); // next_header
365        assert_eq!(decompressed[7], pkt[7]); // hop_limit
366        assert_eq!(&decompressed[40..], &pkt[40..]); // payload
367    }
368}