tio: accept VIRTIO_NET_HDR_GSO_ECN-qualified superpackets

TUN_F_TSO_ECN is negotiated, so once ECN feedback flows the kernel hands
us TSO superpackets typed TCPV4|GSO_ECN (CWR set). protoFromGSOType
treated the qualifier bit as an unknown type and the read path dropped
every such superpacket - a latent bug that only fires when a congested
hop CE-marks the flow, exactly when drops hurt most. The segmenter
already handles CWR (first segment only); just mask the bit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
JackDoan
2026-07-14 17:08:12 -05:00
parent 69fa9e4a2e
commit 1d768ac4e4
2 changed files with 38 additions and 1 deletions
+8 -1
View File
@@ -14,8 +14,15 @@ import (
// 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 // segment-time helpers use. Returns an error for GSO_NONE or any unknown
// value — the caller should only invoke this on a confirmed superpacket. // 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.
func protoFromGSOType(t uint8) (GSOProto, error) { func protoFromGSOType(t uint8) (GSOProto, error) {
switch t { switch t &^ unix.VIRTIO_NET_HDR_GSO_ECN {
case unix.VIRTIO_NET_HDR_GSO_TCPV4, unix.VIRTIO_NET_HDR_GSO_TCPV6: case unix.VIRTIO_NET_HDR_GSO_TCPV4, unix.VIRTIO_NET_HDR_GSO_TCPV6:
return GSOProtoTCP, nil return GSOProtoTCP, nil
case unix.VIRTIO_NET_HDR_GSO_UDP_L4: case unix.VIRTIO_NET_HDR_GSO_UDP_L4:
+30
View File
@@ -19,6 +19,36 @@ import (
// worst-case 64 KiB superpacket plus replicated per-segment headers). // worst-case 64 KiB superpacket plus replicated per-segment headers).
const testSegScratchSize = 192 * 1024 const testSegScratchSize = 192 * 1024
// TestProtoFromGSOTypeMasksECN guards the CWR-superpacket drop bug: the
// kernel qualifies a TSO superpacket whose TCP header carries CWR with
// VIRTIO_NET_HDR_GSO_ECN (we negotiate TUN_F_TSO_ECN, so it WILL send
// them once ECN feedback flows), and the decoder must mask that bit
// rather than reject the packet as an unknown type.
func TestProtoFromGSOTypeMasksECN(t *testing.T) {
cases := []struct {
typ uint8
want GSOProto
}{
{unix.VIRTIO_NET_HDR_GSO_TCPV4, GSOProtoTCP},
{unix.VIRTIO_NET_HDR_GSO_TCPV4 | unix.VIRTIO_NET_HDR_GSO_ECN, GSOProtoTCP},
{unix.VIRTIO_NET_HDR_GSO_TCPV6, GSOProtoTCP},
{unix.VIRTIO_NET_HDR_GSO_TCPV6 | unix.VIRTIO_NET_HDR_GSO_ECN, GSOProtoTCP},
{unix.VIRTIO_NET_HDR_GSO_UDP_L4, GSOProtoUDP},
}
for _, c := range cases {
got, err := protoFromGSOType(c.typ)
if err != nil || got != c.want {
t.Errorf("protoFromGSOType(%#x) = (%v, %v), want (%v, nil)", c.typ, got, err, c.want)
}
}
if _, err := protoFromGSOType(unix.VIRTIO_NET_HDR_GSO_NONE); err == nil {
t.Error("GSO_NONE must still be rejected")
}
if _, err := protoFromGSOType(unix.VIRTIO_NET_HDR_GSO_ECN); err == nil {
t.Error("a bare ECN bit with no base type must still be rejected")
}
}
// verifyChecksum confirms that the one's-complement sum across `b`, seeded // verifyChecksum confirms that the one's-complement sum across `b`, seeded
// with a folded pseudo-header sum, equals all-ones (valid). // with a folded pseudo-header sum, equals all-ones (valid).
func verifyChecksum(b []byte, pseudo uint16) bool { func verifyChecksum(b []byte, pseudo uint16) bool {