diff --git a/.env.example b/.env.example index 953c1962fc..d0872ca263 100644 --- a/.env.example +++ b/.env.example @@ -57,6 +57,11 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # ALL_PROXY=socks5://127.0.0.1:7890 # NO_PROXY=localhost,127.0.0.1 +# TLS fingerprint spoofing (opt-in) — mimics Chrome 124 TLS handshake via wreq-js +# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google) +# Requires wreq-js to be installed (included in dependencies) +# ENABLE_TLS_FINGERPRINT=true + # Optional CLI runtime overrides (Docker/host integration) # CLI_MODE=auto # CLI_EXTRA_PATHS=/host-cli/bin diff --git a/open-sse/utils/proxyFetch.js b/open-sse/utils/proxyFetch.js index 5c45b4f715..92a62c7497 100644 --- a/open-sse/utils/proxyFetch.js +++ b/open-sse/utils/proxyFetch.js @@ -5,6 +5,14 @@ import { proxyConfigToUrl, proxyUrlForLogs, } from "./proxyDispatcher.js"; +import tlsClient from "./tlsClient.js"; + +function isTlsFingerprintEnabled() { + return process.env.ENABLE_TLS_FINGERPRINT === "true"; +} + +/** Per-request tracking of whether TLS fingerprint was used */ +const tlsFingerprintContext = new AsyncLocalStorage(); const isCloud = typeof caches !== "undefined" && typeof caches === "object"; const PATCH_STATE_KEY = Symbol.for("omniroute.proxyFetch.state"); @@ -135,6 +143,21 @@ async function patchedFetch(input, options = {}) { const { source, proxyUrl } = resolved; if (!proxyUrl) { + // TLS fingerprint spoofing for direct connections (no proxy configured) + if (isTlsFingerprintEnabled() && tlsClient.available) { + try { + const store = tlsFingerprintContext.getStore(); + if (store) store.used = true; + return await tlsClient.fetch(targetUrl, options); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn( + `[ProxyFetch] TLS fingerprint failed, falling back to native fetch: ${message}` + ); + const store = tlsFingerprintContext.getStore(); + if (store) store.used = false; + } + } return originalFetch(input, options); } @@ -153,4 +176,19 @@ if (!isCloud && !patchState.isPatched) { patchState.isPatched = true; } +/** + * Run a function with TLS fingerprint tracking context. + * After fn completes, returns { result, tlsFingerprintUsed }. + */ +export async function runWithTlsTracking(fn) { + const store = { used: false }; + const result = await tlsFingerprintContext.run(store, fn); + return { result, tlsFingerprintUsed: store.used }; +} + +/** Check if TLS fingerprint is enabled and available */ +export function isTlsFingerprintActive() { + return isTlsFingerprintEnabled() && tlsClient.available; +} + export default isCloud ? originalFetch : patchedFetch; diff --git a/open-sse/utils/tlsClient.js b/open-sse/utils/tlsClient.js new file mode 100644 index 0000000000..a8e08b4268 --- /dev/null +++ b/open-sse/utils/tlsClient.js @@ -0,0 +1,94 @@ +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); + +let createSession; +try { + ({ createSession } = require("wreq-js")); +} catch { + createSession = null; +} + +/** + * Get proxy URL from environment variables. + * Priority: HTTPS_PROXY > HTTP_PROXY > ALL_PROXY + */ +function getProxyFromEnv() { + return ( + process.env.HTTPS_PROXY || + process.env.https_proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.ALL_PROXY || + process.env.all_proxy || + undefined + ); +} + +/** + * TLS Client — Chrome 124 TLS fingerprint spoofing via wreq-js + * Singleton instance used to disguise Node.js TLS handshake as Chrome browser. + * + * wreq-js natively supports proxy — TLS fingerprinting works through proxy. + * Proxy URL is read from environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY). + */ +class TlsClient { + constructor() { + this.session = null; + this.available = !!createSession; + } + + async getSession() { + if (!this.available) return null; + if (this.session) return this.session; + + const proxy = getProxyFromEnv(); + const sessionOpts = { + browser: "chrome_124", + os: "macos", + }; + if (proxy) { + sessionOpts.proxy = proxy; + console.log(`[TlsClient] Using proxy: ${proxy}`); + } + + this.session = await createSession(sessionOpts); + console.log("[TlsClient] Session created (Chrome 124 TLS fingerprint)"); + return this.session; + } + + /** + * Fetch with Chrome 124 TLS fingerprint. + * wreq-js Response is already fetch-compatible (headers, text(), json(), clone(), body). + */ + async fetch(url, options = {}) { + const session = await this.getSession(); + if (!session) throw new Error("wreq-js not available"); + + const method = (options.method || "GET").toUpperCase(); + + const wreqOptions = { + method, + headers: options.headers, + body: options.body, + redirect: options.redirect === "manual" ? "manual" : "follow", + }; + + // Pass signal through if available + if (options.signal) { + wreqOptions.signal = options.signal; + } + + const response = await session.fetch(url, wreqOptions); + return response; + } + + async exit() { + if (this.session) { + await this.session.close(); + this.session = null; + } + } +} + +export default new TlsClient(); diff --git a/package.json b/package.json index eef30d7b85..548624447a 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "selfsigned": "^5.5.0", "undici": "^7.19.2", "uuid": "^13.0.0", + "wreq-js": "^2.0.1", "zod": "^4.3.6", "zustand": "^5.0.10" }, diff --git a/src/lib/proxyLogger.js b/src/lib/proxyLogger.js index e69b8d7f80..96ac258db2 100644 --- a/src/lib/proxyLogger.js +++ b/src/lib/proxyLogger.js @@ -38,6 +38,7 @@ export function logProxyEvent(entry) { connectionId: entry.connectionId || null, comboId: entry.comboId || null, account: entry.account || null, + tlsFingerprint: entry.tlsFingerprint || false, }; proxyLogs.unshift(log); // newest first diff --git a/src/shared/components/ProxyLogDetail.js b/src/shared/components/ProxyLogDetail.js index bf31f9e11d..5ab8008e71 100644 --- a/src/shared/components/ProxyLogDetail.js +++ b/src/shared/components/ProxyLogDetail.js @@ -139,6 +139,21 @@ export default function ProxyLogDetail({ log, onClose }) {