{"record":{"id":"b92bd99c36fac8c6","repo":"router-for-me/CLIProxyAPI","slug":"upstream-webrtc-answer-exceeds-the-d-candidate-li","errorCode":null,"errorMessage":"upstream WebRTC answer exceeds the %d candidate limit","messagePattern":"upstream WebRTC answer exceeds the (.+?) candidate limit","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/client/codex/live/tcp_proxy.go","lineNumber":127,"sourceCode":"\t\treturn \"\", nil, fmt.Errorf(\"read upstream WebRTC offer ICE credentials: %w\", errCredentials)\n\t}\n\n\tplans := make([]tcpCandidatePlan, 0, 4)\n\tcandidateCount := 0\n\tfor mediaIndex, media := range remoteDescription.MediaDescriptions {\n\t\tif media == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfiltered := make([]sdp.Attribute, 0, len(media.Attributes))\n\t\tfor attributeIndex := range media.Attributes {\n\t\t\tattribute := media.Attributes[attributeIndex]\n\t\t\tif !attribute.IsICECandidate() {\n\t\t\t\tfiltered = append(filtered, attribute)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcandidateCount++\n\t\t\tif candidateCount > maxUpstreamICECandidates {\n\t\t\t\treturn \"\", nil, fmt.Errorf(\"upstream WebRTC answer exceeds the %d candidate limit\", maxUpstreamICECandidates)\n\t\t\t}\n\t\t\tplan, keep, errCandidate := proxiedTCPCandidatePlan(attribute.Value)\n\t\t\tif errCandidate != nil {\n\t\t\t\treturn \"\", nil, errCandidate\n\t\t\t}\n\t\t\tif !keep {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(plans) >= maxProxiedTCPCandidates {\n\t\t\t\treturn \"\", nil, fmt.Errorf(\"upstream WebRTC answer exceeds the %d TCP candidate proxy limit\", maxProxiedTCPCandidates)\n\t\t\t}\n\t\t\tplan.mediaIndex = mediaIndex\n\t\t\tplan.attributeIndex = len(filtered)\n\t\t\tfiltered = append(filtered, attribute)\n\t\t\tplans = append(plans, plan)\n\t\t}\n\t\tmedia.Attributes = filtered\n\t}","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/client/codex/live/tcp_proxy.go#L109-L145","documentation":"A defensive DoS cap: the proxy counts every a=candidate attribute in the upstream answer and hard-fails past maxUpstreamICECandidates (64). An answer with more than 64 candidates is treated as hostile or broken rather than being processed.","triggerScenarios":"The upstream answer contains more than 64 ICE candidate attributes across all media sections (count includes UDP and non-TCP candidates).","commonSituations":"Upstream infrastructure change flooding SDP with candidates; a malicious/buggy peer; a fuzzing or load test feeding oversized SDP.","solutions":["Log the candidate count and inspect the answer; if legitimate, raise maxUpstreamICECandidates in tcp_proxy.go (it is a local const, currently 64).","If unexpected, report the oversized SDP to the upstream service — normal Codex answers are far below 64 candidates.","Rate-limit or reject the session at a higher layer if a peer repeatedly sends oversized answers."],"exampleFix":"// before\nmaxUpstreamICECandidates = 64\n\n// after (only if upstream legitimately sends more)\nmaxUpstreamICECandidates = 128","handlingStrategy":"validation","validationCode":"// Cheap pre-count of candidates in the raw SDP\nfunc candidateCount(sdp string) int { return strings.Count(sdp, \"a=candidate:\") + strings.Count(sdp, \"a=candidate \") }\nif candidateCount(answer) > maxUpstreamICECandidates { /* reject early */ }","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"candidate limit\") {\n\tlog.Warn(\"possible SDP flooding from upstream; dropping session\")\n\treturn errSessionDropped\n}","preventionTips":["Treat the 64-candidate cap as a security invariant — do not remove it to make an anomaly pass.","Alert on repeated limit hits from the same upstream; it signals an upstream defect or attack."],"tags":["webrtc","sdp","dos-protection","codex-live","limits"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}