tailscale/tailscale · error

couldn't check system's UDP GRO forwarding configuration, fa

Error message

couldn't check system's UDP GRO forwarding configuration, failed to retrieve TUN device features: %v

What it means

The GRO check reads the offload feature bitmap of the Tailscale TUN interface via ethtool.Features and this call failed: the interface name is wrong or absent, the TUN lives in another network namespace, or the TUN driver does not implement ethtool feature reporting.

Source

Thrown at net/netkernelconf/netkernelconf_linux.go:38

// CheckUDPGROForwarding checks if the machine is optimally configured to
// forward UDP packets between the default route and Tailscale TUN interfaces.
// It returns a non-nil warn in the case that the configuration is suboptimal.
// It returns a non-nil err in the case that an error is encountered while
// performing the check.
func CheckUDPGROForwarding(tunInterface, defaultRouteInterface string) (warn, err error) {
	const kbLink = "\nSee https://tailscale.com/s/ethtool-config-udp-gro"
	errWithPrefix := func(format string, a ...any) error {
		const errPrefix = "couldn't check system's UDP GRO forwarding configuration, "
		return fmt.Errorf(errPrefix+format, a...)
	}
	e, err := ethtool.NewEthtool()
	if err != nil {
		return nil, errWithPrefix("failed to init ethtool: %v", err)
	}
	defer e.Close()
	tunFeatures, err := e.Features(tunInterface)
	if err != nil {
		return nil, errWithPrefix("failed to retrieve TUN device features: %v", err)
	}
	if !tunFeatures[txFeature] {
		// if txFeature is disabled/nonexistent on the TUN then UDP GRO
		// forwarding doesn't matter, we won't be taking advantage of it.
		return nil, nil
	}
	defaultFeatures, err := e.Features(defaultRouteInterface)
	if err != nil {
		return nil, errWithPrefix("failed to retrieve default route interface features: %v", err)
	}
	defaultHasRxWant, ok := defaultFeatures[rxWantFeature]
	if !ok {
		// unlikely the feature is nonexistent with txFeature in the TUN driver
		// being added to the kernel later than rxWantFeature, but let's be sure
		return nil, nil
	}
	if !defaultHasRxWant || defaultFeatures[rxDoNotWantFeature] {
		return fmt.Errorf("UDP GRO forwarding is suboptimally configured on %s, UDP forwarding throughput capability will increase with a configuration change.%s", defaultRouteInterface, kbLink), nil

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Verify the interface exists first: ip link show <tunInterface>
  2. Run the check after the TUN is up, or pass the actual interface name
  3. Run in the same network namespace as the TUN device
Defensive patterns

Strategy: validation

Validate before calling

if _, err := net.InterfaceByName(tunInterface); err != nil {
 return fmt.Errorf("TUN interface %q not present: %w", tunInterface, err)
}
warn, err := netkernelconf.CheckUDPGROForwarding(tunInterface, defaultRouteInterface)

Try / catch

warn, err := netkernelconf.CheckUDPGROForwarding(tun, def)
if err != nil && strings.Contains(err.Error(), "failed to retrieve TUN device features") {
 // wrong interface name or netns: verify with `ip link` and retry after TUN creation
 log.Printf("TUN feature query failed; retry after tailscaled is up: %v", err)
}

Prevention

When it happens

Trigger: Passing a TUN interface name that does not exist (check runs before tailscale0 is created), a non-TUN interface name, containerized setups where the TUN device belongs to a different netns, or TUN drivers without ethtool ops.

Common situations: Health checks racing with tailscaled startup, custom TUN names (--tun=...), Dockerized tailscaled with mismatched namespaces, swapped argument order.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/7119cc7ed70a4cee. Report an issue: GitHub.