{"record":{"id":"022fd439f5162258","repo":"juanfont/headscale","slug":"invalid-port-range-first-port-is-greater-than-las","errorCode":null,"errorMessage":"invalid port range: first port is greater than last port","messagePattern":"invalid port range: first port is greater than last port","errorType":"validation","errorClass":"ErrPortRangeInverted","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/utils.go","lineNumber":20,"sourceCode":"\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\").\n\t// Strip brackets after validation and fall through to normal parsing.","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/utils.go#L2-L38","documentation":"ErrPortRangeInverted is returned by parsePortRange (hscontrol/policy/v2/utils.go:117) when a port range's first port is numerically greater than its last. Ranges are written low-high; headscale does not auto-swap an inverted range but rejects it so the intent is never silently misread.","triggerScenarios":"Dst port sections like \"host:443-80\" or \"host:8080-80\". Raised after both endpoints parse successfully and first > last.","commonSituations":"Writing the well-known port second out of habit (\"443-80\"); swapping variables in a template; quick manual edits of range endpoints.","solutions":["Swap the endpoints so the smaller port comes first: \"80-443\"","If you meant a single port, write it without the range: \"443\"","Double-check any templated start/end variables for reversed binding"],"exampleFix":"// before\n\"dst\": [\"tag:web:443-80\"]\n// after\n\"dst\": [\"tag:web:80-443\"]","handlingStrategy":"validation","validationCode":"func portRangeOrdered(tok string) bool {\n    if !strings.Contains(tok, \"-\") { return true }\n    parts := strings.SplitN(tok, \"-\", 2)\n    a, e1 := strconv.Atoi(parts[0]); b, e2 := strconv.Atoi(parts[1])\n    return e1 == nil && e2 == nil && a <= b\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, policyv2.ErrPortRangeInverted) {\n    // swap endpoints so the smaller port is first\n}","preventionTips":["Always write ranges low-high","Bind template start/end variables in the right order","headscale does not auto-swap; typos surface as errors, not silent fixes"],"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"}