{"record":{"id":"f559348bb6987a29","repo":"netbirdio/netbird","slug":"invalid-server-ip","errorCode":null,"errorMessage":"invalid server IP","messagePattern":"invalid server IP","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/relay/client/client.go","lineNumber":468,"sourceCode":"\t\trd.WithSequential()\n\t}\n\treturn rd.Dial(ctx)\n}\n\n// substituteHost replaces the host portion of a rel/rels URL with ip,\n// preserving the scheme and port. Returns the rewritten URL and the\n// original host to use as the TLS ServerName, or empty if the original\n// host is itself an IP literal (SNI requires a DNS name).\nfunc substituteHost(serverURL string, ip netip.Addr) (string, string, error) {\n\tu, err := url.Parse(serverURL)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"parse %q: %w\", serverURL, err)\n\t}\n\tif u.Scheme == \"\" || u.Host == \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"invalid relay URL %q\", serverURL)\n\t}\n\tif !ip.IsValid() {\n\t\treturn \"\", \"\", errors.New(\"invalid server IP\")\n\t}\n\torigHost := u.Hostname()\n\tif _, err := netip.ParseAddr(origHost); err == nil {\n\t\torigHost = \"\"\n\t}\n\tip = ip.Unmap()\n\tnewHost := ip.String()\n\tif ip.Is6() {\n\t\tnewHost = \"[\" + newHost + \"]\"\n\t}\n\tif port := u.Port(); port != \"\" {\n\t\tu.Host = newHost + \":\" + port\n\t} else {\n\t\tu.Host = newHost\n\t}\n\treturn u.String(), origHost, nil\n}\n","sourceCodeStart":450,"sourceCodeEnd":486,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/shared/relay/client/client.go#L450-L486","documentation":"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.","triggerScenarios":"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.","commonSituations":"Custom integrations calling substituteHost with a lookup result not checked; races between a resolver goroutine and the URL-rewrite path.","solutions":["Check ip.IsValid() before calling substituteHost","Propagate and handle resolver errors instead of continuing with the zero Addr","Log the resolution failure that produced the empty address"],"exampleFix":"// before\nu, name, err := substituteHost(relayURL, resolvedAddr)\n\n// after\nif !resolvedAddr.IsValid() {\n\treturn fmt.Errorf(\"relay host %q did not resolve to a usable address\", host)\n}\nu, name, err := substituteHost(relayURL, resolvedAddr)","handlingStrategy":"validation","validationCode":"addrs, err := net.DefaultResolver.LookupNetIP(ctx, \"ip\", host)\nif err != nil || len(addrs) == 0 {\n\treturn fmt.Errorf(\"resolve relay host %q: %w\", host, err)\n}\nip := addrs[0].Unmap()\nif !ip.IsValid() {\n\treturn fmt.Errorf(\"resolver returned invalid address for %q\", host)\n}","typeGuard":"func isValidAddr(ip netip.Addr) bool {\n\treturn ip.IsValid()\n}","tryCatchPattern":"if _, _, err := substituteHost(serverURL, ip); err != nil {\n\tif err.Error() == \"invalid server IP\" {\n\t\t// re-run resolution and propagate its error instead of ignoring it\n\t}\n\treturn err\n}","preventionTips":["Always check resolver errors before using the result","Call Unmap() on resolved addresses before passing them on","Guard shared resolver state with a mutex or pass values, not shared fields"],"tags":["relay","dns","netip","client"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}