router-for-me/CLIProxyAPI · error

read upstream WebRTC offer ICE credentials: %w

Error message

read upstream WebRTC offer ICE credentials: %w

What it means

Identical credential extraction, but for the local offer SDP. The proxy needs the local ICE password to validate that connections arriving on the loopback tunnel listener genuinely come from the local ICE agent. If the local offer lacks ICE credentials, the proxy cannot secure the tunnels and refuses to continue.

Source

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

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
			}
			candidateCount++
			if candidateCount > maxUpstreamICECandidates {
				return "", nil, fmt.Errorf("upstream WebRTC answer exceeds the %d candidate limit", maxUpstreamICECandidates)

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Verify the stored localOffer is the localDescription.SDP of the real RTCPeerConnection.
  2. Inspect it for a=ice-ufrag/a=ice-pwd lines; if absent, fix the offer generation path.
  3. Add a unit test asserting bundledICECredentials succeeds on the offer you actually store.
Defensive patterns

Strategy: validation

Validate before calling

if !strings.Contains(s.localOffer, "a=ice-pwd:") {
	return errors.New("local offer has no ICE password; regenerate offer")
}

Prevention

When it happens

Trigger: localOffer parses as SDP but has no readable ice-ufrag/ice-pwd in its media descriptions — empty/placeholder offer, offer generated by a stack that omits ICE attributes, or the wrong string (e.g. a candidate list) was stored as the offer.

Common situations: Local offer built manually for testing without ICE attributes; session state populated from a different field; refactor changed which string is kept as the offer.

Related errors


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