{"record":{"id":"8ed88ae55030b730","repo":"juanfont/headscale","slug":"first-port-must-be-0-or-use-for-wildcard","errorCode":null,"errorMessage":"first port must be >0, or use '*' for wildcard","messagePattern":"first port must be >0, or use '\\*' for wildcard","errorType":"validation","errorClass":"ErrPortMustBePositive","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/utils.go","lineNumber":21,"sourceCode":"import (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"tailscale.com/tailcfg\"\n)\n\n// Port parsing errors.\nvar (\n\tErrInputMissingColon      = errors.New(\"input must contain a colon character separating destination and port\")\n\tErrInputStartsWithColon   = errors.New(\"input cannot start with a colon character\")\n\tErrInputEndsWithColon     = errors.New(\"input cannot end with a colon character\")\n\tErrInvalidPortRangeFormat = errors.New(\"invalid port range format\")\n\tErrPortRangeInverted      = errors.New(\"invalid port range: first port is greater than last port\")\n\tErrPortMustBePositive     = errors.New(\"first port must be >0, or use '*' for wildcard\")\n\tErrInvalidPortNumber      = errors.New(\"invalid first integer\")\n\tErrPortNumberOutOfRange   = errors.New(\"port number out of range\")\n\tErrBracketsNotIPv6        = errors.New(\"square brackets are only valid around IPv6 addresses\")\n)\n\n// splitDestinationAndPort takes an input string and returns the destination and port as a tuple, or an error if the input is invalid.\n// It supports two bracketed IPv6 forms:\n//   - \"[addr]:port\" (RFC 3986, e.g. \"[::1]:80\")\n//   - \"[addr]/prefix:port\" (e.g. \"[fd7a::1]/128:80,443\")\n//\n// Brackets are only accepted around IPv6 addresses, not IPv4, hostnames, or other alias types.\n// Bracket stripping reduces both forms to bare \"addr:port\" or \"addr/prefix:port\",\n// which the normal [strings.LastIndex] of \":\" split handles correctly because\n// port strings never contain colons.\nfunc splitDestinationAndPort(input string) (string, string, error) {\n\t// Handle RFC 3986 bracketed IPv6 (e.g. \"[::1]:80\" or \"[fd7a::1]/128:80,443\").\n\t// Strip brackets after validation and fall through to normal parsing.\n\tif strings.HasPrefix(input, \"[\") {","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/utils.go#L3-L39","documentation":"ErrPortMustBePositive is returned by parsePortRange (hscontrol/policy/v2/utils.go:128) when a single (non-range) port token parses to a number less than 1 — in practice port 0. Port 0 is not a usable destination port; the wildcard (which covers 0-65535) must be written as \"*\" instead.","triggerScenarios":"Dst port sections like \"host:0\" as a single token. Raised in the non-hyphen branch of parsePortRange when parsePort succeeds but port < 1.","commonSituations":"Using 0 as shorthand for 'any port' (coming from other firewall syntaxes); templating a port variable that defaults to 0; off-by-one loops generating port lists starting at 0.","solutions":["Use \"*\" for the wildcard: \"host:*\"","Or specify a real port >= 1","Fix generators/templates that emit port 0"],"exampleFix":"// before\n\"dst\": [\"tag:web:0\"]\n// after\n\"dst\": [\"tag:web:*\"]","handlingStrategy":"validation","validationCode":"func portPositive(tok string) bool {\n    if tok == \"*\" { return true }\n    n, err := strconv.Atoi(tok)\n    return err == nil && n >= 1\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, policyv2.ErrPortMustBePositive) {\n    // replace 0 with \"*\" or a port >= 1\n}","preventionTips":["Port 0 is never valid; use \"*\" for wildcard","Fix generators that default ports to 0"],"tags":["policy","acl","parsing","ports"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}