juanfont/headscale · critical
building trusted_proxies middleware: %w
Error message
building trusted_proxies middleware: %w
What it means
trustedProxyRealIP failed while building the real-IP middleware from cfg.TrustedProxies (hscontrol/realip.go:26). The config value is a []netip.Prefix — the middleware constructor parses/validates each proxy entry as an IP or CIDR prefix and errors when an entry is not a valid netip address or prefix. This only runs when trusted_proxies is non-empty in the config.
Source
Thrown at hscontrol/app.go:138
return nil, fmt.Errorf("reading or creating Noise protocol private key: %w", err)
}
s, err := state.NewState(cfg)
if err != nil {
return nil, fmt.Errorf("init state: %w", err)
}
app := Headscale{
cfg: cfg,
noisePrivateKey: noisePrivateKey,
clientStreamsOpen: sync.WaitGroup{},
state: s,
}
if len(cfg.TrustedProxies) > 0 {
app.realIPMiddleware, err = trustedProxyRealIP(cfg.TrustedProxies)
if err != nil {
return nil, fmt.Errorf("building trusted_proxies middleware: %w", err)
}
}
// Initialize ephemeral garbage collector
ephemeralGC := db.NewEphemeralGarbageCollector(func(ni types.NodeID) {
node, ok := app.state.GetNodeByID(ni)
if !ok {
log.Error().Uint64("node.id", ni.Uint64()).Msg("ephemeral node deletion failed")
log.Debug().Caller().Uint64("node.id", ni.Uint64()).Msg("ephemeral node deletion failed because node not found in NodeStore")
return
}
policyChanged, err := app.state.DeleteNode(node)
if err != nil {
log.Error().Err(err).EmbedObject(node).Msg("ephemeral node deletion failed")
return
}View on GitHub (pinned to 565fd254d0)
Solutions
- Make every trusted_proxies entry a literal IP (100.64.0.1) or CIDR (10.0.0.0/8) — netip does not resolve DNS names
- Trim whitespace and remove surrounding quotes from each YAML value
- Drop empty list items (check for trailing commas or empty lines in the config list)
- Restart headscale after fixing the config
Example fix
# before trusted_proxies: - "proxy.internal" - "10.0.0.0/8 " # after trusted_proxies: - 10.0.0.0/8 - 192.168.1.1
Defensive patterns
Strategy: validation
Validate before calling
for _, p := range cfg.TrustedProxies {
if _, err := netip.ParsePrefix(p); err != nil {
if _, aerr := netip.ParseAddr(p); aerr != nil {
return fmt.Errorf("trusted_proxies entry %q is not an IP or CIDR", p)
}
}
} Type guard
func isValidProxyEntry(s string) bool {
_, perr := netip.ParsePrefix(s)
_, aerr := netip.ParseAddr(s)
return perr == nil || aerr == nil
} Prevention
- Use only literal IPs or CIDRs in trusted_proxies — DNS names are never resolved
- Lint the config with `headscale configtest` (or equivalent) before deploy
- Avoid quoting/whitespace in list items; keep entries single-line scalars
When it happens
Trigger: Any trusted_proxies entry that netip.ParsePrefix/ParseAddr rejects: bare hostnames ('proxy.internal'), malformed CIDRs ('10.0.0.0/8 '), IPv4 with IPv6 mask notation, stray quotes/whitespace, or trailing commas producing an empty string entry.
Common situations: YAML/JSON config copied from nginx docs using hostname-style proxy names; values with surrounding quotes retained from shell examples; a list item left empty by a templating bug; IPv6 addresses missing brackets.
Related errors
- getting DERPMap: %w
- binding to TCP address: %w
- configuration error: %w
- init state: %w
- DERP server private key and noise private key are the same:
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/a00a7fe38ae36c68.
Report an issue: GitHub.