juanfont/headscale · warning
resolving destination: %w
Error message
resolving destination: %w
What it means
The destination alias in a policy test parsed but failed to resolve against the policy, users, and nodes. The wrapped error names the real cause — typically ErrHostResolve (unknown host) or a user/autogroup problem — occurring on the destination side of an accept/deny check.
Source
Thrown at hscontrol/policy/v2/test.go:354
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, nil
}
}
return true, nil
}View on GitHub (pinned to 565fd254d0)
Solutions
- Read the wrapped error after 'resolving destination:' to get the exact cause.
- If a hostname, add it to the 'hosts' section of the policy (e.g. "web": "100.64.0.10/32") or use the IP/CIDR directly.
- If a user/tag destination, confirm the user exists or the tag is used somewhere in the policy.
- Avoid autogroups as destinations; they are source-oriented.
Example fix
// before
"hosts": {},
"tests": [{"src": "a@example.com", "accept": ["web:80"]}]
// after
"hosts": {"web": "100.64.0.10/32"},
"tests": [{"src": "a@example.com", "accept": ["web:80"]}] Defensive patterns
Strategy: validation
Validate before calling
// Ensure every hostname used in tests exists in the hosts map.
func hostDeclared(h string, hosts map[v2.Host]netip.Prefix) bool {
_, ok := hosts[v2.Host(h)]
return ok
} Try / catch
if err := runPolicyTests(...); err != nil {
if errors.Is(err, v2.ErrHostResolve) {
// destination host missing from hosts map: add it or switch to CIDR
}
return err
} Prevention
- Define every referenced hostname in the hosts section in the same commit.
- Prefer CIDR destinations in tests to decouple from hosts-map churn.
- Grep policy files for hostnames absent from the hosts block before merging.
When it happens
Trigger: A test destination like 'unknownhost:80' where 'unknownhost' is not defined in the policy's hosts map, or an autogroup/tag destination the engine cannot resolve. awp.Resolve(pol, users, nodes) errors inside evalReachability.
Common situations: Destination hostnames drift out of sync with the hosts section after renames; tests written against Tailscale semantics using destination aliases headscale cannot resolve (e.g. autogroup:self as a destination).
Related errors
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/86d1d01fbaf9cc6e.
Report an issue: GitHub.