Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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}/"
Expand All @@ -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
Expand Down Expand Up @@ -130,13 +131,49 @@ 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.1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

NO_PROXY hardcodes 10.200.0.1 instead of using ${PROXY_HOST}.

Lines 154 and 166 hardcode 10.200.0.1 in NO_PROXY, while HTTP_PROXY and HTTPS_PROXY use the configurable ${PROXY_HOST}. If a user overrides NEMOCLAW_PROXY_HOST to a different IP, the proxy host won't be bypassed, potentially causing requests to the proxy to be routed through itself.

🐛 Proposed fix for consistency
-export NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.1"
+export NO_PROXY="localhost,127.0.0.1,::1,inference.local,${PROXY_HOST}"

And in the heredoc (line 166):

-export NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.1"
+export NO_PROXY="localhost,127.0.0.1,::1,inference.local,${PROXY_HOST}"

Also applies to: 166-166

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/nemoclaw-start.sh` at line 154, The NO_PROXY export currently
hardcodes 10.200.0.1 which breaks custom proxy host overrides; update the
NO_PROXY value to include the configurable variable (use ${PROXY_HOST} or
${NEMOCLAW_PROXY_HOST} consistent with how HTTP_PROXY/HTTPS_PROXY are set) so
the proxy host is bypassed correctly, and make the same replacement inside the
heredoc where the hardcoded 10.200.0.1 is used; ensure you reference the
NO_PROXY export and the heredoc occurrence when making the edits.

# 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 <<PROXYPROFILE
# Set by nemoclaw-start.sh — restores full NO_PROXY after OpenShell injection.
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"
PROXYPROFILE
fi

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
138 changes: 137 additions & 1 deletion test/service-env.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { resolveOpenshell } from "../bin/lib/resolve-openshell";

describe("service environment", () => {
Expand Down Expand Up @@ -95,4 +98,137 @@ 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 = {}) {
// 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" }
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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(),
'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, wrapper, { 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("::1");
expect(noProxy).toContain("inference.local");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("NO_PROXY excludes OpenShell gateway IP (undici does not support CIDR)", () => {
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" <<PROXYPROFILE`,
`# Set by nemoclaw-start.sh — restores full NO_PROXY after OpenShell injection.`,
`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"`,
`PROXYPROFILE`,
`fi`,
].join("\n");
writeFileSync(tmpFile, wrapper, { mode: 0o700 });
execFileSync("bash", [tmpFile], { encoding: "utf-8" });

const snippetPath = join(profileDir, "nemoclaw-proxy.sh");
const snippet = readFileSync(snippetPath, "utf-8");

expect(snippet).toContain("export HTTP_PROXY=");
expect(snippet).toContain("export HTTPS_PROXY=");
expect(snippet).toContain("export NO_PROXY=");
expect(snippet).toContain("inference.local");
expect(snippet).toContain("10.200.0.1");
} finally {
try { unlinkSync(tmpFile); } catch { /* ignore */ }
try { execFileSync("rm", ["-rf", profileDir]); } catch { /* ignore */ }
}
});
});
});
Loading