router-for-me/CLIProxyAPI · error
upstream WebRTC TCP proxy candidate is malformed
Error message
upstream WebRTC TCP proxy candidate is malformed
What it means
Thrown when a proxied TCP candidate passed all earlier checks but splitting the raw candidate line on whitespace produced fewer than 8 fields. A well-formed ICE candidate has at least 'candidate:<foundation> <component> <transport> <priority> <address> <port> typ <type>' = 8 fields; fewer means the line is truncated or non-standard even though the pion parser accepted it.
Source
Thrown at internal/client/codex/live/tcp_proxy.go:213
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() {
return false
}
for _, prefix := range nonRoutableProxyTargetPrefixes {
if prefix.Contains(address) {
return false
}
}
return trueView on GitHub (pinned to 78f0c4079e)
Solutions
- Log the raw candidate line at debug level and compare it against RFC 8839 candidate grammar
- Report the malformed SDP to the upstream (Codex service) — the relay cannot safely rewrite a malformed line
- As a workaround, disable TCP proxying for sessions from that upstream version
Defensive patterns
Strategy: validation
Validate before calling
// Validate candidate line shape before the proxy path sees it
for _, line := range strings.Split(answerSDP, "\r\n") {
t := strings.TrimSpace(line)
if strings.HasPrefix(t, "a=candidate:") && len(strings.Fields(t)) < 8 {
return fmt.Errorf("malformed candidate line: %q", t)
}
} Type guard
func isWellFormedCandidate(line string) bool {
t := strings.TrimSpace(line)
return strings.HasPrefix(t, "a=candidate:") && len(strings.Fields(t)) >= 8
} Try / catch
if _, _, err := live.PrepareProxiedUpstreamAnswer(answer, offer, dialer); err != nil {
if strings.Contains(err.Error(), "malformed") {
log.WithError(err).Warn("upstream sent malformed candidate; skipping TCP proxy")
return applyDirect(answer)
}
return err
} Prevention
- Validate SDP candidate grammar (RFC 8839) when logging upstream answers
- Watch for middleboxes that truncate SDP lines; capture signaling untouched for comparison
When it happens
Trigger: An upstream SDP candidate line with missing components, e.g. missing 'typ host' tail or a malformed generation extension, while the TCP proxy dialer is enabled.
Common situations: Interoperability bugs in the upstream server's SDP generation, or middleboxes/proxies rewriting and truncating candidate lines.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- upstream WebRTC answer has no supported public TCP passive c
- upstream WebRTC TCP proxy candidate address must be an IP
- parse upstream WebRTC answer for TCP proxy: %w
- upstream WebRTC TCP proxy candidate address must be globally
- SDP contains incomplete ICE credentials
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/7f4f7b96ef970498.
Report an issue: GitHub.