fix(network): skip Chrome TLS impersonation for Groq (#13445)

Groq is now excluded from Chrome TLS impersonation in both the direct and the proxied dispatch branch, even when `TLS_FINGERPRINT_PROVIDERS` is unset or explicitly lists it — Cloudflare answers the spoofed fingerprint with 1010 `browser_signature_banned` (#13225).

Scope is contained: the whole path is behind `ENABLE_TLS_FINGERPRINT`, which defaults to off, so nothing changes for operators who never opted in.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
This commit is contained in:
Bob.Hou
2026-09-16 00:54:00 -04:00
committed by GitHub
parent ce98c30cfb
commit 4902ac8128
5 changed files with 133 additions and 6 deletions

View File

@@ -0,0 +1 @@
- **fix(network):** skip Chrome TLS impersonation for Groq (`api.groq.com`); Cloudflare 1010s that JA3 while native undici reaches the API ([#13445](https://github.com/diegosouzapw/OmniRoute/pull/13445)) (#13225)

View File

@@ -1,4 +1,5 @@
{
"_rebaseline_2026_09_15_13445_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/utils/proxyFetch.ts->1296. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
"_rebaseline_2026_09_15_13643_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/executors/codex.ts->1528. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
"_rebaseline_2026_09_15_13609_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/services/accountFallback.ts->2507. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
"_rebaseline_2026_09_15_13602_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): src/sse/handlers/chatHelpers.ts->1214. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
@@ -465,7 +466,7 @@
"open-sse/services/combo/executeTargetAttempt.ts": 1228,
"open-sse/translator/response/openai-responses.ts": 1466,
"open-sse/utils/cursorAgentProtobuf.ts": 1547,
"open-sse/utils/proxyFetch.ts": 1275,
"open-sse/utils/proxyFetch.ts": 1296,
"open-sse/utils/stream.ts": 3098,
"open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/browser-worker.ts": 4398,
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1335,

View File

@@ -373,7 +373,7 @@ Route upstream LLM provider calls through an HTTP or SOCKS5 proxy for egress con
| `SOCKS_HANDSHAKE_TIMEOUT_MS` | `10000` | `open-sse/utils/socksConnectorWithFamily.ts` | SOCKS5 handshake (connect) timeout in ms. Raise it when a single residential gateway host is hit by high concurrency (e.g. 100 simultaneous requests) — the real handshake can exceed 10s under a saturated pool even though the proxy is reachable, which otherwise surfaces as a false `[Proxy Fast-Fail] Proxy unreachable`. Capped at `120000`. |
| `PROXY_FAIL_OPEN` | `false` | `src/sse/handlers/chatHelpers.ts` | When `false` (default), a request whose assigned proxy fails to resolve is **refused (fail-closed)** rather than falling back to a direct connection — prevents real-IP leaks. Set `true` to restore the legacy DIRECT fallback. |
| `ENABLE_TLS_FINGERPRINT` | `false` | `open-sse/executors` | Spoof TLS fingerprint using wreq-js (mimics Chrome 124). Counters JA3/JA4 blocking. |
| `TLS_FINGERPRINT_PROVIDERS` | _(unset)_ | `open-sse/utils/proxyFetch.ts` | Comma-separated provider allowlist for the new proxied TLS routing (`open-sse/utils/proxyFetch.ts`). Direct TLS keeps its legacy behavior when unset; only these providers route through the Chrome-124 fingerprint bridge. |
| `TLS_FINGERPRINT_PROVIDERS` | _(unset)_ | `open-sse/utils/proxyFetch.ts` | Comma-separated provider allowlist for the new proxied TLS routing (`open-sse/utils/proxyFetch.ts`). Direct TLS keeps its legacy behavior when unset; only these providers route through the Chrome-124 fingerprint bridge. Groq (`api.groq.com`) is excluded even when the allowlist is unset, because Chrome JA3 triggers Cloudflare 1010 while native undici reaches the API. |
| `OMNIROUTE_TURNSTILE_IGNORE_TLS_ERRORS` | `false` | `open-sse/services/claudeTurnstileSolver.ts` | Allow the Claude Turnstile Playwright browser context to ignore HTTPS certificate errors. |
### Scenarios

View File

@@ -85,12 +85,33 @@ function isTlsFingerprintEnabled() {
return process.env.ENABLE_TLS_FINGERPRINT === "true";
}
function isGroqTlsFingerprintHost(url: string | null | undefined): boolean {
if (!url) return false;
try {
const host = new URL(url).hostname.toLowerCase();
return host === "api.groq.com" || host.endsWith(".groq.com");
} catch {
return /(?:^|[./])api\.groq\.com(?:[:/?]|$)/i.test(String(url));
}
}
function isGroqTlsFingerprintProvider(
provider: string | null | undefined
): boolean {
const normalized = provider?.trim().toLowerCase();
return normalized === "groq";
}
function tlsFingerprintProviderAllowed(
provider: string | null | undefined,
proxied: boolean
proxied: boolean,
url?: string | null
): boolean {
if (isGroqTlsFingerprintProvider(provider) || isGroqTlsFingerprintHost(url)) {
return false;
}
const configured = process.env.TLS_FINGERPRINT_PROVIDERS?.trim();
// Preserve the legacy direct-only opt-in. The new proxied transport requires
// Preserve legacy direct-only opt-in. The new proxied transport requires
// an explicit allowlist so enabling TLS cannot silently change proxy traffic.
if (!configured) return !proxied;
if (!provider) return false;
@@ -793,7 +814,7 @@ async function patchedFetchUnrecorded(
if (
isTlsFingerprintEnabled() &&
activeTlsClient.available &&
tlsFingerprintProviderAllowed(tlsStore?.provider, false) &&
tlsFingerprintProviderAllowed(tlsStore?.provider, false, targetUrl) &&
isTlsRequestEligible(input, options)
) {
try {
@@ -1085,7 +1106,7 @@ async function patchedFetchUnrecorded(
typeof tlsStore?.sessionScope === "string" &&
tlsStore.sessionScope.trim().length > 0 &&
activeTlsClient.available &&
tlsFingerprintProviderAllowed(tlsStore?.provider, true) &&
tlsFingerprintProviderAllowed(tlsStore?.provider, true, targetUrl) &&
isTlsRequestEligible(input, options) &&
isWreqProxySupported(proxyUrl)
) {

View File

@@ -6,6 +6,7 @@ import {
resolveProxyForRequest,
runWithProxyContext,
runWithTlsTracking,
isTlsFingerprintActive,
setTlsClientForTest,
} from "../../open-sse/utils/proxyFetch.ts";
import tlsClient, {
@@ -321,6 +322,109 @@ test("new proxied TLS transport requires an explicit provider allowlist", async
);
});
test("direct TLS fingerprint skips Groq even when the provider allowlist is unset", async () => {
await withEnv(
{
ENABLE_TLS_FINGERPRINT: "true",
TLS_FINGERPRINT_PROVIDERS: undefined,
},
async () => {
let tlsCalls = 0;
let dispatcherCalls = 0;
setTlsClientForTest(
fakeTlsClient(async () => {
tlsCalls++;
return new Response("tls");
}),
);
const tracked = await runWithTlsTracking("groq", () =>
proxyFetch("https://api.groq.com/openai/v1/models", {}, {
undiciFetch: async () => {
dispatcherCalls++;
return new Response("dispatcher");
},
}),
);
assert.equal(tlsCalls, 0);
assert.equal(dispatcherCalls, 1);
assert.equal(await tracked.result.text(), "dispatcher");
assert.equal(tracked.tlsFingerprintUsed, false);
assert.equal(isTlsFingerprintActive("groq"), false);
},
);
});
test("direct TLS fingerprint skips api.groq.com when the tracking store has no provider", async () => {
await withEnv(
{
ENABLE_TLS_FINGERPRINT: "true",
TLS_FINGERPRINT_PROVIDERS: undefined,
},
async () => {
let tlsCalls = 0;
let dispatcherCalls = 0;
setTlsClientForTest(
fakeTlsClient(async () => {
tlsCalls++;
return new Response("tls");
}),
);
const tracked = await runWithTlsTracking(async () =>
proxyFetch("https://api.groq.com/openai/v1/chat/completions", {
method: "POST",
body: "{}",
}, {
undiciFetch: async () => {
dispatcherCalls++;
return new Response("dispatcher");
},
}),
);
assert.equal(tlsCalls, 0);
assert.equal(dispatcherCalls, 1);
assert.equal(await tracked.result.text(), "dispatcher");
assert.equal(tracked.tlsFingerprintUsed, false);
},
);
});
test("direct TLS fingerprint still spoofs non-Groq hosts when the allowlist is unset", async () => {
await withEnv(
{
ENABLE_TLS_FINGERPRINT: "true",
TLS_FINGERPRINT_PROVIDERS: undefined,
},
async () => {
let tlsCalls = 0;
let dispatcherCalls = 0;
setTlsClientForTest(
fakeTlsClient(async () => {
tlsCalls++;
return new Response("tls");
}),
);
const tracked = await runWithTlsTracking("openai", () =>
proxyFetch("https://api.openai.com/v1/models", {}, {
undiciFetch: async () => {
dispatcherCalls++;
return new Response("dispatcher");
},
}),
);
assert.equal(tlsCalls, 1);
assert.equal(dispatcherCalls, 0);
assert.equal(await tracked.result.text(), "tls");
assert.equal(tracked.tlsFingerprintUsed, true);
},
);
});
test("caller abort propagates unchanged and never falls back", async () => {
await withEnv(
{