router-for-me/CLIProxyAPI · error

upstream WebRTC TCP proxy candidate uses disallowed port %d

Error message

upstream WebRTC TCP proxy candidate uses disallowed port %d

What it means

Security policy of the TCP proxy: only TCP host candidates listening on port 443 are proxied (so tunneled traffic looks like ordinary HTTPS). Any TCP candidate advertising a different port is rejected outright with this error instead of being ignored.

Source

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

}

func proxiedTCPCandidatePlan(rawCandidate string) (tcpCandidatePlan, bool, error) {
	trimmed := strings.TrimSpace(rawCandidate)
	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
}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Confirm the upstream's current TCP candidate port from its published docs/behavior; if it moved off 443, update the check (and the isPublicProxyTarget policy) deliberately in tcp_proxy.go.
  2. If the candidate is not needed, make the filter skip non-443 TCP candidates (return keep=false) instead of erroring — but only if the upstream guarantees a 443 candidate exists, since the code later requires at least one.
  3. For test fixtures, set the TCP candidate port to 443.

Example fix

// before
if candidate.Port() != 443 {
	return tcpCandidatePlan{}, false, fmt.Errorf("upstream WebRTC TCP proxy candidate uses disallowed port %d", candidate.Port())
}

// after: skip instead of fail (only if a 443 candidate is otherwise guaranteed)
if candidate.Port() != 443 {
	return tcpCandidatePlan{}, false, nil
}
Defensive patterns

Strategy: validation

Try / catch

if err != nil && strings.Contains(err.Error(), "disallowed port") {
	log.Warn("upstream moved TCP candidates off port 443; TCP proxy unavailable this session")
	return err // or fall back to UDP/direct path if architecturally allowed
}

Prevention

When it happens

Trigger: A candidate in the upstream answer is TCP4/TCP6, passive, component RTP, host type — but its port differs from 443 (e.g. 3478, 8443, or an ephemeral port).

Common situations: Upstream rotates its media ingress to a non-443 TCP port during an infrastructure change; a test fixture copied from a UDP/TURN setup uses 3478.

Related errors


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