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
+36 -1
View File
@@ -130,6 +130,14 @@ type userspaceEngine struct {
reconfigureVPN func() error // or nil
conn25PacketHooks Conn25PacketHooks // or nil
// lastAppliedDisableTUNUDPGRO and lastAppliedDisableTUNTCPGRO cache the
// controlknobs values that were last applied to the TUN device. They are
// read and updated under e.mu and only consulted when buildfeatures.HasGRO
// is true. Note: wireguard-go's GRO disablement is one-way (sticky), so
// transitions from disabled back to enabled require a client restart.
lastAppliedDisableTUNUDPGRO bool
lastAppliedDisableTUNTCPGRO bool
mu sync.Mutex // guards following; see lock order comment below
netMap *netmap.NetworkMap // or nil
closing bool // Close was called (even if we're still closing)
@@ -564,7 +572,15 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
if err := e.router.Up(); err != nil {
return nil, fmt.Errorf("router.Up: %w", err)
}
tsTUNDev.SetLinkFeaturesPostUp()
tsTUNDev.SetLinkFeaturesPostUp(e.controlKnobs)
if buildfeatures.HasGRO && runtime.GOOS == "linux" && e.controlKnobs != nil {
// Seed the cached "last applied" TUN GRO knob values so the first
// netmap update doesn't spuriously call ApplyGROKnobs:
// SetLinkFeaturesPostUp above already applied these same values. We
// only do this on Linux because ApplyGROKnobs is a no-op elsewhere.
e.lastAppliedDisableTUNUDPGRO = e.controlKnobs.DisableTUNUDPGRO.Load()
e.lastAppliedDisableTUNTCPGRO = e.controlKnobs.DisableTUNTCPGRO.Load()
}
// It's a little pointless to apply no-op settings here (they
// should already be empty?), but it at least exercises the
@@ -1278,7 +1294,26 @@ func (e *userspaceEngine) linkChange(delta *netmon.ChangeDelta) {
func (e *userspaceEngine) SetNetworkMap(nm *netmap.NetworkMap) {
e.mu.Lock()
e.netMap = nm
tunGROKnobsChanged := false
var curUDP, curTCP bool
if buildfeatures.HasGRO && runtime.GOOS == "linux" && e.controlKnobs != nil {
curUDP = e.controlKnobs.DisableTUNUDPGRO.Load()
curTCP = e.controlKnobs.DisableTUNTCPGRO.Load()
// Only act on transitions toward "disabled"; wireguard-go's GRO
// disablement is sticky and cannot be reversed without restart.
if (curUDP && !e.lastAppliedDisableTUNUDPGRO) ||
(curTCP && !e.lastAppliedDisableTUNTCPGRO) {
tunGROKnobsChanged = true
}
e.lastAppliedDisableTUNUDPGRO = curUDP
e.lastAppliedDisableTUNTCPGRO = curTCP
}
e.mu.Unlock()
if buildfeatures.HasGRO && tunGROKnobsChanged {
e.logf("wgengine: TUN GRO knobs changed (DisableTUNUDPGRO=%v DisableTUNTCPGRO=%v); applying",
curUDP, curTCP)
e.tundev.ApplyGROKnobs(e.controlKnobs)
}
if e.networkLogger.Running() {
e.networkLogger.ReconfigNetworkMap(nm)
}