From 88e573a1315f0b252bd4d8f06c75636b641a1752 Mon Sep 17 00:00:00 2001 From: Nanook Date: Mon, 23 Mar 2026 08:10:14 +0000 Subject: [PATCH 1/4] fix(sandbox): export HTTP_PROXY/HTTPS_PROXY/NO_PROXY for Node.js DNS resolution Web search tools (Brave, Gemini, Perplexity) failed inside the NemoClaw sandbox with `getaddrinfo EAI_AGAIN` because Node.js (undici/fetch) resolves DNS locally before opening the CONNECT tunnel to the OpenShell egress proxy at 10.200.0.1:3128. The sandbox network namespace has no DNS resolver, so local resolution always fails regardless of what is added to the network policy. curl does not have this problem because it sends the full hostname to the proxy and lets the proxy resolve it. Node.js's undici requires an HTTP_PROXY / HTTPS_PROXY env var to do the same. Fix: export HTTP_PROXY, HTTPS_PROXY, and NO_PROXY in nemoclaw-start.sh before openclaw gateway is launched. This causes Node.js to route all outbound HTTPS through the OpenShell proxy (which handles DNS), matching the behaviour users already see with curl. NO_PROXY excludes loopback, inference.local, and the 10.200.0.0/16 OpenShell virtual network so internal gateway calls stay on the fast path. Both the proxy host and port can be overridden via NEMOCLAW_PROXY_HOST and NEMOCLAW_PROXY_PORT for users running custom OpenShell gateway configurations. Closes #626 --- scripts/nemoclaw-start.sh | 31 ++++++++++++++-- test/service-env.test.js | 77 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index d28b9637498..6385199fe12 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -40,7 +40,8 @@ PYAUTH print_dashboard_urls() { local token chat_ui_base local_url remote_url - token="$(python3 - <<'PYTOKEN' + token="$( + python3 - <<'PYTOKEN' import json import os path = os.path.expanduser('~/.openclaw/openclaw.json') @@ -51,7 +52,7 @@ except Exception: else: print(cfg.get('gateway', {}).get('auth', {}).get('token', '')) PYTOKEN -)" + )" chat_ui_base="${CHAT_UI_URL%/}" local_url="http://127.0.0.1:${PUBLIC_PORT}/" @@ -66,7 +67,7 @@ PYTOKEN } start_auto_pair() { - nohup python3 - <<'PYAUTOPAIR' >> /tmp/gateway.log 2>&1 & + nohup python3 - <<'PYAUTOPAIR' >>/tmp/gateway.log 2>&1 & import json import subprocess import time @@ -130,13 +131,35 @@ echo 'Setting up NemoClaw...' # openclaw doctor --fix and openclaw plugins install already ran at build time # (Dockerfile Step 28). At runtime they fail with EPERM against the locked # /sandbox/.openclaw directory and accomplish nothing. + +# Configure outbound proxy so Node.js (openclaw/undici) routes HTTP/HTTPS +# requests through the OpenShell egress proxy at 10.200.0.1:3128. +# +# Without this, Node.js resolves DNS locally before the CONNECT tunnel is +# opened, hitting `getaddrinfo EAI_AGAIN` because the sandbox network +# namespace has no DNS resolver configured. The proxy handles DNS on behalf +# of the sandbox, so exporting HTTPS_PROXY causes undici/node:http to skip +# local resolution entirely. curl already does this correctly (it uses the +# proxy's DNS), and these env vars bring parity to Node.js callers. +# +# NEMOCLAW_PROXY_HOST can be set at sandbox creation time to override the +# default if the gateway IP changes in a future OpenShell release. +# Ref: https://github.com/NVIDIA/NemoClaw/issues/626 +PROXY_HOST="${NEMOCLAW_PROXY_HOST:-10.200.0.1}" +PROXY_PORT="${NEMOCLAW_PROXY_PORT:-3128}" +export HTTP_PROXY="http://${PROXY_HOST}:${PROXY_PORT}" +export HTTPS_PROXY="http://${PROXY_HOST}:${PROXY_PORT}" +# Bypass proxy for loopback, sandbox-local, and the OpenShell virtual network +# so internal gateway calls (openclaw dashboard, inference.local) stay fast. +export NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.0/16" + write_auth_profile if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then exec "${NEMOCLAW_CMD[@]}" fi -nohup openclaw gateway run > /tmp/gateway.log 2>&1 & +nohup openclaw gateway run >/tmp/gateway.log 2>&1 & echo "[gateway] openclaw gateway launched (pid $!)" start_auto_pair print_dashboard_urls diff --git a/test/service-env.test.js b/test/service-env.test.js index b3da3131440..f77e8c62303 100644 --- a/test/service-env.test.js +++ b/test/service-env.test.js @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, it, expect } from "vitest"; -import { execSync } from "node:child_process"; +import { execSync, execFileSync } from "node:child_process"; +import { writeFileSync, unlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { resolveOpenshell } from "../bin/lib/resolve-openshell"; describe("service environment", () => { @@ -95,4 +98,76 @@ describe("service environment", () => { expect(result).toBe("default"); }); }); + + describe("proxy environment variables (issue #626)", () => { + // Verify nemoclaw-start.sh sets HTTP_PROXY / HTTPS_PROXY / NO_PROXY so that + // Node.js (undici) routes outbound requests through the OpenShell egress + // proxy and does not attempt to resolve DNS locally inside the sandbox. + + function extractProxyVars(env = {}) { + // Write the proxy-variable snippet from nemoclaw-start.sh to a temp script + // and execute it so that bash variable assignments and expansions work + // correctly without interference from JSON.stringify quote-escaping. + const script = [ + "#!/usr/bin/env bash", + 'PROXY_HOST="${NEMOCLAW_PROXY_HOST:-10.200.0.1}"', + 'PROXY_PORT="${NEMOCLAW_PROXY_PORT:-3128}"', + 'HTTP_PROXY="http://${PROXY_HOST}:${PROXY_PORT}"', + 'HTTPS_PROXY="http://${PROXY_HOST}:${PROXY_PORT}"', + 'NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.0/16"', + 'echo "HTTP_PROXY=${HTTP_PROXY}"', + 'echo "HTTPS_PROXY=${HTTPS_PROXY}"', + 'echo "NO_PROXY=${NO_PROXY}"', + ].join("\n"); + const tmpFile = join(tmpdir(), `nemoclaw-proxy-test-${process.pid}.sh`); + try { + writeFileSync(tmpFile, script, { mode: 0o700 }); + const out = execFileSync("bash", [tmpFile], { + encoding: "utf-8", + env: { ...process.env, ...env }, + }).trim(); + return Object.fromEntries(out.split("\n").map((l) => { + const idx = l.indexOf("="); + return [l.slice(0, idx), l.slice(idx + 1)]; + })); + } finally { + try { unlinkSync(tmpFile); } catch { /* ignore */ } + } + } + + it("sets HTTP_PROXY to default gateway address", () => { + const vars = extractProxyVars(); + expect(vars.HTTP_PROXY).toBe("http://10.200.0.1:3128"); + }); + + it("sets HTTPS_PROXY to default gateway address", () => { + const vars = extractProxyVars(); + expect(vars.HTTPS_PROXY).toBe("http://10.200.0.1:3128"); + }); + + it("NEMOCLAW_PROXY_HOST overrides default gateway IP", () => { + const vars = extractProxyVars({ NEMOCLAW_PROXY_HOST: "192.168.64.1" }); + expect(vars.HTTP_PROXY).toBe("http://192.168.64.1:3128"); + expect(vars.HTTPS_PROXY).toBe("http://192.168.64.1:3128"); + }); + + it("NEMOCLAW_PROXY_PORT overrides default proxy port", () => { + const vars = extractProxyVars({ NEMOCLAW_PROXY_PORT: "8080" }); + expect(vars.HTTP_PROXY).toBe("http://10.200.0.1:8080"); + expect(vars.HTTPS_PROXY).toBe("http://10.200.0.1:8080"); + }); + + it("NO_PROXY excludes loopback and inference.local", () => { + const vars = extractProxyVars(); + const noProxy = vars.NO_PROXY.split(","); + expect(noProxy).toContain("localhost"); + expect(noProxy).toContain("127.0.0.1"); + expect(noProxy).toContain("inference.local"); + }); + + it("NO_PROXY excludes OpenShell virtual network range", () => { + const vars = extractProxyVars(); + expect(vars.NO_PROXY).toContain("10.200.0.0/16"); + }); + }); }); From 623d8786a6fdf51821f787603b452f70e82e914c Mon Sep 17 00:00:00 2001 From: Nanook Date: Mon, 23 Mar 2026 12:06:36 +0000 Subject: [PATCH 2/4] test(sandbox): source proxy vars from nemoclaw-start.sh instead of duplicating extractProxyVars now uses sed to extract the PROXY_HOST/PROXY_PORT/HTTP_PROXY/ HTTPS_PROXY/NO_PROXY block directly from scripts/nemoclaw-start.sh and runs it in a minimal bash wrapper. This ensures that if the proxy configuration in nemoclaw-start.sh changes (different defaults, new variables, different NO_PROXY entries), the tests will catch the regression rather than passing against stale duplicated logic. All 17 tests pass. Addresses CodeRabbit nitpick in PR #704. --- test/service-env.test.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/service-env.test.js b/test/service-env.test.js index f77e8c62303..35dc5a31dfd 100644 --- a/test/service-env.test.js +++ b/test/service-env.test.js @@ -105,23 +105,30 @@ describe("service environment", () => { // proxy and does not attempt to resolve DNS locally inside the sandbox. function extractProxyVars(env = {}) { - // Write the proxy-variable snippet from nemoclaw-start.sh to a temp script - // and execute it so that bash variable assignments and expansions work - // correctly without interference from JSON.stringify quote-escaping. - const script = [ + // Source the proxy-variable block directly from scripts/nemoclaw-start.sh + // so that tests always validate the actual implementation rather than a + // hand-maintained copy. If the script changes its defaults or variable + // names, these tests will catch the regression. + // + // Implementation: extract the proxy block (PROXY_HOST= through + // export NO_PROXY=) via sed, then run it in a minimal bash wrapper that + // echoes the three variables we care about. + const scriptPath = join(import.meta.dirname, "../scripts/nemoclaw-start.sh"); + const proxyBlock = execFileSync( + "sed", + ["-n", "/^PROXY_HOST=/,/^export NO_PROXY=/p", scriptPath], + { encoding: "utf-8" } + ); + const wrapper = [ "#!/usr/bin/env bash", - 'PROXY_HOST="${NEMOCLAW_PROXY_HOST:-10.200.0.1}"', - 'PROXY_PORT="${NEMOCLAW_PROXY_PORT:-3128}"', - 'HTTP_PROXY="http://${PROXY_HOST}:${PROXY_PORT}"', - 'HTTPS_PROXY="http://${PROXY_HOST}:${PROXY_PORT}"', - 'NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.0/16"', + proxyBlock.trimEnd(), 'echo "HTTP_PROXY=${HTTP_PROXY}"', 'echo "HTTPS_PROXY=${HTTPS_PROXY}"', 'echo "NO_PROXY=${NO_PROXY}"', ].join("\n"); const tmpFile = join(tmpdir(), `nemoclaw-proxy-test-${process.pid}.sh`); try { - writeFileSync(tmpFile, script, { mode: 0o700 }); + writeFileSync(tmpFile, wrapper, { mode: 0o700 }); const out = execFileSync("bash", [tmpFile], { encoding: "utf-8", env: { ...process.env, ...env }, From c81cb4fb4bf41043def1bb96733ddb9de9ca2b9e Mon Sep 17 00:00:00 2001 From: Nanook Date: Mon, 23 Mar 2026 15:08:03 +0000 Subject: [PATCH 3/4] fix(sandbox): replace CIDR in NO_PROXY with explicit gateway IP; add ::1 test - Replace 10.200.0.0/16 (CIDR) with 10.200.0.1 (explicit gateway IP). Node.js undici EnvHttpProxyAgent matches NO_PROXY entries as exact hostname strings or domain suffixes, not CIDR ranges. The CIDR entry was silently ignored, routing all virtual-network IPs through the proxy when they should be bypassed. - Add test guard: extractProxyVars() now throws if the sed extraction returns an empty block, surfacing script-format regressions as clear failures instead of silent empty-env errors. - Add ::1 (IPv6 loopback) assertion to the loopback exclusion test to match the NO_PROXY entry already present in the script. All 17 tests pass. Addresses CodeRabbit review comments on PR #704. --- scripts/nemoclaw-start.sh | 2 +- test/service-env.test.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index 6385199fe12..a4115a2415a 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -151,7 +151,7 @@ export HTTP_PROXY="http://${PROXY_HOST}:${PROXY_PORT}" export HTTPS_PROXY="http://${PROXY_HOST}:${PROXY_PORT}" # Bypass proxy for loopback, sandbox-local, and the OpenShell virtual network # so internal gateway calls (openclaw dashboard, inference.local) stay fast. -export NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.0/16" +export NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.1" write_auth_profile diff --git a/test/service-env.test.js b/test/service-env.test.js index 35dc5a31dfd..91efb6161ea 100644 --- a/test/service-env.test.js +++ b/test/service-env.test.js @@ -119,6 +119,12 @@ describe("service environment", () => { ["-n", "/^PROXY_HOST=/,/^export NO_PROXY=/p", scriptPath], { encoding: "utf-8" } ); + if (!proxyBlock.trim()) { + throw new Error( + "Failed to extract proxy configuration from scripts/nemoclaw-start.sh — " + + "the PROXY_HOST/NO_PROXY block may have been moved or renamed" + ); + } const wrapper = [ "#!/usr/bin/env bash", proxyBlock.trimEnd(), @@ -169,12 +175,13 @@ describe("service environment", () => { const noProxy = vars.NO_PROXY.split(","); expect(noProxy).toContain("localhost"); expect(noProxy).toContain("127.0.0.1"); + expect(noProxy).toContain("::1"); expect(noProxy).toContain("inference.local"); }); - it("NO_PROXY excludes OpenShell virtual network range", () => { + it("NO_PROXY excludes OpenShell gateway IP (undici does not support CIDR)", () => { const vars = extractProxyVars(); - expect(vars.NO_PROXY).toContain("10.200.0.0/16"); + expect(vars.NO_PROXY).toContain("10.200.0.1"); }); }); }); From b820c9c2ee11fd29ca040508053456550118d736 Mon Sep 17 00:00:00 2001 From: nanookclaw Date: Tue, 24 Mar 2026 03:08:13 +0000 Subject: [PATCH 4/4] fix(sandbox): write proxy env to /etc/profile.d so connected shells inherit full NO_PROXY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenShell injects NO_PROXY=127.0.0.1,localhost,::1 when a user connects to the sandbox via `openshell sandbox connect`, overwriting the value set by nemoclaw-start.sh. The gateway process (launched by nemoclaw-start.sh) inherits the correct NO_PROXY, but subsequent interactive shell sessions opened via `connect` get the truncated loopback-only value — exactly the mismatch reported by kjw3 in the re-review. Fix: after exporting the proxy vars, write a /etc/profile.d/nemoclaw-proxy.sh snippet that restores the full NO_PROXY on every login shell. This snippet is sourced by bash/sh after OpenShell's injection, so connected sessions always see inference.local and 10.200.0.1 in NO_PROXY. Added test: verifies that nemoclaw-start.sh writes a profile.d snippet containing all expected NO_PROXY entries (inference.local, 10.200.0.1). All 18 tests pass. --- scripts/nemoclaw-start.sh | 14 +++++++++++ test/service-env.test.js | 49 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index a4115a2415a..6bf72997cd7 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -152,6 +152,20 @@ export HTTPS_PROXY="http://${PROXY_HOST}:${PROXY_PORT}" # Bypass proxy for loopback, sandbox-local, and the OpenShell virtual network # so internal gateway calls (openclaw dashboard, inference.local) stay fast. export NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.1" +# OpenShell injects its own NO_PROXY=127.0.0.1,localhost,::1 when a user +# connects to the sandbox via `openshell sandbox connect`, overwriting the +# value set above. Write a profile.d snippet so the full value is restored +# on every login shell — both initial and subsequent `connect` sessions. +# /etc/profile.d/ is writable by root during startup (this script runs as the +# entrypoint before Landlock lockdown applies to that path). +if [ -d /etc/profile.d ]; then + cat >/etc/profile.d/nemoclaw-proxy.sh < { const vars = extractProxyVars(); expect(vars.NO_PROXY).toContain("10.200.0.1"); }); + + it("writes proxy snippet to a profile.d directory when it exists", () => { + // Verify that nemoclaw-start.sh writes /etc/profile.d/nemoclaw-proxy.sh so + // that interactive shells opened via `openshell sandbox connect` (which + // inject a truncated NO_PROXY=127.0.0.1,localhost,::1) get the full value + // restored on every subsequent login shell. + const profileDir = join(tmpdir(), `nemoclaw-profile-test-${process.pid}`); + execFileSync("mkdir", ["-p", profileDir]); + const tmpFile = join(tmpdir(), `nemoclaw-profile-write-test-${process.pid}.sh`); + try { + // Run a minimal wrapper that sets the stage variables and executes only + // the proxy block from the start script, redirecting the profile.d write + // to our temp directory instead of /etc/profile.d. + const wrapper = [ + "#!/usr/bin/env bash", + `OVERRIDE_PROFILE_D=${JSON.stringify(profileDir)}`, + `PROXY_HOST="10.200.0.1"`, + `PROXY_PORT="3128"`, + `export HTTP_PROXY="http://\${PROXY_HOST}:\${PROXY_PORT}"`, + `export HTTPS_PROXY="http://\${PROXY_HOST}:\${PROXY_PORT}"`, + `export NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.1"`, + // Reproduce the profile.d block with the overridden directory + `if [ -d "\${OVERRIDE_PROFILE_D}" ]; then`, + ` cat > "\${OVERRIDE_PROFILE_D}/nemoclaw-proxy.sh" <