router-for-me/CLIProxyAPI · error
upstream WebRTC TCP proxy candidate address must be an IP
Error message
upstream WebRTC TCP proxy candidate address must be an IP
What it means
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.
Source
Thrown at internal/client/codex/live/tcp_proxy.go:205
candidate, errCandidate := ice.UnmarshalCandidate(trimmed)
if errCandidate != nil {
return tcpCandidatePlan{}, false, fmt.Errorf("parse upstream WebRTC candidate: %w", errCandidate)
}
if candidate.NetworkType() != ice.NetworkTypeTCP4 && candidate.NetworkType() != ice.NetworkTypeTCP6 {
return tcpCandidatePlan{}, false, nil
}
if candidate.TCPType() != ice.TCPTypePassive {
return tcpCandidatePlan{}, false, nil
}
if candidate.Component() != uint16(ice.ComponentRTP) || candidate.Type() != ice.CandidateTypeHost {
return tcpCandidatePlan{}, false, nil
}
if candidate.Port() != 443 {
return tcpCandidatePlan{}, false, fmt.Errorf("upstream WebRTC TCP proxy candidate uses disallowed port %d", candidate.Port())
}
address, errAddress := netip.ParseAddr(candidate.Address())
if errAddress != nil {
return tcpCandidatePlan{}, false, errors.New("upstream WebRTC TCP proxy candidate address must be an IP")
}
address = address.Unmap()
if !isPublicProxyTarget(address) {
return tcpCandidatePlan{}, false, errors.New("upstream WebRTC TCP proxy candidate address must be globally routable")
}
fields := strings.Fields(trimmed)
if len(fields) < 8 {
return tcpCandidatePlan{}, false, errors.New("upstream WebRTC TCP proxy candidate is malformed")
}
return tcpCandidatePlan{
fields: fields,
target: netip.AddrPortFrom(address, uint16(candidate.Port())),
}, true, nil
}
func isPublicProxyTarget(address netip.Addr) bool {
if !address.IsValid() || !address.IsGlobalUnicast() || address.IsUnspecified() || address.IsLoopback() ||
address.IsPrivate() || address.IsLinkLocalUnicast() || address.IsLinkLocalMulticast() || address.IsMulticast() {View on GitHub (pinned to 78f0c4079e)
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
Defensive patterns
Strategy: validation
Validate before calling
// Pre-check candidate addresses before invoking the proxied-answer path
for _, line := range strings.Split(answerSDP, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "a=candidate:") {
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) >= 6 {
if netip.ParseAddr(fields[4]) != nil { // error means hostname/mdns
log.Debug().Str("addr", fields[4]).Msg("non-IP candidate address; proxy will reject")
}
}
}
} Type guard
func isIPLiteralCandidate(sdpLine string) bool {
fields := strings.Fields(strings.TrimSpace(sdpLine))
if len(fields) < 6 || fields[0] != "a=candidate:" {
return false
}
_, err := netip.ParseAddr(fields[4])
return err == nil
} Try / catch
if _, _, err := live.PrepareProxiedUpstreamAnswer(answer, offer, dialer); err != nil {
if strings.Contains(err.Error(), "candidate address must be an IP") {
// mDNS/hostname candidate: skip proxying for this session
return applyDirect(answer)
}
return err
} Prevention
- Detect mDNS (.local) candidates in answers before enabling TCP proxying
- Prefer upstream endpoints that advertise IP literals in SDP
When it happens
Trigger: 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.
Common situations: Browsers and some WebRTC stacks hide local IPs behind mDNS hostnames; newer upstream Codex builds enabling IP obfuscation will produce these candidates.
Related errors
- upstream WebRTC answer has no supported public TCP passive c
- upstream WebRTC TCP proxy candidate is malformed
- upstream WebRTC TCP proxy candidate address must be globally
- SDP contains incomplete ICE credentials
- SDP contains inconsistent bundled ICE credentials
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/4eb7f0a6b7a97d64.
Report an issue: GitHub.