{"record":{"id":"813d8915738c97a2","repo":"juanfont/headscale","slug":"w-ip-s-not-in-prefix-s","errorCode":null,"errorMessage":"%w: ip(%s) not in prefix(%s)","messagePattern":"%w: ip\\((.+?)\\) not in prefix\\((.+?)\\)","errorType":"exception","errorClass":"errGeneratedIPNotInPrefix","httpStatus":null,"severity":"warning","filePath":"hscontrol/db/ip.go","lineNumber":280,"sourceCode":"\t}\n\n\tout, err := rand.Int(rand.Reader, tempMax)\n\tif err != nil {\n\t\treturn netip.Addr{}, fmt.Errorf(\"generating random IP: %w\", err)\n\t}\n\n\tvalInRange := big.NewInt(0).Add(&from, out)\n\n\t// big.Int.Bytes() strips leading zero bytes, so a value with a zero high\n\t// byte yields a too-short slice that AddrFromSlice rejects. Pad to the\n\t// prefix's address width.\n\tip, ok := netip.AddrFromSlice(valInRange.FillBytes(make([]byte, len(fromIP.AsSlice()))))\n\tif !ok {\n\t\treturn netip.Addr{}, errGeneratedIPBytesInvalid\n\t}\n\n\tif !pfx.Contains(ip) {\n\t\treturn netip.Addr{}, fmt.Errorf(\n\t\t\t\"%w: ip(%s) not in prefix(%s)\",\n\t\t\terrGeneratedIPNotInPrefix,\n\t\t\tip.String(),\n\t\t\tpfx.String(),\n\t\t)\n\t}\n\n\treturn ip, nil\n}\n\nfunc isTailscaleReservedIP(ip netip.Addr) bool {\n\treturn tsaddr.ChromeOSVMRange().Contains(ip) ||\n\t\ttsaddr.TailscaleServiceIP() == ip ||\n\t\ttsaddr.TailscaleServiceIPv6() == ip\n}\n\n// BackfillNodeIPs will take a database transaction, and\n// iterate through all of the current nodes ([types.Node]) in headscale","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/ip.go#L262-L298","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Normalize prefixes so network bits only: use 10.0.0.0/24, not 10.0.0.1/24.","If using a dev/custom build, review randomNext's from/to computation against netip.Prefix.Contains semantics; report upstream with the prefix value.","Switch to sequential strategy as a workaround while investigating."],"exampleFix":"# before (config.yaml)\nip_prefixes:\n  - 10.0.0.1/24 # host bits set, contains-check can diverge from range math\n\n# after\nip_prefixes:\n  - 10.0.0.0/24 # properly masked network prefix","handlingStrategy":"validation","validationCode":"// Ensure every configured prefix is masked before use:\np, err := netip.ParsePrefix(s)\nif err != nil { return err }\np = p.Masked() // reject or normalize instead of passing host-bit prefixes","typeGuard":null,"tryCatchPattern":"// Treat as an internal invariant failure: log the prefix that triggered\n// it, normalize masked prefixes in config, and report upstream if it\n// reproduces with a properly masked prefix.","preventionTips":["Always configure masked network prefixes (10.0.0.0/24, not 10.0.0.1/24).","Run the prefix linter from error 414's defense in CI."],"tags":["ip-allocation","invariant","cidr","defensive-check"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}