juanfont/headscale · error · ErrInvalidPortRangeFormat
invalid port range format
Error message
invalid port range format
What it means
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.
Source
Thrown at hscontrol/policy/v2/utils.go:19
package v2
import (
"errors"
"fmt"
"net/netip"
"slices"
"strconv"
"strings"
"tailscale.com/tailcfg"
)
// Port parsing errors.
var (
ErrInputMissingColon = errors.New("input must contain a colon character separating destination and port")
ErrInputStartsWithColon = errors.New("input cannot start with a colon character")
ErrInputEndsWithColon = errors.New("input cannot end with a colon character")
ErrInvalidPortRangeFormat = errors.New("invalid port range format")
ErrPortRangeInverted = errors.New("invalid port range: first port is greater than last port")
ErrPortMustBePositive = errors.New("first port must be >0, or use '*' for wildcard")
ErrInvalidPortNumber = errors.New("invalid first integer")
ErrPortNumberOutOfRange = errors.New("port number out of range")
ErrBracketsNotIPv6 = errors.New("square brackets are only valid around IPv6 addresses")
)
// splitDestinationAndPort takes an input string and returns the destination and port as a tuple, or an error if the input is invalid.
// It supports two bracketed IPv6 forms:
// - "[addr]:port" (RFC 3986, e.g. "[::1]:80")
// - "[addr]/prefix:port" (e.g. "[fd7a::1]/128:80,443")
//
// Brackets are only accepted around IPv6 addresses, not IPv4, hostnames, or other alias types.
// Bracket stripping reduces both forms to bare "addr:port" or "addr/prefix:port",
// which the normal [strings.LastIndex] of ":" split handles correctly because
// port strings never contain colons.
func splitDestinationAndPort(input string) (string, string, error) {
// Handle RFC 3986 bracketed IPv6 (e.g. "[::1]:80" or "[fd7a::1]/128:80,443").View on GitHub (pinned to 565fd254d0)
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
Example fix
// before "dst": ["tag:web:80-"] // after "dst": ["tag:web:80-443"]
Defensive patterns
Strategy: validation
Validate before calling
// Validate a port token is a clean "a-b" range or single number
func portTokenValid(tok string) bool {
if !strings.Contains(tok, "-") { return true }
parts := strings.Split(tok, "-")
nonEmpty := 0
for _, p := range parts { if p != "" { nonEmpty++ } }
return nonEmpty == 2
} Try / catch
if errors.Is(err, policyv2.ErrInvalidPortRangeFormat) {
// fix the range to exactly "first-last"
} Prevention
- Ranges are strictly two numeric endpoints
- Never leave one side of a range empty
- Check rendered template ranges for empty variables
When it happens
Trigger: 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.
Common situations: Typos when writing ranges; templating a range as "${start}-${end}" with one variable empty; accidentally double hyphens; port lists like "80,-443".
Related errors
- invalid port range: first port is greater than last port
- input must contain a colon character separating destination
- input cannot start with a colon character
- input cannot end with a colon character
- first port must be >0, or use '*' for wildcard
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/adf7361e9c4348a3.
Report an issue: GitHub.