tailscale/tailscale · error

add return cgnat range rule v4: %w

Error message

add return cgnat range rule v4: %w

What it means

The CGNATModeReturn branch of AddExternalCGNATRules adds a rule that lets traffic sourced from the CGNAT range on non-Tailscale interfaces return to normal INPUT processing instead of being dropped, used when the host itself legitimately uses CGNAT space upstream. Failure comes from addReturnCGNATRangeRule: either createRangeRule cannot build the rule, or conn.Flush returns the kernel's rejection ("add rule: <errno>") for the queued insert.

Source

Thrown at util/linuxfw/nftables_runner.go:1634

// interfaces.
func (n *nftablesRunner) AddExternalCGNATRules(mode CGNATMode, tunname string) error {
	conn := n.conn

	inputChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameInput)
	if err != nil {
		return fmt.Errorf("get input chain v4: %v", err)
	}
	switch mode {
	case CGNATModeDrop:
		if err = addReturnChromeOSVMRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
			return fmt.Errorf("add return chromeos vm range rule v4: %w", err)
		}
		if err = addDropCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
			return fmt.Errorf("add drop cgnat range rule v4: %w", err)
		}
	case CGNATModeReturn:
		if err = addReturnCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
			return fmt.Errorf("add return cgnat range rule v4: %w", err)
		}
	default:
		return fmt.Errorf("unsupported cgnat mode %q", mode)
	}
	if err = conn.Flush(); err != nil {
		return fmt.Errorf("flush cgnat rules v4: %w", err)
	}
	return nil
}

// DelExternalCGNATRules removes the rules created by AddExternalCGNATRules,
// if they exist.
func (n *nftablesRunner) DelExternalCGNATRules(mode CGNATMode, tunname string) error {
	conn := n.conn

	inputChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameInput)
	if err != nil {
		return fmt.Errorf("get input chain v4: %v", err)

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Check CAP_NET_ADMIN and retry
  2. Confirm INPUT and ts-input chains exist and are stable during the call
  3. Coordinate with external firewall tooling to avoid mid-batch flushes
  4. Validate kernel nftables support with a manual rule add

Example fix

// before
if err := nfr.AddExternalCGNATRules(linuxfw.CGNATModeReturn, tunname); err != nil {
    return err
}

// after
if err := nfr.AddExternalCGNATRules(linuxfw.CGNATModeReturn, tunname); err != nil {
    var opErr *netlink.OpError
    if errors.As(err, &opErr) {
        return fmt.Errorf("netlink op %q failed: %w", opErr.Op, err)
    }
    return err
}
Defensive patterns

Strategy: try-catch

Type guard

func isNetlinkOpError(err error) (*netlink.OpError, bool) {
    var opErr *netlink.OpError
    return opErr, errors.As(err, &opErr)
}

Try / catch

if err := nfr.AddExternalCGNATRules(linuxfw.CGNATModeReturn, tunname); err != nil {
    if opErr, ok := isNetlinkOpError(err); ok {
        log.Printf("netlink op %s failed: %v", opErr.Op, opErr.Err)
    }
    return err
}

Prevention

When it happens

Trigger: AddExternalCGNATRules(CGNATModeReturn, tunname) with missing capabilities, a vanished or stale chain handle after concurrent flush, unsupported kernel expressions, or a closed netlink conn.

Common situations: ISPs behind CGNAT (100.64.0.0/10 used on the WAN side) where Return mode is chosen; firewall reload races; unprivileged execution.

Related errors


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