mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 20:02:45 +03:00
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:
1
changelog.d/fixes/13445-groq-tls-fingerprint.md
Normal file
1
changelog.d/fixes/13445-groq-tls-fingerprint.md
Normal 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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user