Skip to main content

pcap_toolkit/
transform.rs

1//! Phase 4: Packet-level transformations applied during the sort second pass.
2//!
3//! Transformations are applied in this order for each packet:
4//! 1. **IP address mapping** — replace IP addresses in-place or re-frame the
5//!    packet when the mapping is cross-family (IPv4↔IPv6).
6//! 2. **Payload truncation** — shorten the payload and update length fields.
7//! 3. **Checksum recalculation** — recompute Layer 3 (IP) and Layer 4 (TCP/UDP)
8//!    checksums whenever the above steps modified the packet bytes.
9//!
10//! Timestamp shifting is computed once before the loop and applied per-packet
11//! without touching the byte payload.
12//!
13//! Only Ethernet frames (link type 1) carrying IPv4 or IPv6 are supported.
14//! A single 802.1Q VLAN tag is transparently handled. Other link types and
15//! encapsulations are passed through untouched.
16//!
17//! ## Cross-family IP mapping
18//!
19//! When a mapping changes the IP version of an address (e.g. `10.0.0.1=::1`),
20//! the entire Ethernet payload must be re-framed:
21//!
22//! - **IPv4 → IPv6**: The packet gains a 40-byte IPv6 header (vs. the original
23//!   ≥20-byte IPv4 header). Unmapped IPv4 addresses are embedded as
24//!   IPv4-mapped IPv6 addresses (`::ffff:a.b.c.d`).
25//! - **IPv6 → IPv4**: The packet shrinks by 20 bytes (IPv6 40 B → IPv4 20 B).
26//!   Unmapped IPv6 addresses must be IPv4-mapped (`::ffff:a.b.c.d`); if they
27//!   are not, the re-frame is skipped and the packet is passed through
28//!   unchanged.
29//!
30//! After re-framing, all checksums are recalculated using the new pseudo-header.
31
32use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
33
34use thiserror::Error;
35
36// ── EtherType / protocol constants ───────────────────────────────────────────
37
38const ET_IPV4: u16 = 0x0800;
39const ET_IPV6: u16 = 0x86DD;
40const ET_VLAN: u16 = 0x8100;
41
42const PROTO_TCP: u8 = 6;
43const PROTO_UDP: u8 = 17;
44
45// ── Public types ─────────────────────────────────────────────────────────────
46
47/// Errors produced during transform option parsing.
48#[derive(Debug, Error)]
49pub enum TransformError {
50    #[error("invalid IP mapping '{0}': expected OLD_IP=NEW_IP")]
51    InvalidMapping(String),
52}
53
54/// One IP address replacement rule.
55#[derive(Debug, Clone)]
56pub struct IpMapping {
57    /// Address to search for in packet headers.
58    pub old: IpAddr,
59    /// Address to write in place of `old`.
60    pub new: IpAddr,
61}
62
63/// A per-protocol payload truncation rule.
64///
65/// When the packet's IP protocol matches `proto`, the payload is truncated to
66/// at most `max_payload_bytes` bytes. Per-protocol rules take precedence over
67/// the global [`TransformOptions::max_payload_bytes`] limit.
68#[derive(Debug, Clone)]
69pub struct ProtocolTruncation {
70    /// IP protocol number (e.g. `6` = TCP, `17` = UDP).
71    pub proto: u8,
72    /// Maximum payload bytes to keep for this protocol.
73    pub max_payload_bytes: u32,
74}
75
76/// All packet-level transformation options for a sort run.
77#[derive(Debug, Default, Clone)]
78pub struct TransformOptions {
79    /// Truncate the payload of each packet to at most this many bytes.
80    /// Ethernet, IP, and transport headers are always preserved.
81    ///
82    /// Overridden per-protocol by [`proto_truncation`](Self::proto_truncation).
83    pub max_payload_bytes: Option<u32>,
84    /// When set, all timestamps are shifted so the first sorted packet starts
85    /// at exactly this nanosecond epoch value.
86    ///
87    /// The delta is computed once in [`second_pass`] and passed as `ts_delta`.
88    pub timestamp_start_ns: Option<u64>,
89    /// IP address replacement rules, evaluated in order for every packet.
90    pub ip_map: Vec<IpMapping>,
91    /// Per-protocol payload truncation rules (TOML: `[[transform.truncate_by_proto]]`).
92    ///
93    /// The first rule whose `proto` matches the packet's IP protocol is used.
94    /// Falls back to [`max_payload_bytes`](Self::max_payload_bytes) when no
95    /// rule matches.
96    pub proto_truncation: Vec<ProtocolTruncation>,
97}
98
99impl TransformOptions {
100    /// Returns `true` when no transformations are configured.
101    pub fn is_empty(&self) -> bool {
102        self.max_payload_bytes.is_none()
103            && self.timestamp_start_ns.is_none()
104            && self.ip_map.is_empty()
105            && self.proto_truncation.is_empty()
106    }
107}
108
109// ── Public helpers ────────────────────────────────────────────────────────────
110
111/// Parse `"OLD_IP=NEW_IP"` into an [`IpMapping`].
112///
113/// Both sides must be valid IP addresses. Cross-family mappings (IPv4↔IPv6)
114/// are supported and will cause the entire Ethernet payload to be re-framed
115/// when applied; see the module-level documentation for details.
116///
117/// # Errors
118/// [`TransformError::InvalidMapping`] — format wrong or address unparsable.
119pub fn parse_ip_mapping(s: &str) -> Result<IpMapping, TransformError> {
120    let (old_s, new_s) = s
121        .split_once('=')
122        .ok_or_else(|| TransformError::InvalidMapping(s.to_owned()))?;
123
124    let old: IpAddr = old_s
125        .trim()
126        .parse()
127        .map_err(|_| TransformError::InvalidMapping(s.to_owned()))?;
128    let new: IpAddr = new_s
129        .trim()
130        .parse()
131        .map_err(|_| TransformError::InvalidMapping(s.to_owned()))?;
132
133    Ok(IpMapping { old, new })
134}
135
136// ── Main entry point ──────────────────────────────────────────────────────────
137
138/// Apply all configured transformations to one captured packet.
139///
140/// Mutates `data` in place and returns `(new_timestamp_ns, new_caplen, new_origlen)`.
141///
142/// - `ts_delta` — pre-computed nanosecond shift; `0` disables timestamp shifting.
143/// - `origlen` — original wire length from the PCAP record header.
144pub fn apply(
145    data: &mut Vec<u8>,
146    timestamp_ns: u64,
147    ts_delta: i64,
148    origlen: u32,
149    opts: &TransformOptions,
150) -> (u64, u32, u32) {
151    let new_ts = if ts_delta != 0 {
152        (timestamp_ns as i64).saturating_add(ts_delta).max(0) as u64
153    } else {
154        timestamp_ns
155    };
156
157    let (ip_changed, reframed) = if !opts.ip_map.is_empty() {
158        apply_ip_map(data, &opts.ip_map)
159    } else {
160        (false, false)
161    };
162
163    // After a cross-family re-frame the packet length changes, so origlen must
164    // be updated to the new frame size before the truncation step.
165    let origlen = if reframed { data.len() as u32 } else { origlen };
166
167    let (new_origlen, truncated) = match effective_truncation_limit(data, opts) {
168        Some(max_bytes) => do_truncate(data, max_bytes, origlen),
169        None => (origlen, false),
170    };
171
172    if ip_changed || truncated {
173        recalculate_checksums(data);
174    }
175
176    let new_caplen = data.len() as u32;
177    (new_ts, new_caplen, new_origlen)
178}
179
180// ── Truncation limit resolution ───────────────────────────────────────────────
181
182/// Return the IP protocol number from an Ethernet frame, if parseable.
183fn detect_protocol(data: &[u8]) -> Option<u8> {
184    let (ip_off, et) = find_ip(data)?;
185    match et {
186        ET_IPV4 => {
187            if data.len() >= ip_off + 20 {
188                Some(data[ip_off + 9])
189            } else {
190                None
191            }
192        }
193        ET_IPV6 => {
194            if data.len() >= ip_off + 40 {
195                Some(data[ip_off + 6])
196            } else {
197                None
198            }
199        }
200        _ => None,
201    }
202}
203
204/// Determine the effective payload truncation limit for one packet.
205///
206/// Per-protocol rules are checked first; the first matching rule wins.
207/// Falls back to the global `max_payload_bytes` when no rule matches.
208fn effective_truncation_limit(data: &[u8], opts: &TransformOptions) -> Option<u32> {
209    if !opts.proto_truncation.is_empty()
210        && let Some(proto) = detect_protocol(data)
211    {
212        for rule in &opts.proto_truncation {
213            if rule.proto == proto {
214                return Some(rule.max_payload_bytes);
215            }
216        }
217    }
218    opts.max_payload_bytes
219}
220
221// ── Frame navigation ──────────────────────────────────────────────────────────
222
223/// Return `(ip_header_offset, ethertype)` for IPv4/IPv6 frames.
224///
225/// Handles a single 802.1Q VLAN tag. Returns `None` for non-IP frames or
226/// frames that are too short to parse.
227fn find_ip(data: &[u8]) -> Option<(usize, u16)> {
228    if data.len() < 14 {
229        return None;
230    }
231    let et = u16::from_be_bytes([data[12], data[13]]);
232    if et == ET_VLAN {
233        if data.len() < 18 {
234            return None;
235        }
236        let inner = u16::from_be_bytes([data[16], data[17]]);
237        if inner == ET_IPV4 || inner == ET_IPV6 {
238            Some((18, inner))
239        } else {
240            None
241        }
242    } else if et == ET_IPV4 || et == ET_IPV6 {
243        Some((14, et))
244    } else {
245        None
246    }
247}
248
249// ── IP address mapping ────────────────────────────────────────────────────────
250
251/// Replace IP addresses in the packet's IP header.
252///
253/// Returns `(changed, reframed)`:
254/// - `changed` — `true` if at least one address was modified.
255/// - `reframed` — `true` if the packet was re-framed to a different IP version
256///   (origlen must be updated by the caller).
257fn apply_ip_map(data: &mut Vec<u8>, mappings: &[IpMapping]) -> (bool, bool) {
258    let Some((ip_off, et)) = find_ip(data) else {
259        return (false, false);
260    };
261
262    // Fast path: if no cross-family mappings exist, skip reframe logic.
263    let has_cross = mappings.iter().any(|m| {
264        matches!(
265            (&m.old, &m.new),
266            (IpAddr::V4(_), IpAddr::V6(_)) | (IpAddr::V6(_), IpAddr::V4(_))
267        )
268    });
269
270    if has_cross {
271        if et == ET_IPV4 && needs_v4_to_v6_reframe(data, ip_off, mappings) {
272            if let Some(new_data) = reframe_ipv4_to_ipv6(data, ip_off, mappings) {
273                *data = new_data;
274                return (true, true);
275            }
276        } else if et == ET_IPV6
277            && needs_v6_to_v4_reframe(data, ip_off, mappings)
278            && let Some(new_data) = reframe_ipv6_to_ipv4(data, ip_off, mappings)
279        {
280            *data = new_data;
281            return (true, true);
282        }
283    }
284
285    // Same-family in-place replacements.
286    let mut changed = false;
287
288    if et == ET_IPV4 {
289        if data.len() < ip_off + 20 {
290            return (false, false);
291        }
292        for m in mappings {
293            if let (IpAddr::V4(old_v4), IpAddr::V4(new_v4)) = (&m.old, &m.new) {
294                let old_b = old_v4.octets();
295                let new_b = new_v4.octets();
296                // src IP: bytes 12–16 of the IPv4 header.
297                if data[ip_off + 12..ip_off + 16] == old_b {
298                    data[ip_off + 12..ip_off + 16].copy_from_slice(&new_b);
299                    changed = true;
300                }
301                // dst IP: bytes 16–20 of the IPv4 header.
302                if data[ip_off + 16..ip_off + 20] == old_b {
303                    data[ip_off + 16..ip_off + 20].copy_from_slice(&new_b);
304                    changed = true;
305                }
306            }
307        }
308    } else if et == ET_IPV6 {
309        if data.len() < ip_off + 40 {
310            return (false, false);
311        }
312        for m in mappings {
313            if let (IpAddr::V6(old_v6), IpAddr::V6(new_v6)) = (&m.old, &m.new) {
314                let old_b = old_v6.octets();
315                let new_b = new_v6.octets();
316                // src IP: bytes 8–24 of the IPv6 header.
317                if data[ip_off + 8..ip_off + 24] == old_b {
318                    data[ip_off + 8..ip_off + 24].copy_from_slice(&new_b);
319                    changed = true;
320                }
321                // dst IP: bytes 24–40 of the IPv6 header.
322                if data[ip_off + 24..ip_off + 40] == old_b {
323                    data[ip_off + 24..ip_off + 40].copy_from_slice(&new_b);
324                    changed = true;
325                }
326            }
327        }
328    }
329
330    (changed, false)
331}
332
333// ── Cross-family reframing ────────────────────────────────────────────────────
334
335/// Return `true` if the IPv4 packet's src or dst matches a v4→v6 mapping.
336fn needs_v4_to_v6_reframe(data: &[u8], ip_off: usize, mappings: &[IpMapping]) -> bool {
337    if data.len() < ip_off + 20 {
338        return false;
339    }
340    let src = &data[ip_off + 12..ip_off + 16];
341    let dst = &data[ip_off + 16..ip_off + 20];
342    mappings.iter().any(|m| {
343        if let (IpAddr::V4(old), IpAddr::V6(_)) = (&m.old, &m.new) {
344            let b = old.octets();
345            src == b || dst == b
346        } else {
347            false
348        }
349    })
350}
351
352/// Return `true` if the IPv6 packet's src or dst matches a v6→v4 mapping.
353fn needs_v6_to_v4_reframe(data: &[u8], ip_off: usize, mappings: &[IpMapping]) -> bool {
354    if data.len() < ip_off + 40 {
355        return false;
356    }
357    let src = &data[ip_off + 8..ip_off + 24];
358    let dst = &data[ip_off + 24..ip_off + 40];
359    mappings.iter().any(|m| {
360        if let (IpAddr::V6(old), IpAddr::V4(_)) = (&m.old, &m.new) {
361            let b = old.octets();
362            src == b || dst == b
363        } else {
364            false
365        }
366    })
367}
368
369/// Re-frame an IPv4 Ethernet packet as IPv6.
370///
371/// Applies v4→v6 (and v4→v4) mappings to both addresses. Unmapped IPv4
372/// addresses are embedded as `::ffff:a.b.c.d`.
373///
374/// Returns `None` if the packet is malformed.
375fn reframe_ipv4_to_ipv6(data: &[u8], ip_off: usize, mappings: &[IpMapping]) -> Option<Vec<u8>> {
376    if data.len() < ip_off + 20 {
377        return None;
378    }
379    let ihl = ((data[ip_off] & 0x0F) * 4) as usize;
380    if data.len() < ip_off + ihl {
381        return None;
382    }
383
384    let ttl = data[ip_off + 8];
385    let proto = data[ip_off + 9];
386    let src_v4 = Ipv4Addr::from(<[u8; 4]>::try_from(&data[ip_off + 12..ip_off + 16]).ok()?);
387    let dst_v4 = Ipv4Addr::from(<[u8; 4]>::try_from(&data[ip_off + 16..ip_off + 20]).ok()?);
388
389    let src_v6 = resolve_ipv4_to_ipv6(src_v4, mappings);
390    let dst_v6 = resolve_ipv4_to_ipv6(dst_v4, mappings);
391
392    let transport = &data[ip_off + ihl..];
393    let payload_len = transport.len() as u16;
394
395    let mut out = Vec::with_capacity(ip_off + 40 + transport.len());
396    write_ethernet_preamble(&mut out, data, ip_off, ET_IPV6);
397
398    // IPv6 fixed header (40 bytes).
399    out.extend_from_slice(&[0x60, 0x00, 0x00, 0x00]); // version=6, TC=0, flow=0
400    out.extend_from_slice(&payload_len.to_be_bytes());
401    out.push(proto); // next header
402    out.push(ttl); // hop limit
403    out.extend_from_slice(&src_v6);
404    out.extend_from_slice(&dst_v6);
405
406    // Transport + payload verbatim; checksums recalculated by caller.
407    out.extend_from_slice(transport);
408
409    Some(out)
410}
411
412/// Re-frame an IPv6 Ethernet packet as IPv4.
413///
414/// Applies v6→v4 (and v6→v6) mappings to both addresses. Unmapped IPv6
415/// addresses must be IPv4-mapped (`::ffff:a.b.c.d`); if they are not, the
416/// re-frame is skipped and `None` is returned.
417///
418/// Returns `None` if the packet is malformed or cannot be converted cleanly.
419fn reframe_ipv6_to_ipv4(data: &[u8], ip_off: usize, mappings: &[IpMapping]) -> Option<Vec<u8>> {
420    if data.len() < ip_off + 40 {
421        return None;
422    }
423
424    let hop_limit = data[ip_off + 7]; // → TTL
425    let proto = data[ip_off + 6]; // next header → protocol
426    let src_v6 = Ipv6Addr::from(<[u8; 16]>::try_from(&data[ip_off + 8..ip_off + 24]).ok()?);
427    let dst_v6 = Ipv6Addr::from(<[u8; 16]>::try_from(&data[ip_off + 24..ip_off + 40]).ok()?);
428
429    // Both addresses must resolve to IPv4; otherwise skip.
430    let src_v4 = resolve_ipv6_to_ipv4(src_v6, mappings)?;
431    let dst_v4 = resolve_ipv6_to_ipv4(dst_v6, mappings)?;
432
433    let transport = &data[ip_off + 40..];
434    let ip_total = (20u16).saturating_add(transport.len() as u16);
435
436    let mut out = Vec::with_capacity(ip_off + 20 + transport.len());
437    write_ethernet_preamble(&mut out, data, ip_off, ET_IPV4);
438
439    // IPv4 header (20 bytes, no options).
440    out.push(0x45); // version=4, IHL=5
441    out.push(0x00); // DSCP/ECN
442    out.extend_from_slice(&ip_total.to_be_bytes());
443    out.extend_from_slice(&[0x00, 0x00]); // identification
444    out.extend_from_slice(&[0x40, 0x00]); // DF flag, fragment offset=0
445    out.push(hop_limit); // TTL
446    out.push(proto); // protocol
447    out.extend_from_slice(&[0x00, 0x00]); // checksum — recalculated by caller
448    out.extend_from_slice(&src_v4);
449    out.extend_from_slice(&dst_v4);
450
451    // Transport + payload verbatim; checksums recalculated by caller.
452    out.extend_from_slice(transport);
453
454    Some(out)
455}
456
457/// Write the Ethernet preamble (MACs + EtherType, with optional VLAN tag) into `out`.
458///
459/// `ip_off` is 14 for untagged frames and 18 for 802.1Q-tagged frames.
460fn write_ethernet_preamble(out: &mut Vec<u8>, src: &[u8], ip_off: usize, ethertype: u16) {
461    out.extend_from_slice(&src[0..12]); // dst MAC + src MAC
462    if ip_off == 18 {
463        out.extend_from_slice(&src[12..16]); // 0x8100 + VLAN TCI
464        out.extend_from_slice(&ethertype.to_be_bytes()); // inner EtherType
465    } else {
466        out.extend_from_slice(&ethertype.to_be_bytes());
467    }
468}
469
470/// Resolve an IPv4 address to its IPv6 representation after applying mappings.
471///
472/// Priority:
473/// 1. A matching v4→v6 mapping → use the mapped IPv6 address directly.
474/// 2. A matching v4→v4 mapping → embed the new IPv4 as `::ffff:new_v4`.
475/// 3. No mapping → embed the original as `::ffff:addr`.
476fn resolve_ipv4_to_ipv6(addr: Ipv4Addr, mappings: &[IpMapping]) -> [u8; 16] {
477    for m in mappings {
478        match (&m.old, &m.new) {
479            (IpAddr::V4(old), IpAddr::V6(new)) if *old == addr => return new.octets(),
480            (IpAddr::V4(old), IpAddr::V4(new)) if *old == addr => {
481                return ipv4_mapped_to_ipv6(new.octets());
482            }
483            _ => {}
484        }
485    }
486    ipv4_mapped_to_ipv6(addr.octets())
487}
488
489/// Resolve an IPv6 address to its IPv4 representation after applying mappings.
490///
491/// Priority:
492/// 1. A matching v6→v4 mapping → use the mapped IPv4 address directly.
493/// 2. A matching v6→v6 mapping → try to extract IPv4 from an IPv4-mapped result.
494/// 3. No mapping → try to extract IPv4 from an IPv4-mapped address (`::ffff:…`).
495///
496/// Returns `None` if the address cannot be expressed as IPv4.
497fn resolve_ipv6_to_ipv4(addr: Ipv6Addr, mappings: &[IpMapping]) -> Option<[u8; 4]> {
498    for m in mappings {
499        match (&m.old, &m.new) {
500            (IpAddr::V6(old), IpAddr::V4(new)) if *old == addr => return Some(new.octets()),
501            (IpAddr::V6(old), IpAddr::V6(new)) if *old == addr => {
502                return new.to_ipv4_mapped().map(|v4| v4.octets());
503            }
504            _ => {}
505        }
506    }
507    addr.to_ipv4_mapped().map(|v4| v4.octets())
508}
509
510/// Embed an IPv4 address as an IPv4-mapped IPv6 address (`::ffff:a.b.c.d`).
511fn ipv4_mapped_to_ipv6(v4: [u8; 4]) -> [u8; 16] {
512    let mut v6 = [0u8; 16];
513    v6[10] = 0xFF;
514    v6[11] = 0xFF;
515    v6[12..16].copy_from_slice(&v4);
516    v6
517}
518
519// ── Payload truncation ────────────────────────────────────────────────────────
520
521/// Truncate the packet payload to `max_bytes` and update IP/UDP length fields.
522///
523/// Returns `(new_origlen, did_truncate)`.
524fn do_truncate(data: &mut Vec<u8>, max_bytes: u32, origlen: u32) -> (u32, bool) {
525    let Some((ip_off, et)) = find_ip(data) else {
526        return (origlen, false);
527    };
528
529    let (proto, transport_start, transport_hdr_len) = match et {
530        ET_IPV4 => {
531            if data.len() < ip_off + 20 {
532                return (origlen, false);
533            }
534            let ihl = ((data[ip_off] & 0x0F) * 4) as usize;
535            if data.len() < ip_off + ihl {
536                return (origlen, false);
537            }
538            let proto = data[ip_off + 9];
539            let ts = ip_off + ihl;
540            let th = match proto {
541                PROTO_TCP => {
542                    if data.len() < ts + 20 {
543                        return (origlen, false);
544                    }
545                    let th = ((data[ts + 12] >> 4) * 4) as usize;
546                    if th < 20 {
547                        return (origlen, false);
548                    }
549                    th
550                }
551                PROTO_UDP => 8,
552                _ => return (origlen, false),
553            };
554            (proto, ts, th)
555        }
556        ET_IPV6 => {
557            if data.len() < ip_off + 40 {
558                return (origlen, false);
559            }
560            let proto = data[ip_off + 6]; // next header
561            let ts = ip_off + 40;
562            let th = match proto {
563                PROTO_TCP => {
564                    if data.len() < ts + 20 {
565                        return (origlen, false);
566                    }
567                    let th = ((data[ts + 12] >> 4) * 4) as usize;
568                    if th < 20 {
569                        return (origlen, false);
570                    }
571                    th
572                }
573                PROTO_UDP => 8,
574                _ => return (origlen, false),
575            };
576            (proto, ts, th)
577        }
578        _ => return (origlen, false),
579    };
580
581    let payload_start = transport_start + transport_hdr_len;
582    let max_total = payload_start + max_bytes as usize;
583
584    if data.len() <= max_total {
585        return (origlen, false); // already short enough
586    }
587
588    data.truncate(max_total);
589    let new_len = data.len();
590
591    // Update IP total length / payload length.
592    if et == ET_IPV4 {
593        let new_ip_total = (new_len - ip_off) as u16;
594        data[ip_off + 2..ip_off + 4].copy_from_slice(&new_ip_total.to_be_bytes());
595    } else {
596        // IPv6 payload length = everything after the 40-byte fixed header.
597        let new_plen = (new_len - ip_off - 40) as u16;
598        data[ip_off + 4..ip_off + 6].copy_from_slice(&new_plen.to_be_bytes());
599    }
600
601    // Update UDP length. TCP has no explicit payload length field.
602    if proto == PROTO_UDP {
603        let new_udp_len = (new_len - transport_start) as u16;
604        data[transport_start + 4..transport_start + 6].copy_from_slice(&new_udp_len.to_be_bytes());
605    }
606
607    (new_len as u32, true)
608}
609
610// ── Checksum recalculation ────────────────────────────────────────────────────
611
612/// Recompute all checksums in `data` (IPv4 header + TCP/UDP transport).
613fn recalculate_checksums(data: &mut [u8]) {
614    let Some((ip_off, et)) = find_ip(data) else {
615        return;
616    };
617    match et {
618        ET_IPV4 => recalc_ipv4(data, ip_off),
619        ET_IPV6 => recalc_ipv6(data, ip_off),
620        _ => {}
621    }
622}
623
624fn recalc_ipv4(data: &mut [u8], ip_off: usize) {
625    if data.len() < ip_off + 20 {
626        return;
627    }
628    let ihl = ((data[ip_off] & 0x0F) * 4) as usize;
629    if data.len() < ip_off + ihl {
630        return;
631    }
632
633    // Recompute IPv4 header checksum (field at offset 10–12 within the header).
634    data[ip_off + 10] = 0;
635    data[ip_off + 11] = 0;
636    let csum = internet_checksum(&data[ip_off..ip_off + ihl]);
637    data[ip_off + 10..ip_off + 12].copy_from_slice(&csum.to_be_bytes());
638
639    let proto = data[ip_off + 9];
640    let ts = ip_off + ihl;
641    if proto == PROTO_TCP || proto == PROTO_UDP {
642        recalc_transport_v4(data, ip_off, ts, proto);
643    }
644}
645
646fn recalc_transport_v4(data: &mut [u8], ip_off: usize, ts: usize, proto: u8) {
647    let csum_off = if proto == PROTO_TCP { ts + 16 } else { ts + 6 };
648    if data.len() < csum_off + 2 {
649        return;
650    }
651    let src: [u8; 4] = data[ip_off + 12..ip_off + 16].try_into().unwrap();
652    let dst: [u8; 4] = data[ip_off + 16..ip_off + 20].try_into().unwrap();
653    data[csum_off] = 0;
654    data[csum_off + 1] = 0;
655    let csum = transport_checksum_v4(src, dst, proto, &data[ts..]);
656    data[csum_off..csum_off + 2].copy_from_slice(&csum.to_be_bytes());
657}
658
659fn recalc_ipv6(data: &mut [u8], ip_off: usize) {
660    if data.len() < ip_off + 40 {
661        return;
662    }
663    let proto = data[ip_off + 6]; // next header
664    let ts = ip_off + 40;
665    if proto == PROTO_TCP || proto == PROTO_UDP {
666        recalc_transport_v6(data, ip_off, ts, proto);
667    }
668}
669
670fn recalc_transport_v6(data: &mut [u8], ip_off: usize, ts: usize, proto: u8) {
671    let csum_off = if proto == PROTO_TCP { ts + 16 } else { ts + 6 };
672    if data.len() < csum_off + 2 {
673        return;
674    }
675    let src: [u8; 16] = data[ip_off + 8..ip_off + 24].try_into().unwrap();
676    let dst: [u8; 16] = data[ip_off + 24..ip_off + 40].try_into().unwrap();
677    data[csum_off] = 0;
678    data[csum_off + 1] = 0;
679    let csum = transport_checksum_v6(src, dst, proto, &data[ts..]);
680    data[csum_off..csum_off + 2].copy_from_slice(&csum.to_be_bytes());
681}
682
683// ── Checksum math ─────────────────────────────────────────────────────────────
684
685/// Standard Internet checksum (RFC 1071): one's complement of the 16-bit ones-complement sum.
686fn internet_checksum(data: &[u8]) -> u16 {
687    let mut sum: u32 = 0;
688    let mut iter = data.chunks_exact(2);
689    for chunk in &mut iter {
690        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
691    }
692    if let [byte] = iter.remainder() {
693        sum += (*byte as u32) << 8;
694    }
695    while sum >> 16 != 0 {
696        sum = (sum & 0xFFFF) + (sum >> 16);
697    }
698    !(sum as u16)
699}
700
701/// Transport-layer checksum using an IPv4 pseudo-header.
702fn transport_checksum_v4(src: [u8; 4], dst: [u8; 4], proto: u8, segment: &[u8]) -> u16 {
703    let len = segment.len() as u32;
704    let mut sum: u32 = 0;
705    // Pseudo-header: src IP, dst IP, zero, proto, segment length.
706    sum += u16::from_be_bytes([src[0], src[1]]) as u32;
707    sum += u16::from_be_bytes([src[2], src[3]]) as u32;
708    sum += u16::from_be_bytes([dst[0], dst[1]]) as u32;
709    sum += u16::from_be_bytes([dst[2], dst[3]]) as u32;
710    sum += proto as u32;
711    sum += len & 0xFFFF;
712    let mut iter = segment.chunks_exact(2);
713    for chunk in &mut iter {
714        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
715    }
716    if let [byte] = iter.remainder() {
717        sum += (*byte as u32) << 8;
718    }
719    while sum >> 16 != 0 {
720        sum = (sum & 0xFFFF) + (sum >> 16);
721    }
722    !(sum as u16)
723}
724
725/// Transport-layer checksum using an IPv6 pseudo-header.
726fn transport_checksum_v6(src: [u8; 16], dst: [u8; 16], proto: u8, segment: &[u8]) -> u16 {
727    let len = segment.len() as u32;
728    let mut sum: u32 = 0;
729    // src and dst addresses (16 bytes each = 8 words each).
730    for i in (0..16).step_by(2) {
731        sum += u16::from_be_bytes([src[i], src[i + 1]]) as u32;
732    }
733    for i in (0..16).step_by(2) {
734        sum += u16::from_be_bytes([dst[i], dst[i + 1]]) as u32;
735    }
736    // Upper-layer packet length (4 bytes, big-endian).
737    sum += len >> 16;
738    sum += len & 0xFFFF;
739    // Next header (3 zero bytes + 1 byte proto).
740    sum += proto as u32;
741    let mut iter = segment.chunks_exact(2);
742    for chunk in &mut iter {
743        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
744    }
745    if let [byte] = iter.remainder() {
746        sum += (*byte as u32) << 8;
747    }
748    while sum >> 16 != 0 {
749        sum = (sum & 0xFFFF) + (sum >> 16);
750    }
751    !(sum as u16)
752}
753
754// ── Unit tests ────────────────────────────────────────────────────────────────
755
756#[cfg(test)]
757mod tests {
758    use super::*;
759
760    // ── Frame builders ────────────────────────────────────────────────────────
761
762    fn eth_ipv4_udp(src: [u8; 4], dst: [u8; 4], sport: u16, dport: u16, payload: &[u8]) -> Vec<u8> {
763        let udp_len = (8 + payload.len()) as u16;
764        let ip_total = 20 + udp_len;
765        let mut f = Vec::new();
766        f.extend_from_slice(&[0xFF; 6]); // dst MAC
767        f.extend_from_slice(&[0x00; 6]); // src MAC
768        f.extend_from_slice(&[0x08, 0x00]); // IPv4
769        f.push(0x45); // version=4, IHL=5
770        f.push(0x00); // DSCP/ECN
771        f.extend_from_slice(&ip_total.to_be_bytes());
772        f.extend_from_slice(&[0x00, 0x01, 0x00, 0x00]); // ID, flags/frag
773        f.push(64);
774        f.push(17); // TTL, UDP
775        f.extend_from_slice(&[0x00, 0x00]); // header checksum (zeroed)
776        f.extend_from_slice(&src);
777        f.extend_from_slice(&dst);
778        f.extend_from_slice(&sport.to_be_bytes());
779        f.extend_from_slice(&dport.to_be_bytes());
780        f.extend_from_slice(&udp_len.to_be_bytes());
781        f.extend_from_slice(&[0x00, 0x00]); // UDP checksum
782        f.extend_from_slice(payload);
783        f
784    }
785
786    fn eth_ipv4_tcp(src: [u8; 4], dst: [u8; 4], sport: u16, dport: u16, payload: &[u8]) -> Vec<u8> {
787        let ip_total = (20 + 20 + payload.len()) as u16;
788        let mut f = Vec::new();
789        f.extend_from_slice(&[0xFF; 6]);
790        f.extend_from_slice(&[0x00; 6]);
791        f.extend_from_slice(&[0x08, 0x00]);
792        f.push(0x45);
793        f.push(0x00);
794        f.extend_from_slice(&ip_total.to_be_bytes());
795        f.extend_from_slice(&[0x00, 0x01, 0x00, 0x00]);
796        f.push(64);
797        f.push(6); // TCP
798        f.extend_from_slice(&[0x00, 0x00]);
799        f.extend_from_slice(&src);
800        f.extend_from_slice(&dst);
801        // TCP header
802        f.extend_from_slice(&sport.to_be_bytes());
803        f.extend_from_slice(&dport.to_be_bytes());
804        f.extend_from_slice(&[0x00; 4]); // seq
805        f.extend_from_slice(&[0x00; 4]); // ack
806        f.push(0x50); // data offset=5
807        f.push(0x02); // SYN
808        f.extend_from_slice(&[0xFF, 0xFF]); // window
809        f.extend_from_slice(&[0x00, 0x00]); // checksum
810        f.extend_from_slice(&[0x00, 0x00]); // urgent
811        f.extend_from_slice(payload);
812        f
813    }
814
815    /// Build an Ethernet/IPv6/UDP frame.
816    fn eth_ipv6_udp(
817        src: [u8; 16],
818        dst: [u8; 16],
819        sport: u16,
820        dport: u16,
821        payload: &[u8],
822    ) -> Vec<u8> {
823        let udp_len = (8 + payload.len()) as u16;
824        let payload_len = udp_len; // IPv6 payload length = transport + data
825        let mut f = Vec::new();
826        f.extend_from_slice(&[0xFF; 6]); // dst MAC
827        f.extend_from_slice(&[0x00; 6]); // src MAC
828        f.extend_from_slice(&[0x86, 0xDD]); // IPv6
829        f.extend_from_slice(&[0x60, 0x00, 0x00, 0x00]); // version=6, TC=0, flow=0
830        f.extend_from_slice(&payload_len.to_be_bytes());
831        f.push(17); // next header = UDP
832        f.push(64); // hop limit
833        f.extend_from_slice(&src);
834        f.extend_from_slice(&dst);
835        f.extend_from_slice(&sport.to_be_bytes());
836        f.extend_from_slice(&dport.to_be_bytes());
837        f.extend_from_slice(&udp_len.to_be_bytes());
838        f.extend_from_slice(&[0x00, 0x00]); // UDP checksum
839        f.extend_from_slice(payload);
840        f
841    }
842
843    // ── parse_ip_mapping ──────────────────────────────────────────────────────
844
845    #[test]
846    fn test_parse_ip_mapping_valid_v4() {
847        let m = parse_ip_mapping("10.0.0.1=192.168.1.1").unwrap();
848        assert_eq!(m.old, "10.0.0.1".parse::<IpAddr>().unwrap());
849        assert_eq!(m.new, "192.168.1.1".parse::<IpAddr>().unwrap());
850    }
851
852    #[test]
853    fn test_parse_ip_mapping_valid_v6() {
854        let m = parse_ip_mapping("::1=::2").unwrap();
855        assert_eq!(m.old, "::1".parse::<IpAddr>().unwrap());
856        assert_eq!(m.new, "::2".parse::<IpAddr>().unwrap());
857    }
858
859    #[test]
860    fn test_parse_ip_mapping_cross_family_v4_to_v6() {
861        // Cross-family mappings are now supported.
862        let m = parse_ip_mapping("10.0.0.1=::1").unwrap();
863        assert_eq!(m.old, "10.0.0.1".parse::<IpAddr>().unwrap());
864        assert_eq!(m.new, "::1".parse::<IpAddr>().unwrap());
865    }
866
867    #[test]
868    fn test_parse_ip_mapping_cross_family_v6_to_v4() {
869        let m = parse_ip_mapping("2001:db8::1=192.168.1.1").unwrap();
870        assert_eq!(m.old, "2001:db8::1".parse::<IpAddr>().unwrap());
871        assert_eq!(m.new, "192.168.1.1".parse::<IpAddr>().unwrap());
872    }
873
874    #[test]
875    fn test_parse_ip_mapping_no_equals() {
876        assert!(parse_ip_mapping("10.0.0.1").is_err());
877    }
878
879    #[test]
880    fn test_parse_ip_mapping_invalid_ip() {
881        assert!(parse_ip_mapping("notanip=192.168.1.1").is_err());
882    }
883
884    // ── internet_checksum ─────────────────────────────────────────────────────
885
886    #[test]
887    fn test_internet_checksum_all_zeros() {
888        // One's complement of zero is 0xFFFF.
889        assert_eq!(internet_checksum(&[0u8; 20]), 0xFFFF);
890    }
891
892    #[test]
893    fn test_internet_checksum_verify_roundtrip() {
894        // Compute checksum of a zeroed-out IPv4 header, insert it, then
895        // re-checking the complete header (including inserted checksum) should
896        // yield 0x0000 — the one's complement of the sum 0xFFFF, meaning valid.
897        let mut header: [u8; 20] = [
898            0x45, 0x00, 0x00, 0x28, // version/IHL, total length=40
899            0x00, 0x01, 0x00, 0x00, // ID, flags
900            0x40, 0x06, 0x00, 0x00, // TTL, TCP, checksum=0
901            0x0a, 0x00, 0x00, 0x01, // src 10.0.0.1
902            0x08, 0x08, 0x08, 0x08, // dst 8.8.8.8
903        ];
904        let csum = internet_checksum(&header);
905        header[10] = (csum >> 8) as u8;
906        header[11] = (csum & 0xFF) as u8;
907        // A valid header's 16-bit word sum = 0xFFFF → !0xFFFF = 0x0000.
908        assert_eq!(internet_checksum(&header), 0x0000);
909    }
910
911    // ── timestamp shift ───────────────────────────────────────────────────────
912
913    #[test]
914    fn test_timestamp_shift_positive() {
915        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &[]);
916        let origlen = data.len() as u32;
917        let (new_ts, _, _) = apply(
918            &mut data,
919            1_000_000_000,
920            500_000_000,
921            origlen,
922            &TransformOptions::default(),
923        );
924        assert_eq!(new_ts, 1_500_000_000);
925    }
926
927    #[test]
928    fn test_timestamp_shift_clamped_to_zero() {
929        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &[]);
930        let origlen = data.len() as u32;
931        // Delta pushes timestamp below zero → clamp to 0.
932        let (new_ts, _, _) = apply(&mut data, 100, -200, origlen, &TransformOptions::default());
933        assert_eq!(new_ts, 0);
934    }
935
936    // ── no-op ─────────────────────────────────────────────────────────────────
937
938    #[test]
939    fn test_no_transform_leaves_data_unchanged() {
940        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &[0xAA; 10]);
941        let original = data.clone();
942        let origlen = data.len() as u32;
943        let (new_ts, new_caplen, new_origlen) =
944            apply(&mut data, 42, 0, origlen, &TransformOptions::default());
945        assert_eq!(new_ts, 42);
946        assert_eq!(new_caplen, origlen);
947        assert_eq!(new_origlen, origlen);
948        assert_eq!(data, original);
949    }
950
951    // ── Same-family IP mapping ────────────────────────────────────────────────
952
953    #[test]
954    fn test_ip_mapping_replaces_src_ip() {
955        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &[0u8; 4]);
956        let origlen = data.len() as u32;
957        let opts = TransformOptions {
958            ip_map: vec![parse_ip_mapping("10.0.0.1=192.168.1.1").unwrap()],
959            ..Default::default()
960        };
961        apply(&mut data, 0, 0, origlen, &opts);
962        // IPv4 src IP: Ethernet(14) + IP offset 12 = byte 26
963        assert_eq!(&data[26..30], &[192, 168, 1, 1]);
964        assert_eq!(&data[30..34], &[8, 8, 8, 8]); // dst unchanged
965    }
966
967    #[test]
968    fn test_ip_mapping_replaces_dst_ip() {
969        let mut data = eth_ipv4_udp([1, 2, 3, 4], [10, 0, 0, 2], 1234, 53, &[0u8; 4]);
970        let origlen = data.len() as u32;
971        let opts = TransformOptions {
972            ip_map: vec![parse_ip_mapping("10.0.0.2=172.16.0.1").unwrap()],
973            ..Default::default()
974        };
975        apply(&mut data, 0, 0, origlen, &opts);
976        assert_eq!(&data[30..34], &[172, 16, 0, 1]);
977    }
978
979    #[test]
980    fn test_ip_mapping_updates_ipv4_header_checksum() {
981        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &[0u8; 4]);
982        let origlen = data.len() as u32;
983        let opts = TransformOptions {
984            ip_map: vec![parse_ip_mapping("10.0.0.1=192.168.1.1").unwrap()],
985            ..Default::default()
986        };
987        apply(&mut data, 0, 0, origlen, &opts);
988        // A valid header: one's complement sum of all words = 0xFFFF → !0xFFFF = 0x0000.
989        let ihl = ((data[14] & 0x0F) * 4) as usize;
990        assert_eq!(
991            internet_checksum(&data[14..14 + ihl]),
992            0x0000,
993            "IPv4 header checksum must be valid after IP mapping"
994        );
995    }
996
997    #[test]
998    fn test_ip_mapping_no_match_leaves_data_unchanged() {
999        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &[0xBB; 4]);
1000        let original = data.clone();
1001        let origlen = data.len() as u32;
1002        // Mapping for an IP not present in the packet.
1003        let opts = TransformOptions {
1004            ip_map: vec![parse_ip_mapping("10.0.0.99=10.0.0.1").unwrap()],
1005            ..Default::default()
1006        };
1007        apply(&mut data, 0, 0, origlen, &opts);
1008        assert_eq!(data, original);
1009    }
1010
1011    // ── Cross-family reframe: IPv4 → IPv6 ────────────────────────────────────
1012
1013    #[test]
1014    fn test_cross_family_ipv4_to_ipv6_src_mapped() {
1015        // IPv4 packet, src=10.0.0.1, dst=8.8.8.8
1016        // Mapping: 10.0.0.1 → 2001:db8::1 (v4→v6)
1017        // Expected: IPv6 packet, src=2001:db8::1, dst=::ffff:8.8.8.8
1018        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &[0xAB; 4]);
1019        let origlen = data.len() as u32;
1020        let opts = TransformOptions {
1021            ip_map: vec![parse_ip_mapping("10.0.0.1=2001:db8::1").unwrap()],
1022            ..Default::default()
1023        };
1024        let (_, new_caplen, new_origlen) = apply(&mut data, 0, 0, origlen, &opts);
1025
1026        // EtherType must be IPv6.
1027        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV6);
1028        // IPv6 header: version nibble.
1029        assert_eq!(data[14] >> 4, 6);
1030        // src address at offset 22: must be 2001:db8::1
1031        let src_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[22..38]).unwrap());
1032        assert_eq!(src_v6, "2001:db8::1".parse::<Ipv6Addr>().unwrap());
1033        // dst address at offset 38: must be ::ffff:8.8.8.8
1034        let dst_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[38..54]).unwrap());
1035        assert_eq!(dst_v6, "::ffff:8.8.8.8".parse::<Ipv6Addr>().unwrap());
1036        // Payload is preserved: 4 bytes 0xAB at the end.
1037        assert_eq!(&data[data.len() - 4..], &[0xAB; 4]);
1038        // Lengths were updated.
1039        assert_eq!(new_caplen, data.len() as u32);
1040        assert_eq!(new_origlen, data.len() as u32);
1041    }
1042
1043    #[test]
1044    fn test_cross_family_ipv4_to_ipv6_dst_mapped() {
1045        // IPv4 packet, src=1.2.3.4, dst=10.0.0.2
1046        // Mapping: 10.0.0.2 → ::1 (v4→v6)
1047        // Expected: IPv6 packet, src=::ffff:1.2.3.4, dst=::1
1048        let mut data = eth_ipv4_udp([1, 2, 3, 4], [10, 0, 0, 2], 5000, 80, &[]);
1049        let origlen = data.len() as u32;
1050        let opts = TransformOptions {
1051            ip_map: vec![parse_ip_mapping("10.0.0.2=::1").unwrap()],
1052            ..Default::default()
1053        };
1054        apply(&mut data, 0, 0, origlen, &opts);
1055
1056        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV6);
1057        let src_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[22..38]).unwrap());
1058        assert_eq!(src_v6, "::ffff:1.2.3.4".parse::<Ipv6Addr>().unwrap());
1059        let dst_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[38..54]).unwrap());
1060        assert_eq!(dst_v6, "::1".parse::<Ipv6Addr>().unwrap());
1061    }
1062
1063    #[test]
1064    fn test_cross_family_ipv4_to_ipv6_both_mapped() {
1065        // Both src and dst are mapped to IPv6 addresses.
1066        let mut data = eth_ipv4_udp([10, 0, 0, 1], [10, 0, 0, 2], 1000, 2000, &[0xCC; 8]);
1067        let origlen = data.len() as u32;
1068        let opts = TransformOptions {
1069            ip_map: vec![
1070                parse_ip_mapping("10.0.0.1=2001:db8::1").unwrap(),
1071                parse_ip_mapping("10.0.0.2=2001:db8::2").unwrap(),
1072            ],
1073            ..Default::default()
1074        };
1075        apply(&mut data, 0, 0, origlen, &opts);
1076
1077        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV6);
1078        let src_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[22..38]).unwrap());
1079        let dst_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[38..54]).unwrap());
1080        assert_eq!(src_v6, "2001:db8::1".parse::<Ipv6Addr>().unwrap());
1081        assert_eq!(dst_v6, "2001:db8::2".parse::<Ipv6Addr>().unwrap());
1082    }
1083
1084    #[test]
1085    fn test_cross_family_ipv4_to_ipv6_checksum_valid() {
1086        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &[0xDE; 16]);
1087        let origlen = data.len() as u32;
1088        let opts = TransformOptions {
1089            ip_map: vec![parse_ip_mapping("10.0.0.1=2001:db8::1").unwrap()],
1090            ..Default::default()
1091        };
1092        apply(&mut data, 0, 0, origlen, &opts);
1093
1094        // Verify that the UDP checksum in the IPv6 packet is non-zero (was computed).
1095        // Transport starts at byte 54 (14 Ethernet + 40 IPv6).
1096        let udp_csum = u16::from_be_bytes([data[54 + 6], data[54 + 7]]);
1097        assert_ne!(udp_csum, 0, "UDP checksum must be set in IPv6 packet");
1098    }
1099
1100    #[test]
1101    fn test_cross_family_ipv4_to_ipv6_size_change() {
1102        // IPv4 → IPv6 adds (40 - 20) = 20 bytes to the IP header.
1103        let payload = [0u8; 10];
1104        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1000, 2000, &payload);
1105        let ipv4_len = data.len();
1106        let origlen = data.len() as u32;
1107        let opts = TransformOptions {
1108            ip_map: vec![parse_ip_mapping("10.0.0.1=::1").unwrap()],
1109            ..Default::default()
1110        };
1111        let (_, new_caplen, new_origlen) = apply(&mut data, 0, 0, origlen, &opts);
1112
1113        // 14 (Eth) + 40 (IPv6) + 8 (UDP) + 10 (payload) = 72
1114        // Original: 14 (Eth) + 20 (IPv4) + 8 (UDP) + 10 (payload) = 52
1115        assert_eq!(data.len(), ipv4_len + 20);
1116        assert_eq!(new_caplen, data.len() as u32);
1117        assert_eq!(new_origlen, data.len() as u32);
1118    }
1119
1120    // ── Cross-family reframe: IPv6 → IPv4 ────────────────────────────────────
1121
1122    #[test]
1123    fn test_cross_family_ipv6_to_ipv4_src_mapped() {
1124        // IPv6 packet, src=2001:db8::1, dst=::ffff:8.8.8.8
1125        // Mapping: 2001:db8::1 → 10.0.0.1 (v6→v4)
1126        let src_v6: [u8; 16] = "2001:db8::1".parse::<Ipv6Addr>().unwrap().octets();
1127        let dst_v6: [u8; 16] = "::ffff:8.8.8.8".parse::<Ipv6Addr>().unwrap().octets();
1128        let mut data = eth_ipv6_udp(src_v6, dst_v6, 5000, 80, &[0xBB; 4]);
1129        let origlen = data.len() as u32;
1130        let opts = TransformOptions {
1131            ip_map: vec![parse_ip_mapping("2001:db8::1=10.0.0.1").unwrap()],
1132            ..Default::default()
1133        };
1134        apply(&mut data, 0, 0, origlen, &opts);
1135
1136        // EtherType must be IPv4.
1137        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV4);
1138        assert_eq!(data[14] >> 4, 4);
1139        // src IP at offset 26: 10.0.0.1
1140        assert_eq!(&data[26..30], &[10, 0, 0, 1]);
1141        // dst IP at offset 30: 8.8.8.8 (extracted from ::ffff:8.8.8.8)
1142        assert_eq!(&data[30..34], &[8, 8, 8, 8]);
1143        // Payload preserved.
1144        assert_eq!(&data[data.len() - 4..], &[0xBB; 4]);
1145    }
1146
1147    #[test]
1148    fn test_cross_family_ipv6_to_ipv4_dst_mapped() {
1149        // IPv6 packet, src=::ffff:1.2.3.4, dst=2001:db8::2
1150        // Mapping: 2001:db8::2 → 192.168.1.2 (v6→v4)
1151        let src_v6: [u8; 16] = "::ffff:1.2.3.4".parse::<Ipv6Addr>().unwrap().octets();
1152        let dst_v6: [u8; 16] = "2001:db8::2".parse::<Ipv6Addr>().unwrap().octets();
1153        let mut data = eth_ipv6_udp(src_v6, dst_v6, 1234, 443, &[]);
1154        let origlen = data.len() as u32;
1155        let opts = TransformOptions {
1156            ip_map: vec![parse_ip_mapping("2001:db8::2=192.168.1.2").unwrap()],
1157            ..Default::default()
1158        };
1159        apply(&mut data, 0, 0, origlen, &opts);
1160
1161        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV4);
1162        assert_eq!(&data[26..30], &[1, 2, 3, 4]); // extracted from ::ffff:1.2.3.4
1163        assert_eq!(&data[30..34], &[192, 168, 1, 2]);
1164    }
1165
1166    #[test]
1167    fn test_cross_family_ipv6_to_ipv4_skipped_when_non_mapped_addr() {
1168        // IPv6 packet where dst is a native IPv6 addr (not IPv4-mapped) and
1169        // not covered by any mapping → reframe must be skipped.
1170        let src_v6: [u8; 16] = "2001:db8::1".parse::<Ipv6Addr>().unwrap().octets();
1171        let dst_v6: [u8; 16] = "2001:db8::2".parse::<Ipv6Addr>().unwrap().octets();
1172        let mut data = eth_ipv6_udp(src_v6, dst_v6, 1000, 2000, &[0xFF; 4]);
1173        let original = data.clone();
1174        let origlen = data.len() as u32;
1175        // Mapping only covers src; dst is a native IPv6 addr with no mapping → skip.
1176        let opts = TransformOptions {
1177            ip_map: vec![parse_ip_mapping("2001:db8::1=10.0.0.1").unwrap()],
1178            ..Default::default()
1179        };
1180        apply(&mut data, 0, 0, origlen, &opts);
1181        // Packet must be unchanged (reframe was skipped).
1182        assert_eq!(data, original);
1183    }
1184
1185    #[test]
1186    fn test_cross_family_ipv6_to_ipv4_checksum_valid() {
1187        let src_v6: [u8; 16] = "2001:db8::1".parse::<Ipv6Addr>().unwrap().octets();
1188        let dst_v6: [u8; 16] = "::ffff:8.8.8.8".parse::<Ipv6Addr>().unwrap().octets();
1189        let mut data = eth_ipv6_udp(src_v6, dst_v6, 5000, 53, &[0xDE; 8]);
1190        let origlen = data.len() as u32;
1191        let opts = TransformOptions {
1192            ip_map: vec![parse_ip_mapping("2001:db8::1=10.0.0.1").unwrap()],
1193            ..Default::default()
1194        };
1195        apply(&mut data, 0, 0, origlen, &opts);
1196
1197        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV4);
1198        let ihl = ((data[14] & 0x0F) * 4) as usize;
1199        assert_eq!(
1200            internet_checksum(&data[14..14 + ihl]),
1201            0x0000,
1202            "IPv4 header checksum must be valid after v6→v4 reframe"
1203        );
1204    }
1205
1206    #[test]
1207    fn test_cross_family_ipv6_to_ipv4_size_change() {
1208        // IPv6 → IPv4 removes 20 bytes (40-byte header → 20-byte header).
1209        let src_v6: [u8; 16] = "2001:db8::1".parse::<Ipv6Addr>().unwrap().octets();
1210        let dst_v6: [u8; 16] = "::ffff:8.8.8.8".parse::<Ipv6Addr>().unwrap().octets();
1211        let mut data = eth_ipv6_udp(src_v6, dst_v6, 1000, 2000, &[0u8; 10]);
1212        let ipv6_len = data.len();
1213        let origlen = data.len() as u32;
1214        let opts = TransformOptions {
1215            ip_map: vec![parse_ip_mapping("2001:db8::1=10.0.0.1").unwrap()],
1216            ..Default::default()
1217        };
1218        let (_, new_caplen, new_origlen) = apply(&mut data, 0, 0, origlen, &opts);
1219        assert_eq!(data.len(), ipv6_len - 20);
1220        assert_eq!(new_caplen, data.len() as u32);
1221        assert_eq!(new_origlen, data.len() as u32);
1222    }
1223
1224    // ── Payload truncation ────────────────────────────────────────────────────
1225
1226    #[test]
1227    fn test_truncation_udp_updates_lengths() {
1228        let payload = vec![0xBB; 100];
1229        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &payload);
1230        let orig = data.len() as u32;
1231        let opts = TransformOptions {
1232            max_payload_bytes: Some(10),
1233            ..Default::default()
1234        };
1235        let (_, new_caplen, new_origlen) = apply(&mut data, 0, 0, orig, &opts);
1236
1237        // Ethernet(14) + IPv4(20) + UDP(8) + 10 = 52
1238        assert_eq!(data.len(), 52);
1239        assert_eq!(new_caplen, 52);
1240        assert_eq!(new_origlen, 52);
1241
1242        // IPv4 total length: 20 + 8 + 10 = 38
1243        let ip_total = u16::from_be_bytes([data[16], data[17]]);
1244        assert_eq!(ip_total, 38, "IPv4 total length not updated");
1245
1246        // UDP length field is at offset 4 within the UDP header (transport_start=34).
1247        // 8 + 10 = 18
1248        let udp_len = u16::from_be_bytes([data[38], data[39]]);
1249        assert_eq!(udp_len, 18, "UDP length not updated");
1250    }
1251
1252    #[test]
1253    fn test_truncation_tcp_updates_ip_length() {
1254        let payload = vec![0xCC; 50];
1255        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 443, &payload);
1256        let orig = data.len() as u32;
1257        let opts = TransformOptions {
1258            max_payload_bytes: Some(5),
1259            ..Default::default()
1260        };
1261        apply(&mut data, 0, 0, orig, &opts);
1262
1263        // Ethernet(14) + IPv4(20) + TCP(20) + 5 = 59
1264        assert_eq!(data.len(), 59);
1265        // IPv4 total: 20 + 20 + 5 = 45
1266        let ip_total = u16::from_be_bytes([data[16], data[17]]);
1267        assert_eq!(ip_total, 45);
1268    }
1269
1270    #[test]
1271    fn test_truncation_noop_when_short_enough() {
1272        let payload = vec![0xAA; 5];
1273        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &payload);
1274        let original = data.clone();
1275        let orig = data.len() as u32;
1276        let opts = TransformOptions {
1277            max_payload_bytes: Some(100),
1278            ..Default::default()
1279        };
1280        apply(&mut data, 0, 0, orig, &opts);
1281        assert_eq!(data, original);
1282    }
1283
1284    #[test]
1285    fn test_truncation_checksums_valid() {
1286        let payload = vec![0xDE; 50];
1287        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &payload);
1288        let orig = data.len() as u32;
1289        let opts = TransformOptions {
1290            max_payload_bytes: Some(8),
1291            ..Default::default()
1292        };
1293        apply(&mut data, 0, 0, orig, &opts);
1294        let ihl = ((data[14] & 0x0F) * 4) as usize;
1295        assert_eq!(
1296            internet_checksum(&data[14..14 + ihl]),
1297            0x0000,
1298            "IPv4 header checksum must be valid after truncation"
1299        );
1300    }
1301
1302    #[test]
1303    fn test_truncation_zero_payload_bytes() {
1304        // max_payload_bytes=0 keeps only headers, no payload.
1305        let payload = vec![0xFF; 20];
1306        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &payload);
1307        let orig = data.len() as u32;
1308        let opts = TransformOptions {
1309            max_payload_bytes: Some(0),
1310            ..Default::default()
1311        };
1312        apply(&mut data, 0, 0, orig, &opts);
1313        // Ethernet(14) + IPv4(20) + UDP(8) = 42
1314        assert_eq!(data.len(), 42);
1315    }
1316
1317    // ── Per-protocol truncation ───────────────────────────────────────────────
1318
1319    #[test]
1320    fn test_proto_truncation_tcp_uses_rule() {
1321        let payload = vec![0xAA; 100];
1322        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 443, &payload);
1323        let orig = data.len() as u32;
1324        let opts = TransformOptions {
1325            proto_truncation: vec![ProtocolTruncation {
1326                proto: PROTO_TCP,
1327                max_payload_bytes: 10,
1328            }],
1329            ..Default::default()
1330        };
1331        apply(&mut data, 0, 0, orig, &opts);
1332        // Ethernet(14) + IPv4(20) + TCP(20) + 10 = 64
1333        assert_eq!(data.len(), 64);
1334    }
1335
1336    #[test]
1337    fn test_proto_truncation_udp_uses_rule() {
1338        let payload = vec![0xBB; 80];
1339        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 53, &payload);
1340        let orig = data.len() as u32;
1341        let opts = TransformOptions {
1342            proto_truncation: vec![ProtocolTruncation {
1343                proto: PROTO_UDP,
1344                max_payload_bytes: 8,
1345            }],
1346            ..Default::default()
1347        };
1348        apply(&mut data, 0, 0, orig, &opts);
1349        // Ethernet(14) + IPv4(20) + UDP(8) + 8 = 50
1350        assert_eq!(data.len(), 50);
1351    }
1352
1353    #[test]
1354    fn test_proto_truncation_overrides_global() {
1355        // TCP rule (10 bytes) should win over global limit (50 bytes).
1356        let payload = vec![0xCC; 100];
1357        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 80, &payload);
1358        let orig = data.len() as u32;
1359        let opts = TransformOptions {
1360            max_payload_bytes: Some(50),
1361            proto_truncation: vec![ProtocolTruncation {
1362                proto: PROTO_TCP,
1363                max_payload_bytes: 10,
1364            }],
1365            ..Default::default()
1366        };
1367        apply(&mut data, 0, 0, orig, &opts);
1368        // Ethernet(14) + IPv4(20) + TCP(20) + 10 = 64
1369        assert_eq!(data.len(), 64);
1370    }
1371
1372    #[test]
1373    fn test_proto_truncation_fallback_to_global() {
1374        // No rule for UDP → falls back to global 20 bytes.
1375        let payload = vec![0xDD; 80];
1376        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 53, &payload);
1377        let orig = data.len() as u32;
1378        let opts = TransformOptions {
1379            max_payload_bytes: Some(20),
1380            proto_truncation: vec![ProtocolTruncation {
1381                proto: PROTO_TCP, // TCP rule only, no UDP rule
1382                max_payload_bytes: 5,
1383            }],
1384            ..Default::default()
1385        };
1386        apply(&mut data, 0, 0, orig, &opts);
1387        // Ethernet(14) + IPv4(20) + UDP(8) + 20 = 62
1388        assert_eq!(data.len(), 62);
1389    }
1390
1391    #[test]
1392    fn test_proto_truncation_no_match_no_global_no_truncation() {
1393        // UDP packet, only a TCP rule, no global — no truncation applied.
1394        let payload = vec![0xEE; 50];
1395        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 53, &payload);
1396        let original = data.clone();
1397        let orig = data.len() as u32;
1398        let opts = TransformOptions {
1399            proto_truncation: vec![ProtocolTruncation {
1400                proto: PROTO_TCP,
1401                max_payload_bytes: 10,
1402            }],
1403            ..Default::default()
1404        };
1405        apply(&mut data, 0, 0, orig, &opts);
1406        assert_eq!(data, original);
1407    }
1408
1409    // ── Combined transforms ───────────────────────────────────────────────────
1410
1411    #[test]
1412    fn test_ip_mapping_and_truncation_combined() {
1413        let payload = vec![0xAB; 80];
1414        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &payload);
1415        let orig = data.len() as u32;
1416        let opts = TransformOptions {
1417            ip_map: vec![parse_ip_mapping("10.0.0.1=192.168.99.1").unwrap()],
1418            max_payload_bytes: Some(16),
1419            ..Default::default()
1420        };
1421        apply(&mut data, 0, 0, orig, &opts);
1422
1423        // IP was replaced.
1424        assert_eq!(&data[26..30], &[192, 168, 99, 1]);
1425        // Length was truncated: 14 + 20 + 8 + 16 = 58.
1426        assert_eq!(data.len(), 58);
1427        // Header checksum is valid (word sum = 0xFFFF → !0xFFFF = 0x0000).
1428        let ihl = ((data[14] & 0x0F) * 4) as usize;
1429        assert_eq!(internet_checksum(&data[14..14 + ihl]), 0x0000);
1430    }
1431
1432    #[test]
1433    fn test_cross_family_and_truncation_combined() {
1434        // IPv4 → IPv6 reframe, then truncate payload.
1435        let payload = vec![0xCD; 100];
1436        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &payload);
1437        let orig = data.len() as u32;
1438        let opts = TransformOptions {
1439            ip_map: vec![parse_ip_mapping("10.0.0.1=2001:db8::1").unwrap()],
1440            max_payload_bytes: Some(10),
1441            ..Default::default()
1442        };
1443        apply(&mut data, 0, 0, orig, &opts);
1444
1445        // Must be an IPv6 packet.
1446        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV6);
1447        // Ethernet(14) + IPv6(40) + UDP(8) + 10 = 72
1448        assert_eq!(data.len(), 72);
1449    }
1450
1451    // ── Corrupt TCP data-offset ───────────────────────────────────────────────
1452
1453    #[test]
1454    fn test_truncation_tcp_corrupt_data_offset_ipv4_rejected() {
1455        // Build a valid IPv4/TCP frame, then corrupt the data-offset nibble to 4
1456        // (16 bytes < 20-byte minimum). Truncation must be skipped entirely.
1457        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 443, &[0xAA; 30]);
1458        // TCP data-offset byte: Ethernet(14) + IPv4(20) + TCP offset 12 = index 46.
1459        // Nibble 4 → 4*4 = 16 bytes, which is below the 20-byte minimum.
1460        data[46] = 0x40;
1461        let original = data.clone();
1462        let orig = data.len() as u32;
1463        let opts = TransformOptions {
1464            max_payload_bytes: Some(0),
1465            ..Default::default()
1466        };
1467        apply(&mut data, 0, 0, orig, &opts);
1468        assert_eq!(
1469            data, original,
1470            "corrupt data-offset must leave packet unchanged"
1471        );
1472    }
1473
1474    #[test]
1475    fn test_truncation_tcp_corrupt_data_offset_ipv6_rejected() {
1476        // Same check for an IPv6/TCP frame.
1477        // Build frame manually: Ethernet(14) + IPv6(40) + TCP(20) + payload.
1478        let payload = [0xBB; 30];
1479        let tcp_len = (20 + payload.len()) as u16;
1480        let mut f = Vec::new();
1481        f.extend_from_slice(&[0xFF; 6]); // dst MAC
1482        f.extend_from_slice(&[0x00; 6]); // src MAC
1483        f.extend_from_slice(&[0x86, 0xDD]); // IPv6
1484        f.extend_from_slice(&[0x60, 0x00, 0x00, 0x00]); // version=6
1485        f.extend_from_slice(&tcp_len.to_be_bytes()); // payload length
1486        f.push(6); // next header = TCP
1487        f.push(64); // hop limit
1488        f.extend_from_slice(&[0x20, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); // src
1489        f.extend_from_slice(&[0x20, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); // dst
1490        // TCP header (20 bytes)
1491        f.extend_from_slice(&100u16.to_be_bytes()); // sport
1492        f.extend_from_slice(&443u16.to_be_bytes()); // dport
1493        f.extend_from_slice(&[0x00; 4]); // seq
1494        f.extend_from_slice(&[0x00; 4]); // ack
1495        f.push(0x50); // data offset = 5 (will be corrupted below)
1496        f.push(0x02); // SYN
1497        f.extend_from_slice(&[0xFF, 0xFF]); // window
1498        f.extend_from_slice(&[0x00, 0x00]); // checksum
1499        f.extend_from_slice(&[0x00, 0x00]); // urgent
1500        f.extend_from_slice(&payload);
1501        // TCP data-offset byte: Ethernet(14) + IPv6(40) + TCP offset 12 = index 66.
1502        f[66] = 0x30; // nibble 3 → 3*4 = 12 bytes < 20-byte minimum
1503        let original = f.clone();
1504        let orig = f.len() as u32;
1505        let opts = TransformOptions {
1506            max_payload_bytes: Some(0),
1507            ..Default::default()
1508        };
1509        apply(&mut f, 0, 0, orig, &opts);
1510        assert_eq!(
1511            f, original,
1512            "corrupt data-offset must leave packet unchanged"
1513        );
1514    }
1515
1516    #[test]
1517    fn test_truncation_tcp_zero_data_offset_rejected() {
1518        // data-offset nibble = 0 → th = 0, which is also < 20.
1519        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 80, &[0xCC; 10]);
1520        data[46] = 0x00;
1521        let original = data.clone();
1522        let orig = data.len() as u32;
1523        let opts = TransformOptions {
1524            max_payload_bytes: Some(0),
1525            ..Default::default()
1526        };
1527        apply(&mut data, 0, 0, orig, &opts);
1528        assert_eq!(data, original);
1529    }
1530
1531    #[test]
1532    fn test_truncation_tcp_min_valid_data_offset_accepted() {
1533        // data-offset nibble = 5 → th = 20 bytes (minimum valid). Truncation
1534        // must proceed normally.
1535        let payload = vec![0xDD; 50];
1536        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 443, &payload);
1537        // data[46] is already 0x50 (offset = 5) from the helper.
1538        let orig = data.len() as u32;
1539        let opts = TransformOptions {
1540            max_payload_bytes: Some(10),
1541            ..Default::default()
1542        };
1543        apply(&mut data, 0, 0, orig, &opts);
1544        // Ethernet(14) + IPv4(20) + TCP(20) + 10 = 64
1545        assert_eq!(data.len(), 64);
1546    }
1547}