{"record":{"id":"7216066397b475f0","repo":"tailscale/tailscale","slug":"unsupported-mode-q-721606","errorCode":null,"errorMessage":"unsupported mode %q","messagePattern":"unsupported mode %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"util/linuxfw/nftables_runner.go","lineNumber":1667,"sourceCode":"\n\tinputChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameInput)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get input chain v4: %v\", err)\n\t}\n\tswitch mode {\n\tcase CGNATModeDrop:\n\t\tif err = delReturnChromeOSVMRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {\n\t\t\treturn fmt.Errorf(\"del return chromeos vm range rule v4: %w\", err)\n\t\t}\n\t\tif err = delDropCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {\n\t\t\treturn fmt.Errorf(\"del drop cgnat range rule v4: %w\", err)\n\t\t}\n\tcase CGNATModeReturn:\n\t\tif err = delReturnCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {\n\t\t\treturn fmt.Errorf(\"del return cgnat range rule v4: %w\", err)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported mode %q\", mode)\n\t}\n\tif err = conn.Flush(); err != nil {\n\t\treturn fmt.Errorf(\"flush cgnat rules v4: %w\", err)\n\t}\n\treturn nil\n}\n\n// createAcceptIncomingPacketRule creates a rule to accept incoming packets to\n// the given interface.\nfunc createAcceptIncomingPacketRule(table *nftables.Table, chain *nftables.Chain, tunname string) *nftables.Rule {\n\treturn &nftables.Rule{\n\t\tTable: table,\n\t\tChain: chain,\n\t\tExprs: []expr.Any{\n\t\t\t&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},\n\t\t\t&expr.Cmp{\n\t\t\t\tOp:       expr.CmpOpEq,\n\t\t\t\tRegister: 1,","sourceCodeStart":1649,"sourceCodeEnd":1685,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/util/linuxfw/nftables_runner.go#L1649-L1685","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass only linuxfw.CGNATModeDrop or linuxfw.CGNATModeReturn","Validate any string-sourced mode against both constants at the config layer before it reaches the router","Keep the mode chosen for Add and reuse the identical value for Del"],"exampleFix":"// before\nif err := nfr.DelExternalCGNATRules(linuxfw.CGNATMode(\"\"), tunname); err != nil {\n    return err // unsupported mode \"\"\n}\n\n// after\nif mode != linuxfw.CGNATModeDrop && mode != linuxfw.CGNATModeReturn {\n    return fmt.Errorf(\"invalid cgnat mode %q\", mode)\n}\nif err := nfr.DelExternalCGNATRules(mode, tunname); err != nil {\n    return err\n}","handlingStrategy":"validation","validationCode":"if mode != linuxfw.CGNATModeDrop && mode != linuxfw.CGNATModeReturn {\n    return fmt.Errorf(\"refusing teardown with invalid cgnat mode %q\", mode)\n}","typeGuard":"func isValidCGNATMode(m linuxfw.CGNATMode) bool {\n    return m == linuxfw.CGNATModeDrop || m == linuxfw.CGNATModeReturn\n}","tryCatchPattern":"if err := nfr.DelExternalCGNATRules(mode, tunname); err != nil {\n    if strings.Contains(err.Error(), \"unsupported mode\") {\n        return fmt.Errorf(\"caller bug: mode %q is not a CGNATMode constant\", mode)\n    }\n    return err\n}","preventionTips":["Never construct CGNATMode from raw strings without mapping","Persist the chosen mode and replay it on teardown","Validate at config load, not at teardown time"],"tags":["nftables","validation","cgnat","enum","tailscale","go"],"backgroundTag":"unsupported-enum-value","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}