juanfont/headscale · error · ErrGrantDefaultRouteCIDR
dst %q: %w
Error message
dst %q: %w
What it means
Thrown in Policy.validate()'s grants loop (hscontrol/policy/v2/types.go:2566) when a grant destination is a raw CIDR with prefix length 0 — i.e. 0.0.0.0/0 or ::/0. Tailscale's semantics require expressing 'all IP addresses' as the wildcard "*" or "autogroup:internet" instead; the sentinel ErrGrantDefaultRouteCIDR's text says exactly that. Only the first offending dst is reported (break after append).
Source
Thrown at hscontrol/policy/v2/types.go:2566
// Validate that app grants are not used with autogroup:internet.
if hasApp {
for _, dst := range grant.Destinations {
if ag, ok := dst.(*AutoGroup); ok && ag.Is(AutoGroupInternet) {
errs = append(errs, ErrGrantAppWithAutogroupInternet)
break
}
}
}
// Validate destinations do not contain raw default route CIDRs.
// Tailscale rejects 0.0.0.0/0 and ::/0 as grant dst, requiring
// "*" or "autogroup:internet" instead.
for _, dst := range grant.Destinations {
if p, ok := dst.(*Prefix); ok {
prefix := netip.Prefix(*p)
if prefix.Bits() == 0 {
errs = append(errs, fmt.Errorf(
"dst %q: %w",
prefix.String(), ErrGrantDefaultRouteCIDR,
))
break
}
}
}
// Validate sources (empty arrays are allowed — they produce no rules)
for _, src := range grant.Sources {
switch src := src.(type) {
case *Host:
h := src
if !p.Hosts.exist(*h) {
errs = append(errs, fmt.Errorf("%w: %q", ErrHostNotDefined, *h))
}
case *AutoGroup:View on GitHub (pinned to 565fd254d0)
Solutions
- Use "*" as the grant dst for all addresses ("dst": ["*"])
- Or use "autogroup:internet" to scope the rule to publicly routable addresses only
- Keep specific CIDRs (non-/0) as-is; only the two default routes are rejected
Example fix
// before
"grants": [{ "src": ["group:eng"], "dst": ["0.0.0.0/0", "::/0"], "ip": ["tcp/443"] }]
// after
"grants": [{ "src": ["group:eng"], "dst": ["*"], "ip": ["tcp/443"] }] Defensive patterns
Strategy: validation
Validate before calling
func grantDstHasDefaultRoute(g *policyv2.GrantGrant) bool {
for _, d := range g.Destinations {
if p, ok := d.(*policyv2.Prefix); ok { return netip.Prefix(*p).Bits() == 0 }
}
return false
} Type guard
func isDefaultRouteCIDR(s string) bool {
p, err := netip.ParsePrefix(strings.SplitN(s, ":", 2)[0])
return err == nil && p.Bits() == 0
} Try / catch
if err := pol.Validate(); errors.Is(err, policyv2.ErrGrantDefaultRouteCIDR) { /* replace 0.0.0.0/0 / ::/0 with "*" or autogroup:internet */ } Prevention
- Use "*" for all-address grants and autogroup:internet for public-internet-only grants
- Only /0 prefixes are rejected — specific CIDRs are fine
When it happens
Trigger: A grants entry with "dst": ["0.0.0.0/0"] or "dst": ["::/0"] (optionally with ports, e.g. "0.0.0.0/0:443"). Affects exit-node/internet egress style rules written as raw CIDRs.
Common situations: Writing an internet-egress grant as 0.0.0.0/0 out of firewall habit; converting firewall rules or k8s NetworkPolicies to grants; partial migration from ACLs where a wildcard dst "*" was used and someone 'simplified' it to the CIDR.
Related errors
- invalid prefix
- test destination must be a single host, not a CIDR range
- tag %q not found
- test(s) failed
- tags in SSH source cannot access user-owned devices
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/80cf0ac4db06cb76.
Report an issue: GitHub.