router-for-me/CLIProxyAPI · error

read upstream WebRTC answer ICE credentials: %w

Error message

read upstream WebRTC answer ICE credentials: %w

What it means

After parsing the upstream answer, the proxy extracts the ICE ufrag/password (bundledICECredentials) to validate incoming TCP tunnel connections. If the answer's media sections lack ice-ufrag/ice-pwd attributes (or they are malformed), credential extraction fails and the proxy aborts.

Source

Thrown at internal/client/codex/live/tcp_proxy.go:105

	fields         []string
	target         netip.AddrPort
}

func prepareProxiedUpstreamAnswer(answer, localOffer string, dialer proxy.ContextDialer) (string, []*tcpCandidateTunnel, error) {
	if dialer == nil {
		return "", nil, errors.New("Codex live TCP proxy dialer is unavailable")
	}
	var remoteDescription sdp.SessionDescription
	if errUnmarshal := remoteDescription.UnmarshalString(answer); errUnmarshal != nil {
		return "", nil, fmt.Errorf("parse upstream WebRTC answer for TCP proxy: %w", errUnmarshal)
	}
	var localDescription sdp.SessionDescription
	if errUnmarshal := localDescription.UnmarshalString(localOffer); errUnmarshal != nil {
		return "", nil, fmt.Errorf("parse upstream WebRTC offer for TCP proxy: %w", errUnmarshal)
	}
	remoteCredentials, errCredentials := bundledICECredentials(&remoteDescription)
	if errCredentials != nil {
		return "", nil, fmt.Errorf("read upstream WebRTC answer ICE credentials: %w", errCredentials)
	}
	localCredentials, errCredentials := bundledICECredentials(&localDescription)
	if errCredentials != nil {
		return "", nil, fmt.Errorf("read upstream WebRTC offer ICE credentials: %w", errCredentials)
	}

	plans := make([]tcpCandidatePlan, 0, 4)
	candidateCount := 0
	for mediaIndex, media := range remoteDescription.MediaDescriptions {
		if media == nil {
			continue
		}
		filtered := make([]sdp.Attribute, 0, len(media.Attributes))
		for attributeIndex := range media.Attributes {
			attribute := media.Attributes[attributeIndex]
			if !attribute.IsICECandidate() {
				filtered = append(filtered, attribute)
				continue

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Dump the answer SDP and check each m= section for a=ice-ufrag and a=ice-pwd.
  2. If upstream now uses trickle ICE or session-level-only credentials, extend bundledICECredentials to fall back to session-level attributes.
  3. If the upstream regression is real, pin/roll back to the upstream version that emits full credentials.
Defensive patterns

Strategy: try-catch

Validate before calling

// Check the answer carries ICE credentials before proxying
hasICE := false
for _, m := range parsed.MediaDescriptions {
	for _, a := range m.Attributes {
		if a.Key == "ice-ufrag" || a.Key == "ice-pwd" {
			hasICE = true
		}
	}
}
if !hasICE { /* skip TCP proxying, use default path */ }

Try / catch

if _, _, err := prepareProxiedUpstreamAnswer(answer, offer, dialer); err != nil {
	if strings.Contains(err.Error(), "ICE credentials") {
		log.Warn("answer lacks ICE credentials; falling back to direct connection")
		return useDirectPath(answer)
	}
}

Prevention

When it happens

Trigger: The upstream answer SDP parses but contains no session-level or media-level a=ice-ufrag / a=ice-pwd attributes — e.g. a trickle-ICE answer that defers credentials, or an m-line with missing ICE attributes.

Common situations: Upstream changed to trickle ICE or a different SDP shape; attributes present only at session level while the parser expects them per media section (or vice versa); a stripped-down test SDP used in development.

Related errors


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