netbirdio/netbird · error

invalid server IP

Error message

invalid server IP

What it means

substituteHost rewrites a relay URL's host to a literal IP (keeping scheme and port) and requires that IP to be a valid netip.Addr. The error means the passed Addr is the zero value (IsValid() false): resolution was skipped, failed upstream, or its error was ignored.

Source

Thrown at shared/relay/client/client.go:468

		rd.WithSequential()
	}
	return rd.Dial(ctx)
}

// substituteHost replaces the host portion of a rel/rels URL with ip,
// preserving the scheme and port. Returns the rewritten URL and the
// original host to use as the TLS ServerName, or empty if the original
// host is itself an IP literal (SNI requires a DNS name).
func substituteHost(serverURL string, ip netip.Addr) (string, string, error) {
	u, err := url.Parse(serverURL)
	if err != nil {
		return "", "", fmt.Errorf("parse %q: %w", serverURL, err)
	}
	if u.Scheme == "" || u.Host == "" {
		return "", "", fmt.Errorf("invalid relay URL %q", serverURL)
	}
	if !ip.IsValid() {
		return "", "", errors.New("invalid server IP")
	}
	origHost := u.Hostname()
	if _, err := netip.ParseAddr(origHost); err == nil {
		origHost = ""
	}
	ip = ip.Unmap()
	newHost := ip.String()
	if ip.Is6() {
		newHost = "[" + newHost + "]"
	}
	if port := u.Port(); port != "" {
		u.Host = newHost + ":" + port
	} else {
		u.Host = newHost
	}
	return u.String(), origHost, nil
}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Check ip.IsValid() before calling substituteHost
  2. Propagate and handle resolver errors instead of continuing with the zero Addr
  3. Log the resolution failure that produced the empty address

Example fix

// before
u, name, err := substituteHost(relayURL, resolvedAddr)

// after
if !resolvedAddr.IsValid() {
	return fmt.Errorf("relay host %q did not resolve to a usable address", host)
}
u, name, err := substituteHost(relayURL, resolvedAddr)
Defensive patterns

Strategy: validation

Validate before calling

addrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
if err != nil || len(addrs) == 0 {
	return fmt.Errorf("resolve relay host %q: %w", host, err)
}
ip := addrs[0].Unmap()
if !ip.IsValid() {
	return fmt.Errorf("resolver returned invalid address for %q", host)
}

Type guard

func isValidAddr(ip netip.Addr) bool {
	return ip.IsValid()
}

Try / catch

if _, _, err := substituteHost(serverURL, ip); err != nil {
	if err.Error() == "invalid server IP" {
		// re-run resolution and propagate its error instead of ignoring it
	}
	return err
}

Prevention

When it happens

Trigger: Passing netip.Addr{} because the resolver returned no addresses or its error was swallowed; a race where the address is consumed before DNS resolution completes.

Common situations: Custom integrations calling substituteHost with a lookup result not checked; races between a resolver goroutine and the URL-rewrite path.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/f559348bb6987a29. Report an issue: GitHub.