Skip to content

Commit f1f7ffc

Browse files
committed
fix: guard debug proxy CONNECT under managed proxy
1 parent 3617778 commit f1f7ffc

5 files changed

Lines changed: 149 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Docs: https://docs.openclaw.ai
2626

2727
### Fixes
2828

29+
- Proxy/debugging: disable debug proxy CONNECT upstream forwarding while managed proxy mode is active unless `OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY=1` is explicitly set for approved local diagnostics.
30+
2931
- Google Meet: route stateful CLI session commands through the gateway-owned runtime so joined realtime sessions survive after the starting CLI process exits. Fixes #76344. Thanks @coltonharris-wq.
3032
- Memory/status: keep plain `openclaw memory status` and `openclaw memory status --json` on the cheap read-only path by reserving vector and embedding provider probes for `--deep` or `--index`. Fixes #76769. Thanks @daruire.
3133
- Telegram: suppress stale same-session replies when a newer accepted message arrives before an older in-flight Telegram dispatch finalizes. Fixes #76642. Thanks @chinar-amrutkar.

docs/cli/proxy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ semantics.
6868

6969
- `start` defaults to `127.0.0.1` unless `--host` is set.
7070
- `run` starts a local debug proxy and then runs the command after `--`.
71+
- The debug proxy's CONNECT forwarding opens upstream TCP sockets for diagnostics. When OpenClaw managed proxy mode is active, CONNECT forwarding is disabled by default; set `OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY=1` only for approved local diagnostics.
7172
- `validate` exits with code 1 when proxy config or destination checks fail.
7273
- Captures are local debugging data; use `openclaw proxy purge` when finished.
7374

docs/security/network-proxy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ proxy:
193193

194194
- The proxy improves coverage for process-local JavaScript HTTP and WebSocket clients, but it is not an OS-level network sandbox.
195195
- Raw `net`, `tls`, and `http2` sockets, native addons, and child processes may bypass Node-level proxy routing unless they inherit and respect proxy environment variables.
196+
- The local debug proxy is diagnostic tooling and its CONNECT upstream forwarding is disabled by default while managed proxy mode is active; enable direct CONNECT forwarding only for approved local diagnostics.
196197
- User local WebUIs and local model servers should be allowlisted in the operator proxy policy when needed; OpenClaw does not expose a general local-network bypass for them.
197198
- Gateway control-plane proxy bypass is intentionally limited to `localhost` and literal loopback IP URLs. Use `ws://127.0.0.1:18789`, `ws://[::1]:18789`, or `ws://localhost:18789` for local direct Gateway control-plane connections; other hostnames route like ordinary hostname-based traffic.
198199
- OpenClaw does not inspect, test, or certify your proxy policy.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { rm } from "node:fs/promises";
2+
import { Socket } from "node:net";
3+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
4+
import { assertDebugProxyDirectConnectAllowed, startDebugProxyServer } from "./proxy-server.js";
5+
6+
const testBlobDir = ".tmp-debug-proxy-test-blobs";
7+
const testCertDir = ".tmp-debug-proxy-test-certs";
8+
9+
async function cleanupTestDirs(): Promise<void> {
10+
await Promise.all([
11+
rm(testBlobDir, { recursive: true, force: true }),
12+
rm(testCertDir, { recursive: true, force: true }),
13+
]);
14+
}
15+
16+
function makeSettings() {
17+
return {
18+
enabled: true,
19+
required: false,
20+
dbPath: ":memory:",
21+
blobDir: testBlobDir,
22+
certDir: testCertDir,
23+
sessionId: "debug-proxy-managed-proxy-test",
24+
sourceProcess: "test",
25+
};
26+
}
27+
28+
async function connectThroughProxy(proxyUrl: string): Promise<string> {
29+
const target = new URL(proxyUrl);
30+
const socket = new Socket();
31+
let data = "";
32+
socket.setEncoding("utf8");
33+
socket.on("data", (chunk) => {
34+
data += chunk;
35+
});
36+
await new Promise<void>((resolve, reject) => {
37+
socket.once("error", reject);
38+
socket.connect(Number(target.port), target.hostname, resolve);
39+
});
40+
socket.write("CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n");
41+
await new Promise<void>((resolve) => socket.once("end", resolve));
42+
socket.destroy();
43+
return data;
44+
}
45+
46+
describe("debug proxy managed-proxy CONNECT policy", () => {
47+
const originalProxyActive = process.env["OPENCLAW_PROXY_ACTIVE"];
48+
const originalAllowDirect =
49+
process.env["OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY"];
50+
51+
beforeEach(async () => {
52+
await cleanupTestDirs();
53+
delete process.env["OPENCLAW_PROXY_ACTIVE"];
54+
delete process.env["OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY"];
55+
});
56+
57+
afterEach(async () => {
58+
if (originalProxyActive === undefined) {
59+
delete process.env["OPENCLAW_PROXY_ACTIVE"];
60+
} else {
61+
process.env["OPENCLAW_PROXY_ACTIVE"] = originalProxyActive;
62+
}
63+
if (originalAllowDirect === undefined) {
64+
delete process.env["OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY"];
65+
} else {
66+
process.env["OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY"] =
67+
originalAllowDirect;
68+
}
69+
await cleanupTestDirs();
70+
});
71+
72+
it("allows direct CONNECT upstreams when managed proxy mode is inactive", () => {
73+
expect(() => assertDebugProxyDirectConnectAllowed()).not.toThrow();
74+
});
75+
76+
it("rejects direct CONNECT upstreams while managed proxy mode is active", () => {
77+
process.env["OPENCLAW_PROXY_ACTIVE"] = "1";
78+
79+
expect(() => assertDebugProxyDirectConnectAllowed()).toThrow(
80+
/Debug proxy CONNECT upstream forwarding is disabled/,
81+
);
82+
});
83+
84+
it("allows direct CONNECT upstreams with explicit diagnostic override", () => {
85+
process.env["OPENCLAW_PROXY_ACTIVE"] = "1";
86+
process.env["OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY"] = "1";
87+
88+
expect(() => assertDebugProxyDirectConnectAllowed()).not.toThrow();
89+
});
90+
91+
it("rejects CONNECT upstreams before opening direct sockets while managed proxy mode is active", async () => {
92+
process.env["OPENCLAW_PROXY_ACTIVE"] = "1";
93+
const server = await startDebugProxyServer({ settings: makeSettings() });
94+
try {
95+
const response = await connectThroughProxy(server.proxyUrl);
96+
97+
expect(response).toContain("502 Bad Gateway");
98+
expect(response).toContain("Debug proxy CONNECT upstream forwarding is disabled");
99+
} finally {
100+
await server.stop();
101+
}
102+
});
103+
});

src/proxy-capture/proxy-server.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@ import { ensureDebugProxyCa } from "./ca.js";
88
import type { DebugProxySettings } from "./env.js";
99
import { getDebugProxyCaptureStore } from "./store.sqlite.js";
1010

11+
const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
12+
const DEBUG_PROXY_DIRECT_CONNECT_OVERRIDE =
13+
"OPENCLAW_DEBUG_PROXY_ALLOW_DIRECT_CONNECT_WITH_MANAGED_PROXY";
14+
15+
function isManagedProxyActive(env: NodeJS.ProcessEnv = process.env): boolean {
16+
return env["OPENCLAW_PROXY_ACTIVE"] === "1";
17+
}
18+
19+
function allowsDirectConnectWithManagedProxy(env: NodeJS.ProcessEnv = process.env): boolean {
20+
return TRUTHY_ENV.has((env[DEBUG_PROXY_DIRECT_CONNECT_OVERRIDE] ?? "").toLowerCase());
21+
}
22+
23+
export function assertDebugProxyDirectConnectAllowed(env: NodeJS.ProcessEnv = process.env): void {
24+
if (!isManagedProxyActive(env) || allowsDirectConnectWithManagedProxy(env)) {
25+
return;
26+
}
27+
throw new Error(
28+
"Debug proxy CONNECT upstream forwarding is disabled while managed proxy mode is active. " +
29+
`Set ${DEBUG_PROXY_DIRECT_CONNECT_OVERRIDE}=1 only for approved local diagnostics.`,
30+
);
31+
}
32+
1133
type DebugProxyServerHandle = {
1234
proxyUrl: string;
1335
stop: () => Promise<void>;
@@ -187,6 +209,26 @@ export async function startDebugProxyServer(params: {
187209
path: req.url ?? "",
188210
headersJson: JSON.stringify(req.headers),
189211
});
212+
try {
213+
assertDebugProxyDirectConnectAllowed();
214+
} catch (error) {
215+
const message = error instanceof Error ? error.message : String(error);
216+
store.recordEvent({
217+
sessionId: params.settings.sessionId,
218+
ts: Date.now(),
219+
sourceScope: "openclaw",
220+
sourceProcess: params.settings.sourceProcess,
221+
protocol: "connect",
222+
direction: "local",
223+
kind: "error",
224+
flowId,
225+
host: hostname,
226+
path: req.url ?? "",
227+
errorText: message,
228+
});
229+
clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\n\r\n${message}`);
230+
return;
231+
}
190232
const upstreamSocket = net.connect(port, hostname, () => {
191233
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
192234
if (head.length > 0) {

0 commit comments

Comments
 (0)