juanfont/headscale · warning · errGeneratedIPNotInPrefix

%w: ip(%s) not in prefix(%s)

Error message

%w: ip(%s) not in prefix(%s)

What it means

Defensive check in randomNext: after computing a random address arithmetically inside the prefix's numeric range, it verifies pfx.Contains(ip) and otherwise returns this error (wrapping errGeneratedIPNotInPrefix). With correct arithmetic this is unreachable; seeing it means the range math produced an address outside the prefix — a code-level invariant break, not an environmental issue.

Source

Thrown at hscontrol/db/ip.go:280

	}

	out, err := rand.Int(rand.Reader, tempMax)
	if err != nil {
		return netip.Addr{}, fmt.Errorf("generating random IP: %w", err)
	}

	valInRange := big.NewInt(0).Add(&from, out)

	// big.Int.Bytes() strips leading zero bytes, so a value with a zero high
	// byte yields a too-short slice that AddrFromSlice rejects. Pad to the
	// prefix's address width.
	ip, ok := netip.AddrFromSlice(valInRange.FillBytes(make([]byte, len(fromIP.AsSlice()))))
	if !ok {
		return netip.Addr{}, errGeneratedIPBytesInvalid
	}

	if !pfx.Contains(ip) {
		return netip.Addr{}, fmt.Errorf(
			"%w: ip(%s) not in prefix(%s)",
			errGeneratedIPNotInPrefix,
			ip.String(),
			pfx.String(),
		)
	}

	return ip, nil
}

func isTailscaleReservedIP(ip netip.Addr) bool {
	return tsaddr.ChromeOSVMRange().Contains(ip) ||
		tsaddr.TailscaleServiceIP() == ip ||
		tsaddr.TailscaleServiceIPv6() == ip
}

// BackfillNodeIPs will take a database transaction, and
// iterate through all of the current nodes ([types.Node]) in headscale

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Normalize prefixes so network bits only: use 10.0.0.0/24, not 10.0.0.1/24.
  2. If using a dev/custom build, review randomNext's from/to computation against netip.Prefix.Contains semantics; report upstream with the prefix value.
  3. Switch to sequential strategy as a workaround while investigating.

Example fix

# before (config.yaml)
ip_prefixes:
  - 10.0.0.1/24 # host bits set, contains-check can diverge from range math

# after
ip_prefixes:
  - 10.0.0.0/24 # properly masked network prefix
Defensive patterns

Strategy: validation

Validate before calling

// Ensure every configured prefix is masked before use:
p, err := netip.ParsePrefix(s)
if err != nil { return err }
p = p.Masked() // reject or normalize instead of passing host-bit prefixes

Try / catch

// Treat as an internal invariant failure: log the prefix that triggered
// it, normalize masked prefixes in config, and report upstream if it
// reproduces with a properly masked prefix.

Prevention

When it happens

Trigger: Only reachable if the from/to big.Int bounds and the padding logic in randomNext ever disagree with pfx — e.g. after a refactor of the range computation, or an unusual prefix (host-bits-set prefix like 10.0.0.1/24 where Contains semantics differ from raw range math).

Common situations: Custom ip_prefixes entries that are not properly masked (host bits set); running a modified or in-development build of headscale where randomNext was changed.

Related errors


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