{"record":{"id":"adf7361e9c4348a3","repo":"juanfont/headscale","slug":"invalid-port-range-format","errorCode":null,"errorMessage":"invalid port range format","messagePattern":"invalid port range format","errorType":"validation","errorClass":"ErrInvalidPortRangeFormat","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/utils.go","lineNumber":19,"sourceCode":"package v2\n\nimport (\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\").","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/utils.go#L1-L37","documentation":"ErrInvalidPortRangeFormat is returned by parsePortRange (hscontrol/policy/v2/utils.go:103) when a port range part split on '-' does not yield exactly two non-empty components. Ranges must be \"first-last\" with both endpoints present; forms like \"80-\", \"-80\", \"80--90\", or \"1-2-3\" (after empty segments are removed) fail this check.","triggerScenarios":"Dst port sections like \"host:80-\", \"host:-443\", \"host:80-100-200\", or \"host:80--90\". Raised when strings.Split(part, \"-\") minus empty strings != 2 elements.","commonSituations":"Typos when writing ranges; templating a range as \"${start}-${end}\" with one variable empty; accidentally double hyphens; port lists like \"80,-443\".","solutions":["Rewrite the port as a proper two-endpoint range \"80-443\"","For a single port, drop the hyphen entirely: \"443\"","Check templated variables used in the port field render non-empty"],"exampleFix":"// before\n\"dst\": [\"tag:web:80-\"]\n// after\n\"dst\": [\"tag:web:80-443\"]","handlingStrategy":"validation","validationCode":"// Validate a port token is a clean \"a-b\" range or single number\nfunc portTokenValid(tok string) bool {\n    if !strings.Contains(tok, \"-\") { return true }\n    parts := strings.Split(tok, \"-\")\n    nonEmpty := 0\n    for _, p := range parts { if p != \"\" { nonEmpty++ } }\n    return nonEmpty == 2\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, policyv2.ErrInvalidPortRangeFormat) {\n    // fix the range to exactly \"first-last\"\n}","preventionTips":["Ranges are strictly two numeric endpoints","Never leave one side of a range empty","Check rendered template ranges for empty variables"],"tags":["policy","acl","parsing","port-range"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}