juanfont/headscale · error · ErrInvalidPortNumber
invalid first integer
Error message
invalid first integer
What it means
ErrInvalidPortNumber is returned by parsePort (hscontrol/policy/v2/utils.go:142) when a port token is not a parseable integer at all — strconv.Atoi fails. This catches service names ("http", "ssh"), alphabetic strings, trailing units ("8080tcp"), and other non-numeric garbage in the port position. Note the sentinel text says 'invalid first integer' but it applies to any port token, first or last.
Source
Thrown at hscontrol/policy/v2/utils.go:22
"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").
// Strip brackets after validation and fall through to normal parsing.
if strings.HasPrefix(input, "[") {
closeBracket := strings.Index(input, "]")View on GitHub (pinned to 565fd254d0)
Solutions
- Replace the service name with its numeric port (http -> 80, ssh -> 22, https -> 443)
- Remove any non-numeric suffixes/characters from the token
- Check range endpoints too — "80-https" fails with the same error
Example fix
// before "dst": ["tag:web:http"] // after "dst": ["tag:web:80"]
Defensive patterns
Strategy: validation
Validate before calling
func portNumeric(tok string) bool {
for _, p := range strings.Split(tok, ",") {
for _, q := range strings.Split(p, "-") {
if q == "" || q == "*" { continue }
if _, err := strconv.Atoi(q); err != nil { return false }
}
}
return true
} Try / catch
if errors.Is(err, policyv2.ErrInvalidPortNumber) {
// replace service names (http/ssh) with numeric ports; strip stray characters
} Prevention
- headscale takes numeric ports only — keep an IANA port map handy
- Watch for letter/digit confusion (O vs 0, l vs 1)
- Validate both endpoints of ranges
When it happens
Trigger: Dst port sections like "host:http", "host:ssh-443", or "host:8o" (letter o). Raised when strconv.Atoi(portStr) errors inside parsePort, reached for single ports and both range endpoints.
Common situations: Writing IANA service names instead of numbers (headscale requires numeric ports); OCR/typing errors (letter l vs 1, O vs 0); copy-pasting from docs that use names; localized keyboards producing odd digits.
Related errors
- 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
- port number out of range
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/9d1b9dcb3ddefa9e.
Report an issue: GitHub.