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 true

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Log the raw candidate line at debug level and compare it against RFC 8839 candidate grammar
  2. Report the malformed SDP to the upstream (Codex service) — the relay cannot safely rewrite a malformed line
  3. 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

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

Related errors


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