{"record":{"id":"b642098d24db992c","repo":"juanfont/headscale","slug":"w-q-overlaps-q","errorCode":null,"errorMessage":"%w: %q overlaps %q","messagePattern":"%w: %q overlaps %q","errorType":"validation","errorClass":"ErrNodeAttrsIPPoolReserved","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2102,"sourceCode":"\n\treturn nil\n}\n\n// validateNodeAttrIPPool rejects ipPool entries outside the CGNAT range or\n// overlapping the Tailscale-reserved subranges (MagicDNS, Quad100/IPN). A\n// [netip.Prefix] is considered \"within\" CGNAT when it is at least as specific as\n// 100.64.0.0/10 and its first address lies inside it.\nfunc validateNodeAttrIPPool(prefix netip.Prefix) error {\n\tcgnat := tsaddr.CGNATRange()\n\tmasked := prefix.Masked()\n\n\tif masked.Bits() < cgnat.Bits() || !cgnat.Contains(masked.Addr()) {\n\t\treturn fmt.Errorf(\"%w: %q\", ErrNodeAttrsIPPoolOutOfRange, prefix)\n\t}\n\n\tfor _, reserved := range reservedTSRanges {\n\t\tif masked.Overlaps(reserved) {\n\t\t\treturn fmt.Errorf(\"%w: %q overlaps %q\", ErrNodeAttrsIPPoolReserved, prefix, reserved)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc validateAutogroupForSSHSrc(src *AutoGroup) error {\n\tif src == nil {\n\t\treturn nil\n\t}\n\n\tif src.Is(AutoGroupInternet) {\n\t\treturn ErrAutogroupInternetSrc\n\t}\n\n\tif !slices.Contains(autogroupForSSHSrc, *src) {\n\t\treturn fmt.Errorf(\"%w: %q, can be %v\", ErrAutogroupNotSupportedSSHSrc, *src, autogroupForSSHSrc)\n\t}","sourceCodeStart":2084,"sourceCodeEnd":2120,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2084-L2120","documentation":"Thrown by validateNodeAttrIPPool (hscontrol/policy/v2/types.go:2102). The nodeAttrs ipPool prefix lies inside CGNAT but overlaps a Tailscale-reserved subrange: 100.100.100.0/24 (MagicDNS/TSMP) or 100.115.92.0/23 (Quad100/IPN service), defined in reservedTSRanges (types.go:2015). These subranges carry infrastructure services and must not be handed out as pool space. Like all ipPool entries it is additionally rejected by ErrNodeAttrIPPoolUnsupported because headscale lacks the IP allocator.","triggerScenarios":"A nodeAttrs ipPool such as \"100.64.0.0/10\" (the whole CGNAT range, which necessarily covers both reserved blocks), \"100.100.100.0/24\", or \"100.115.92.0/23\" in a validated policy.","commonSituations":"Using the full /10 as a pool for simplicity; guessing a pool that happens to cover the Quad100 address 100.100.100.100 or the IPN range; copying example configs written for the SaaS ip-pool KB article.","solutions":["Remove the ipPool key entirely — headscale does not implement ipPool (issue 2912)","If you must keep the entry for SaaS round-tripping, carve a pool that excludes both reserved blocks, e.g. 100.64.0.0/11 covers 100.64.0.0–100.95.255.255 which avoids both"],"exampleFix":"// before\n\"ipPool\": [\"100.64.0.0/10\"]\n\n// after\n(no ipPool key; or \"100.64.0.0/11\")","handlingStrategy":"validation","validationCode":"var reserved = []netip.Prefix{netip.MustParsePrefix(\"100.100.100.0/24\"), netip.MustParsePrefix(\"100.115.92.0/23\")}\nfunc overlapsReserved(p netip.Prefix) bool {\n    m := p.Masked()\n    for _, r := range reserved { if m.Overlaps(r) { return true } }\n    return false\n}","typeGuard":"func isSafeIPPool(p netip.Prefix) bool {\n    c := tsaddr.CGNATRange(); m := p.Masked()\n    if m.Bits() < c.Bits() || !c.Contains(m.Addr()) { return false }\n    for _, r := range reserved { if m.Overlaps(r) { return false } }\n    return true\n}","tryCatchPattern":"if err := pol.Validate(); errors.Is(err, policyv2.ErrNodeAttrsIPPoolReserved) { /* shrink pool below 100.100.100.0 */ }","preventionTips":["Avoid 100.100.100.0/24 (MagicDNS/Quad100) and 100.115.92.0/23 (IPN) in any pool","Never use the full 100.64.0.0/10 as a pool — it always overlaps both reserved ranges"],"tags":["policy","nodeattrs","ippool","reserved-ranges","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}