Files
tailscale/net/tstun/wrap_linux.go
Jordan Whited 8fc6dca15e all: use ~128KiB packet buf for batched i/o
This commit bumps the wireguard-go dependency to incorporate changes to
the packet memory model and the tun.Device.Read and conn.ReceiveFunc I/O
interfaces. It updates their implementations accordingly.

These changes improve throughput in all measured benchmarks and reduce
peak RSS in six of eight cases. The two regressions will be addressed in
a follow-up commit that reduces peak RSS below the baseline measured at
1e69418. That work is kept separate to simplify review.

The following throughput and peak RSS benchmarks were performed with
iperf3 between two Intel i5-12400 nodes running Ubuntu 24.04 (Linux 6.8).

The UDP benchmarks did not use UDP GSO on the sender, so they were
roughly equivalent to single packet I/O through wireguard-go.

TCP/1 signifies one TCP stream; TCP/128 signifies 128 parallel TCP
streams.

Throughput (Mb/s)
  Test     1e69418  After  Change
  TCP/1    10,371  11,354   +9.5%
  TCP/128   7,886   8,404   +6.6%
  UDP/1     2,111   2,853  +35.1%
  UDP/128   1,747   2,235  +28.0%

Peak memory (VmHWM, kB)
  Test     Side   1e69418    After   Change
  TCP/1    TX      98,240   52,596   -46.5%
           RX     287,748   73,384   -74.5%
  TCP/128  TX     101,196   52,812   -47.8%
           RX     290,420   63,620   -78.1%
  UDP/1    TX      58,864  160,840  +173.2%
           RX     137,516   49,900   -63.7%
  UDP/128  TX      66,148  116,096   +75.5%
           RX     154,384   56,556   -63.4%

Updates tailscale/corp#46716
Updates tailscale/corp#22467
Updates tailscale/corp#36989
Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-04 09:32:55 -07:00

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, WritePacketStartOffset+totalLen, WritePacketStartOffset+(totalLen*2))
ipv4H := header.IPv4(bufs[i][WritePacketStartOffset:])
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][WritePacketStartOffset+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][WritePacketStartOffset+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][WritePacketStartOffset+iphLen+tcphLen:], pseudoCsum)
tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum))
}
_, err := dev.Write(bufs, WritePacketStartOffset)
return err
}