mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-20 06:00:06 +08:00
fix(sandbox): bypass proxy for localhost traffic (#290)
This commit is contained in:
@@ -415,7 +415,7 @@ Authorization is performed by the gateway (token validation + sandbox readiness
|
||||
2. Clones the master fd for reading and writing
|
||||
3. Configures the shell command with environment variables:
|
||||
- `OPENSHELL_SANDBOX=1`, `HOME=/sandbox`, `USER=sandbox`, `TERM=<from pty request>`
|
||||
- Proxy vars: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `http_proxy`, `https_proxy`, `grpc_proxy`, `NODE_USE_ENV_PROXY=1` so Node.js `fetch` honors the proxy env
|
||||
- Proxy vars: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY=127.0.0.1,localhost,::1`, `http_proxy`, `https_proxy`, `grpc_proxy`, `no_proxy=127.0.0.1,localhost,::1`, `NODE_USE_ENV_PROXY=1` so Node.js `fetch` honors the proxy env while localhost stays direct
|
||||
- TLS trust vars: `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`
|
||||
- Provider credential env vars (from the provider registry)
|
||||
4. Installs a `pre_exec` hook that:
|
||||
|
||||
@@ -277,8 +277,9 @@ inherited environment without clearing it. The spawn path also explicitly remove
|
||||
`NEMOCLAW_SSH_HANDSHAKE_SECRET` so the handshake secret does not leak into the agent
|
||||
entrypoint process.
|
||||
|
||||
After provider env vars, proxy env vars (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, etc.)
|
||||
are also set when `NetworkMode` is `Proxy`. The child is then launched with namespace
|
||||
After provider env vars, proxy env vars (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`,
|
||||
`NO_PROXY=127.0.0.1,localhost,::1`, lowercase variants, etc.) are also set when
|
||||
`NetworkMode` is `Proxy`. The child is then launched with namespace
|
||||
isolation, privilege dropping, seccomp, and Landlock restrictions via `pre_exec`.
|
||||
|
||||
**2. SSH shell sessions** (`crates/openshell-sandbox/src/ssh.rs`):
|
||||
|
||||
@@ -929,7 +929,7 @@ Wraps `tokio::process::Child` + PID. Platform-specific `spawn()` methods delegat
|
||||
**Environment setup** (both Linux and non-Linux):
|
||||
- `OPENSHELL_SANDBOX=1` (always set)
|
||||
- Provider credentials (from `GetSandboxProviderEnvironment` RPC)
|
||||
- Proxy URLs: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (uppercase for curl/wget), `http_proxy`, `https_proxy`, `grpc_proxy` (lowercase for gRPC C-core), `NODE_USE_ENV_PROXY=1` (required for Node.js built-in `fetch`/`http` clients to honor proxy env vars)
|
||||
- Proxy URLs: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (uppercase for curl/wget), `NO_PROXY=127.0.0.1,localhost,::1` for localhost bypass, `http_proxy`, `https_proxy`, `grpc_proxy` (lowercase for gRPC C-core), `no_proxy=127.0.0.1,localhost,::1`, `NODE_USE_ENV_PROXY=1` (required for Node.js built-in `fetch`/`http` clients to honor proxy env vars)
|
||||
- TLS trust store: `NODE_EXTRA_CA_CERTS` (standalone CA cert), `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` (combined bundle)
|
||||
|
||||
**Pre-exec closure** (runs in child after fork, before exec -- async-signal-safe):
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn proxy_env_vars(proxy_url: &str) -> [(&'static str, String); 7] {
|
||||
const LOCAL_NO_PROXY: &str = "127.0.0.1,localhost,::1";
|
||||
|
||||
pub(crate) fn proxy_env_vars(proxy_url: &str) -> [(&'static str, String); 9] {
|
||||
[
|
||||
("ALL_PROXY", proxy_url.to_owned()),
|
||||
("HTTP_PROXY", proxy_url.to_owned()),
|
||||
("HTTPS_PROXY", proxy_url.to_owned()),
|
||||
("NO_PROXY", LOCAL_NO_PROXY.to_owned()),
|
||||
("http_proxy", proxy_url.to_owned()),
|
||||
("https_proxy", proxy_url.to_owned()),
|
||||
("no_proxy", LOCAL_NO_PROXY.to_owned()),
|
||||
("grpc_proxy", proxy_url.to_owned()),
|
||||
// Node.js only honors HTTP(S)_PROXY for built-in fetch/http clients when
|
||||
// proxy support is explicitly enabled at process startup.
|
||||
@@ -38,7 +42,7 @@ mod tests {
|
||||
use std::process::Stdio;
|
||||
|
||||
#[test]
|
||||
fn apply_proxy_env_includes_node_proxy_opt_in() {
|
||||
fn apply_proxy_env_includes_node_proxy_opt_in_and_local_bypass() {
|
||||
let mut cmd = Command::new("/usr/bin/env");
|
||||
cmd.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
@@ -52,7 +56,9 @@ mod tests {
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
|
||||
assert!(stdout.contains("HTTP_PROXY=http://10.200.0.1:3128"));
|
||||
assert!(stdout.contains("NO_PROXY=127.0.0.1,localhost,::1"));
|
||||
assert!(stdout.contains("NODE_USE_ENV_PROXY=1"));
|
||||
assert!(stdout.contains("no_proxy=127.0.0.1,localhost,::1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![cfg(feature = "e2e")]
|
||||
|
||||
use openshell_e2e::harness::sandbox::SandboxGuard;
|
||||
|
||||
fn localhost_bypass_script() -> &'static str {
|
||||
r#"
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
expected_no_proxy = '127.0.0.1,localhost,::1'
|
||||
assert os.environ['HTTP_PROXY'].startswith('http://')
|
||||
assert os.environ['HTTPS_PROXY'].startswith('http://')
|
||||
assert os.environ['NO_PROXY'] == expected_no_proxy
|
||||
assert os.environ['no_proxy'] == expected_no_proxy
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"message":"hello"}')
|
||||
|
||||
server = HTTPServer(('127.0.0.1', 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(f'http://127.0.0.1:{server.server_port}', timeout=10) as response:
|
||||
print(json.dumps({
|
||||
'no_proxy': os.environ['NO_PROXY'],
|
||||
'payload': json.loads(response.read().decode()),
|
||||
}), flush=True)
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=5)
|
||||
server.server_close()
|
||||
"#
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sandbox_bypasses_proxy_for_localhost_http() {
|
||||
let guard = SandboxGuard::create(&["python3", "-c", localhost_bypass_script()])
|
||||
.await
|
||||
.expect("sandbox create with localhost proxy bypass check");
|
||||
|
||||
assert!(
|
||||
guard.create_output.contains(
|
||||
r#"{"no_proxy": "127.0.0.1,localhost,::1", "payload": {"message": "hello"}}"#
|
||||
),
|
||||
"expected localhost HTTP request to bypass proxy and succeed:\n{}",
|
||||
guard.create_output
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user