{"record":{"id":"dea5db255c2995dd","repo":"juanfont/headscale","slug":"allocating-ipv4-for-node-d-w","errorCode":null,"errorMessage":"allocating IPv4 for node(%d): %w","messagePattern":"allocating IPv4 for node\\((.+?)\\): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/db/ip.go","lineNumber":332,"sourceCode":"\t\t\treturn fmt.Errorf(\"backfilling IPs: %w\", errIPAllocatorNil)\n\t\t}\n\n\t\tlog.Trace().Caller().Msgf(\"starting to backfill IPs\")\n\n\t\tnodes, err := ListNodes(tx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"listing nodes to backfill IPs: %w\", err)\n\t\t}\n\n\t\tfor _, node := range nodes {\n\t\t\tlog.Trace().Caller().EmbedObject(node).Msg(\"ip backfill check started because node found in database\")\n\n\t\t\tchanged := false\n\t\t\t// IPv4 prefix is set, but node ip is missing, alloc\n\t\t\tif i.prefix4 != nil && node.IPv4 == nil {\n\t\t\t\tret4, err := i.allocateNext(&i.prev4, i.prefix4)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"allocating IPv4 for node(%d): %w\", node.ID, err)\n\t\t\t\t}\n\n\t\t\t\tnode.IPv4 = ret4\n\t\t\t\tchanged = true\n\n\t\t\t\tret = append(ret, fmt.Sprintf(\"assigned IPv4 %q to Node(%d) %q\", ret4.String(), node.ID, node.Hostname))\n\t\t\t}\n\n\t\t\t// IPv6 prefix is set, but node ip is missing, alloc\n\t\t\tif i.prefix6 != nil && node.IPv6 == nil {\n\t\t\t\tret6, err := i.allocateNext(&i.prev6, i.prefix6)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"allocating IPv6 for node(%d): %w\", node.ID, err)\n\t\t\t\t}\n\n\t\t\t\tnode.IPv6 = ret6\n\t\t\t\tchanged = true\n","sourceCodeStart":314,"sourceCodeEnd":350,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/ip.go#L314-L350","documentation":"During backfill, a node lacks an IPv4 address while an IPv4 prefix is configured, so the allocator tries allocateNext. This error means the allocator could not find a free address in the prefix — most commonly the prefix is exhausted relative to the number of registered nodes. The transaction aborts, so no half-backfilled state persists.","triggerScenarios":"prefix4 sized smaller than the node count (e.g. a /24 with >253 nodes when infrastructure addresses are excluded); leaked addresses from nodes deleted in the DB but still tracked by the allocator's prev4 state; a node registered while backfill holds a stale cursor.","commonSituations":"Operator set a tiny prefix like 10.0.0.0/28 for testing, then registered more nodes; growth past the planned address space; allocator state desynchronized after restoring a database from backup.","solutions":["Enlarge prefixes.v4 (e.g. move from /24 to /23 or the 100.64.0.0/10 CGNAT range) and restart","Prune expired/obsolete nodes to free addresses","Verify the allocator is rebuilt from current DB state on startup rather than reusing stale prev4"],"exampleFix":"# before\nprefixes:\n  v4: 10.1.0.0/28\n\n# after\nprefixes:\n  v4: 10.1.0.0/23","handlingStrategy":"validation","validationCode":"// Estimate capacity before backfill: usable hosts must exceed node count\nusable := prefixBitsAvailable(cfg.Prefix4) // e.g. 2^(32-mask)-2\nnodeCount, _ := db.CountNodes()\nif usable <= nodeCount {\n\treturn fmt.Errorf(\"IPv4 prefix %s too small for %d nodes\", cfg.Prefix4, nodeCount)\n}","typeGuard":"func prefixHasCapacity(p netip.Prefix, nodes int) bool {\n\tif !p.IsValid() || p.IsSingleIP() {\n\t\treturn false\n\t}\n\tsize := p.Bits()\n\thosts := 1 << uint(32-size)\n\treturn hosts-2 > nodes\n}","tryCatchPattern":null,"preventionTips":["Size prefixes for expected growth at install time (CGNAT /10 gives room)","Monitor allocated-address counts vs prefix size","Prune expired nodes regularly"],"tags":["go","database","ip-allocation","capacity","configuration"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}