{"record":{"id":"80cf0ac4db06cb76","repo":"juanfont/headscale","slug":"dst-q-w-80cf0a","errorCode":null,"errorMessage":"dst %q: %w","messagePattern":"dst %q: %w","errorType":"validation","errorClass":"ErrGrantDefaultRouteCIDR","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2566,"sourceCode":"\t\t// Validate that app grants are not used with autogroup:internet.\n\t\tif hasApp {\n\t\t\tfor _, dst := range grant.Destinations {\n\t\t\t\tif ag, ok := dst.(*AutoGroup); ok && ag.Is(AutoGroupInternet) {\n\t\t\t\t\terrs = append(errs, ErrGrantAppWithAutogroupInternet)\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate destinations do not contain raw default route CIDRs.\n\t\t// Tailscale rejects 0.0.0.0/0 and ::/0 as grant dst, requiring\n\t\t// \"*\" or \"autogroup:internet\" instead.\n\t\tfor _, dst := range grant.Destinations {\n\t\t\tif p, ok := dst.(*Prefix); ok {\n\t\t\t\tprefix := netip.Prefix(*p)\n\t\t\t\tif prefix.Bits() == 0 {\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\n\t\t\t\t\t\t\"dst %q: %w\",\n\t\t\t\t\t\tprefix.String(), ErrGrantDefaultRouteCIDR,\n\t\t\t\t\t))\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate sources (empty arrays are allowed — they produce no rules)\n\t\tfor _, src := range grant.Sources {\n\t\t\tswitch src := src.(type) {\n\t\t\tcase *Host:\n\t\t\t\th := src\n\t\t\t\tif !p.Hosts.exist(*h) {\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"%w: %q\", ErrHostNotDefined, *h))\n\t\t\t\t}\n\t\t\tcase *AutoGroup:","sourceCodeStart":2548,"sourceCodeEnd":2584,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2548-L2584","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n\"grants\": [{ \"src\": [\"group:eng\"], \"dst\": [\"0.0.0.0/0\", \"::/0\"], \"ip\": [\"tcp/443\"] }]\n\n// after\n\"grants\": [{ \"src\": [\"group:eng\"], \"dst\": [\"*\"], \"ip\": [\"tcp/443\"] }]","handlingStrategy":"validation","validationCode":"func grantDstHasDefaultRoute(g *policyv2.GrantGrant) bool {\n    for _, d := range g.Destinations {\n        if p, ok := d.(*policyv2.Prefix); ok { return netip.Prefix(*p).Bits() == 0 }\n    }\n    return false\n}","typeGuard":"func isDefaultRouteCIDR(s string) bool {\n    p, err := netip.ParsePrefix(strings.SplitN(s, \":\", 2)[0])\n    return err == nil && p.Bits() == 0\n}","tryCatchPattern":"if err := pol.Validate(); errors.Is(err, policyv2.ErrGrantDefaultRouteCIDR) { /* replace 0.0.0.0/0 / ::/0 with \"*\" or autogroup:internet */ }","preventionTips":["Use \"*\" for all-address grants and autogroup:internet for public-internet-only grants","Only /0 prefixes are rejected — specific CIDRs are fine"],"tags":["policy","grants","cidr","default-route","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}