diff --git a/overlay/tio/tun_linux_offload.go b/overlay/tio/tun_linux_offload.go index 9eb46729..b3016442 100644 --- a/overlay/tio/tun_linux_offload.go +++ b/overlay/tio/tun_linux_offload.go @@ -14,8 +14,15 @@ import ( // 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. 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: return GSOProtoTCP, nil case unix.VIRTIO_NET_HDR_GSO_UDP_L4: diff --git a/overlay/tio/tun_linux_offload_test.go b/overlay/tio/tun_linux_offload_test.go index 48a7df8a..d2b69fae 100644 --- a/overlay/tio/tun_linux_offload_test.go +++ b/overlay/tio/tun_linux_offload_test.go @@ -19,6 +19,36 @@ import ( // worst-case 64 KiB superpacket plus replicated per-segment headers). 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 // with a folded pseudo-header sum, equals all-ones (valid). func verifyChecksum(b []byte, pseudo uint16) bool {