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                    ((data[ts + 12] >> 4) * 4) as usize
546                }
547                PROTO_UDP => 8,
548                _ => return (origlen, false),
549            };
550            (proto, ts, th)
551        }
552        ET_IPV6 => {
553            if data.len() < ip_off + 40 {
554                return (origlen, false);
555            }
556            let proto = data[ip_off + 6]; // next header
557            let ts = ip_off + 40;
558            let th = match proto {
559                PROTO_TCP => {
560                    if data.len() < ts + 20 {
561                        return (origlen, false);
562                    }
563                    ((data[ts + 12] >> 4) * 4) as usize
564                }
565                PROTO_UDP => 8,
566                _ => return (origlen, false),
567            };
568            (proto, ts, th)
569        }
570        _ => return (origlen, false),
571    };
572
573    let payload_start = transport_start + transport_hdr_len;
574    let max_total = payload_start + max_bytes as usize;
575
576    if data.len() <= max_total {
577        return (origlen, false); // already short enough
578    }
579
580    data.truncate(max_total);
581    let new_len = data.len();
582
583    // Update IP total length / payload length.
584    if et == ET_IPV4 {
585        let new_ip_total = (new_len - ip_off) as u16;
586        data[ip_off + 2..ip_off + 4].copy_from_slice(&new_ip_total.to_be_bytes());
587    } else {
588        // IPv6 payload length = everything after the 40-byte fixed header.
589        let new_plen = (new_len - ip_off - 40) as u16;
590        data[ip_off + 4..ip_off + 6].copy_from_slice(&new_plen.to_be_bytes());
591    }
592
593    // Update UDP length. TCP has no explicit payload length field.
594    if proto == PROTO_UDP {
595        let new_udp_len = (new_len - transport_start) as u16;
596        data[transport_start + 4..transport_start + 6].copy_from_slice(&new_udp_len.to_be_bytes());
597    }
598
599    (new_len as u32, true)
600}
601
602// ── Checksum recalculation ────────────────────────────────────────────────────
603
604/// Recompute all checksums in `data` (IPv4 header + TCP/UDP transport).
605fn recalculate_checksums(data: &mut [u8]) {
606    let Some((ip_off, et)) = find_ip(data) else {
607        return;
608    };
609    match et {
610        ET_IPV4 => recalc_ipv4(data, ip_off),
611        ET_IPV6 => recalc_ipv6(data, ip_off),
612        _ => {}
613    }
614}
615
616fn recalc_ipv4(data: &mut [u8], ip_off: usize) {
617    if data.len() < ip_off + 20 {
618        return;
619    }
620    let ihl = ((data[ip_off] & 0x0F) * 4) as usize;
621    if data.len() < ip_off + ihl {
622        return;
623    }
624
625    // Recompute IPv4 header checksum (field at offset 10–12 within the header).
626    data[ip_off + 10] = 0;
627    data[ip_off + 11] = 0;
628    let csum = internet_checksum(&data[ip_off..ip_off + ihl]);
629    data[ip_off + 10..ip_off + 12].copy_from_slice(&csum.to_be_bytes());
630
631    let proto = data[ip_off + 9];
632    let ts = ip_off + ihl;
633    if proto == PROTO_TCP || proto == PROTO_UDP {
634        recalc_transport_v4(data, ip_off, ts, proto);
635    }
636}
637
638fn recalc_transport_v4(data: &mut [u8], ip_off: usize, ts: usize, proto: u8) {
639    let csum_off = if proto == PROTO_TCP { ts + 16 } else { ts + 6 };
640    if data.len() < csum_off + 2 {
641        return;
642    }
643    let src: [u8; 4] = data[ip_off + 12..ip_off + 16].try_into().unwrap();
644    let dst: [u8; 4] = data[ip_off + 16..ip_off + 20].try_into().unwrap();
645    data[csum_off] = 0;
646    data[csum_off + 1] = 0;
647    let csum = transport_checksum_v4(src, dst, proto, &data[ts..]);
648    data[csum_off..csum_off + 2].copy_from_slice(&csum.to_be_bytes());
649}
650
651fn recalc_ipv6(data: &mut [u8], ip_off: usize) {
652    if data.len() < ip_off + 40 {
653        return;
654    }
655    let proto = data[ip_off + 6]; // next header
656    let ts = ip_off + 40;
657    if proto == PROTO_TCP || proto == PROTO_UDP {
658        recalc_transport_v6(data, ip_off, ts, proto);
659    }
660}
661
662fn recalc_transport_v6(data: &mut [u8], ip_off: usize, ts: usize, proto: u8) {
663    let csum_off = if proto == PROTO_TCP { ts + 16 } else { ts + 6 };
664    if data.len() < csum_off + 2 {
665        return;
666    }
667    let src: [u8; 16] = data[ip_off + 8..ip_off + 24].try_into().unwrap();
668    let dst: [u8; 16] = data[ip_off + 24..ip_off + 40].try_into().unwrap();
669    data[csum_off] = 0;
670    data[csum_off + 1] = 0;
671    let csum = transport_checksum_v6(src, dst, proto, &data[ts..]);
672    data[csum_off..csum_off + 2].copy_from_slice(&csum.to_be_bytes());
673}
674
675// ── Checksum math ─────────────────────────────────────────────────────────────
676
677/// Standard Internet checksum (RFC 1071): one's complement of the 16-bit ones-complement sum.
678fn internet_checksum(data: &[u8]) -> u16 {
679    let mut sum: u32 = 0;
680    let mut iter = data.chunks_exact(2);
681    for chunk in &mut iter {
682        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
683    }
684    if let [byte] = iter.remainder() {
685        sum += (*byte as u32) << 8;
686    }
687    while sum >> 16 != 0 {
688        sum = (sum & 0xFFFF) + (sum >> 16);
689    }
690    !(sum as u16)
691}
692
693/// Transport-layer checksum using an IPv4 pseudo-header.
694fn transport_checksum_v4(src: [u8; 4], dst: [u8; 4], proto: u8, segment: &[u8]) -> u16 {
695    let len = segment.len() as u32;
696    let mut sum: u32 = 0;
697    // Pseudo-header: src IP, dst IP, zero, proto, segment length.
698    sum += u16::from_be_bytes([src[0], src[1]]) as u32;
699    sum += u16::from_be_bytes([src[2], src[3]]) as u32;
700    sum += u16::from_be_bytes([dst[0], dst[1]]) as u32;
701    sum += u16::from_be_bytes([dst[2], dst[3]]) as u32;
702    sum += proto as u32;
703    sum += len & 0xFFFF;
704    let mut iter = segment.chunks_exact(2);
705    for chunk in &mut iter {
706        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
707    }
708    if let [byte] = iter.remainder() {
709        sum += (*byte as u32) << 8;
710    }
711    while sum >> 16 != 0 {
712        sum = (sum & 0xFFFF) + (sum >> 16);
713    }
714    !(sum as u16)
715}
716
717/// Transport-layer checksum using an IPv6 pseudo-header.
718fn transport_checksum_v6(src: [u8; 16], dst: [u8; 16], proto: u8, segment: &[u8]) -> u16 {
719    let len = segment.len() as u32;
720    let mut sum: u32 = 0;
721    // src and dst addresses (16 bytes each = 8 words each).
722    for i in (0..16).step_by(2) {
723        sum += u16::from_be_bytes([src[i], src[i + 1]]) as u32;
724    }
725    for i in (0..16).step_by(2) {
726        sum += u16::from_be_bytes([dst[i], dst[i + 1]]) as u32;
727    }
728    // Upper-layer packet length (4 bytes, big-endian).
729    sum += len >> 16;
730    sum += len & 0xFFFF;
731    // Next header (3 zero bytes + 1 byte proto).
732    sum += proto as u32;
733    let mut iter = segment.chunks_exact(2);
734    for chunk in &mut iter {
735        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
736    }
737    if let [byte] = iter.remainder() {
738        sum += (*byte as u32) << 8;
739    }
740    while sum >> 16 != 0 {
741        sum = (sum & 0xFFFF) + (sum >> 16);
742    }
743    !(sum as u16)
744}
745
746// ── Unit tests ────────────────────────────────────────────────────────────────
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    // ── Frame builders ────────────────────────────────────────────────────────
753
754    fn eth_ipv4_udp(src: [u8; 4], dst: [u8; 4], sport: u16, dport: u16, payload: &[u8]) -> Vec<u8> {
755        let udp_len = (8 + payload.len()) as u16;
756        let ip_total = 20 + udp_len;
757        let mut f = Vec::new();
758        f.extend_from_slice(&[0xFF; 6]); // dst MAC
759        f.extend_from_slice(&[0x00; 6]); // src MAC
760        f.extend_from_slice(&[0x08, 0x00]); // IPv4
761        f.push(0x45); // version=4, IHL=5
762        f.push(0x00); // DSCP/ECN
763        f.extend_from_slice(&ip_total.to_be_bytes());
764        f.extend_from_slice(&[0x00, 0x01, 0x00, 0x00]); // ID, flags/frag
765        f.push(64);
766        f.push(17); // TTL, UDP
767        f.extend_from_slice(&[0x00, 0x00]); // header checksum (zeroed)
768        f.extend_from_slice(&src);
769        f.extend_from_slice(&dst);
770        f.extend_from_slice(&sport.to_be_bytes());
771        f.extend_from_slice(&dport.to_be_bytes());
772        f.extend_from_slice(&udp_len.to_be_bytes());
773        f.extend_from_slice(&[0x00, 0x00]); // UDP checksum
774        f.extend_from_slice(payload);
775        f
776    }
777
778    fn eth_ipv4_tcp(src: [u8; 4], dst: [u8; 4], sport: u16, dport: u16, payload: &[u8]) -> Vec<u8> {
779        let ip_total = (20 + 20 + payload.len()) as u16;
780        let mut f = Vec::new();
781        f.extend_from_slice(&[0xFF; 6]);
782        f.extend_from_slice(&[0x00; 6]);
783        f.extend_from_slice(&[0x08, 0x00]);
784        f.push(0x45);
785        f.push(0x00);
786        f.extend_from_slice(&ip_total.to_be_bytes());
787        f.extend_from_slice(&[0x00, 0x01, 0x00, 0x00]);
788        f.push(64);
789        f.push(6); // TCP
790        f.extend_from_slice(&[0x00, 0x00]);
791        f.extend_from_slice(&src);
792        f.extend_from_slice(&dst);
793        // TCP header
794        f.extend_from_slice(&sport.to_be_bytes());
795        f.extend_from_slice(&dport.to_be_bytes());
796        f.extend_from_slice(&[0x00; 4]); // seq
797        f.extend_from_slice(&[0x00; 4]); // ack
798        f.push(0x50); // data offset=5
799        f.push(0x02); // SYN
800        f.extend_from_slice(&[0xFF, 0xFF]); // window
801        f.extend_from_slice(&[0x00, 0x00]); // checksum
802        f.extend_from_slice(&[0x00, 0x00]); // urgent
803        f.extend_from_slice(payload);
804        f
805    }
806
807    /// Build an Ethernet/IPv6/UDP frame.
808    fn eth_ipv6_udp(
809        src: [u8; 16],
810        dst: [u8; 16],
811        sport: u16,
812        dport: u16,
813        payload: &[u8],
814    ) -> Vec<u8> {
815        let udp_len = (8 + payload.len()) as u16;
816        let payload_len = udp_len; // IPv6 payload length = transport + data
817        let mut f = Vec::new();
818        f.extend_from_slice(&[0xFF; 6]); // dst MAC
819        f.extend_from_slice(&[0x00; 6]); // src MAC
820        f.extend_from_slice(&[0x86, 0xDD]); // IPv6
821        f.extend_from_slice(&[0x60, 0x00, 0x00, 0x00]); // version=6, TC=0, flow=0
822        f.extend_from_slice(&payload_len.to_be_bytes());
823        f.push(17); // next header = UDP
824        f.push(64); // hop limit
825        f.extend_from_slice(&src);
826        f.extend_from_slice(&dst);
827        f.extend_from_slice(&sport.to_be_bytes());
828        f.extend_from_slice(&dport.to_be_bytes());
829        f.extend_from_slice(&udp_len.to_be_bytes());
830        f.extend_from_slice(&[0x00, 0x00]); // UDP checksum
831        f.extend_from_slice(payload);
832        f
833    }
834
835    // ── parse_ip_mapping ──────────────────────────────────────────────────────
836
837    #[test]
838    fn test_parse_ip_mapping_valid_v4() {
839        let m = parse_ip_mapping("10.0.0.1=192.168.1.1").unwrap();
840        assert_eq!(m.old, "10.0.0.1".parse::<IpAddr>().unwrap());
841        assert_eq!(m.new, "192.168.1.1".parse::<IpAddr>().unwrap());
842    }
843
844    #[test]
845    fn test_parse_ip_mapping_valid_v6() {
846        let m = parse_ip_mapping("::1=::2").unwrap();
847        assert_eq!(m.old, "::1".parse::<IpAddr>().unwrap());
848        assert_eq!(m.new, "::2".parse::<IpAddr>().unwrap());
849    }
850
851    #[test]
852    fn test_parse_ip_mapping_cross_family_v4_to_v6() {
853        // Cross-family mappings are now supported.
854        let m = parse_ip_mapping("10.0.0.1=::1").unwrap();
855        assert_eq!(m.old, "10.0.0.1".parse::<IpAddr>().unwrap());
856        assert_eq!(m.new, "::1".parse::<IpAddr>().unwrap());
857    }
858
859    #[test]
860    fn test_parse_ip_mapping_cross_family_v6_to_v4() {
861        let m = parse_ip_mapping("2001:db8::1=192.168.1.1").unwrap();
862        assert_eq!(m.old, "2001:db8::1".parse::<IpAddr>().unwrap());
863        assert_eq!(m.new, "192.168.1.1".parse::<IpAddr>().unwrap());
864    }
865
866    #[test]
867    fn test_parse_ip_mapping_no_equals() {
868        assert!(parse_ip_mapping("10.0.0.1").is_err());
869    }
870
871    #[test]
872    fn test_parse_ip_mapping_invalid_ip() {
873        assert!(parse_ip_mapping("notanip=192.168.1.1").is_err());
874    }
875
876    // ── internet_checksum ─────────────────────────────────────────────────────
877
878    #[test]
879    fn test_internet_checksum_all_zeros() {
880        // One's complement of zero is 0xFFFF.
881        assert_eq!(internet_checksum(&[0u8; 20]), 0xFFFF);
882    }
883
884    #[test]
885    fn test_internet_checksum_verify_roundtrip() {
886        // Compute checksum of a zeroed-out IPv4 header, insert it, then
887        // re-checking the complete header (including inserted checksum) should
888        // yield 0x0000 — the one's complement of the sum 0xFFFF, meaning valid.
889        let mut header: [u8; 20] = [
890            0x45, 0x00, 0x00, 0x28, // version/IHL, total length=40
891            0x00, 0x01, 0x00, 0x00, // ID, flags
892            0x40, 0x06, 0x00, 0x00, // TTL, TCP, checksum=0
893            0x0a, 0x00, 0x00, 0x01, // src 10.0.0.1
894            0x08, 0x08, 0x08, 0x08, // dst 8.8.8.8
895        ];
896        let csum = internet_checksum(&header);
897        header[10] = (csum >> 8) as u8;
898        header[11] = (csum & 0xFF) as u8;
899        // A valid header's 16-bit word sum = 0xFFFF → !0xFFFF = 0x0000.
900        assert_eq!(internet_checksum(&header), 0x0000);
901    }
902
903    // ── timestamp shift ───────────────────────────────────────────────────────
904
905    #[test]
906    fn test_timestamp_shift_positive() {
907        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &[]);
908        let origlen = data.len() as u32;
909        let (new_ts, _, _) = apply(
910            &mut data,
911            1_000_000_000,
912            500_000_000,
913            origlen,
914            &TransformOptions::default(),
915        );
916        assert_eq!(new_ts, 1_500_000_000);
917    }
918
919    #[test]
920    fn test_timestamp_shift_clamped_to_zero() {
921        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &[]);
922        let origlen = data.len() as u32;
923        // Delta pushes timestamp below zero → clamp to 0.
924        let (new_ts, _, _) = apply(&mut data, 100, -200, origlen, &TransformOptions::default());
925        assert_eq!(new_ts, 0);
926    }
927
928    // ── no-op ─────────────────────────────────────────────────────────────────
929
930    #[test]
931    fn test_no_transform_leaves_data_unchanged() {
932        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &[0xAA; 10]);
933        let original = data.clone();
934        let origlen = data.len() as u32;
935        let (new_ts, new_caplen, new_origlen) =
936            apply(&mut data, 42, 0, origlen, &TransformOptions::default());
937        assert_eq!(new_ts, 42);
938        assert_eq!(new_caplen, origlen);
939        assert_eq!(new_origlen, origlen);
940        assert_eq!(data, original);
941    }
942
943    // ── Same-family IP mapping ────────────────────────────────────────────────
944
945    #[test]
946    fn test_ip_mapping_replaces_src_ip() {
947        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &[0u8; 4]);
948        let origlen = data.len() as u32;
949        let opts = TransformOptions {
950            ip_map: vec![parse_ip_mapping("10.0.0.1=192.168.1.1").unwrap()],
951            ..Default::default()
952        };
953        apply(&mut data, 0, 0, origlen, &opts);
954        // IPv4 src IP: Ethernet(14) + IP offset 12 = byte 26
955        assert_eq!(&data[26..30], &[192, 168, 1, 1]);
956        assert_eq!(&data[30..34], &[8, 8, 8, 8]); // dst unchanged
957    }
958
959    #[test]
960    fn test_ip_mapping_replaces_dst_ip() {
961        let mut data = eth_ipv4_udp([1, 2, 3, 4], [10, 0, 0, 2], 1234, 53, &[0u8; 4]);
962        let origlen = data.len() as u32;
963        let opts = TransformOptions {
964            ip_map: vec![parse_ip_mapping("10.0.0.2=172.16.0.1").unwrap()],
965            ..Default::default()
966        };
967        apply(&mut data, 0, 0, origlen, &opts);
968        assert_eq!(&data[30..34], &[172, 16, 0, 1]);
969    }
970
971    #[test]
972    fn test_ip_mapping_updates_ipv4_header_checksum() {
973        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &[0u8; 4]);
974        let origlen = data.len() as u32;
975        let opts = TransformOptions {
976            ip_map: vec![parse_ip_mapping("10.0.0.1=192.168.1.1").unwrap()],
977            ..Default::default()
978        };
979        apply(&mut data, 0, 0, origlen, &opts);
980        // A valid header: one's complement sum of all words = 0xFFFF → !0xFFFF = 0x0000.
981        let ihl = ((data[14] & 0x0F) * 4) as usize;
982        assert_eq!(
983            internet_checksum(&data[14..14 + ihl]),
984            0x0000,
985            "IPv4 header checksum must be valid after IP mapping"
986        );
987    }
988
989    #[test]
990    fn test_ip_mapping_no_match_leaves_data_unchanged() {
991        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &[0xBB; 4]);
992        let original = data.clone();
993        let origlen = data.len() as u32;
994        // Mapping for an IP not present in the packet.
995        let opts = TransformOptions {
996            ip_map: vec![parse_ip_mapping("10.0.0.99=10.0.0.1").unwrap()],
997            ..Default::default()
998        };
999        apply(&mut data, 0, 0, origlen, &opts);
1000        assert_eq!(data, original);
1001    }
1002
1003    // ── Cross-family reframe: IPv4 → IPv6 ────────────────────────────────────
1004
1005    #[test]
1006    fn test_cross_family_ipv4_to_ipv6_src_mapped() {
1007        // IPv4 packet, src=10.0.0.1, dst=8.8.8.8
1008        // Mapping: 10.0.0.1 → 2001:db8::1 (v4→v6)
1009        // Expected: IPv6 packet, src=2001:db8::1, dst=::ffff:8.8.8.8
1010        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &[0xAB; 4]);
1011        let origlen = data.len() as u32;
1012        let opts = TransformOptions {
1013            ip_map: vec![parse_ip_mapping("10.0.0.1=2001:db8::1").unwrap()],
1014            ..Default::default()
1015        };
1016        let (_, new_caplen, new_origlen) = apply(&mut data, 0, 0, origlen, &opts);
1017
1018        // EtherType must be IPv6.
1019        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV6);
1020        // IPv6 header: version nibble.
1021        assert_eq!(data[14] >> 4, 6);
1022        // src address at offset 22: must be 2001:db8::1
1023        let src_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[22..38]).unwrap());
1024        assert_eq!(src_v6, "2001:db8::1".parse::<Ipv6Addr>().unwrap());
1025        // dst address at offset 38: must be ::ffff:8.8.8.8
1026        let dst_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[38..54]).unwrap());
1027        assert_eq!(dst_v6, "::ffff:8.8.8.8".parse::<Ipv6Addr>().unwrap());
1028        // Payload is preserved: 4 bytes 0xAB at the end.
1029        assert_eq!(&data[data.len() - 4..], &[0xAB; 4]);
1030        // Lengths were updated.
1031        assert_eq!(new_caplen, data.len() as u32);
1032        assert_eq!(new_origlen, data.len() as u32);
1033    }
1034
1035    #[test]
1036    fn test_cross_family_ipv4_to_ipv6_dst_mapped() {
1037        // IPv4 packet, src=1.2.3.4, dst=10.0.0.2
1038        // Mapping: 10.0.0.2 → ::1 (v4→v6)
1039        // Expected: IPv6 packet, src=::ffff:1.2.3.4, dst=::1
1040        let mut data = eth_ipv4_udp([1, 2, 3, 4], [10, 0, 0, 2], 5000, 80, &[]);
1041        let origlen = data.len() as u32;
1042        let opts = TransformOptions {
1043            ip_map: vec![parse_ip_mapping("10.0.0.2=::1").unwrap()],
1044            ..Default::default()
1045        };
1046        apply(&mut data, 0, 0, origlen, &opts);
1047
1048        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV6);
1049        let src_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[22..38]).unwrap());
1050        assert_eq!(src_v6, "::ffff:1.2.3.4".parse::<Ipv6Addr>().unwrap());
1051        let dst_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[38..54]).unwrap());
1052        assert_eq!(dst_v6, "::1".parse::<Ipv6Addr>().unwrap());
1053    }
1054
1055    #[test]
1056    fn test_cross_family_ipv4_to_ipv6_both_mapped() {
1057        // Both src and dst are mapped to IPv6 addresses.
1058        let mut data = eth_ipv4_udp([10, 0, 0, 1], [10, 0, 0, 2], 1000, 2000, &[0xCC; 8]);
1059        let origlen = data.len() as u32;
1060        let opts = TransformOptions {
1061            ip_map: vec![
1062                parse_ip_mapping("10.0.0.1=2001:db8::1").unwrap(),
1063                parse_ip_mapping("10.0.0.2=2001:db8::2").unwrap(),
1064            ],
1065            ..Default::default()
1066        };
1067        apply(&mut data, 0, 0, origlen, &opts);
1068
1069        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV6);
1070        let src_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[22..38]).unwrap());
1071        let dst_v6: Ipv6Addr = Ipv6Addr::from(<[u8; 16]>::try_from(&data[38..54]).unwrap());
1072        assert_eq!(src_v6, "2001:db8::1".parse::<Ipv6Addr>().unwrap());
1073        assert_eq!(dst_v6, "2001:db8::2".parse::<Ipv6Addr>().unwrap());
1074    }
1075
1076    #[test]
1077    fn test_cross_family_ipv4_to_ipv6_checksum_valid() {
1078        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &[0xDE; 16]);
1079        let origlen = data.len() as u32;
1080        let opts = TransformOptions {
1081            ip_map: vec![parse_ip_mapping("10.0.0.1=2001:db8::1").unwrap()],
1082            ..Default::default()
1083        };
1084        apply(&mut data, 0, 0, origlen, &opts);
1085
1086        // Verify that the UDP checksum in the IPv6 packet is non-zero (was computed).
1087        // Transport starts at byte 54 (14 Ethernet + 40 IPv6).
1088        let udp_csum = u16::from_be_bytes([data[54 + 6], data[54 + 7]]);
1089        assert_ne!(udp_csum, 0, "UDP checksum must be set in IPv6 packet");
1090    }
1091
1092    #[test]
1093    fn test_cross_family_ipv4_to_ipv6_size_change() {
1094        // IPv4 → IPv6 adds (40 - 20) = 20 bytes to the IP header.
1095        let payload = [0u8; 10];
1096        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1000, 2000, &payload);
1097        let ipv4_len = data.len();
1098        let origlen = data.len() as u32;
1099        let opts = TransformOptions {
1100            ip_map: vec![parse_ip_mapping("10.0.0.1=::1").unwrap()],
1101            ..Default::default()
1102        };
1103        let (_, new_caplen, new_origlen) = apply(&mut data, 0, 0, origlen, &opts);
1104
1105        // 14 (Eth) + 40 (IPv6) + 8 (UDP) + 10 (payload) = 72
1106        // Original: 14 (Eth) + 20 (IPv4) + 8 (UDP) + 10 (payload) = 52
1107        assert_eq!(data.len(), ipv4_len + 20);
1108        assert_eq!(new_caplen, data.len() as u32);
1109        assert_eq!(new_origlen, data.len() as u32);
1110    }
1111
1112    // ── Cross-family reframe: IPv6 → IPv4 ────────────────────────────────────
1113
1114    #[test]
1115    fn test_cross_family_ipv6_to_ipv4_src_mapped() {
1116        // IPv6 packet, src=2001:db8::1, dst=::ffff:8.8.8.8
1117        // Mapping: 2001:db8::1 → 10.0.0.1 (v6→v4)
1118        let src_v6: [u8; 16] = "2001:db8::1".parse::<Ipv6Addr>().unwrap().octets();
1119        let dst_v6: [u8; 16] = "::ffff:8.8.8.8".parse::<Ipv6Addr>().unwrap().octets();
1120        let mut data = eth_ipv6_udp(src_v6, dst_v6, 5000, 80, &[0xBB; 4]);
1121        let origlen = data.len() as u32;
1122        let opts = TransformOptions {
1123            ip_map: vec![parse_ip_mapping("2001:db8::1=10.0.0.1").unwrap()],
1124            ..Default::default()
1125        };
1126        apply(&mut data, 0, 0, origlen, &opts);
1127
1128        // EtherType must be IPv4.
1129        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV4);
1130        assert_eq!(data[14] >> 4, 4);
1131        // src IP at offset 26: 10.0.0.1
1132        assert_eq!(&data[26..30], &[10, 0, 0, 1]);
1133        // dst IP at offset 30: 8.8.8.8 (extracted from ::ffff:8.8.8.8)
1134        assert_eq!(&data[30..34], &[8, 8, 8, 8]);
1135        // Payload preserved.
1136        assert_eq!(&data[data.len() - 4..], &[0xBB; 4]);
1137    }
1138
1139    #[test]
1140    fn test_cross_family_ipv6_to_ipv4_dst_mapped() {
1141        // IPv6 packet, src=::ffff:1.2.3.4, dst=2001:db8::2
1142        // Mapping: 2001:db8::2 → 192.168.1.2 (v6→v4)
1143        let src_v6: [u8; 16] = "::ffff:1.2.3.4".parse::<Ipv6Addr>().unwrap().octets();
1144        let dst_v6: [u8; 16] = "2001:db8::2".parse::<Ipv6Addr>().unwrap().octets();
1145        let mut data = eth_ipv6_udp(src_v6, dst_v6, 1234, 443, &[]);
1146        let origlen = data.len() as u32;
1147        let opts = TransformOptions {
1148            ip_map: vec![parse_ip_mapping("2001:db8::2=192.168.1.2").unwrap()],
1149            ..Default::default()
1150        };
1151        apply(&mut data, 0, 0, origlen, &opts);
1152
1153        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV4);
1154        assert_eq!(&data[26..30], &[1, 2, 3, 4]); // extracted from ::ffff:1.2.3.4
1155        assert_eq!(&data[30..34], &[192, 168, 1, 2]);
1156    }
1157
1158    #[test]
1159    fn test_cross_family_ipv6_to_ipv4_skipped_when_non_mapped_addr() {
1160        // IPv6 packet where dst is a native IPv6 addr (not IPv4-mapped) and
1161        // not covered by any mapping → reframe must be skipped.
1162        let src_v6: [u8; 16] = "2001:db8::1".parse::<Ipv6Addr>().unwrap().octets();
1163        let dst_v6: [u8; 16] = "2001:db8::2".parse::<Ipv6Addr>().unwrap().octets();
1164        let mut data = eth_ipv6_udp(src_v6, dst_v6, 1000, 2000, &[0xFF; 4]);
1165        let original = data.clone();
1166        let origlen = data.len() as u32;
1167        // Mapping only covers src; dst is a native IPv6 addr with no mapping → skip.
1168        let opts = TransformOptions {
1169            ip_map: vec![parse_ip_mapping("2001:db8::1=10.0.0.1").unwrap()],
1170            ..Default::default()
1171        };
1172        apply(&mut data, 0, 0, origlen, &opts);
1173        // Packet must be unchanged (reframe was skipped).
1174        assert_eq!(data, original);
1175    }
1176
1177    #[test]
1178    fn test_cross_family_ipv6_to_ipv4_checksum_valid() {
1179        let src_v6: [u8; 16] = "2001:db8::1".parse::<Ipv6Addr>().unwrap().octets();
1180        let dst_v6: [u8; 16] = "::ffff:8.8.8.8".parse::<Ipv6Addr>().unwrap().octets();
1181        let mut data = eth_ipv6_udp(src_v6, dst_v6, 5000, 53, &[0xDE; 8]);
1182        let origlen = data.len() as u32;
1183        let opts = TransformOptions {
1184            ip_map: vec![parse_ip_mapping("2001:db8::1=10.0.0.1").unwrap()],
1185            ..Default::default()
1186        };
1187        apply(&mut data, 0, 0, origlen, &opts);
1188
1189        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV4);
1190        let ihl = ((data[14] & 0x0F) * 4) as usize;
1191        assert_eq!(
1192            internet_checksum(&data[14..14 + ihl]),
1193            0x0000,
1194            "IPv4 header checksum must be valid after v6→v4 reframe"
1195        );
1196    }
1197
1198    #[test]
1199    fn test_cross_family_ipv6_to_ipv4_size_change() {
1200        // IPv6 → IPv4 removes 20 bytes (40-byte header → 20-byte header).
1201        let src_v6: [u8; 16] = "2001:db8::1".parse::<Ipv6Addr>().unwrap().octets();
1202        let dst_v6: [u8; 16] = "::ffff:8.8.8.8".parse::<Ipv6Addr>().unwrap().octets();
1203        let mut data = eth_ipv6_udp(src_v6, dst_v6, 1000, 2000, &[0u8; 10]);
1204        let ipv6_len = data.len();
1205        let origlen = data.len() as u32;
1206        let opts = TransformOptions {
1207            ip_map: vec![parse_ip_mapping("2001:db8::1=10.0.0.1").unwrap()],
1208            ..Default::default()
1209        };
1210        let (_, new_caplen, new_origlen) = apply(&mut data, 0, 0, origlen, &opts);
1211        assert_eq!(data.len(), ipv6_len - 20);
1212        assert_eq!(new_caplen, data.len() as u32);
1213        assert_eq!(new_origlen, data.len() as u32);
1214    }
1215
1216    // ── Payload truncation ────────────────────────────────────────────────────
1217
1218    #[test]
1219    fn test_truncation_udp_updates_lengths() {
1220        let payload = vec![0xBB; 100];
1221        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &payload);
1222        let orig = data.len() as u32;
1223        let opts = TransformOptions {
1224            max_payload_bytes: Some(10),
1225            ..Default::default()
1226        };
1227        let (_, new_caplen, new_origlen) = apply(&mut data, 0, 0, orig, &opts);
1228
1229        // Ethernet(14) + IPv4(20) + UDP(8) + 10 = 52
1230        assert_eq!(data.len(), 52);
1231        assert_eq!(new_caplen, 52);
1232        assert_eq!(new_origlen, 52);
1233
1234        // IPv4 total length: 20 + 8 + 10 = 38
1235        let ip_total = u16::from_be_bytes([data[16], data[17]]);
1236        assert_eq!(ip_total, 38, "IPv4 total length not updated");
1237
1238        // UDP length field is at offset 4 within the UDP header (transport_start=34).
1239        // 8 + 10 = 18
1240        let udp_len = u16::from_be_bytes([data[38], data[39]]);
1241        assert_eq!(udp_len, 18, "UDP length not updated");
1242    }
1243
1244    #[test]
1245    fn test_truncation_tcp_updates_ip_length() {
1246        let payload = vec![0xCC; 50];
1247        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 443, &payload);
1248        let orig = data.len() as u32;
1249        let opts = TransformOptions {
1250            max_payload_bytes: Some(5),
1251            ..Default::default()
1252        };
1253        apply(&mut data, 0, 0, orig, &opts);
1254
1255        // Ethernet(14) + IPv4(20) + TCP(20) + 5 = 59
1256        assert_eq!(data.len(), 59);
1257        // IPv4 total: 20 + 20 + 5 = 45
1258        let ip_total = u16::from_be_bytes([data[16], data[17]]);
1259        assert_eq!(ip_total, 45);
1260    }
1261
1262    #[test]
1263    fn test_truncation_noop_when_short_enough() {
1264        let payload = vec![0xAA; 5];
1265        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &payload);
1266        let original = data.clone();
1267        let orig = data.len() as u32;
1268        let opts = TransformOptions {
1269            max_payload_bytes: Some(100),
1270            ..Default::default()
1271        };
1272        apply(&mut data, 0, 0, orig, &opts);
1273        assert_eq!(data, original);
1274    }
1275
1276    #[test]
1277    fn test_truncation_checksums_valid() {
1278        let payload = vec![0xDE; 50];
1279        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &payload);
1280        let orig = data.len() as u32;
1281        let opts = TransformOptions {
1282            max_payload_bytes: Some(8),
1283            ..Default::default()
1284        };
1285        apply(&mut data, 0, 0, orig, &opts);
1286        let ihl = ((data[14] & 0x0F) * 4) as usize;
1287        assert_eq!(
1288            internet_checksum(&data[14..14 + ihl]),
1289            0x0000,
1290            "IPv4 header checksum must be valid after truncation"
1291        );
1292    }
1293
1294    #[test]
1295    fn test_truncation_zero_payload_bytes() {
1296        // max_payload_bytes=0 keeps only headers, no payload.
1297        let payload = vec![0xFF; 20];
1298        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 200, &payload);
1299        let orig = data.len() as u32;
1300        let opts = TransformOptions {
1301            max_payload_bytes: Some(0),
1302            ..Default::default()
1303        };
1304        apply(&mut data, 0, 0, orig, &opts);
1305        // Ethernet(14) + IPv4(20) + UDP(8) = 42
1306        assert_eq!(data.len(), 42);
1307    }
1308
1309    // ── Per-protocol truncation ───────────────────────────────────────────────
1310
1311    #[test]
1312    fn test_proto_truncation_tcp_uses_rule() {
1313        let payload = vec![0xAA; 100];
1314        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 443, &payload);
1315        let orig = data.len() as u32;
1316        let opts = TransformOptions {
1317            proto_truncation: vec![ProtocolTruncation {
1318                proto: PROTO_TCP,
1319                max_payload_bytes: 10,
1320            }],
1321            ..Default::default()
1322        };
1323        apply(&mut data, 0, 0, orig, &opts);
1324        // Ethernet(14) + IPv4(20) + TCP(20) + 10 = 64
1325        assert_eq!(data.len(), 64);
1326    }
1327
1328    #[test]
1329    fn test_proto_truncation_udp_uses_rule() {
1330        let payload = vec![0xBB; 80];
1331        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 53, &payload);
1332        let orig = data.len() as u32;
1333        let opts = TransformOptions {
1334            proto_truncation: vec![ProtocolTruncation {
1335                proto: PROTO_UDP,
1336                max_payload_bytes: 8,
1337            }],
1338            ..Default::default()
1339        };
1340        apply(&mut data, 0, 0, orig, &opts);
1341        // Ethernet(14) + IPv4(20) + UDP(8) + 8 = 50
1342        assert_eq!(data.len(), 50);
1343    }
1344
1345    #[test]
1346    fn test_proto_truncation_overrides_global() {
1347        // TCP rule (10 bytes) should win over global limit (50 bytes).
1348        let payload = vec![0xCC; 100];
1349        let mut data = eth_ipv4_tcp([1, 2, 3, 4], [5, 6, 7, 8], 100, 80, &payload);
1350        let orig = data.len() as u32;
1351        let opts = TransformOptions {
1352            max_payload_bytes: Some(50),
1353            proto_truncation: vec![ProtocolTruncation {
1354                proto: PROTO_TCP,
1355                max_payload_bytes: 10,
1356            }],
1357            ..Default::default()
1358        };
1359        apply(&mut data, 0, 0, orig, &opts);
1360        // Ethernet(14) + IPv4(20) + TCP(20) + 10 = 64
1361        assert_eq!(data.len(), 64);
1362    }
1363
1364    #[test]
1365    fn test_proto_truncation_fallback_to_global() {
1366        // No rule for UDP → falls back to global 20 bytes.
1367        let payload = vec![0xDD; 80];
1368        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 53, &payload);
1369        let orig = data.len() as u32;
1370        let opts = TransformOptions {
1371            max_payload_bytes: Some(20),
1372            proto_truncation: vec![ProtocolTruncation {
1373                proto: PROTO_TCP, // TCP rule only, no UDP rule
1374                max_payload_bytes: 5,
1375            }],
1376            ..Default::default()
1377        };
1378        apply(&mut data, 0, 0, orig, &opts);
1379        // Ethernet(14) + IPv4(20) + UDP(8) + 20 = 62
1380        assert_eq!(data.len(), 62);
1381    }
1382
1383    #[test]
1384    fn test_proto_truncation_no_match_no_global_no_truncation() {
1385        // UDP packet, only a TCP rule, no global — no truncation applied.
1386        let payload = vec![0xEE; 50];
1387        let mut data = eth_ipv4_udp([1, 2, 3, 4], [5, 6, 7, 8], 100, 53, &payload);
1388        let original = data.clone();
1389        let orig = data.len() as u32;
1390        let opts = TransformOptions {
1391            proto_truncation: vec![ProtocolTruncation {
1392                proto: PROTO_TCP,
1393                max_payload_bytes: 10,
1394            }],
1395            ..Default::default()
1396        };
1397        apply(&mut data, 0, 0, orig, &opts);
1398        assert_eq!(data, original);
1399    }
1400
1401    // ── Combined transforms ───────────────────────────────────────────────────
1402
1403    #[test]
1404    fn test_ip_mapping_and_truncation_combined() {
1405        let payload = vec![0xAB; 80];
1406        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &payload);
1407        let orig = data.len() as u32;
1408        let opts = TransformOptions {
1409            ip_map: vec![parse_ip_mapping("10.0.0.1=192.168.99.1").unwrap()],
1410            max_payload_bytes: Some(16),
1411            ..Default::default()
1412        };
1413        apply(&mut data, 0, 0, orig, &opts);
1414
1415        // IP was replaced.
1416        assert_eq!(&data[26..30], &[192, 168, 99, 1]);
1417        // Length was truncated: 14 + 20 + 8 + 16 = 58.
1418        assert_eq!(data.len(), 58);
1419        // Header checksum is valid (word sum = 0xFFFF → !0xFFFF = 0x0000).
1420        let ihl = ((data[14] & 0x0F) * 4) as usize;
1421        assert_eq!(internet_checksum(&data[14..14 + ihl]), 0x0000);
1422    }
1423
1424    #[test]
1425    fn test_cross_family_and_truncation_combined() {
1426        // IPv4 → IPv6 reframe, then truncate payload.
1427        let payload = vec![0xCD; 100];
1428        let mut data = eth_ipv4_udp([10, 0, 0, 1], [8, 8, 8, 8], 1234, 53, &payload);
1429        let orig = data.len() as u32;
1430        let opts = TransformOptions {
1431            ip_map: vec![parse_ip_mapping("10.0.0.1=2001:db8::1").unwrap()],
1432            max_payload_bytes: Some(10),
1433            ..Default::default()
1434        };
1435        apply(&mut data, 0, 0, orig, &opts);
1436
1437        // Must be an IPv6 packet.
1438        assert_eq!(u16::from_be_bytes([data[12], data[13]]), ET_IPV6);
1439        // Ethernet(14) + IPv6(40) + UDP(8) + 10 = 72
1440        assert_eq!(data.len(), 72);
1441    }
1442}