Skip to main content

yip_io/
tun_offload.rs

1//! Local TUN `virtio_net_hdr` (GSO/GRO) framing + the RX coalescer / TX splitter.
2//! Purely local to the yipd↔kernel-TUN boundary — never touches the wire.
3
4pub const VNET_HDR_LEN: usize = 10;
5pub const GSO_NONE: u8 = 0;
6pub const GSO_TCPV4: u8 = 1;
7pub const GSO_TCPV6: u8 = 4;
8pub const GSO_UDP_L4: u8 = 5;
9pub const F_NEEDS_CSUM: u8 = 1;
10pub const F_DATA_VALID: u8 = 2;
11
12#[repr(C)]
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub struct VirtioNetHdr {
15    pub flags: u8,
16    pub gso_type: u8,
17    pub hdr_len: u16,
18    pub gso_size: u16,
19    pub csum_start: u16,
20    pub csum_offset: u16,
21}
22
23pub type VnetHdr = VirtioNetHdr;
24
25/// Slices a GSO super-packet payload into segments of at most `mss` bytes.
26///
27/// Returns an empty vector if `buf` is empty. If `mss == 0`, returns a single slice containing `buf`.
28pub fn slice_gso_payload(buf: &[u8], mss: usize) -> Vec<&[u8]> {
29    if buf.is_empty() {
30        return Vec::new();
31    }
32    if mss == 0 {
33        return vec![buf];
34    }
35    buf.chunks(mss).collect()
36}
37
38/// Slices a GSO super-packet into segments of at most `mss` bytes.
39#[inline]
40pub fn slice_gso_packet(buf: &[u8], mss: usize) -> Vec<&[u8]> {
41    slice_gso_payload(buf, mss)
42}
43
44pub fn read_vnet_hdr(buf: &[u8]) -> Option<VirtioNetHdr> {
45    if buf.len() < VNET_HDR_LEN {
46        return None;
47    }
48    let u16h = |a: usize| u16::from_ne_bytes([buf[a], buf[a + 1]]);
49    Some(VirtioNetHdr {
50        flags: buf[0],
51        gso_type: buf[1],
52        hdr_len: u16h(2),
53        gso_size: u16h(4),
54        csum_start: u16h(6),
55        csum_offset: u16h(8),
56    })
57}
58
59pub fn write_vnet_hdr(h: &VirtioNetHdr, out: &mut [u8]) {
60    assert!(out.len() >= VNET_HDR_LEN);
61    out[0] = h.flags;
62    out[1] = h.gso_type;
63    out[2..4].copy_from_slice(&h.hdr_len.to_ne_bytes());
64    out[4..6].copy_from_slice(&h.gso_size.to_ne_bytes());
65    out[6..8].copy_from_slice(&h.csum_start.to_ne_bytes());
66    out[8..10].copy_from_slice(&h.csum_offset.to_ne_bytes());
67}
68
69pub(crate) const TCP_FLAG_FIN: u8 = 0x01;
70pub(crate) const TCP_FLAG_RST: u8 = 0x04;
71pub(crate) const TCP_FLAG_PSH: u8 = 0x08;
72pub(crate) const TCP_FLAG_URG: u8 = 0x20;
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub(crate) struct FlowKey {
76    pub src: [u8; 4],
77    pub dst: [u8; 4],
78    pub sport: u16,
79    pub dport: u16,
80}
81
82pub(crate) struct Ipv4Tcp<'a> {
83    pub ip_hdr_len: usize,
84    #[cfg_attr(
85        not(test),
86        expect(
87            dead_code,
88            reason = "read only by parse_ipv4_tcp_extracts_fields; split_gro/Coalescer derive \
89                      the segment header length from payload_off instead"
90        )
91    )]
92    pub tcp_hdr_len: usize,
93    pub total_len: usize,
94    pub key: FlowKey,
95    pub seq: u32,
96    pub flags: u8,
97    pub payload_off: usize,
98    pub payload_len: usize,
99    pub bytes: &'a [u8],
100}
101
102pub(crate) fn parse_ipv4_tcp(pkt: &[u8]) -> Option<Ipv4Tcp<'_>> {
103    if pkt.len() < 20 || (pkt[0] >> 4) != 4 {
104        return None;
105    }
106    let ihl = usize::from(pkt[0] & 0x0F) * 4;
107    if ihl < 20 || pkt.len() < ihl {
108        return None;
109    }
110    if pkt[9] != 6 {
111        return None;
112    } // not TCP
113      // fragmentation: MF (bit) or non-zero frag offset in bytes 6..8
114    let frag = u16::from_be_bytes([pkt[6], pkt[7]]);
115    if (frag & 0x2000) != 0 || (frag & 0x1FFF) != 0 {
116        return None;
117    }
118    let total_len = usize::from(u16::from_be_bytes([pkt[2], pkt[3]]));
119    if total_len < ihl || pkt.len() < total_len {
120        return None;
121    }
122    if total_len < ihl + 20 {
123        return None;
124    }
125    let t = ihl;
126    let data_off = usize::from(pkt[t + 12] >> 4) * 4;
127    if data_off < 20 || total_len < ihl + data_off {
128        return None;
129    }
130    let sport = u16::from_be_bytes([pkt[t], pkt[t + 1]]);
131    let dport = u16::from_be_bytes([pkt[t + 2], pkt[t + 3]]);
132    let seq = u32::from_be_bytes([pkt[t + 4], pkt[t + 5], pkt[t + 6], pkt[t + 7]]);
133    let flags = pkt[t + 13];
134    let payload_off = ihl + data_off;
135    Some(Ipv4Tcp {
136        ip_hdr_len: ihl,
137        tcp_hdr_len: data_off,
138        total_len,
139        key: FlowKey {
140            src: [pkt[12], pkt[13], pkt[14], pkt[15]],
141            dst: [pkt[16], pkt[17], pkt[18], pkt[19]],
142            sport,
143            dport,
144        },
145        seq,
146        flags,
147        payload_off,
148        payload_len: total_len - payload_off,
149        bytes: pkt,
150    })
151}
152
153pub(crate) fn ipv4_checksum(hdr: &mut [u8]) {
154    hdr[10] = 0;
155    hdr[11] = 0;
156    let mut sum: u32 = 0;
157    for c in hdr.chunks(2) {
158        let word = if c.len() == 2 {
159            u16::from_be_bytes([c[0], c[1]])
160        } else {
161            u16::from(c[0]) << 8
162        };
163        sum += u32::from(word);
164    }
165    while (sum >> 16) != 0 {
166        sum = (sum & 0xFFFF) + (sum >> 16);
167    }
168    let ck = !u16::try_from(sum & 0xFFFF).expect("folded sum fits u16");
169    hdr[10..12].copy_from_slice(&ck.to_be_bytes());
170}
171
172pub(crate) fn tcp_checksum(pkt: &mut [u8], ip_hdr_len: usize) {
173    let total = pkt.len();
174    let tcp_off = ip_hdr_len;
175    // zero the checksum field
176    pkt[tcp_off + 16] = 0;
177    pkt[tcp_off + 17] = 0;
178    let tcp_len = total - tcp_off;
179    let mut sum: u32 = 0;
180    // pseudo-header: src(4)+dst(4)+zero+proto(6)+tcp_len
181    for i in (12..20).step_by(2) {
182        sum += u32::from(u16::from_be_bytes([pkt[i], pkt[i + 1]]));
183    }
184    sum += u32::from(6u16);
185    sum += u32::try_from(tcp_len).expect("tcp_len fits u32") & 0xFFFF;
186    for c in pkt[tcp_off..total].chunks(2) {
187        let w = if c.len() == 2 {
188            u16::from_be_bytes([c[0], c[1]])
189        } else {
190            u16::from(c[0]) << 8
191        };
192        sum += u32::from(w);
193    }
194    while (sum >> 16) != 0 {
195        sum = (sum & 0xFFFF) + (sum >> 16);
196    }
197    let ck = !u16::try_from(sum & 0xFFFF).expect("folded fits u16");
198    pkt[tcp_off + 16..tcp_off + 18].copy_from_slice(&ck.to_be_bytes());
199}
200
201/// Complete a partial (`F_NEEDS_CSUM`) L4 checksum in `pkt` (an L3 packet) as
202/// delivered by a vnet_hdr TUN read. The 16-bit field at `csum_start +
203/// csum_offset` holds the pseudo-header partial sum; the final checksum is the
204/// one's-complement of the folded internet-checksum sum over `pkt[csum_start..]`
205/// (which includes that partial field). A computed 0 on a UDP packet (proto 17)
206/// is transmitted as `0xFFFF` per RFC 768. Malformed offsets are left untouched.
207fn complete_partial_csum(pkt: &mut [u8], csum_start: usize, csum_offset: usize) {
208    let field = csum_start + csum_offset;
209    if csum_start >= pkt.len() || field + 2 > pkt.len() {
210        return;
211    }
212    let region = &pkt[csum_start..];
213    let mut sum: u32 = 0;
214    let mut i = 0;
215    while i + 1 < region.len() {
216        sum += u32::from(u16::from_be_bytes([region[i], region[i + 1]]));
217        i += 2;
218    }
219    if i < region.len() {
220        sum += u32::from(u16::from(region[i]) << 8);
221    }
222    while (sum >> 16) != 0 {
223        sum = (sum & 0xFFFF) + (sum >> 16);
224    }
225    let mut ck = !u16::try_from(sum & 0xFFFF).expect("folded sum fits u16");
226    if ck == 0 && pkt.get(9) == Some(&17) {
227        ck = 0xFFFF; // UDP: 0 means "no checksum", the real zero is 0xFFFF
228    }
229    pkt[field..field + 2].copy_from_slice(&ck.to_be_bytes());
230}
231
232pub fn split_gro(frame: &[u8], out: &mut Vec<u8>, offsets: &mut Vec<(usize, usize)>) -> bool {
233    out.clear();
234    offsets.clear();
235    let Some(h) = read_vnet_hdr(frame) else {
236        return false;
237    };
238    let body = &frame[VNET_HDR_LEN..];
239    if h.gso_type == GSO_NONE || h.gso_size == 0 {
240        let start = out.len();
241        out.extend_from_slice(body);
242        // A vnet_hdr read with F_NEEDS_CSUM carries an *incomplete* L4 checksum
243        // (the kernel offloaded it): the 16-bit field at csum_start+csum_offset
244        // holds the pseudo-header partial. We complete it here, because this
245        // packet is about to be encrypted and shipped over the wire — the far
246        // end writes it back to its TUN as a plain (F_NEEDS_CSUM=0) frame and its
247        // kernel/apps expect a valid checksum. Without this, large offloaded
248        // packets (e.g. bulk transfers) arrive with a bad checksum and are
249        // silently dropped, while tiny packets the kernel checksums in full
250        // (e.g. pings) pass — the classic vnet_hdr read-side gotcha.
251        if (h.flags & F_NEEDS_CSUM) != 0 {
252            let len = out.len() - start;
253            complete_partial_csum(
254                &mut out[start..start + len],
255                usize::from(h.csum_start),
256                usize::from(h.csum_offset),
257            );
258        }
259        offsets.push((start, body.len()));
260        return true;
261    }
262    let Some(x) = parse_ipv4_tcp(body) else {
263        return false;
264    };
265    let seg = usize::from(h.gso_size);
266    let hdr = x.payload_off; // ip_hdr_len + tcp_hdr_len
267    let payload = &body[x.payload_off..x.payload_off + x.payload_len];
268    let mut seq = x.seq;
269    let mut ip_id = u16::from_be_bytes([body[4], body[5]]);
270    let mut off = 0usize;
271    while off < payload.len() {
272        let n = seg.min(payload.len() - off);
273        let start = out.len();
274        out.extend_from_slice(&body[..hdr]); // clone IP+TCP header
275        out.extend_from_slice(&payload[off..off + n]);
276        let s = &mut out[start..start + hdr + n];
277        // patch IP total-length, IP ID, TCP seq; recompute both checksums
278        let total = u16::try_from(hdr + n).expect("segment fits u16");
279        s[2..4].copy_from_slice(&total.to_be_bytes());
280        s[4..6].copy_from_slice(&ip_id.to_be_bytes());
281        s[x.ip_hdr_len + 4..x.ip_hdr_len + 8].copy_from_slice(&seq.to_be_bytes());
282        ipv4_checksum(&mut s[..x.ip_hdr_len]);
283        tcp_checksum(s, x.ip_hdr_len);
284        offsets.push((start, hdr + n));
285        seq = seq.wrapping_add(u32::try_from(n).expect("n fits u32"));
286        ip_id = ip_id.wrapping_add(1);
287        off += n;
288    }
289    true
290}
291
292pub const MAX_GSO_SEGMENTS: usize = 64;
293pub const MAX_GSO_PAYLOAD: usize = 65_535;
294
295pub struct Coalescer {
296    pending: Vec<u8>, // [vnet_hdr | ip+tcp hdr | payloads]; empty ⇒ nothing pending
297    out: Vec<u8>,     // holds a flushed frame for the returned borrow
298    has_pending: bool,
299    is_tcp_run: bool, // false ⇒ pending is a GSO_NONE singleton
300    sealed: bool,     // pending cannot be extended (PSH/FIN/non-TCP/no-payload)
301    key: FlowKey,
302    next_seq: u32,
303    gso_size: u16,
304    ip_hdr_len: usize,
305    l3_hdr_len: usize, // ip_hdr_len + tcp_hdr_len (payload offset within the L3 packet)
306    segs: usize,
307}
308
309impl Coalescer {
310    pub fn new() -> Self {
311        Self {
312            pending: Vec::with_capacity(MAX_GSO_PAYLOAD + 64),
313            out: Vec::with_capacity(MAX_GSO_PAYLOAD + 64),
314            has_pending: false,
315            is_tcp_run: false,
316            sealed: false,
317            key: FlowKey {
318                src: [0; 4],
319                dst: [0; 4],
320                sport: 0,
321                dport: 0,
322            },
323            next_seq: 0,
324            gso_size: 0,
325            ip_hdr_len: 0,
326            l3_hdr_len: 0,
327            segs: 0,
328        }
329    }
330
331    /// Finalize the pending run into `self.out` and return the borrow (or None).
332    fn take_pending(&mut self) -> Option<&[u8]> {
333        if !self.has_pending {
334            return None;
335        }
336        if self.is_tcp_run {
337            // patch IP total-length + IP checksum, then set the vnet_hdr.
338            let ip = VNET_HDR_LEN;
339            let l3_len = self.pending.len() - VNET_HDR_LEN;
340            let total = u16::try_from(l3_len).unwrap_or(u16::MAX);
341            self.pending[ip + 2..ip + 4].copy_from_slice(&total.to_be_bytes());
342            ipv4_checksum(&mut self.pending[ip..ip + self.ip_hdr_len]);
343            let h = VnetHdr {
344                flags: F_NEEDS_CSUM,
345                gso_type: GSO_TCPV4,
346                hdr_len: u16::try_from(self.l3_hdr_len).unwrap_or(0),
347                gso_size: self.gso_size,
348                csum_start: u16::try_from(self.ip_hdr_len).unwrap_or(0), // L4 offset within L3 frame
349                csum_offset: 16,                                         // TCP checksum offset
350            };
351            write_vnet_hdr(&h, &mut self.pending[..VNET_HDR_LEN]);
352        }
353        // (non-TCP singleton already has its GSO_NONE vnet_hdr written at start_singleton)
354        std::mem::swap(&mut self.pending, &mut self.out);
355        self.pending.clear();
356        self.has_pending = false;
357        Some(&self.out)
358    }
359
360    fn start_tcp_run(&mut self, x: &Ipv4Tcp<'_>, sealed: bool) {
361        self.pending.clear();
362        self.pending.resize(VNET_HDR_LEN, 0);
363        self.pending.extend_from_slice(&x.bytes[..x.total_len]); // full first L3 packet
364        self.has_pending = true;
365        self.is_tcp_run = true;
366        self.sealed = sealed;
367        self.key = x.key;
368        self.ip_hdr_len = x.ip_hdr_len;
369        self.l3_hdr_len = x.payload_off;
370        self.gso_size = u16::try_from(x.payload_len).unwrap_or(0);
371        self.next_seq = x
372            .seq
373            .wrapping_add(u32::try_from(x.payload_len).unwrap_or(0));
374        self.segs = 1;
375    }
376
377    fn start_singleton(&mut self, pkt: &[u8]) {
378        self.pending.clear();
379        self.pending.resize(VNET_HDR_LEN, 0);
380        write_vnet_hdr(
381            &VnetHdr {
382                gso_type: GSO_NONE,
383                ..VnetHdr::default()
384            },
385            &mut self.pending,
386        );
387        self.pending.extend_from_slice(pkt);
388        self.has_pending = true;
389        self.is_tcp_run = false;
390        self.sealed = true;
391    }
392
393    pub fn push(&mut self, pkt: &[u8]) -> Option<&[u8]> {
394        let parsed = parse_ipv4_tcp(pkt);
395        // Can this packet extend the current (open, tcp) pending run?
396        if self.has_pending && self.is_tcp_run && !self.sealed {
397            if let Some(x) = &parsed {
398                let cont = x.payload_len > 0
399                    && (x.flags & (TCP_FLAG_PSH | TCP_FLAG_FIN | TCP_FLAG_RST | TCP_FLAG_URG)) == 0
400                    && x.key == self.key
401                    && x.seq == self.next_seq
402                    && x.payload_off == self.l3_hdr_len
403                    && self.segs < MAX_GSO_SEGMENTS
404                    && (self.pending.len() - VNET_HDR_LEN - self.l3_hdr_len) + x.payload_len
405                        <= MAX_GSO_PAYLOAD;
406                if cont {
407                    self.pending
408                        .extend_from_slice(&x.bytes[x.payload_off..x.payload_off + x.payload_len]);
409                    self.next_seq = self
410                        .next_seq
411                        .wrapping_add(u32::try_from(x.payload_len).unwrap_or(0));
412                    self.segs += 1;
413                    return None;
414                }
415            }
416        }
417        // Cannot extend: flush the pending run (if any), then start a new one from `pkt`.
418        // Buffer the flushed frame's bytes so we can start the new run before returning it.
419        let flushed: Option<Vec<u8>> = self.take_pending().map(<[u8]>::to_vec);
420        match &parsed {
421            Some(x) if x.payload_len > 0 => {
422                let sealed =
423                    (x.flags & (TCP_FLAG_PSH | TCP_FLAG_FIN | TCP_FLAG_RST | TCP_FLAG_URG)) != 0;
424                self.start_tcp_run(x, sealed);
425            }
426            _ => self.start_singleton(pkt),
427        }
428        match flushed {
429            Some(f) => {
430                self.out = f;
431                Some(&self.out)
432            }
433            None => None,
434        }
435    }
436
437    pub fn flush(&mut self) -> Option<&[u8]> {
438        self.take_pending()
439    }
440}
441
442impl Default for Coalescer {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    // Internet-checksum fold of a 16-bit word sum over `data`.
453    fn csum16(data: &[u8]) -> u16 {
454        let mut sum: u32 = 0;
455        let mut i = 0;
456        while i + 1 < data.len() {
457            sum += u32::from(u16::from_be_bytes([data[i], data[i + 1]]));
458            i += 2;
459        }
460        if i < data.len() {
461            sum += u32::from(u16::from(data[i]) << 8);
462        }
463        while (sum >> 16) != 0 {
464            sum = (sum & 0xFFFF) + (sum >> 16);
465        }
466        u16::try_from(sum & 0xFFFF).unwrap()
467    }
468
469    #[test]
470    fn complete_partial_csum_matches_full_udp_checksum() {
471        // IPv4(20) + UDP(8) + payload, csum_start=20, csum_offset=6.
472        let payload = [0x11u8, 0x22, 0x33, 0x44, 0x55];
473        let udp_len = 8 + payload.len();
474        let total = 20 + udp_len;
475        let mut p = vec![0u8; total];
476        p[0] = 0x45;
477        p[2..4].copy_from_slice(&u16::try_from(total).unwrap().to_be_bytes());
478        p[8] = 64;
479        p[9] = 17; // UDP
480        p[12..16].copy_from_slice(&[10, 0, 0, 1]);
481        p[16..20].copy_from_slice(&[10, 0, 0, 2]);
482        p[20..22].copy_from_slice(&1111u16.to_be_bytes()); // sport
483        p[22..24].copy_from_slice(&2222u16.to_be_bytes()); // dport
484        p[24..26].copy_from_slice(&u16::try_from(udp_len).unwrap().to_be_bytes());
485        p[28..].copy_from_slice(&payload);
486
487        // Pseudo-header 16-bit sum: src + dst + proto + udp_len.
488        let mut pseudo = vec![0u8; 12];
489        pseudo[..4].copy_from_slice(&[10, 0, 0, 1]);
490        pseudo[4..8].copy_from_slice(&[10, 0, 0, 2]);
491        pseudo[9] = 17;
492        pseudo[10..12].copy_from_slice(&u16::try_from(udp_len).unwrap().to_be_bytes());
493        let pseudo_sum = csum16(&pseudo);
494
495        // Reference full UDP checksum (field zeroed): ~fold(pseudo + L4-with-zero-field).
496        let good = !{
497            let mut s = u32::from(pseudo_sum) + u32::from(csum16(&p[20..]));
498            while (s >> 16) != 0 {
499                s = (s & 0xFFFF) + (s >> 16);
500            }
501            u16::try_from(s & 0xFFFF).unwrap()
502        };
503
504        // Kernel delivers F_NEEDS_CSUM with the field pre-seeded to the pseudo sum.
505        p[20 + 6..20 + 8].copy_from_slice(&pseudo_sum.to_be_bytes());
506        complete_partial_csum(&mut p, 20, 6);
507        let completed = u16::from_be_bytes([p[26], p[27]]);
508        assert_eq!(
509            completed, good,
510            "completed checksum must equal the full UDP checksum"
511        );
512    }
513
514    fn mk_tcp(
515        src: [u8; 4],
516        dst: [u8; 4],
517        sport: u16,
518        dport: u16,
519        seq: u32,
520        flags: u8,
521        payload: &[u8],
522    ) -> Vec<u8> {
523        let ihl = 20usize;
524        let thl = 20usize;
525        let total = ihl + thl + payload.len();
526        let mut p = vec![0u8; total];
527        p[0] = 0x45; // v4, IHL=5
528        p[2..4].copy_from_slice(&(u16::try_from(total).unwrap()).to_be_bytes());
529        p[8] = 64;
530        p[9] = 6; // TTL, proto=TCP
531        p[12..16].copy_from_slice(&src);
532        p[16..20].copy_from_slice(&dst);
533        // TCP
534        let t = ihl;
535        p[t..t + 2].copy_from_slice(&sport.to_be_bytes());
536        p[t + 2..t + 4].copy_from_slice(&dport.to_be_bytes());
537        p[t + 4..t + 8].copy_from_slice(&seq.to_be_bytes());
538        p[t + 12] = 0x50; // data offset = 5 (20 bytes)
539        p[t + 13] = flags;
540        p[t + 20..].copy_from_slice(payload);
541        ipv4_checksum(&mut p[..ihl]);
542        p
543    }
544
545    #[test]
546    fn parse_ipv4_tcp_extracts_fields() {
547        let p = mk_tcp(
548            [10, 0, 0, 1],
549            [10, 0, 0, 2],
550            1234,
551            80,
552            1000,
553            TCP_FLAG_PSH,
554            b"hello",
555        );
556        let x = parse_ipv4_tcp(&p).expect("parse");
557        assert_eq!(
558            x.key,
559            FlowKey {
560                src: [10, 0, 0, 1],
561                dst: [10, 0, 0, 2],
562                sport: 1234,
563                dport: 80
564            }
565        );
566        assert_eq!(x.seq, 1000);
567        assert_eq!(x.flags & TCP_FLAG_PSH, TCP_FLAG_PSH);
568        assert_eq!(x.ip_hdr_len, 20);
569        assert_eq!(x.tcp_hdr_len, 20);
570        assert_eq!(x.payload_len, 5);
571        assert_eq!(
572            &x.bytes[x.payload_off..x.payload_off + x.payload_len],
573            b"hello"
574        );
575    }
576
577    #[test]
578    fn parse_rejects_non_tcp_and_fragments() {
579        let mut udp = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 1, 2, 0, 0, b"x");
580        udp[9] = 17; // proto=UDP
581        assert!(parse_ipv4_tcp(&udp).is_none());
582        let mut frag = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 1, 2, 0, 0, b"x");
583        frag[6] = 0x20; // MF set
584        assert!(parse_ipv4_tcp(&frag).is_none());
585    }
586
587    #[test]
588    fn ipv4_checksum_is_valid() {
589        let p = mk_tcp([1, 2, 3, 4], [5, 6, 7, 8], 1, 2, 0, 0, b"z");
590        // A correct IPv4 checksum makes the one's-complement sum of the header 0xFFFF.
591        let sum: u32 = p[..20]
592            .chunks(2)
593            .map(|c| u32::from(u16::from_be_bytes([c[0], c[1]])))
594            .sum();
595        let folded = u16::try_from((sum & 0xFFFF) + (sum >> 16)).expect("folded sum fits u16");
596        assert_eq!(folded, 0xFFFF);
597    }
598
599    #[test]
600    fn vnet_hdr_roundtrip() {
601        let h = VnetHdr {
602            flags: F_NEEDS_CSUM,
603            gso_type: GSO_TCPV4,
604            hdr_len: 40,
605            gso_size: 1400,
606            csum_start: 20,
607            csum_offset: 16,
608        };
609        let mut buf = [0u8; VNET_HDR_LEN];
610        write_vnet_hdr(&h, &mut buf);
611        let got = read_vnet_hdr(&buf).expect("parse");
612        assert_eq!(got.gso_type, GSO_TCPV4);
613        assert_eq!(got.gso_size, 1400);
614        assert_eq!(got.hdr_len, 40);
615        assert_eq!(got.csum_start, 20);
616        assert_eq!(got.csum_offset, 16);
617        assert_eq!(got.flags, F_NEEDS_CSUM);
618    }
619
620    #[test]
621    fn read_vnet_hdr_rejects_short() {
622        assert!(read_vnet_hdr(&[0u8; VNET_HDR_LEN - 1]).is_none());
623    }
624
625    // Helper: run a sequence of packets through a Coalescer, collecting every emitted frame
626    // (both push-flushes and the final flush), returned as owned Vecs.
627    fn run_coalescer(pkts: &[Vec<u8>]) -> Vec<Vec<u8>> {
628        let mut c = Coalescer::new();
629        let mut out = Vec::new();
630        for p in pkts {
631            if let Some(f) = c.push(p) {
632                out.push(f.to_vec());
633            }
634        }
635        if let Some(f) = c.flush() {
636            out.push(f.to_vec());
637        }
638        out
639    }
640
641    #[test]
642    fn coalesces_contiguous_same_flow() {
643        // three 100-byte segments, seq 0,100,200 — must merge into ONE super-frame.
644        let p0 = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 0, 0, &[0xAA; 100]);
645        let p1 = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 100, 0, &[0xBB; 100]);
646        let p2 = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 200, 0, &[0xCC; 100]);
647        let out = run_coalescer(&[p0, p1, p2]);
648        assert_eq!(out.len(), 1, "one coalesced super-frame");
649        let h = read_vnet_hdr(&out[0]).unwrap();
650        assert_eq!(h.gso_type, GSO_TCPV4);
651        assert_eq!(h.gso_size, 100);
652        // super-frame payload = 300 bytes after vnet_hdr + IP(20) + TCP(20)
653        assert_eq!(out[0].len(), VNET_HDR_LEN + 20 + 20 + 300);
654    }
655
656    #[test]
657    fn flushes_on_seq_gap() {
658        let p0 = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 0, 0, &[0; 100]);
659        let gap = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 500, 0, &[0; 100]); // non-contiguous
660        assert_eq!(run_coalescer(&[p0, gap]).len(), 2);
661    }
662
663    #[test]
664    fn flushes_on_flow_change() {
665        let a = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 0, 0, &[0; 100]);
666        let b = mk_tcp([10, 0, 0, 1], [10, 0, 0, 3], 9, 80, 0, 0, &[0; 100]); // different dst
667        assert_eq!(run_coalescer(&[a, b]).len(), 2);
668    }
669
670    #[test]
671    fn psh_and_fin_force_immediate_flush() {
672        let p0 = mk_tcp(
673            [10, 0, 0, 1],
674            [10, 0, 0, 2],
675            9,
676            80,
677            0,
678            TCP_FLAG_PSH,
679            &[0; 100],
680        );
681        let p1 = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 100, 0, &[0; 100]);
682        // p0 has PSH → emitted immediately as its own frame; p1 starts a new run flushed at end.
683        assert_eq!(run_coalescer(&[p0, p1]).len(), 2);
684    }
685
686    #[test]
687    fn non_tcp_is_singleton_passthrough() {
688        let mut udp = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 0, 0, &[0; 100]);
689        udp[9] = 17;
690        let out = run_coalescer(&[udp.clone()]);
691        assert_eq!(out.len(), 1);
692        // singleton: gso_type NONE, body == original packet bytes exactly
693        assert_eq!(read_vnet_hdr(&out[0]).unwrap().gso_type, GSO_NONE);
694        assert_eq!(&out[0][VNET_HDR_LEN..], &udp[..]);
695    }
696
697    #[test]
698    fn split_none_passthrough() {
699        let pkt = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 0, TCP_FLAG_PSH, b"hi");
700        let mut frame = vec![0u8; VNET_HDR_LEN];
701        write_vnet_hdr(
702            &VnetHdr {
703                gso_type: GSO_NONE,
704                ..Default::default()
705            },
706            &mut frame,
707        );
708        frame.extend_from_slice(&pkt);
709        let (mut out, mut offs) = (Vec::new(), Vec::new());
710        assert!(split_gro(&frame, &mut out, &mut offs));
711        assert_eq!(offs.len(), 1);
712        assert_eq!(&out[offs[0].0..offs[0].0 + offs[0].1], &pkt[..]);
713    }
714
715    #[test]
716    fn split_gro_segments_and_checksums() {
717        // one super-frame: gso_size=100, 250 bytes payload → segments of 100,100,50.
718        let mut big = mk_tcp(
719            [10, 0, 0, 1],
720            [10, 0, 0, 2],
721            9,
722            80,
723            1000,
724            0,
725            &vec![0xEE; 250],
726        );
727        // fix IP total-length already set by mk_tcp; build the framed input:
728        let mut frame = vec![0u8; VNET_HDR_LEN];
729        write_vnet_hdr(
730            &VnetHdr {
731                flags: F_NEEDS_CSUM,
732                gso_type: GSO_TCPV4,
733                hdr_len: 40,
734                gso_size: 100,
735                csum_start: 20,
736                csum_offset: 16,
737            },
738            &mut frame,
739        );
740        frame.extend_from_slice(&big);
741        let (mut out, mut offs) = (Vec::new(), Vec::new());
742        assert!(split_gro(&frame, &mut out, &mut offs));
743        assert_eq!(offs.len(), 3);
744        // each segment parses as valid IPv4/TCP with the right seq + payload size
745        let sizes = [100usize, 100, 50];
746        let mut expect_seq = 1000u32;
747        for (i, &(s, l)) in offs.iter().enumerate() {
748            let seg = &out[s..s + l];
749            let x = parse_ipv4_tcp(seg).expect("valid segment");
750            assert_eq!(x.payload_len, sizes[i]);
751            assert_eq!(x.seq, expect_seq);
752            expect_seq += u32::try_from(sizes[i]).unwrap();
753            // IP checksum valid (header folds to 0xFFFF)
754            let sum: u32 = seg[..20]
755                .chunks(2)
756                .map(|c| u32::from(u16::from_be_bytes([c[0], c[1]])))
757                .sum();
758            assert_eq!(
759                u16::try_from((sum & 0xFFFF) + (sum >> 16)).expect("folded sum fits u16"),
760                0xFFFF
761            );
762        }
763        let _ = &mut big;
764    }
765
766    #[test]
767    fn split_then_coalesce_roundtrip() {
768        // Distinct (non-constant) bytes so any off-by-one/duplication bug is caught.
769        let payload: Vec<u8> = (0..250usize)
770            .map(|i| u8::try_from(i % 256).expect("fits u8"))
771            .collect();
772        let big = mk_tcp([10, 0, 0, 1], [10, 0, 0, 2], 9, 80, 1000, 0, &payload);
773        let mut frame = vec![0u8; VNET_HDR_LEN];
774        write_vnet_hdr(
775            &VnetHdr {
776                flags: F_NEEDS_CSUM,
777                gso_type: GSO_TCPV4,
778                hdr_len: 40,
779                gso_size: 100,
780                csum_start: 20,
781                csum_offset: 16,
782            },
783            &mut frame,
784        );
785        frame.extend_from_slice(&big);
786
787        let (mut out, mut offs) = (Vec::new(), Vec::new());
788        assert!(split_gro(&frame, &mut out, &mut offs));
789        assert_eq!(offs.len(), 3, "100,100,50-byte segments");
790
791        // Feed each split segment (a raw IP/TCP packet, no vnet_hdr) through the Coalescer,
792        // exactly as the poll RX path would after receiving them off the wire.
793        let mut c = Coalescer::new();
794        let mut frames: Vec<Vec<u8>> = Vec::new();
795        for &(s, l) in &offs {
796            let seg = &out[s..s + l];
797            if let Some(f) = c.push(seg) {
798                frames.push(f.to_vec());
799            }
800        }
801        if let Some(f) = c.flush() {
802            frames.push(f.to_vec());
803        }
804
805        // Reassemble the coalesced TCP payload bytes and compare to the original, byte-exact.
806        let mut coalesced_payload = Vec::new();
807        for f in &frames {
808            let body = &f[VNET_HDR_LEN..];
809            let x = parse_ipv4_tcp(body).expect("valid coalesced frame");
810            coalesced_payload
811                .extend_from_slice(&body[x.payload_off..x.payload_off + x.payload_len]);
812        }
813        assert_eq!(coalesced_payload, payload);
814    }
815}