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
- 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.
- 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.
- 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
- The 443-only rule is a deliberate egress-camouflage policy — change it only via an explicit security review.
- Watch upstream release notes for media port changes.
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
- parse upstream WebRTC answer for TCP proxy: %w
- parse upstream WebRTC offer for TCP proxy: %w
- read upstream WebRTC answer ICE credentials: %w
- read upstream WebRTC offer ICE credentials: %w
- upstream WebRTC answer exceeds the %d TCP candidate proxy li
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/39367565345242fc.
Report an issue: GitHub.