{"record":{"id":"4eb7f0a6b7a97d64","repo":"router-for-me/CLIProxyAPI","slug":"upstream-webrtc-tcp-proxy-candidate-address-must-b","errorCode":null,"errorMessage":"upstream WebRTC TCP proxy candidate address must be an IP","messagePattern":"upstream WebRTC TCP proxy candidate address must be an IP","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/client/codex/live/tcp_proxy.go","lineNumber":205,"sourceCode":"\tcandidate, errCandidate := ice.UnmarshalCandidate(trimmed)\n\tif errCandidate != nil {\n\t\treturn tcpCandidatePlan{}, false, fmt.Errorf(\"parse upstream WebRTC candidate: %w\", errCandidate)\n\t}\n\tif candidate.NetworkType() != ice.NetworkTypeTCP4 && candidate.NetworkType() != ice.NetworkTypeTCP6 {\n\t\treturn tcpCandidatePlan{}, false, nil\n\t}\n\tif candidate.TCPType() != ice.TCPTypePassive {\n\t\treturn tcpCandidatePlan{}, false, nil\n\t}\n\tif candidate.Component() != uint16(ice.ComponentRTP) || candidate.Type() != ice.CandidateTypeHost {\n\t\treturn tcpCandidatePlan{}, false, nil\n\t}\n\tif candidate.Port() != 443 {\n\t\treturn tcpCandidatePlan{}, false, fmt.Errorf(\"upstream WebRTC TCP proxy candidate uses disallowed port %d\", candidate.Port())\n\t}\n\taddress, errAddress := netip.ParseAddr(candidate.Address())\n\tif errAddress != nil {\n\t\treturn tcpCandidatePlan{}, false, errors.New(\"upstream WebRTC TCP proxy candidate address must be an IP\")\n\t}\n\taddress = address.Unmap()\n\tif !isPublicProxyTarget(address) {\n\t\treturn tcpCandidatePlan{}, false, errors.New(\"upstream WebRTC TCP proxy candidate address must be globally routable\")\n\t}\n\tfields := strings.Fields(trimmed)\n\tif len(fields) < 8 {\n\t\treturn tcpCandidatePlan{}, false, errors.New(\"upstream WebRTC TCP proxy candidate is malformed\")\n\t}\n\treturn tcpCandidatePlan{\n\t\tfields: fields,\n\t\ttarget: netip.AddrPortFrom(address, uint16(candidate.Port())),\n\t}, true, nil\n}\n\nfunc isPublicProxyTarget(address netip.Addr) bool {\n\tif !address.IsValid() || !address.IsGlobalUnicast() || address.IsUnspecified() || address.IsLoopback() ||\n\t\taddress.IsPrivate() || address.IsLinkLocalUnicast() || address.IsLinkLocalMulticast() || address.IsMulticast() {","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/client/codex/live/tcp_proxy.go#L187-L223","documentation":"Thrown while planning a proxied TCP candidate: the candidate address from the upstream SDP failed netip.ParseAddr. Pion's ICE parser accepted the line, but the address is not a literal IPv4/IPv6 address — typically an mDNS hostname (.local) or a DNS name, which the TCP proxy cannot rewrite into a local listener target.","triggerScenarios":"Upstream answer contains a=candidate entries whose address component is a hostname (e.g. <uuid>.local from mDNS obfuscation) instead of an IP literal, while the TCP proxy dialer is enabled.","commonSituations":"Browsers and some WebRTC stacks hide local IPs behind mDNS hostnames; newer upstream Codex builds enabling IP obfuscation will produce these candidates.","solutions":["Inspect the upstream SDP candidates; if they are mDNS .local names, the current proxy path cannot use them — disable the TCP proxy dialer or rely on the server's public 443 candidate","Resolve the hostname externally and confirm the deployment really needs proxying; mDNS candidates are link-local anyway and not proxyable","If a stable DNS name is used, ask upstream for the IP-literal candidate or file an issue to support resolved hostnames"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Pre-check candidate addresses before invoking the proxied-answer path\nfor _, line := range strings.Split(answerSDP, \"\\n\") {\n    if strings.HasPrefix(strings.TrimSpace(line), \"a=candidate:\") {\n        fields := strings.Fields(strings.TrimSpace(line))\n        if len(fields) >= 6 {\n            if netip.ParseAddr(fields[4]) != nil { // error means hostname/mdns\n                log.Debug().Str(\"addr\", fields[4]).Msg(\"non-IP candidate address; proxy will reject\")\n            }\n        }\n    }\n}","typeGuard":"func isIPLiteralCandidate(sdpLine string) bool {\n    fields := strings.Fields(strings.TrimSpace(sdpLine))\n    if len(fields) < 6 || fields[0] != \"a=candidate:\" {\n        return false\n    }\n    _, err := netip.ParseAddr(fields[4])\n    return err == nil\n}","tryCatchPattern":"if _, _, err := live.PrepareProxiedUpstreamAnswer(answer, offer, dialer); err != nil {\n    if strings.Contains(err.Error(), \"candidate address must be an IP\") {\n        // mDNS/hostname candidate: skip proxying for this session\n        return applyDirect(answer)\n    }\n    return err\n}","preventionTips":["Detect mDNS (.local) candidates in answers before enabling TCP proxying","Prefer upstream endpoints that advertise IP literals in SDP"],"tags":["webrtc","ice-candidates","mdns","tcp-proxy","sdp"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}