Skip to content

Commit 6a3e7c9

Browse files
committed
net_tap: include the L4 length in the LSO checksum seed
A segmentation-offload frame carries a partial TCP/UDP checksum in the checksum field. The netvsp LSO path supplies a length-less pseudo-header there, because the guest cannot know the per-segment length. The kernel GSO path expects the opposite: th->check is the pseudo-header over the full L4 length, as __tcp_v4_send_check writes for a GSO skb, which tcp_gso_segment and the hardware TSO drivers adjust down per segment. Passing the length-less seed straight through leaves the per-segment adjustment short by the segment length, so every emitted segment carries a wrong checksum and an off-host receiver drops it. A guest TCP stream over TSO to an off-host peer collapses to a few Mbit/s of retransmits. Checksum offload without segmentation and same-host traffic are unaffected, since their seed already includes the length. Recompute the seed for segmentation frames from the packet's own IP header. The payload is untouched; the kernel or NIC still completes the per-segment checksum.
1 parent 67ef49a commit 6a3e7c9

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

  • vm/devices/net/net_tap/src

vm/devices/net/net_tap/src/lib.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,13 @@ impl Queue for TapQueue {
337337
fixup_ipv6_payload_length(&mut packet, meta.l2_len as usize);
338338
}
339339

340+
// Convert the LSO length-less pseudo-header seed to the convention the
341+
// kernel's GSO path expects (pseudo-header with the full L4 length) so
342+
// segmentation produces correct per-segment checksums.
343+
if meta.flags.offload_tcp_segmentation() || meta.flags.offload_udp_segmentation() {
344+
fixup_gso_pseudo_header(&mut packet, meta.l2_len as usize);
345+
}
346+
340347
let bufs = [
341348
std::io::IoSlice::new(hdr_bytes),
342349
std::io::IoSlice::new(&packet),
@@ -456,6 +463,71 @@ fn fixup_ipv6_payload_length(packet: &mut [u8], l2_len: usize) {
456463
packet[l2_len + 4..l2_len + 6].copy_from_slice(&payload_len.to_be_bytes());
457464
}
458465

466+
/// Rewrite the L4 checksum field of a segmentation-offload (GSO) frame to the
467+
/// pseudo-header checksum that includes the full L4 segment length.
468+
///
469+
/// NDIS/netvsp LSO hands us a length-less pseudo-header seed: the guest cannot know
470+
/// the per-segment length, so it leaves it out. The kernel's GSO path expects the
471+
/// opposite. The stack's own `__tcp_v4_send_check` writes `th->check =
472+
/// ~tcp_v4_check(skb->len, ...)`, the pseudo-header computed over the full skb L4
473+
/// length, and `tcp_gso_segment` (and the hardware TSO drivers) then adjust it down
474+
/// per segment. Passing the length-less seed straight through leaves that adjustment
475+
/// off by the segment length, so every emitted segment carries a wrong checksum and
476+
/// the far end drops it. Recompute the seed here from the packet's own IP header so
477+
/// it does not depend on the value the guest supplied; the data is untouched, the
478+
/// kernel or NIC still completes the per-segment checksum.
479+
fn fixup_gso_pseudo_header(packet: &mut [u8], l2_len: usize) {
480+
if packet.len() < l2_len + 1 {
481+
return;
482+
}
483+
let (l4_off, proto, mut sum) = match packet[l2_len] >> 4 {
484+
4 => {
485+
if packet.len() < l2_len + 20 {
486+
return;
487+
}
488+
let ihl = ((packet[l2_len] & 0x0f) as usize) * 4;
489+
if !(20..=60).contains(&ihl) || packet.len() < l2_len + ihl {
490+
return;
491+
}
492+
let proto = packet[l2_len + 9];
493+
let mut sum: u32 = 0;
494+
for chunk in packet[l2_len + 12..l2_len + 20].chunks_exact(2) {
495+
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
496+
}
497+
(l2_len + ihl, proto, sum)
498+
}
499+
6 => {
500+
const IPV6_HEADER_LEN: usize = 40;
501+
if packet.len() < l2_len + IPV6_HEADER_LEN {
502+
return;
503+
}
504+
let proto = packet[l2_len + 6];
505+
let mut sum: u32 = 0;
506+
for chunk in packet[l2_len + 8..l2_len + IPV6_HEADER_LEN].chunks_exact(2) {
507+
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
508+
}
509+
(l2_len + IPV6_HEADER_LEN, proto, sum)
510+
}
511+
_ => return,
512+
};
513+
let csum_field = match proto {
514+
6 => l4_off + 16,
515+
17 => l4_off + 6,
516+
_ => return,
517+
};
518+
if packet.len() < csum_field + 2 {
519+
return;
520+
}
521+
// Pseudo-header: src and dst addresses, protocol, and the L4 length (header plus
522+
// payload). The folded, non-complemented sum is the CHECKSUM_PARTIAL seed.
523+
let l4_len = packet.len() - l4_off;
524+
sum += proto as u32 + l4_len as u32;
525+
while sum >> 16 != 0 {
526+
sum = (sum & 0xffff) + (sum >> 16);
527+
}
528+
packet[csum_field..csum_field + 2].copy_from_slice(&(sum as u16).to_be_bytes());
529+
}
530+
459531
/// Build a `VirtioNetHdr` from transmit metadata for the TAP device.
460532
///
461533
/// The virtio net header uses fully general `csum_start` / `csum_offset` fields
@@ -702,6 +774,25 @@ mod tests {
702774
assert_eq!(hdr.csum_offset, 6);
703775
}
704776

777+
#[test]
778+
fn gso_pseudo_header_seed_includes_length() {
779+
// 14 eth + 20 IPv4 (TCP) + 20 TCP + 100 payload; the seed must be rewritten to
780+
// the pseudo-header that includes the full L4 length (header plus payload).
781+
let mut pkt = vec![0u8; 14 + 20 + 20 + 100];
782+
pkt[14] = 0x45; // IPv4, ihl = 5
783+
pkt[14 + 9] = 6; // protocol = TCP
784+
pkt[14 + 12..14 + 16].copy_from_slice(&[192, 168, 168, 156]); // src
785+
pkt[14 + 16..14 + 20].copy_from_slice(&[192, 168, 168, 17]); // dst
786+
fixup_gso_pseudo_header(&mut pkt, 14);
787+
let seed = u16::from_be_bytes([pkt[50], pkt[51]]); // TCP checksum field
788+
let l4_len: u32 = 20 + 100;
789+
let mut want: u32 = 0xc0a8 + 0xa89c + 0xc0a8 + 0xa811 + 6 + l4_len;
790+
while want >> 16 != 0 {
791+
want = (want & 0xffff) + (want >> 16);
792+
}
793+
assert_eq!(seed, want as u16);
794+
}
795+
705796
#[test]
706797
fn ipv4_header_checksum_fixup() {
707798
// Ethernet (14) + IPv4 header (20) with zero checksum field.

0 commit comments

Comments
 (0)