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

  1. 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
  2. Trim whitespace and remove surrounding quotes from each YAML value
  3. Drop empty list items (check for trailing commas or empty lines in the config list)
  4. 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

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


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