juanfont/headscale · warning · errTestDestinationNoIP
%w: %q
Error message
%w: %q
What it means
A policy-test destination resolved syntactically but produced an empty IP set, so there is nothing to test reachability against. errTestDestinationNoIP is the dedicated sentinel for 'the alias is valid but names no addresses' — commonly a tag or group with no matching nodes/users in the test fixture.
Source
Thrown at hscontrol/policy/v2/test.go:358
// 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
}
// parseDestinationAlias is a thin wrapper over [AliasWithPorts.UnmarshalJSON]
// so callers can hand it a bare `"host:port"` string without re-implementing
// the parse logic.View on GitHub (pinned to 565fd254d0)
Solutions
- Give the alias at least one address in the test context: attach the tag to a node in the test fixture, add a member to the group, or use a concrete host/IP destination.
- If the alias is intentionally empty, remove that accept/deny entry — an empty destination cannot produce a meaningful pass/fail.
- Verify the destination is spelled the same as in the grants section (tag:server vs tag:servers).
Example fix
// before
"tests": [{"src": "a@example.com", "accept": ["tag:empty:80"]}]
// after
"tests": [{"src": "a@example.com", "accept": ["100.64.0.5:80"]}] Defensive patterns
Strategy: validation
Validate before calling
// Skip destinations that resolve to zero addresses before asserting.
addrs, err := alias.Resolve(pol, users, nodes)
if err != nil { return err }
if addrs == nil || addrs.Empty() {
return fmt.Errorf("destination %q has no IPs; attach a node or drop the entry", dst)
} Try / catch
if err := runPolicyTests(...); err != nil {
if errors.Is(err, v2.ErrTestDestinationNoIP) {
// empty alias: fix fixture (tag a node / add group member), not policy
}
return err
} Prevention
- Attach tags to at least one fixture node before testing tag destinations.
- Populate groups with at least one existing member.
- Treat empty-resolution as a fixture bug, not an ACL result.
When it happens
Trigger: Destination alias references a tag that no node carries, a group with no members, or a hosts entry that would resolve but the alias resolves empty; dstAddrs is nil or Empty() in evalReachability.
Common situations: Testing against 'tag:server' before any node has registered with that tag; group membership lists only users absent from the test user set; hosts entry pointing at an empty expansion.
Related errors
- invalid alias: %w
- resolving: %w
- invalid destination %q: %w
- resolving destination: %w
- dst=%q: port range %q: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/bcd8e5edc68bb21a.
Report an issue: GitHub.