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

  1. 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
  2. Resolve the hostname externally and confirm the deployment really needs proxying; mDNS candidates are link-local anyway and not proxyable
  3. 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

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


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/4eb7f0a6b7a97d64. Report an issue: GitHub.