From 0e5d29b44c4ef1801c8e8fc994608a85fd8d63d7 Mon Sep 17 00:00:00 2001 From: Stefan Meinecke Date: Tue, 28 Apr 2026 20:20:19 +0200 Subject: [PATCH 1/3] feat: add ALLOW_PRIVATE_PROXY_HOSTS config for local proxy testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add opt-in flag to allow private/internal hosts (192.168.x.x, 10.x.x.x, localhost) in dashboard proxy tests. Default remains public-only for security. When enabled, testProxy() uses validateHostFormat() instead of assertPublicUrlHost() — still validates DNS/IP format but skips SSRF guards. New validateHostFormat() helper in net-safety.js mirrors resolvePublicAddresses() structure without private-range rejection. Config wired through .env.example, setup.sh, and config --- .env.example | 6 ++++++ setup.sh | 1 + src/config.js | 3 +++ src/dashboard/api.js | 7 ++++++- src/net-safety.js | 12 ++++++++++++ 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 0bba034..6e4e57c 100644 --- a/.env.example +++ b/.env.example @@ -30,3 +30,9 @@ CODEIUM_API_URL=https://server.self-serve.windsurf.com DEFAULT_MODEL=claude-4.5-sonnet-thinking MAX_TOKENS=8192 LOG_LEVEL=info + +# ========== Security ========== +# Allow private/internal hosts (e.g., 192.168.x.x, 10.x.x.x, localhost) in proxy tests. +# Set to 1 for local deployments where you need to test proxies on private networks. +# Leave empty or set to 0 for public-facing deployments (default: only public hosts allowed). +ALLOW_PRIVATE_PROXY_HOSTS= diff --git a/setup.sh b/setup.sh index 7219227..ce74703 100644 --- a/setup.sh +++ b/setup.sh @@ -32,6 +32,7 @@ LOG_LEVEL=info LS_BINARY_PATH=/opt/windsurf/language_server_linux_x64 LS_PORT=42100 DASHBOARD_PASSWORD= +ALLOW_PRIVATE_PROXY_HOSTS= ENVEOF echo " Edit .env to set your API_KEY and DASHBOARD_PASSWORD" else diff --git a/src/config.js b/src/config.js index 746e58c..de94916 100644 --- a/src/config.js +++ b/src/config.js @@ -83,6 +83,9 @@ export const config = { // Dashboard dashboardPassword: process.env.DASHBOARD_PASSWORD || '', + + // Proxy testing + allowPrivateProxyHosts: process.env.ALLOW_PRIVATE_PROXY_HOSTS === '1', }; const levels = { debug: 0, info: 1, warn: 2, error: 3 }; diff --git a/src/dashboard/api.js b/src/dashboard/api.js index b040bc1..7734f73 100644 --- a/src/dashboard/api.js +++ b/src/dashboard/api.js @@ -25,6 +25,7 @@ import { windsurfLogin, refreshFirebaseToken, reRegisterWithCodeium } from './wi import { getModelAccessConfig, setModelAccessMode, setModelAccessList, addModelToList, removeModelFromList } from './model-access.js'; import { checkMessageRateLimit } from '../windsurf-api.js'; import { assertPublicUrlHost } from '../image.js'; +import { validateHostFormat } from '../net-safety.js'; export function buildBatchProxyBinding(result, proxy) { const accountId = result?.account?.id || null; @@ -746,7 +747,11 @@ async function gitStatus() { } async function testProxy({ host, port, username, password, type }) { - await assertPublicUrlHost(host); + if (config.allowPrivateProxyHosts) { + await validateHostFormat(host); + } else { + await assertPublicUrlHost(host); + } const { isSocks, createSocksTunnel } = await import('../socks.js'); const tls = await import('node:tls'); const targetHost = 'api.ipify.org'; diff --git a/src/net-safety.js b/src/net-safety.js index 3332fc1..12ab894 100644 --- a/src/net-safety.js +++ b/src/net-safety.js @@ -103,3 +103,15 @@ export async function resolvePublicAddresses(hostname, lookupFn = dnsLookup) { return addrs; } +export async function validateHostFormat(hostname, lookupFn = dnsLookup) { + const host = String(hostname || '').replace(/^\[|\]$/g, ''); + if (!host) throw new Error('ERR_INVALID_HOST'); + if (net.isIP(host)) { + return [{ address: host, family: net.isIP(host) }]; + } + const result = await new Promise((resolve, reject) => { + lookupFn(host, { all: true }, (err, addrs) => err ? reject(err) : resolve(addrs)); + }); + return Array.isArray(result) ? result : [result]; +} + From 84d54f42928b350a5861e3561672047722516ed4 Mon Sep 17 00:00:00 2001 From: Stefan Meinecke Date: Tue, 28 Apr 2026 20:31:34 +0200 Subject: [PATCH 2/3] refactor(dashboard): extract parseProxyUrl helper for proxy string parsing Extract proxy URL parsing logic into standalone parseProxyUrl() function and export it. Reuse in buildBatchProxyBinding() and windsurfLoginBatch() instead of duplicating regex. Both call sites now use parseProxyUrl(proxy) instead of inline match + object construction. --- src/dashboard/api.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/dashboard/api.js b/src/dashboard/api.js index 7734f73..bb468b6 100644 --- a/src/dashboard/api.js +++ b/src/dashboard/api.js @@ -27,20 +27,26 @@ import { checkMessageRateLimit } from '../windsurf-api.js'; import { assertPublicUrlHost } from '../image.js'; import { validateHostFormat } from '../net-safety.js'; +export function parseProxyUrl(proxy) { + const proxyParts = String(proxy).match(/^(?:(\w+):\/\/)?(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/); + if (!proxyParts) return null; + return { + type: proxyParts[1] || 'http', + host: proxyParts[4], + port: parseInt(proxyParts[5]), + username: proxyParts[2] || '', + password: proxyParts[3] || '', + }; +} + export function buildBatchProxyBinding(result, proxy) { const accountId = result?.account?.id || null; if (!result?.success || !proxy || !accountId) return null; - const proxyParts = String(proxy).match(/^(?:(\w+):\/\/)?(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/); - if (!proxyParts) return null; + const parsed = parseProxyUrl(proxy); + if (!parsed) return null; return { accountId, - proxy: { - type: proxyParts[1] || 'http', - host: proxyParts[4], - port: parseInt(proxyParts[5]), - username: proxyParts[2] || '', - password: proxyParts[3] || '', - }, + proxy: parsed, }; } @@ -597,7 +603,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) { continue; } try { - const loginProxy = proxy ? { host: proxy } : getProxyConfig().global; + const loginProxy = proxy ? parseProxyUrl(proxy) : getProxyConfig().global; const result = await processWindsurfLogin({ email, password, loginProxy, autoAdd }); const binding = buildBatchProxyBinding(result, proxy); if (binding) { From d1e374cc2f70945f93d0854c8648b4bf40439c03 Mon Sep 17 00:00:00 2001 From: Stefan Meinecke Date: Tue, 28 Apr 2026 21:40:25 +0200 Subject: [PATCH 3/3] docs: add ALLOW_PRIVATE_PROXY_HOSTS to README env tables Document new config flag in both EN/CN READMEs. Explains opt-in for private/internal IPs in proxy tests, default remains public-only. --- README.en.md | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.en.md b/README.en.md index 8118ec9..69e4f08 100644 --- a/README.en.md +++ b/README.en.md @@ -268,6 +268,7 @@ In your client's settings for **Custom OpenAI Compatible**: | `LS_PORT` | `42100` | LS gRPC port. | | `LS_DATA_DIR` | `/opt/windsurf` | Per-proxy LS data directory root. | | `DASHBOARD_PASSWORD` | empty | Dashboard password. Leave empty for no password. | +| `ALLOW_PRIVATE_PROXY_HOSTS` | empty | Set to `1` to allow private/internal IPs (e.g., `192.168.x.x`, `10.x.x.x`) in proxy tests and login. Leave empty to only allow public addresses (default). | | `CASCADE_REUSE_STRICT` | `0` | Set to `1` for strict conversation reuse mode (waits for same fingerprint). | | `CASCADE_REUSE_STRICT_RETRY_MS` | `60000` | Retry delay in ms for strict reuse mode. | | `CASCADE_REUSE_HASH_SYSTEM` | `0` | Set to `1` to include system messages in conversation reuse hash. | diff --git a/README.md b/README.md index 1aa8a38..2cb82c0 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,7 @@ curl http://localhost:3003/v1/messages \ | `LS_BINARY_PATH` | `/opt/windsurf/language_server_linux_x64` | LS 二进制位置 | | `LS_PORT` | `42100` | LS gRPC 端口 | | `DASHBOARD_PASSWORD` | 空 | 后台密码 留空不设密码 | +| `ALLOW_PRIVATE_PROXY_HOSTS` | 空 | 设为 `1` 允许在代理测试和登录时使用内网 IP(如 `192.168.x.x`、`10.x.x.x`)。默认留空仅允许公网地址 | ## Dashboard 功能面板