tailscale/tailscale · error

unsupported mode %q

Error message

unsupported mode %q

What it means

DelExternalCGNATRules accepts only CGNATModeDrop ("DROP") or CGNATModeReturn ("RETURN") and returns this error from the default branch for any other CGNATMode value, before any netlink work. The wording differs slightly from the add path ("unsupported mode %q" vs "unsupported cgnat mode %q") but the semantics are identical. CGNATMode is a string type, so any string converts silently and fails only here at runtime.

Source

Thrown at util/linuxfw/nftables_runner.go:1667

	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 = delReturnChromeOSVMRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
			return fmt.Errorf("del return chromeos vm range rule v4: %w", err)
		}
		if err = delDropCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
			return fmt.Errorf("del drop cgnat range rule v4: %w", err)
		}
	case CGNATModeReturn:
		if err = delReturnCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
			return fmt.Errorf("del return cgnat range rule v4: %w", err)
		}
	default:
		return fmt.Errorf("unsupported mode %q", mode)
	}
	if err = conn.Flush(); err != nil {
		return fmt.Errorf("flush cgnat rules v4: %w", err)
	}
	return nil
}

// createAcceptIncomingPacketRule creates a rule to accept incoming packets to
// the given interface.
func createAcceptIncomingPacketRule(table *nftables.Table, chain *nftables.Chain, tunname string) *nftables.Rule {
	return &nftables.Rule{
		Table: table,
		Chain: chain,
		Exprs: []expr.Any{
			&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Pass only linuxfw.CGNATModeDrop or linuxfw.CGNATModeReturn
  2. Validate any string-sourced mode against both constants at the config layer before it reaches the router
  3. Keep the mode chosen for Add and reuse the identical value for Del

Example fix

// before
if err := nfr.DelExternalCGNATRules(linuxfw.CGNATMode(""), tunname); err != nil {
    return err // unsupported mode ""
}

// after
if mode != linuxfw.CGNATModeDrop && mode != linuxfw.CGNATModeReturn {
    return fmt.Errorf("invalid cgnat mode %q", mode)
}
if err := nfr.DelExternalCGNATRules(mode, tunname); err != nil {
    return err
}
Defensive patterns

Strategy: validation

Validate before calling

if mode != linuxfw.CGNATModeDrop && mode != linuxfw.CGNATModeReturn {
    return fmt.Errorf("refusing teardown with invalid cgnat mode %q", mode)
}

Type guard

func isValidCGNATMode(m linuxfw.CGNATMode) bool {
    return m == linuxfw.CGNATModeDrop || m == linuxfw.CGNATModeReturn
}

Try / catch

if err := nfr.DelExternalCGNATRules(mode, tunname); err != nil {
    if strings.Contains(err.Error(), "unsupported mode") {
        return fmt.Errorf("caller bug: mode %q is not a CGNATMode constant", mode)
    }
    return err
}

Prevention

When it happens

Trigger: DelExternalCGNATRules(mode, tunname) with mode from unvalidated config ("return" lowercase, empty zero value), a stale constant from a vendored older linuxfw, or a mode computed from a protocol string never mapped to the two constants.

Common situations: Config-driven firewall teardown; code that shares one mode variable between iptables and nftables runners but builds it from raw text; version drift after vendoring updates.

Related errors


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