juanfont/headscale · error · ErrNodeAttrsIPPoolReserved
%w: %q overlaps %q
Error message
%w: %q overlaps %q
What it means
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.
Source
Thrown at hscontrol/policy/v2/types.go:2102
return nil
}
// validateNodeAttrIPPool rejects ipPool entries outside the CGNAT range or
// overlapping the Tailscale-reserved subranges (MagicDNS, Quad100/IPN). A
// [netip.Prefix] is considered "within" CGNAT when it is at least as specific as
// 100.64.0.0/10 and its first address lies inside it.
func validateNodeAttrIPPool(prefix netip.Prefix) error {
cgnat := tsaddr.CGNATRange()
masked := prefix.Masked()
if masked.Bits() < cgnat.Bits() || !cgnat.Contains(masked.Addr()) {
return fmt.Errorf("%w: %q", ErrNodeAttrsIPPoolOutOfRange, prefix)
}
for _, reserved := range reservedTSRanges {
if masked.Overlaps(reserved) {
return fmt.Errorf("%w: %q overlaps %q", ErrNodeAttrsIPPoolReserved, prefix, reserved)
}
}
return nil
}
func validateAutogroupForSSHSrc(src *AutoGroup) error {
if src == nil {
return nil
}
if src.Is(AutoGroupInternet) {
return ErrAutogroupInternetSrc
}
if !slices.Contains(autogroupForSSHSrc, *src) {
return fmt.Errorf("%w: %q, can be %v", ErrAutogroupNotSupportedSSHSrc, *src, autogroupForSSHSrc)
}View on GitHub (pinned to 565fd254d0)
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
Example fix
// before "ipPool": ["100.64.0.0/10"] // after (no ipPool key; or "100.64.0.0/11")
Defensive patterns
Strategy: validation
Validate before calling
var reserved = []netip.Prefix{netip.MustParsePrefix("100.100.100.0/24"), netip.MustParsePrefix("100.115.92.0/23")}
func overlapsReserved(p netip.Prefix) bool {
m := p.Masked()
for _, r := range reserved { if m.Overlaps(r) { return true } }
return false
} Type guard
func isSafeIPPool(p netip.Prefix) bool {
c := tsaddr.CGNATRange(); m := p.Masked()
if m.Bits() < c.Bits() || !c.Contains(m.Addr()) { return false }
for _, r := range reserved { if m.Overlaps(r) { return false } }
return true
} Try / catch
if err := pol.Validate(); errors.Is(err, policyv2.ErrNodeAttrsIPPoolReserved) { /* shrink pool below 100.100.100.0 */ } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- nodeAttrs target does not support this autogroup
- nodeAttrs ipPool requires the IP allocator (https://github.c
- %w: %q (%T)
- 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/b642098d24db992c.
Report an issue: GitHub.