mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-31 12:10:38 -04:00
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>
114 lines
3.7 KiB
Go
114 lines
3.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux && !ts_omit_gro
|
|
|
|
package tstun
|
|
|
|
import (
|
|
"errors"
|
|
"net/netip"
|
|
"runtime"
|
|
|
|
"github.com/tailscale/wireguard-go/tun"
|
|
"golang.org/x/sys/unix"
|
|
"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, 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") ||
|
|
(knobs != nil && knobs.DisableTUNUDPGRO.Load()) {
|
|
groDev.DisableUDPGRO()
|
|
}
|
|
if envknob.Bool("TS_TUN_DISABLE_TCP_GRO") ||
|
|
(knobs != nil && knobs.DisableTUNTCPGRO.Load()) {
|
|
groDev.DisableTCPGRO()
|
|
}
|
|
err := probeTCPGRO(groDev)
|
|
if errors.Is(err, unix.EINVAL) {
|
|
groDev.DisableTCPGRO()
|
|
groDev.DisableUDPGRO()
|
|
t.logf("disabled TUN TCP & UDP GRO due to GRO probe error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
segmentSize := len(fingerprint)
|
|
iphLen := 20
|
|
tcphLen := 20
|
|
totalLen := iphLen + tcphLen + segmentSize
|
|
ipAs4 := ipPort.Addr().As4()
|
|
bufs := make([][]byte, 2)
|
|
for i := range bufs {
|
|
bufs[i] = make([]byte, PacketStartOffset+totalLen, PacketStartOffset+(totalLen*2))
|
|
ipv4H := header.IPv4(bufs[i][PacketStartOffset:])
|
|
ipv4H.Encode(&header.IPv4Fields{
|
|
SrcAddr: tcpip.AddrFromSlice(ipAs4[:]),
|
|
DstAddr: tcpip.AddrFromSlice(ipAs4[:]),
|
|
Protocol: unix.IPPROTO_TCP,
|
|
// Use a zero value TTL as best effort means to reduce chance of
|
|
// probe packet leaking further than it needs to.
|
|
TTL: 0,
|
|
TotalLength: uint16(totalLen),
|
|
})
|
|
tcpH := header.TCP(bufs[i][PacketStartOffset+iphLen:])
|
|
tcpH.Encode(&header.TCPFields{
|
|
SrcPort: ipPort.Port(),
|
|
DstPort: ipPort.Port(),
|
|
SeqNum: 1 + uint32(i*segmentSize),
|
|
AckNum: 1,
|
|
DataOffset: 20,
|
|
Flags: header.TCPFlagAck,
|
|
WindowSize: 3000,
|
|
})
|
|
copy(bufs[i][PacketStartOffset+iphLen+tcphLen:], fingerprint)
|
|
ipv4H.SetChecksum(^ipv4H.CalculateChecksum())
|
|
pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(tcphLen+segmentSize))
|
|
pseudoCsum = checksum.Checksum(bufs[i][PacketStartOffset+iphLen+tcphLen:], pseudoCsum)
|
|
tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum))
|
|
}
|
|
_, err := dev.Write(bufs, PacketStartOffset)
|
|
return err
|
|
}
|