From db54e05bfedbeb2dd4004ecb1fbae596d2421d8a Mon Sep 17 00:00:00 2001 From: JackDoan Date: Tue, 28 Jul 2026 12:17:55 -0500 Subject: [PATCH] checkpt --- overlay/tio/tio.go | 5 +- overlay/tio/tio_gso_linux.go | 40 ++++----- overlay/tio/tun_linux_offload.go | 28 ++---- overlay/tio/tun_linux_offload_test.go | 109 +++++++++++------------ overlay/tio/virtio/header_linux.go | 36 +++++++- overlay/tio/virtio/segment_linux.go | 42 ++++----- overlay/tio/virtio/segment_linux_test.go | 36 ++++---- 7 files changed, 152 insertions(+), 144 deletions(-) diff --git a/overlay/tio/tio.go b/overlay/tio/tio.go index c680f452..5be6df9d 100644 --- a/overlay/tio/tio.go +++ b/overlay/tio/tio.go @@ -119,7 +119,8 @@ func QueueCapabilities(q Queue) Capabilities { type GSOProto uint8 const ( - GSOProtoTCP GSOProto = iota + GSOProtoUnknown GSOProto = iota + GSOProtoTCP GSOProtoUDP ) @@ -138,7 +139,7 @@ const ( // full superpacket payload; they are read-only from the writer's // perspective and must remain valid until the call returns. Every segment // in pays except possibly the last is exactly the same size. proto picks -// the L4 protocol so the writer knows which GSOType / CsumOffset to set. +// the L4 protocol so the writer knows which gsoType / CsumOffset to set. // // Callers should also consult CapsProvider (via SupportsGSO or // QueueCapabilities) for the per-protocol negotiated capability; an diff --git a/overlay/tio/tio_gso_linux.go b/overlay/tio/tio_gso_linux.go index 2b7ba0aa..5869c3a4 100644 --- a/overlay/tio/tio_gso_linux.go +++ b/overlay/tio/tio_gso_linux.go @@ -221,13 +221,11 @@ func (r *Offload) Read() ([]Packet, error) { return r.pending, nil } -// decodeRead processes the packet sitting in rxBuf at rxOff (length -// pktLen). The bytes stay in rxBuf — for GSO_NONE we slice them as a -// regular IP datagram (running finishChecksum if NEEDS_CSUM is set); -// for TSO/USO superpackets we attach the corrected GSO metadata so the -// caller can segment lazily at encrypt time. rxOff advances past the -// kernel-supplied body and nothing else, since segmentation no longer -// writes back into rxBuf. +// decodeRead processes the packet sitting in rxBuf at rxOff (length pktLen). +// The bytes stay in rxBuf: +// * for GSO_NONE we slice them as a regular IP datagram (running finishChecksum if NEEDS_CSUM is set); +// * for TSO/USO superpackets we attach the corrected GSO metadata, so the caller can segment lazily at encrypt time. +// rxOff advances by pktLen on success func (r *Offload) decodeRead(pktLen int) error { if pktLen <= 0 { return fmt.Errorf("short tun read: %d", pktLen) @@ -237,7 +235,7 @@ func (r *Offload) decodeRead(pktLen int) error { body := r.rxBuf[r.rxOff : r.rxOff+pktLen] - if hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_NONE { + if hdr.GSOType() == unix.VIRTIO_NET_HDR_GSO_NONE { if hdr.Flags&unix.VIRTIO_NET_HDR_F_NEEDS_CSUM != 0 { if err := virtio.FinishChecksum(body, hdr); err != nil { return err @@ -258,7 +256,7 @@ func (r *Offload) decodeRead(pktLen int) error { if err := virtio.CorrectHdrLen(body, &hdr); err != nil { return err } - proto, err := protoFromGSOType(hdr.GSOType) + proto, err := protoFromGSOType(hdr.GSOType()) if err != nil { return err } @@ -384,24 +382,26 @@ func (r *Offload) WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte, proto if total > maxSuperpacketLen { return fmt.Errorf("tio: WriteGSO superpacket %dB exceeds %d", total, maxSuperpacketLen) } - // GSOType and GSOSize stay zero (GSO_NONE, 0) for single-segment, or an unknown IP version. - vhdr := virtio.Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - HdrLen: uint16(len(hdr) + len(transportHdr)), - CsumStart: uint16(len(hdr)), - CsumOffset: csumOff, - } + // gsoType and GSOSize stay zero (GSO_NONE, 0) for single-segment, or an unknown IP version. + vhdr := virtio.NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_NONE, /*gsoType*/ + uint16(len(hdr)+len(transportHdr)), /*hdrLen*/ + 0, /*gsoSize*/ + uint16(len(hdr)), /*csumStart*/ + csumOff, /*csumOffset*/ + ) if segCount > 1 { ipVer := hdr[0] >> 4 switch { case proto == GSOProtoUDP && (ipVer == 4 || ipVer == 6): - vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_UDP_L4 + vhdr.SetGSOType(unix.VIRTIO_NET_HDR_GSO_UDP_L4) case ipVer == 6: - vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_TCPV6 + vhdr.SetGSOType(unix.VIRTIO_NET_HDR_GSO_TCPV6) case ipVer == 4: - vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_TCPV4 + vhdr.SetGSOType(unix.VIRTIO_NET_HDR_GSO_TCPV4) } - if vhdr.GSOType != unix.VIRTIO_NET_HDR_GSO_NONE { + if vhdr.GSOType() != unix.VIRTIO_NET_HDR_GSO_NONE { vhdr.GSOSize = uint16(segSize) } } diff --git a/overlay/tio/tun_linux_offload.go b/overlay/tio/tun_linux_offload.go index b3016442..c6dd7df4 100644 --- a/overlay/tio/tun_linux_offload.go +++ b/overlay/tio/tun_linux_offload.go @@ -11,16 +11,9 @@ import ( "github.com/slackhq/nebula/overlay/tio/virtio" ) -// protoFromGSOType maps a virtio_net_hdr GSOType to the GSOProto value the +// protoFromGSOType maps a virtio_net_hdr gsoType to the GSOProto value the // segment-time helpers use. Returns an error for GSO_NONE or any unknown -// value — the caller should only invoke this on a confirmed superpacket. -// -// VIRTIO_NET_HDR_GSO_ECN is a qualifier bit, not a type: it marks a TSO -// superpacket whose TCP header has CWR set (SKB_GSO_TCP_ECN) — we asked for -// these via TUN_F_TSO_ECN. The segmenter already emits CWR on the first -// segment only, so the bit just needs masking here. It only appears when -// ECN feedback is actually flowing (a congested hop CE-marked the flow), -// which is precisely when dropping the sender's superpackets hurts most. +// value. The caller should only invoke this on a confirmed superpacket. func protoFromGSOType(t uint8) (GSOProto, error) { switch t &^ unix.VIRTIO_NET_HDR_GSO_ECN { case unix.VIRTIO_NET_HDR_GSO_TCPV4, unix.VIRTIO_NET_HDR_GSO_TCPV6: @@ -32,17 +25,12 @@ func protoFromGSOType(t uint8) (GSOProto, error) { } } -// SegmentSuperpacket invokes fn once per segment of pkt. For non-GSO pkts -// fn is called once with pkt.Bytes (no segmentation, no copy). For GSO/USO -// superpackets fn is called once per segment with a slice of pkt.Bytes -// holding that segment's plaintext (a freshly-patched L3+L4 header sliced -// in front of the original payload chunk). The slide is destructive: pkt is -// consumed by this call and its bytes are in an undefined state when -// SegmentSuperpacket returns. Callers must not retain pkt or any earlier -// seg slice past fn's return for that segment. The scratch parameter is -// unused on the destructive path and kept only for cross-platform -// signature compatibility. Aborts and returns the first error from fn or -// from per-segment construction. +// SegmentSuperpacket invokes fn once per segment of pkt. +// For non-GSO pkts fn is called once with pkt.Bytes. +// For GSO/USO superpackets, fn is called once per segment with a slice of pkt.Bytes holding that segment's plaintext +// (a freshly-patched L3+L4 header sliced in front of the original payload chunk). +// This slicing is destructive: pkt is consumed by this call. +// Aborts and returns the first error from fn or from per-segment construction. func SegmentSuperpacket(pkt Packet, fn func(seg []byte) error) error { if !pkt.GSO.IsSuperpacket() { return fn(pkt.Bytes) diff --git a/overlay/tio/tun_linux_offload_test.go b/overlay/tio/tun_linux_offload_test.go index 79ff3fb8..80eca961 100644 --- a/overlay/tio/tun_linux_offload_test.go +++ b/overlay/tio/tun_linux_offload_test.go @@ -63,7 +63,7 @@ func verifyChecksum(b []byte, pseudo uint16) bool { // returns. Tests pre-set hdr.HdrLen correctly, so correctHdrLen is not // invoked here. func segmentForTest(pkt []byte, hdr virtio.Hdr, out *[][]byte, scratch []byte) error { - if hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_NONE { + if hdr.GSOType() == unix.VIRTIO_NET_HDR_GSO_NONE { cp := append([]byte(nil), pkt...) if hdr.Flags&unix.VIRTIO_NET_HDR_F_NEEDS_CSUM != 0 { if err := virtio.FinishChecksum(cp, hdr); err != nil { @@ -73,7 +73,7 @@ func segmentForTest(pkt []byte, hdr virtio.Hdr, out *[][]byte, scratch []byte) e *out = append(*out, cp) return nil } - proto, err := protoFromGSOType(hdr.GSOType) + proto, err := protoFromGSOType(hdr.GSOType()) if err != nil { return err } @@ -140,15 +140,14 @@ func buildTSOv4(t *testing.T, payLen, mss int) ([]byte, virtio.Hdr) { for i := 0; i < payLen; i++ { pkt[ipLen+tcpLen+i] = byte(i & 0xff) } - - return pkt, virtio.Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV4, - HdrLen: uint16(ipLen + tcpLen), - GSOSize: uint16(mss), - CsumStart: uint16(ipLen), - CsumOffset: 16, - } + return pkt, virtio.NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_TCPV4, /*gsoType*/ + uint16(ipLen+tcpLen), /*hdrLen*/ + uint16(mss), /*gsoSize*/ + uint16(ipLen), /*csumStart*/ + 16, /*csumOffset*/ + ) } func TestSegmentTCPv4(t *testing.T) { @@ -262,14 +261,14 @@ func TestSegmentTCPv6(t *testing.T) { pkt[ipLen+tcpLen+i] = byte(i) } - hdr := virtio.Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV6, - HdrLen: uint16(ipLen + tcpLen), - GSOSize: uint16(mss), - CsumStart: uint16(ipLen), - CsumOffset: 16, - } + hdr := virtio.NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_TCPV6, /*gsoType*/ + uint16(ipLen+tcpLen), /*hdrLen*/ + uint16(mss), /*gsoSize*/ + uint16(ipLen), /*csumStart*/ + 16, /*csumOffset*/ + ) scratch := make([]byte, testSegScratchSize) var out [][]byte @@ -311,7 +310,7 @@ func TestSegmentTCPv6(t *testing.T) { func TestSegmentGSONonePassesThrough(t *testing.T) { pkt, hdr := buildTSOv4(t, 100, 100) - hdr.GSOType = unix.VIRTIO_NET_HDR_GSO_NONE + hdr.SetGSOType(unix.VIRTIO_NET_HDR_GSO_NONE) hdr.Flags = 0 // no NEEDS_CSUM, leave packet untouched scratch := make([]byte, testSegScratchSize) @@ -330,7 +329,7 @@ func TestSegmentGSONonePassesThrough(t *testing.T) { // TestSegmentRejectsLegacyUDPGSO ensures the legacy GSO_UDP (UFO) marker is // still rejected; only modern GSO_UDP_L4 (USO) is supported. func TestSegmentRejectsLegacyUDPGSO(t *testing.T) { - hdr := virtio.Hdr{GSOType: unix.VIRTIO_NET_HDR_GSO_UDP} + hdr := virtio.NewHeader(0, unix.VIRTIO_NET_HDR_GSO_UDP, 0, 0, 0, 0) var out [][]byte if err := segmentForTest(nil, hdr, &out, nil); err == nil { t.Fatalf("expected rejection for legacy UDP GSO") @@ -362,14 +361,14 @@ func buildUSOv4(t *testing.T, payLen, gsoSize int) ([]byte, virtio.Hdr) { pkt[ipLen+udpLen+i] = byte(i & 0xff) } - return pkt, virtio.Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - GSOType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, - HdrLen: uint16(ipLen + udpLen), - GSOSize: uint16(gsoSize), - CsumStart: uint16(ipLen), - CsumOffset: 6, - } + return pkt, virtio.NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_UDP_L4, /*gsoType*/ + uint16(ipLen+udpLen), /*hdrLen*/ + uint16(gsoSize), /*gsoSize*/ + uint16(ipLen), /*csumStart*/ + 6, /*csumOffset*/ + ) } func TestSegmentUDPv4(t *testing.T) { @@ -471,14 +470,14 @@ func TestSegmentUDPv6(t *testing.T) { pkt[ipLen+udpLen+i] = byte(i) } - hdr := virtio.Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - GSOType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, - HdrLen: uint16(ipLen + udpLen), - GSOSize: uint16(gso), - CsumStart: uint16(ipLen), - CsumOffset: 6, - } + hdr := virtio.NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_UDP_L4, /*gsoType*/ + uint16(ipLen+udpLen), /*hdrLen*/ + uint16(gso), /*gsoSize*/ + uint16(ipLen), /*csumStart*/ + 6, /*csumOffset*/ + ) scratch := make([]byte, testSegScratchSize) var out [][]byte @@ -610,14 +609,14 @@ func BenchmarkSegmentTCPv4(b *testing.B) { for i := 0; i < sz.payLen; i++ { pkt[ipLen+tcpLen+i] = byte(i) } - hdr := virtio.Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV4, - HdrLen: uint16(ipLen + tcpLen), - GSOSize: uint16(sz.mss), - CsumStart: uint16(ipLen), - CsumOffset: 16, - } + hdr := virtio.NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_TCPV4, /*gsoType*/ + uint16(ipLen+tcpLen), /*hdrLen*/ + uint16(sz.mss), /*gsoSize*/ + uint16(ipLen), /*csumStart*/ + 16, /*csumOffset*/ + ) scratch := make([]byte, testSegScratchSize) out := make([][]byte, 0, 64) @@ -775,14 +774,14 @@ func TestDecodeReadFitsMaxTSOAtDrainThreshold(t *testing.T) { copy(o.rxBuf[o.rxOff:], pkt) // Encode the matching virtio_net_hdr. - hdr := virtio.Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV6, - HdrLen: uint16(headerLen), - GSOSize: uint16(gsoSize), - CsumStart: uint16(ipv6HdrLen), - CsumOffset: 16, - } + hdr := virtio.NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_TCPV6, /*gsoType*/ + uint16(headerLen), /*hdrLen*/ + uint16(gsoSize), /*gsoSize*/ + uint16(ipv6HdrLen), /*csumStart*/ + 16, /*csumOffset*/ + ) hdr.Encode(o.readVnetScratch[:]) startRxOff := o.rxOff @@ -899,8 +898,8 @@ func TestWriteGSOLeadingEmptyFragmentGeometry(t *testing.T) { } var vhdr virtio.Hdr vhdr.Decode(buf[:virtio.Size]) - if vhdr.GSOType != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { - t.Errorf("GSOType=%d want UDP_L4", vhdr.GSOType) + if vhdr.GSOType() != unix.VIRTIO_NET_HDR_GSO_UDP_L4 { + t.Errorf("gsoType=%d want UDP_L4", vhdr.GSOType()) } if vhdr.GSOSize != 1200 { t.Errorf("GSOSize=%d want 1200 (first non-empty fragment)", vhdr.GSOSize) diff --git a/overlay/tio/virtio/header_linux.go b/overlay/tio/virtio/header_linux.go index 8c5159de..6b9493cb 100644 --- a/overlay/tio/virtio/header_linux.go +++ b/overlay/tio/virtio/header_linux.go @@ -3,7 +3,11 @@ package virtio -import "encoding/binary" +import ( + "encoding/binary" + + "golang.org/x/sys/unix" +) // Size is the on-wire length of struct virtio_net_hdr the kernel // prepends/expects on a TUN opened with IFF_VNET_HDR (TUNSETVNETHDRSZ @@ -13,18 +17,29 @@ const Size = 10 // Hdr is the Go view of the legacy virtio_net_hdr. type Hdr struct { Flags uint8 - GSOType uint8 + gsoType uint8 //private to avoid mistakes wrt the 0x80 VIRTIO_NET_HDR_GSO_ECN flag, ORed with the other "GSO types" HdrLen uint16 GSOSize uint16 CsumStart uint16 CsumOffset uint16 } +func NewHeader(flags, gsoType uint8, hdrLen, gsoSize, csumStart, csumOffset uint16) Hdr { + return Hdr{ + Flags: flags, + gsoType: gsoType, + HdrLen: hdrLen, + GSOSize: gsoSize, + CsumStart: csumStart, + CsumOffset: csumOffset, + } +} + // Decode reads a virtio_net_hdr in host byte order (TUN default; we never // call TUNSETVNETLE so the kernel matches our endianness). func (h *Hdr) Decode(b []byte) { h.Flags = b[0] - h.GSOType = b[1] + h.gsoType = b[1] h.HdrLen = binary.NativeEndian.Uint16(b[2:4]) h.GSOSize = binary.NativeEndian.Uint16(b[4:6]) h.CsumStart = binary.NativeEndian.Uint16(b[6:8]) @@ -35,9 +50,22 @@ func (h *Hdr) Decode(b []byte) { // (must be at least Size bytes). Used to emit a TSO superpacket on egress. func (h *Hdr) Encode(b []byte) { b[0] = h.Flags - b[1] = h.GSOType + b[1] = h.gsoType binary.NativeEndian.PutUint16(b[2:4], h.HdrLen) binary.NativeEndian.PutUint16(b[4:6], h.GSOSize) binary.NativeEndian.PutUint16(b[6:8], h.CsumStart) binary.NativeEndian.PutUint16(b[8:10], h.CsumOffset) } + +// GSOType returns gsoType with the ECN-flag masked out +func (h *Hdr) GSOType() uint8 { + return h.gsoType &^ unix.VIRTIO_NET_HDR_GSO_ECN +} + +func (h *Hdr) HasECNFlag() bool { + return h.gsoType&unix.VIRTIO_NET_HDR_GSO_ECN != 0 +} + +func (h *Hdr) SetGSOType(x uint8) { + h.gsoType = x +} diff --git a/overlay/tio/virtio/segment_linux.go b/overlay/tio/virtio/segment_linux.go index 17a7ff00..45044874 100644 --- a/overlay/tio/virtio/segment_linux.go +++ b/overlay/tio/virtio/segment_linux.go @@ -85,33 +85,27 @@ func CheckValid(pkt []byte, hdr Hdr) error { } ipVersion := pkt[0] >> 4 - //mask out VIRTIO_NET_HDR_GSO_ECN, it's a qualifier, not a type - gsoType := hdr.GSOType &^ unix.VIRTIO_NET_HDR_GSO_ECN - // The ECN qualifier means CWR was set on a TSO superpacket, so it only - // applies to the TCP types. The kernel's virtio_net_hdr_to_skb rejects - // it on anything else; mirror that instead of segmenting nonsense. - if hdr.GSOType&unix.VIRTIO_NET_HDR_GSO_ECN != 0 && - gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV4 && - gsoType != unix.VIRTIO_NET_HDR_GSO_TCPV6 { - return fmt.Errorf("virtio GSO_ECN qualifier on non-TCP GSO type %#x", hdr.GSOType) + gsoType := hdr.GSOType() + if hdr.HasECNFlag() && !(gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV4 || gsoType == unix.VIRTIO_NET_HDR_GSO_TCPV6) { + return fmt.Errorf("virtio GSO_ECN qualifier on non-TCP GSO type %#x", hdr.gsoType) } switch gsoType { case unix.VIRTIO_NET_HDR_GSO_TCPV4: if ipVersion != 4 { - return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType) + return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.gsoType) } case unix.VIRTIO_NET_HDR_GSO_TCPV6: if ipVersion != 6 { - return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType) + return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.gsoType) } case unix.VIRTIO_NET_HDR_GSO_UDP_L4: // USO carries either v4 or v6; the leading nibble disambiguates. if !(ipVersion == 4 || ipVersion == 6) { - return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType) + return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.gsoType) } default: if !(ipVersion == 6 || ipVersion == 4) { - return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType) + return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.gsoType) } } @@ -128,7 +122,7 @@ func CorrectHdrLen(pkt []byte, hdr *Hdr) error { // FORWARD path. Instead, parse the transport header length and add it onto // csumStart, which is synonymous for IP header length. - if hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_UDP_L4 { + if hdr.GSOType() == unix.VIRTIO_NET_HDR_GSO_UDP_L4 { hdr.HdrLen = hdr.CsumStart + 8 } else { if len(pkt) <= int(hdr.CsumStart+tcpDataOffOff) { @@ -157,19 +151,15 @@ func CorrectHdrLen(pkt []byte, hdr *Hdr) error { return nil } -// SegmentTCP walks a TSO superpacket pkt, yielding each segment as a -// slice into pkt itself. Per-segment plaintext is laid out by stamping a -// copy of the original L3+L4 header into pkt at offset i*gsoSize, where it -// sits immediately before that segment's payload chunk in the original -// buffer. The stamp is destructive but harmless: iter i's header write lands -// on pkt[i*G : i*G+hdrLen], which is the tail of seg_{i-1}'s payload (already +// SegmentTCP walks a TSO superpacket pkt, yielding each segment as a slice into pkt. +// Per-segment plaintext is laid out by stamping a copy of the original L3+L4 header into pkt at offset i*gsoSize, +// where it sits immediately before that segment's payload chunk in the original buffer. +// The stamp is destructive: iter i's header write lands on pkt[i*G : i*G+hdrLen], which is the tail of seg_{i-1}'s payload (already // consumed) and ends exactly where seg_i's payload begins, so it never clobbers -// live payload — this holds even when gsoSize < hdrLen. The header bytes are -// sourced from a pristine snapshot taken before the loop (savedHdr), NOT from -// pkt[:hdrLen], because when gsoSize < hdrLen the stamps would otherwise -// overwrite the leading header in place and every stamp after the first would -// copy corrupted bytes. pkt is consumed by this call and must not be inspected -// by the caller after the final yield. +// live payload — this holds even when gsoSize < hdrLen. +// The header bytes are sourced from a pristine snapshot taken before the loop (savedHdr), NOT from pkt[:hdrLen], because when gsoSize < hdrLen the stamps would otherwise +// overwrite the leading header in place and every stamp after the first would copy corrupted bytes. +// pkt is consumed by this call and must not be inspected by the caller after the final yield. func SegmentTCP(pkt []byte, hdrLenU, csumStartU, gsoSizeU uint16, yield func(seg []byte) error) error { if gsoSizeU == 0 { return fmt.Errorf("gso_size is zero") diff --git a/overlay/tio/virtio/segment_linux_test.go b/overlay/tio/virtio/segment_linux_test.go index 0e490d14..16b12015 100644 --- a/overlay/tio/virtio/segment_linux_test.go +++ b/overlay/tio/virtio/segment_linux_test.go @@ -226,13 +226,14 @@ func TestCorrectHdrLenChecksumBound(t *testing.T) { // = 26) accepts. This case FAILS against the CsumStart+CsumStart regression. t.Run("valid-small-uso-accepted", func(t *testing.T) { pkt, _, csumStart := buildUDPv4Super(12) // total len 40 - hdr := Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - GSOType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, - GSOSize: 6, // two 6-byte segments - CsumStart: csumStart, - CsumOffset: 6, - } + hdr := NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_UDP_L4, /*gsoType*/ + 0, /*hdrLen*/ + 6, /*gsoSize: two 6-byte segments*/ + csumStart, /*csumStart*/ + 6, /*csumOffset*/ + ) if err := CorrectHdrLen(pkt, &hdr); err != nil { t.Fatalf("CorrectHdrLen rejected a valid 40-byte USO superpacket: %v", err) } @@ -247,13 +248,14 @@ func TestCorrectHdrLenChecksumBound(t *testing.T) { t.Run("too-short-rejected", func(t *testing.T) { pkt := make([]byte, 25) pkt[0] = 0x45 // IPv4, IHL 5 - hdr := Hdr{ - Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, - GSOType: unix.VIRTIO_NET_HDR_GSO_UDP_L4, - GSOSize: 6, - CsumStart: 20, - CsumOffset: 6, - } + hdr := NewHeader( + unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, /*flags*/ + unix.VIRTIO_NET_HDR_GSO_UDP_L4, /*gsoType*/ + 0, /*hdrLen*/ + 6, /*gsoSize*/ + 20, /*csumStart*/ + 6, /*csumOffset*/ + ) if err := CorrectHdrLen(pkt, &hdr); err == nil { t.Fatalf("CorrectHdrLen accepted a too-short (25-byte) packet") } @@ -355,7 +357,7 @@ func buildUDPv4Single(payload []byte) (pkt []byte, hdr Hdr) { pseudo := pseudoHeaderIPv4(pkt[12:16], pkt[16:20], unix.IPPROTO_UDP, udpLen+len(payload)) binary.BigEndian.PutUint16(pkt[ipLen+udpChecksumOff:ipLen+udpChecksumOff+2], pseudo) - return pkt, Hdr{Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, CsumStart: ipLen, CsumOffset: udpChecksumOff} + return pkt, NewHeader(unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, unix.VIRTIO_NET_HDR_GSO_NONE, 0, 0, ipLen, udpChecksumOff) } // TestFinishChecksumUDPZeroStoresAllOnes pins RFC 768: a UDP checksum that computes to zero goes on the wire as @@ -409,7 +411,7 @@ func TestFinishChecksumTCPZeroPreserved(t *testing.T) { } binary.BigEndian.PutUint16(seg[cs+co:cs+co+2], partial) - hdr := Hdr{Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, CsumStart: cs, CsumOffset: co} + hdr := NewHeader(unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, unix.VIRTIO_NET_HDR_GSO_NONE, 0, 0, cs, co) if err := FinishChecksum(seg, hdr); err != nil { t.Fatalf("FinishChecksum: %v", err) } @@ -457,7 +459,7 @@ func TestCheckValidMasksGSOECN(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := CheckValid(tc.pkt, Hdr{GSOType: tc.gsoType}) + err := CheckValid(tc.pkt, NewHeader(0, tc.gsoType, 0, 0, 0, 0)) if tc.wantErr && err == nil { t.Errorf("CheckValid(gsoType=%#x) = nil, want error", tc.gsoType) }