From f24c3665df62f2acb74e60dc124f0eae21815af8 Mon Sep 17 00:00:00 2001 From: mdigitalbh81 Date: Fri, 18 Sep 2026 12:24:35 -0300 Subject: [PATCH] fix(proxy): skip bare TCP health probe for SOCKS5 data plane (#13571) * fix(proxy): skip bare TCP health probe for SOCKS5 data plane * docs(changelog): add fragment for SOCKS5 bare TCP probe skip fix Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- ...13571-socks5-skip-bare-tcp-health-probe.md | 1 + open-sse/utils/proxyFetch.ts | 2 +- tests/unit/t14-proxy-fast-fail.test.ts | 155 +++++++++++++++++- 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/13571-socks5-skip-bare-tcp-health-probe.md diff --git a/changelog.d/fixes/13571-socks5-skip-bare-tcp-health-probe.md b/changelog.d/fixes/13571-socks5-skip-bare-tcp-health-probe.md new file mode 100644 index 0000000000..997c6508f3 --- /dev/null +++ b/changelog.d/fixes/13571-socks5-skip-bare-tcp-health-probe.md @@ -0,0 +1 @@ +- **fix(proxy):** ordinary SOCKS5 data-plane requests no longer trigger the T14 speculative bare-TCP reachability probe — that probe opened and immediately closed a raw TCP connection, which a SOCKS5 listener (e.g. GOST) sees as an incomplete handshake and logs as `unexpected EOF`; HTTP/HTTPS fast-fail and the explicit `directFallbackOnUnreachable` control-plane probe are unchanged ([#13571](https://github.com/diegosouzapw/OmniRoute/pull/13571)) — thanks @mdigitalbh81 diff --git a/open-sse/utils/proxyFetch.ts b/open-sse/utils/proxyFetch.ts index d14cca17df..1fbcf17072 100644 --- a/open-sse/utils/proxyFetch.ts +++ b/open-sse/utils/proxyFetch.ts @@ -628,7 +628,7 @@ export async function runWithProxyContext( ); return runDirect(); } - } else { + } else if (new URL(resolvedProxyUrl).protocol !== "socks5:") { // Fire the probe WITHOUT awaiting; dispatch optimistically below. unreachableProbe = isProxyReachable(resolvedProxyUrl); } diff --git a/tests/unit/t14-proxy-fast-fail.test.ts b/tests/unit/t14-proxy-fast-fail.test.ts index 2ab954f136..19184ae7ab 100644 --- a/tests/unit/t14-proxy-fast-fail.test.ts +++ b/tests/unit/t14-proxy-fast-fail.test.ts @@ -7,7 +7,7 @@ import { invalidateProxyHealth, __setProxyHealthTcpCheckForTesting, } from "../../src/lib/proxyHealth.ts"; -import { runWithProxyContext } from "../../open-sse/utils/proxyFetch.ts"; +import { runWithProxyContext, proxyFetch } from "../../open-sse/utils/proxyFetch.ts"; test("T14: isProxyReachable caches unreachable proxy result", async () => { const proxyUrl = "http://127.0.0.1:1"; @@ -102,3 +102,156 @@ test("T14: runWithProxyContext fails an in-flight request fast when the proxy is assert.equal(executed, true, "dispatch is optimistic; the request was started before the abort"); releaseRequest(); }); + +test("SOCKS5 ordinary data-plane: no T14 TCP health probe", async () => { + const proxyUrl = "socks5://127.0.0.1:1080"; + invalidateProxyHealth(proxyUrl); + let probeCount = 0; + __setProxyHealthTcpCheckForTesting(async () => { + probeCount += 1; + return true; + }); + try { + let executed = false; + const result = await runWithProxyContext(proxyUrl, async () => { + executed = true; + return "socks5-data-plane-ok"; + }); + assert.equal(result, "socks5-data-plane-ok"); + assert.equal(executed, true); + assert.equal(probeCount, 0, "SOCKS5 ordinary data-plane must not probe"); + } finally { + __setProxyHealthTcpCheckForTesting(null); + invalidateProxyHealth(proxyUrl); + } +}); + +test("SOCKS5 real transport failure: owned by transport path, not T14 probe", async () => { + const proxyUrl = "socks5://127.0.0.1:1"; + invalidateProxyHealth(proxyUrl); + let probeCount = 0; + __setProxyHealthTcpCheckForTesting(async () => { + probeCount += 1; + return false; + }); + try { + await runWithProxyContext(proxyUrl, async () => { + await assert.rejects( + proxyFetch("http://example.com"), + (err: Error & { code?: string; message?: string }) => { + assert.ok( + err.code === "PROXY_REQUEST_FAILED" || err.code === "PROXY_UNREACHABLE", + `expected transport failure code, got ${err.code}` + ); + assert.ok( + !err.message?.includes("[Proxy Fast-Fail]"), + "real SOCKS failure must not manufacture [Proxy Fast-Fail] message" + ); + return true; + } + ); + }); + assert.equal(probeCount, 0, "SOCKS data-plane request must not invoke reachability probe"); + } finally { + __setProxyHealthTcpCheckForTesting(null); + invalidateProxyHealth(proxyUrl); + } +}); + +test("control-plane directFallbackOnUnreachable SOCKS5: blocking probe preserved", async () => { + const proxyUrl = "socks5://127.0.0.1:1080"; + invalidateProxyHealth(proxyUrl); + const prevEnv = process.env.OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK; + process.env.OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK = "true"; + let probeCount = 0; + __setProxyHealthTcpCheckForTesting(async () => { + probeCount += 1; + return false; + }); + try { + let executed = false; + const result = await runWithProxyContext( + proxyUrl, + async () => { + executed = true; + return "fallback-ok"; + }, + { directFallbackOnUnreachable: true } + ); + assert.equal(result, "fallback-ok"); + assert.equal(executed, true); + assert.equal( + probeCount, + 1, + "control-plane direct fallback SOCKS must invoke reachability check" + ); + } finally { + __setProxyHealthTcpCheckForTesting(null); + invalidateProxyHealth(proxyUrl); + if (prevEnv === undefined) { + delete process.env.OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK; + } else { + process.env.OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK = prevEnv; + } + } +}); + +test("control-plane directFallbackOnUnreachable HTTP: blocking probe preserved", async () => { + const proxyUrl = "http://127.0.0.1:8080"; + invalidateProxyHealth(proxyUrl); + const prevEnv = process.env.OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK; + process.env.OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK = "true"; + let probeCount = 0; + __setProxyHealthTcpCheckForTesting(async () => { + probeCount += 1; + return false; + }); + try { + let executed = false; + const result = await runWithProxyContext( + proxyUrl, + async () => { + executed = true; + return "fallback-ok"; + }, + { directFallbackOnUnreachable: true } + ); + assert.equal(result, "fallback-ok"); + assert.equal(executed, true); + assert.equal( + probeCount, + 1, + "control-plane direct fallback HTTP must invoke reachability check" + ); + } finally { + __setProxyHealthTcpCheckForTesting(null); + invalidateProxyHealth(proxyUrl); + if (prevEnv === undefined) { + delete process.env.OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK; + } else { + process.env.OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK = prevEnv; + } + } +}); + +test("nested same-context: probe runs only once", async () => { + const cfg = { type: "http" as const, host: "127.0.0.1", port: 8080 }; + const proxyUrl = "http://127.0.0.1:8080"; + invalidateProxyHealth(proxyUrl); + let probeCount = 0; + __setProxyHealthTcpCheckForTesting(async () => { + probeCount += 1; + return true; + }); + try { + const result = await runWithProxyContext(cfg, async () => { + invalidateProxyHealth(proxyUrl); + return runWithProxyContext(cfg, async () => "nested-marker"); + }); + assert.equal(result, "nested-marker"); + assert.equal(probeCount, 1, "nested same-context call must skip reachability probe"); + } finally { + __setProxyHealthTcpCheckForTesting(null); + invalidateProxyHealth(proxyUrl); + } +});