juanfont/headscale · warning
invalid destination %q: %w
Error message
invalid destination %q: %w
What it means
In the policy-test evaluator, the destination string of an accept/deny check (host:port form) failed parseDestinationAlias. The destination is syntactically invalid: malformed port range, missing ':', or an alias component that does not parse.
Source
Thrown at hscontrol/policy/v2/test.go:349
}
if addrs == nil || addrs.Empty() {
return nil, nil
}
return addrs.Prefixes(), nil
}
// evalReachability reports whether traffic from any srcPrefix to dst (in
// `host:port` form) is allowed by filter for the requested protocol.
//
// Empty proto means the default set the client applies when proto is
// omitted (TCP/UDP/ICMP) — we accept a rule whose IPProto list contains
// any of those, or rules with no IPProto restriction at all.
func evalReachability(srcPrefixes []netip.Prefix, dst string, proto Protocol, pol *Policy, filter []tailcfg.FilterRule, users []types.User, nodes views.Slice[types.NodeView]) (bool, error) {
awp, err := parseDestinationAlias(dst)
if err != nil {
return false, fmt.Errorf("invalid destination %q: %w", dst, err)
}
dstAddrs, err := awp.Resolve(pol, users, nodes)
if err != nil {
return false, fmt.Errorf("resolving destination: %w", err)
}
if dstAddrs == nil || dstAddrs.Empty() {
return false, fmt.Errorf("%w: %q", errTestDestinationNoIP, dst)
}
dstPrefixes := dstAddrs.Prefixes()
// Tailscale's tests semantics: ALL src prefixes must reach the dst for
// the test to consider it allowed. A partial allow is a fail.
for _, src := range srcPrefixes {
if !srcReachesDst(src, dstPrefixes, awp.Ports, proto, filter) {
return false, nilView on GitHub (pinned to 565fd254d0)
Solutions
- Rewrite the destination as 'alias:port' or 'alias:port-port', e.g. 'web:80' or 'web:8080-8090'.
- Ensure every port is in 0-65535 and ranges are 'low-high' with low <= high.
- For multiple ports use a comma-separated list of destinations ('web:80, web:443'), not multiple colons.
- For IPv6 destinations, use the canonical form accepted by the alias parser (a hosts-map name or a documented prefix form), never a bare '::'-style literal with a port.
Example fix
// before "accept": ["web:80,443"] // after "accept": ["web:80", "web:443"]
Defensive patterns
Strategy: validation
Validate before calling
// Validate test destinations before running the suite.
var dstRe = regexp.MustCompile(`^[^:]+:[0-9]+(-[0-9]+)?$`)
if !dstRe.MatchString(dst) {
return fmt.Errorf("bad test destination %q, want alias:port[-port]", dst)
} Try / catch
if err := runPolicyTests(...); err != nil {
if strings.Contains(err.Error(), "invalid destination") {
// rewrite the dst entry as alias:port and re-run once
}
return err
} Prevention
- Always write destinations as 'alias:port'; one colon per entry.
- Use separate entries for extra ports instead of inventing separators.
- Add a policy-file linter (HuJSON schema check) to CI.
When it happens
Trigger: A test entry like "web:80-" , "web:" , "10.0.0.1:99999", or "web:80:443" (multiple colons). parseDestinationAlias(dst) returns an error inside evalReachability.
Common situations: Hand-writing test destinations and using a comma where a colon is expected ("web,80"), open-ended port ranges, ports outside 0-65535, or extra colons from copy-pasting IPv6 addresses without brackets.
Related errors
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/76cd3e6d58a6572d.
Report an issue: GitHub.