control/controlknobs,net/{batching,tstun},wgengine: add nodecaps to disable UDP & TUN GRO/GSO

Add four control-plane node attributes that let us disable UDP GSO/GRO
on the magicsock UDP socket and UDP/TCP GRO on the Tailscale TUN
device.

These complement the pre-existing TS_DEBUG_DISABLE_UDP_{GRO,GSO} and
TS_TUN_DISABLE_{UDP,TCP}_GRO envknobs. They exist so we can mitigate
upstream Linux kernel regressions on a deployed fleet without
requiring a client release, after two incidents (#13041, #19777) where
buggy kernel patches landed upstream and the fix took an excessively
long time to reach downstream distros.

Knob changes are reacted to in setNetworkMapInternal / SetNetworkMap via
a comparison against a cached "last applied" value and only an actual
transition triggers work: magicsock Rebind()+ReSTUN for UDP,
ApplyGROKnobs for TUN. The TUN side is gated by buildfeatures.HasGRO and
is one-way (wireguard-go GRO disablement is sticky); re-enabling
requires a client restart.

Updates #13041
Updates #19777

Change-Id: I802993070afa659cc06809bb0bfbb7f8a0cdb273
Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
James Tucker
2026-05-27 17:10:14 -07:00
committed by James Tucker
parent 94af1b00fb
commit 25b8ed8d9e
13 changed files with 212 additions and 30 deletions
+2 -1
View File
@@ -6,11 +6,12 @@
package batching
import (
"tailscale.com/control/controlknobs"
"tailscale.com/types/nettype"
)
// TryUpgradeToConn is no-op on all platforms except linux.
func TryUpgradeToConn(pconn nettype.PacketConn, _ string, _ int, _ string) nettype.PacketConn {
func TryUpgradeToConn(pconn nettype.PacketConn, _ string, _ int, _ string, _ *controlknobs.Knobs) nettype.PacketConn {
return pconn
}
+15 -7
View File
@@ -20,6 +20,7 @@ import (
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"golang.org/x/sys/unix"
"tailscale.com/control/controlknobs"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/net/neterror"
@@ -426,8 +427,14 @@ func tryEnableRXQOverflowsCounter(pconn nettype.PacketConn) (enabled bool) {
}
// tryEnableUDPOffload attempts to enable the UDP_GRO socket option on pconn,
// and returns two booleans indicating TX and RX UDP offload support.
func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
// and returns two booleans indicating TX and RX UDP offload support. If knobs
// is non-nil, UDP GSO and/or UDP GRO may be disabled via control-plane node
// attributes.
func tryEnableUDPOffload(pconn nettype.PacketConn, knobs *controlknobs.Knobs) (hasTX bool, hasRX bool) {
disableGSO := envknob.Bool("TS_DEBUG_DISABLE_UDP_GSO") ||
(knobs != nil && knobs.DisableUDPGSO.Load())
disableGRO := envknob.Bool("TS_DEBUG_DISABLE_UDP_GRO") ||
(knobs != nil && knobs.DisableUDPGRO.Load())
if c, ok := pconn.(*net.UDPConn); ok {
rc, err := c.SyscallConn()
if err != nil {
@@ -435,11 +442,11 @@ func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
}
err = rc.Control(func(fd uintptr) {
var errSyscall error
if !envknob.Bool("TS_DEBUG_DISABLE_UDP_GSO") {
if !disableGSO {
_, errSyscall = syscall.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT)
hasTX = errSyscall == nil
}
if !envknob.Bool("TS_DEBUG_DISABLE_UDP_GRO") {
if !disableGRO {
errSyscall = syscall.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
hasRX = errSyscall == nil
}
@@ -518,8 +525,9 @@ func getRXQOverflowsMetric(name string) *clientmetric.Metric {
// pconn to a [Conn] if appropriate. A batch size of [IdealBatchSize] is
// suggested for the best performance. If len(rxqOverflowsMetricName) is
// nonzero, then read ops will propagate the SO_RXQ_OVFL control message counter
// to a clientmetric with the supplied name.
func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int, rxqOverflowsMetricName string) nettype.PacketConn {
// to a clientmetric with the supplied name. If knobs is non-nil, UDP GSO
// and/or UDP GRO may be disabled via control-plane node attributes.
func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int, rxqOverflowsMetricName string, knobs *controlknobs.Knobs) nettype.PacketConn {
if runtime.GOOS != "linux" {
// Exclude Android.
return pconn
@@ -569,7 +577,7 @@ func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int, r
panic("bogus network")
}
var txOffload bool
txOffload, b.rxOffload = tryEnableUDPOffload(uc)
txOffload, b.rxOffload = tryEnableUDPOffload(uc, knobs)
b.txOffload.Store(txOffload)
if len(rxqOverflowsMetricName) > 0 && tryEnableRXQOverflowsCounter(uc) {
// Don't register the metric unless the socket option has been
+34 -5
View File
@@ -15,22 +15,26 @@ import (
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
"tailscale.com/control/controlknobs"
"tailscale.com/envknob"
"tailscale.com/net/tsaddr"
)
// SetLinkFeaturesPostUp configures link features on t based on select TS_TUN_
// environment variables and OS feature tests. Callers should ensure t is
// up prior to calling, otherwise OS feature tests may be inconclusive.
func (t *Wrapper) SetLinkFeaturesPostUp() {
// environment variables, control-plane node attributes (via knobs, which may be
// nil), and OS feature tests. Callers should ensure t is up prior to calling,
// otherwise OS feature tests may be inconclusive.
func (t *Wrapper) SetLinkFeaturesPostUp(knobs *controlknobs.Knobs) {
if t.isTAP || runtime.GOOS == "android" {
return
}
if groDev, ok := t.tdev.(tun.GRODevice); ok {
if envknob.Bool("TS_TUN_DISABLE_UDP_GRO") {
if envknob.Bool("TS_TUN_DISABLE_UDP_GRO") ||
(knobs != nil && knobs.DisableTUNUDPGRO.Load()) {
groDev.DisableUDPGRO()
}
if envknob.Bool("TS_TUN_DISABLE_TCP_GRO") {
if envknob.Bool("TS_TUN_DISABLE_TCP_GRO") ||
(knobs != nil && knobs.DisableTUNTCPGRO.Load()) {
groDev.DisableTCPGRO()
}
err := probeTCPGRO(groDev)
@@ -42,6 +46,31 @@ func (t *Wrapper) SetLinkFeaturesPostUp() {
}
}
// ApplyGROKnobs applies the [tailcfg.NodeAttrDisableTUNUDPGRO] and
// [tailcfg.NodeAttrDisableTUNTCPGRO] knob values (via knobs, which must be
// non-nil) to t's underlying device. It is intended to be called when a
// control-plane node attribute change is detected after [SetLinkFeaturesPostUp]
// has already run.
//
// Note: wireguard-go's GRO disablement is one-way (sticky); ApplyGROKnobs can
// move TUN UDP/TCP GRO from enabled to disabled, but the reverse requires a
// client restart.
func (t *Wrapper) ApplyGROKnobs(knobs *controlknobs.Knobs) {
if t.isTAP || runtime.GOOS == "android" || knobs == nil {
return
}
groDev, ok := t.tdev.(tun.GRODevice)
if !ok {
return
}
if knobs.DisableTUNUDPGRO.Load() {
groDev.DisableUDPGRO()
}
if knobs.DisableTUNTCPGRO.Load() {
groDev.DisableTCPGRO()
}
}
func probeTCPGRO(dev tun.GRODevice) error {
ipPort := netip.MustParseAddrPort(tsaddr.TailscaleServiceIPString + ":0")
fingerprint := []byte("tailscale-probe-tun-gro")
+5 -1
View File
@@ -5,4 +5,8 @@
package tstun
func (t *Wrapper) SetLinkFeaturesPostUp() {}
import "tailscale.com/control/controlknobs"
func (t *Wrapper) SetLinkFeaturesPostUp(_ *controlknobs.Knobs) {}
func (t *Wrapper) ApplyGROKnobs(_ *controlknobs.Knobs) {}
+6 -3
View File
@@ -25,6 +25,7 @@ import (
"go4.org/mem"
"golang.org/x/crypto/blake2s"
"golang.org/x/net/ipv6"
"tailscale.com/control/controlknobs"
"tailscale.com/disco"
"tailscale.com/net/batching"
"tailscale.com/net/netaddr"
@@ -83,6 +84,7 @@ type Server struct {
metrics *metrics
netMon *netmon.Monitor
cloudInfo *cloudinfo.CloudInfo // used to query cloud metadata services
controlKnobs *controlknobs.Knobs // or nil
mu sync.Mutex // guards the following fields
macSecrets views.Slice[[blake2s.Size]byte] // [0] is most recent, max 2 elements
@@ -376,8 +378,8 @@ const (
// port selection is left up to the host networking stack. If
// onlyStaticAddrPorts is true, then dynamic addr:port discovery will be
// disabled, and only addr:port's set via [Server.SetStaticAddrPorts] will be
// used. Metrics must be non-nil.
func NewServer(logf logger.Logf, port uint16, onlyStaticAddrPorts bool, metrics *usermetric.Registry) (s *Server, err error) {
// used. Metrics must be non-nil. knobs may be nil.
func NewServer(logf logger.Logf, port uint16, onlyStaticAddrPorts bool, metrics *usermetric.Registry, knobs *controlknobs.Knobs) (s *Server, err error) {
s = &Server{
logf: logf,
disco: key.NewDisco(),
@@ -388,6 +390,7 @@ func NewServer(logf logger.Logf, port uint16, onlyStaticAddrPorts bool, metrics
serverEndpointByDisco: make(map[key.SortedPairOfDiscoPublic]*serverEndpoint),
nextVNI: minVNI,
cloudInfo: cloudinfo.New(logf),
controlKnobs: knobs,
}
s.discoPublic = s.disco.Public()
s.metrics = registerMetrics(metrics)
@@ -689,7 +692,7 @@ func (s *Server) bindSockets(desiredPort uint16) error {
break SocketsLoop
}
}
pc := batching.TryUpgradeToConn(uc, network, batching.IdealBatchSize, "udprelay_rxq_overflows")
pc := batching.TryUpgradeToConn(uc, network, batching.IdealBatchSize, "udprelay_rxq_overflows", s.controlKnobs)
bc, ok := pc.(batching.Conn)
if !ok {
bc = &singlePacketConn{uc}
+1 -1
View File
@@ -214,7 +214,7 @@ func TestServer(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
reg := new(usermetric.Registry)
deregisterMetrics()
server, err := NewServer(t.Logf, 0, true, reg)
server, err := NewServer(t.Logf, 0, true, reg, nil)
if err != nil {
t.Fatal(err)
}