Skip to content

Commit 799c197

Browse files
committed
tun: add method for disabling TCP GRO on Linux
torvalds/linux@e269d79 broke virtio_net TCP & UDP GRO causing GRO writes to return EINVAL. The bug was then resolved later in torvalds/linux@89add40. The offending commit was pulled into various LTS releases. Updates tailscale/tailscale#13041 Signed-off-by: Jordan Whited <[email protected]>
1 parent 71393c5 commit 799c197

File tree

4 files changed

+114
-54
lines changed

4 files changed

+114
-54
lines changed

tun/offload_linux.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ const (
763763
udp6GROCandidate
764764
)
765765

766-
func packetIsGROCandidate(b []byte, canUDPGRO bool) groCandidateType {
766+
func packetIsGROCandidate(b []byte, gro groDisablementFlags) groCandidateType {
767767
if len(b) < 28 {
768768
return notGROCandidate
769769
}
@@ -772,17 +772,17 @@ func packetIsGROCandidate(b []byte, canUDPGRO bool) groCandidateType {
772772
// IPv4 packets w/IP options do not coalesce
773773
return notGROCandidate
774774
}
775-
if b[9] == unix.IPPROTO_TCP && len(b) >= 40 {
775+
if b[9] == unix.IPPROTO_TCP && len(b) >= 40 && gro.canTCPGRO() {
776776
return tcp4GROCandidate
777777
}
778-
if b[9] == unix.IPPROTO_UDP && canUDPGRO {
778+
if b[9] == unix.IPPROTO_UDP && gro.canUDPGRO() {
779779
return udp4GROCandidate
780780
}
781781
} else if b[0]>>4 == 6 {
782-
if b[6] == unix.IPPROTO_TCP && len(b) >= 60 {
782+
if b[6] == unix.IPPROTO_TCP && len(b) >= 60 && gro.canTCPGRO() {
783783
return tcp6GROCandidate
784784
}
785-
if b[6] == unix.IPPROTO_UDP && len(b) >= 48 && canUDPGRO {
785+
if b[6] == unix.IPPROTO_UDP && len(b) >= 48 && gro.canUDPGRO() {
786786
return udp6GROCandidate
787787
}
788788
}
@@ -875,15 +875,15 @@ func udpGRO(bufs [][]byte, offset int, pktI int, table *udpGROTable, isV6 bool)
875875
// handleGRO evaluates bufs for GRO, and writes the indices of the resulting
876876
// packets into toWrite. toWrite, tcpTable, and udpTable should initially be
877877
// empty (but non-nil), and are passed in to save allocs as the caller may reset
878-
// and recycle them across vectors of packets. canUDPGRO indicates if UDP GRO is
879-
// supported.
880-
func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, canUDPGRO bool, toWrite *[]int) error {
878+
// and recycle them across vectors of packets. gro indicates if TCP and UDP GRO
879+
// are supported/enabled.
880+
func handleGRO(bufs [][]byte, offset int, tcpTable *tcpGROTable, udpTable *udpGROTable, gro groDisablementFlags, toWrite *[]int) error {
881881
for i := range bufs {
882882
if offset < virtioNetHdrLen || offset > len(bufs[i])-1 {
883883
return errors.New("invalid offset")
884884
}
885885
var result groResult
886-
switch packetIsGROCandidate(bufs[i][offset:], canUDPGRO) {
886+
switch packetIsGROCandidate(bufs[i][offset:], gro) {
887887
case tcp4GROCandidate:
888888
result = tcpGRO(bufs, offset, i, tcpTable, false)
889889
case tcp6GROCandidate:

tun/offload_linux_test.go

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,11 @@ func Fuzz_handleGRO(f *testing.F) {
286286
pkt9 := udp6Packet(ip6PortA, ip6PortB, 100)
287287
pkt10 := udp6Packet(ip6PortA, ip6PortB, 100)
288288
pkt11 := udp6Packet(ip6PortA, ip6PortC, 100)
289-
f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, true, offset)
290-
f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, canUDPGRO bool, offset int) {
289+
f.Add(pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11, 0, offset)
290+
f.Fuzz(func(t *testing.T, pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11 []byte, gro int, offset int) {
291291
pkts := [][]byte{pkt0, pkt1, pkt2, pkt3, pkt4, pkt5, pkt6, pkt7, pkt8, pkt9, pkt10, pkt11}
292292
toWrite := make([]int, 0, len(pkts))
293-
handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), canUDPGRO, &toWrite)
293+
handleGRO(pkts, offset, newTCPGROTable(), newUDPGROTable(), groDisablementFlags(gro), &toWrite)
294294
if len(toWrite) > len(pkts) {
295295
t.Errorf("len(toWrite): %d > len(pkts): %d", len(toWrite), len(pkts))
296296
}
@@ -311,7 +311,7 @@ func Test_handleGRO(t *testing.T) {
311311
tests := []struct {
312312
name string
313313
pktsIn [][]byte
314-
canUDPGRO bool
314+
gro groDisablementFlags
315315
wantToWrite []int
316316
wantLens []int
317317
wantErr bool
@@ -331,7 +331,7 @@ func Test_handleGRO(t *testing.T) {
331331
udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1
332332
udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1
333333
},
334-
true,
334+
0,
335335
[]int{0, 1, 2, 4, 5, 7, 9},
336336
[]int{240, 228, 128, 140, 260, 160, 248},
337337
false,
@@ -351,7 +351,7 @@ func Test_handleGRO(t *testing.T) {
351351
udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1
352352
udp6Packet(ip6PortA, ip6PortB, 100), // udp6 flow 1
353353
},
354-
false,
354+
udpGRODisabled,
355355
[]int{0, 1, 2, 4, 5, 7, 8, 9, 10},
356356
[]int{240, 128, 128, 140, 260, 160, 128, 148, 148},
357357
false,
@@ -368,7 +368,7 @@ func Test_handleGRO(t *testing.T) {
368368
tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 201), // v6 flow 1
369369
tcp6Packet(ip6PortA, ip6PortB, header.TCPFlagAck, 100, 301), // v6 flow 1
370370
},
371-
true,
371+
0,
372372
[]int{0, 2, 4, 6},
373373
[]int{240, 240, 260, 260},
374374
false,
@@ -383,7 +383,7 @@ func Test_handleGRO(t *testing.T) {
383383
udp4Packet(ip4PortA, ip4PortB, 100),
384384
udp4Packet(ip4PortA, ip4PortB, 100),
385385
},
386-
true,
386+
0,
387387
[]int{0, 1, 3, 4},
388388
[]int{140, 240, 128, 228},
389389
false,
@@ -395,7 +395,7 @@ func Test_handleGRO(t *testing.T) {
395395
tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 1), // v4 flow 1 seq 1 len 100
396396
tcp4Packet(ip4PortA, ip4PortB, header.TCPFlagAck, 100, 201), // v4 flow 1 seq 201 len 100
397397
},
398-
true,
398+
0,
399399
[]int{0},
400400
[]int{340},
401401
false,
@@ -412,7 +412,7 @@ func Test_handleGRO(t *testing.T) {
412412
fields.TTL++
413413
}),
414414
},
415-
true,
415+
0,
416416
[]int{0, 1, 2, 3},
417417
[]int{140, 140, 128, 128},
418418
false,
@@ -429,7 +429,7 @@ func Test_handleGRO(t *testing.T) {
429429
fields.TOS++
430430
}),
431431
},
432-
true,
432+
0,
433433
[]int{0, 1, 2, 3},
434434
[]int{140, 140, 128, 128},
435435
false,
@@ -446,7 +446,7 @@ func Test_handleGRO(t *testing.T) {
446446
fields.Flags = 1
447447
}),
448448
},
449-
true,
449+
0,
450450
[]int{0, 1, 2, 3},
451451
[]int{140, 140, 128, 128},
452452
false,
@@ -463,7 +463,7 @@ func Test_handleGRO(t *testing.T) {
463463
fields.Flags = 2
464464
}),
465465
},
466-
true,
466+
0,
467467
[]int{0, 1, 2, 3},
468468
[]int{140, 140, 128, 128},
469469
false,
@@ -480,7 +480,7 @@ func Test_handleGRO(t *testing.T) {
480480
fields.HopLimit++
481481
}),
482482
},
483-
true,
483+
0,
484484
[]int{0, 1, 2, 3},
485485
[]int{160, 160, 148, 148},
486486
false,
@@ -497,7 +497,7 @@ func Test_handleGRO(t *testing.T) {
497497
fields.TrafficClass++
498498
}),
499499
},
500-
true,
500+
0,
501501
[]int{0, 1, 2, 3},
502502
[]int{160, 160, 148, 148},
503503
false,
@@ -507,7 +507,7 @@ func Test_handleGRO(t *testing.T) {
507507
for _, tt := range tests {
508508
t.Run(tt.name, func(t *testing.T) {
509509
toWrite := make([]int, 0, len(tt.pktsIn))
510-
err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), tt.canUDPGRO, &toWrite)
510+
err := handleGRO(tt.pktsIn, offset, newTCPGROTable(), newUDPGROTable(), tt.gro, &toWrite)
511511
if err != nil {
512512
if tt.wantErr {
513513
return
@@ -552,99 +552,111 @@ func Test_packetIsGROCandidate(t *testing.T) {
552552
udp6TooShort := udp6[:47]
553553

554554
tests := []struct {
555-
name string
556-
b []byte
557-
canUDPGRO bool
558-
want groCandidateType
555+
name string
556+
b []byte
557+
gro groDisablementFlags
558+
want groCandidateType
559559
}{
560560
{
561561
"tcp4",
562562
tcp4,
563-
true,
563+
0,
564564
tcp4GROCandidate,
565565
},
566+
{
567+
"tcp4 no support",
568+
tcp4,
569+
tcpGRODisabled,
570+
notGROCandidate,
571+
},
566572
{
567573
"tcp6",
568574
tcp6,
569-
true,
575+
0,
570576
tcp6GROCandidate,
571577
},
578+
{
579+
"tcp6 no support",
580+
tcp6,
581+
tcpGRODisabled,
582+
notGROCandidate,
583+
},
572584
{
573585
"udp4",
574586
udp4,
575-
true,
587+
0,
576588
udp4GROCandidate,
577589
},
578590
{
579591
"udp4 no support",
580592
udp4,
581-
false,
593+
udpGRODisabled,
582594
notGROCandidate,
583595
},
584596
{
585597
"udp6",
586598
udp6,
587-
true,
599+
0,
588600
udp6GROCandidate,
589601
},
590602
{
591603
"udp6 no support",
592604
udp6,
593-
false,
605+
udpGRODisabled,
594606
notGROCandidate,
595607
},
596608
{
597609
"udp4 too short",
598610
udp4TooShort,
599-
true,
611+
0,
600612
notGROCandidate,
601613
},
602614
{
603615
"udp6 too short",
604616
udp6TooShort,
605-
true,
617+
0,
606618
notGROCandidate,
607619
},
608620
{
609621
"tcp4 too short",
610622
tcp4TooShort,
611-
true,
623+
0,
612624
notGROCandidate,
613625
},
614626
{
615627
"tcp6 too short",
616628
tcp6TooShort,
617-
true,
629+
0,
618630
notGROCandidate,
619631
},
620632
{
621633
"invalid IP version",
622634
[]byte{0x00},
623-
true,
635+
0,
624636
notGROCandidate,
625637
},
626638
{
627639
"invalid IP header len",
628640
ip4InvalidHeaderLen,
629-
true,
641+
0,
630642
notGROCandidate,
631643
},
632644
{
633645
"ip4 invalid protocol",
634646
ip4InvalidProtocol,
635-
true,
647+
0,
636648
notGROCandidate,
637649
},
638650
{
639651
"ip6 invalid protocol",
640652
ip6InvalidProtocol,
641-
true,
653+
0,
642654
notGROCandidate,
643655
},
644656
}
645657
for _, tt := range tests {
646658
t.Run(tt.name, func(t *testing.T) {
647-
if got := packetIsGROCandidate(tt.b, tt.canUDPGRO); got != tt.want {
659+
if got := packetIsGROCandidate(tt.b, tt.gro); got != tt.want {
648660
t.Errorf("packetIsGROCandidate() = %v, want %v", got, tt.want)
649661
}
650662
})

tun/tun.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,25 @@ type Device interface {
5252
BatchSize() int
5353
}
5454

55-
type LinuxDevice interface {
55+
// GRODevice is a Device extended with methods for disabling GRO. Certain OS
56+
// versions may have offload bugs. Where these bugs negatively impact throughput
57+
// or break connectivity entirely we can use these methods to disable the
58+
// related offload.
59+
//
60+
// Linux has the following known, GRO bugs.
61+
//
62+
// torvalds/linux@e269d79c7d35aa3808b1f3c1737d63dab504ddc8 broke virtio_net
63+
// TCP & UDP GRO causing GRO writes to return EINVAL. The bug was then
64+
// resolved later in
65+
// torvalds/linux@89add40066f9ed9abe5f7f886fe5789ff7e0c50e. The offending
66+
// commit was pulled into various LTS releases.
67+
//
68+
// UDP GRO writes end up blackholing/dropping packets destined for a
69+
// vxlan/geneve interface on kernel versions prior to 6.8.5.
70+
type GRODevice interface {
5671
Device
57-
// DisableUDPGRO disables UDP GRO if it is enabled. Certain device drivers
58-
// (e.g. vxlan, geneve) do not properly handle coalesced UDP packets later
59-
// in the stack, resulting in packet loss.
72+
// DisableUDPGRO disables UDP GRO if it is enabled.
6073
DisableUDPGRO()
74+
// DisableTCPGRO disables TCP GRO if it is enabled.
75+
DisableTCPGRO()
6176
}

0 commit comments

Comments
 (0)