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
- Check CAP_NET_ADMIN and retry
- Confirm INPUT and ts-input chains exist and are stable during the call
- Coordinate with external firewall tooling to avoid mid-batch flushes
- 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
- Choose the mode deliberately: Drop unless the upstream network itself uses CGNAT space
- Keep privileges and chain stability during configuration
- Retry after external firewall churn settles
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
- add accept on port rule: %v
- add return chromeos vm range rule v4: %w
- add drop cgnat range rule v4: %w
- flush cgnat rules v4: %w
- del drop cgnat range rule v4: %w
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/1b591bb249376dd9.
Report an issue: GitHub.