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

  1. Use "*" as the grant dst for all addresses ("dst": ["*"])
  2. Or use "autogroup:internet" to scope the rule to publicly routable addresses only
  3. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/80cf0ac4db06cb76. Report an issue: GitHub.