feat(proxy): SOCKS5 enabled by default (opt-out, not opt-in)

SOCKS5 support gated on ENABLE_SOCKS5_PROXY === "true" (default OFF), so a
fresh deploy with no .env (Docker/npm/Electron) silently REJECTED SOCKS5
proxies and the connections fell back to the host IP — a real cause of the
codex shared-IP anomaly when operators shipped SOCKS5 proxies but never set
the flag. Flip to opt-out: ON by default, disabled only by an explicit falsey
value (false/0/no/off).

Centralized semantics applied to all gates: isSocks5ProxyEnabled()
(dispatcher), isSocks5Enabled() (proxy route), socks5Enabled (proxies route),
BUILD_TIME_SOCKS5 (modal), plus updated disabled-state messages and the docs
tab. TDD: 4 tests (unset/empty/true-ish ON, falsey OFF). 18/18 proxy tests
green, typecheck:core=0.
This commit is contained in:
diegosouzapw
2026-06-12 01:04:01 -03:00
parent d03ed605cb
commit 9d6f7a2ab8
6 changed files with 67 additions and 8 deletions

View File

@@ -138,8 +138,15 @@ function buildProxyUrlString(parsed: URL, port: string): string {
return `${parsed.protocol}//${auth}${parsed.hostname}:${port}`;
}
/**
* SOCKS5 proxy support defaults ON (opt-OUT). A fresh deploy with no env set
* should honour SOCKS5 proxies out of the box — they were silently rejected
* before (default OFF), making accounts fall back to the host IP. Only an
* explicit falsey value (false/0/no/off) disables it.
*/
export function isSocks5ProxyEnabled(): boolean {
return process.env.ENABLE_SOCKS5_PROXY === "true";
const raw = (process.env.ENABLE_SOCKS5_PROXY ?? "").trim().toLowerCase();
return !["false", "0", "no", "off"].includes(raw);
}
export function proxyUrlForLogs(proxyUrl: string): string {
@@ -173,7 +180,7 @@ export function normalizeProxyUrl(
}
if (parsed.protocol === "socks5:" && !allowSocks5) {
throw new Error(
"[ProxyDispatcher] SOCKS5 proxy is disabled (set ENABLE_SOCKS5_PROXY=true to enable)"
"[ProxyDispatcher] SOCKS5 proxy is disabled (remove ENABLE_SOCKS5_PROXY=false to enable — it is ON by default)"
);
}
if (!parsed.hostname) {
@@ -233,7 +240,7 @@ export function proxyConfigToUrl(
}
if (protocol === "socks5:" && !allowSocks5) {
throw new Error(
"[ProxyDispatcher] SOCKS5 proxy is disabled (set ENABLE_SOCKS5_PROXY=true to enable)"
"[ProxyDispatcher] SOCKS5 proxy is disabled (remove ENABLE_SOCKS5_PROXY=false to enable — it is ON by default)"
);
}

View File

@@ -41,7 +41,8 @@ export default function DocumentationTab() {
<h3 className="font-semibold mb-2">SOCKS5</h3>
<p className="text-sm text-text-muted">
{t("proxyDocumentationSocks5DescBefore")}{" "}
<code className="bg-surface-alt px-1 rounded">ENABLE_SOCKS5_PROXY=true</code> to enable.
<code className="bg-surface-alt px-1 rounded">ENABLE_SOCKS5_PROXY=false</code> to disable
(ON by default).
</p>
</section>

View File

@@ -42,7 +42,10 @@ export async function GET(request: Request) {
return Response.json({
items: proxies,
total: proxies.length,
socks5Enabled: process.env.ENABLE_SOCKS5_PROXY === "true",
// Default ON (opt-out): only an explicit falsey value disables SOCKS5.
socks5Enabled: !["false", "0", "no", "off"].includes(
(process.env.ENABLE_SOCKS5_PROXY ?? "").trim().toLowerCase()
),
});
} catch (error) {
return createErrorResponseFromUnknown(error, "Failed to load proxies");

View File

@@ -31,7 +31,9 @@ const PROXY_LEVEL_TO_REGISTRY_SCOPE = {
} as const;
function isSocks5Enabled() {
return process.env.ENABLE_SOCKS5_PROXY === "true";
// Default ON (opt-out): only an explicit falsey value disables SOCKS5.
const raw = (process.env.ENABLE_SOCKS5_PROXY ?? "").trim().toLowerCase();
return !["false", "0", "no", "off"].includes(raw);
}
function getSupportedProxyTypes() {
@@ -106,7 +108,7 @@ function normalizeAndValidateProxy(
const type = String(proxy.type || "http").toLowerCase() as NonNullable<ProxyConfigInput["type"]>;
if (type === "socks5" && !isSocks5Enabled()) {
throw createInvalidProxyError(
"SOCKS5 proxy is disabled (set ENABLE_SOCKS5_PROXY=true to enable)"
"SOCKS5 proxy is disabled (remove ENABLE_SOCKS5_PROXY=false to enable — it is ON by default)"
);
}
if (type.startsWith("socks") && type !== "socks5") {

View File

@@ -12,7 +12,10 @@ const ALL_PROXY_TYPES = [
];
// Build-time fallback (static deploys). The live value comes from GET /api/settings/proxies
// (server ENABLE_SOCKS5_PROXY) so a runtime Docker env is honoured — #3508.
const BUILD_TIME_SOCKS5 = process.env.NEXT_PUBLIC_ENABLE_SOCKS5_PROXY === "true";
// Default ON (opt-out) to match the server: only an explicit falsey value hides SOCKS5.
const BUILD_TIME_SOCKS5 = !["false", "0", "no", "off"].includes(
(process.env.NEXT_PUBLIC_ENABLE_SOCKS5_PROXY ?? "").trim().toLowerCase()
);
export function buildProxyTypes(socks5Enabled: boolean) {
return socks5Enabled ? ALL_PROXY_TYPES : ALL_PROXY_TYPES.filter((type) => type.value !== "socks5");
}

View File

@@ -0,0 +1,43 @@
/**
* TDD — SOCKS5 proxy support must default ON (opt-OUT), so a fresh deploy
* (Docker/npm/Electron with no .env) honours SOCKS5 proxies out of the box.
* Previously the code defaulted OFF (`=== "true"`), so SOCKS5 proxies were
* silently rejected unless the operator set the env explicitly — accounts then
* fell back to the host IP. Only an explicit falsey value disables it now.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { isSocks5ProxyEnabled } = await import("../../open-sse/utils/proxyDispatcher.ts");
function withEnv(value: string | undefined, fn: () => void) {
const prev = process.env.ENABLE_SOCKS5_PROXY;
if (value === undefined) delete process.env.ENABLE_SOCKS5_PROXY;
else process.env.ENABLE_SOCKS5_PROXY = value;
try {
fn();
} finally {
if (prev === undefined) delete process.env.ENABLE_SOCKS5_PROXY;
else process.env.ENABLE_SOCKS5_PROXY = prev;
}
}
test("SOCKS5 is ON by default when the env is unset", () => {
withEnv(undefined, () => assert.equal(isSocks5ProxyEnabled(), true));
});
test("SOCKS5 is ON for empty string (treated as unset)", () => {
withEnv("", () => assert.equal(isSocks5ProxyEnabled(), true));
});
test("SOCKS5 stays ON for explicit true-ish values", () => {
for (const v of ["true", "TRUE", "1", "yes", "on"]) {
withEnv(v, () => assert.equal(isSocks5ProxyEnabled(), true, `value ${v} must enable`));
}
});
test("SOCKS5 is OFF only for explicit falsey values (opt-out)", () => {
for (const v of ["false", "FALSE", "0", "no", "off"]) {
withEnv(v, () => assert.equal(isSocks5ProxyEnabled(), false, `value ${v} must disable`));
}
});