Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .azure-pipelines/templates/Package.NpmSdk.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ jobs:

- script: |
set -euo pipefail
find $(sdkDirectory)/bin -name linux-test-proxy -exec chmod +x {} +
displayName: Restore execute permission on linux-test-proxy
find $(sdkDirectory)/bin -name unix-test-proxy -exec chmod +x {} +
displayName: Restore execute permission on unix-test-proxy

# Drop symbol files from the npm package β€” symbols ship separately.
- script: rm -rf $(sdkDirectory)/bin/*/symbols
Expand Down
8 changes: 4 additions & 4 deletions .azure-pipelines/templates/Rust.Build.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
cargoFeatures: hyperlight
signPattern: |
lxc-exec
linux-test-proxy
unix-test-proxy

# Paths
targetTriple: $(triplet)
Expand Down Expand Up @@ -124,13 +124,13 @@ jobs:
${{ else }}:
enableTest: 'false'

# linux_test_proxy is a separate workspace member (not a dep of lxc), so
# unix_test_proxy is a separate workspace member (not a dep of lxc), so
# the workspace-member-scoped build above doesn't pick it up. Build it
# explicitly here so it lands alongside lxc-exec in the target dir and
# gets copied into the artifact via signPattern.
- script: |
cargo build --release --target $(triplet) -p linux_test_proxy --manifest-path $(Build.SourcesDirectory)/src/Cargo.toml
displayName: Build linux-test-proxy
cargo build --release --target $(triplet) -p unix_test_proxy --manifest-path $(Build.SourcesDirectory)/src/Cargo.toml
displayName: Build unix-test-proxy
condition: eq('${{ item.os }}', 'linux')

# plm (Permissive Learning Mode) is functionally Windows-only, but it
Expand Down
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ The workspace is organized into six top-level directories under `src/`:
| `backends/` | Backend-specific code (one subfolder per containment backend) | `appcontainer/common`, `windows_sandbox/{daemon,guest,common,lifecycle}`, `isolation_session/{bindings,common}`, `hyperlight/common`, `nanvix/{common,build_common,binaries,runner}`, `lxc/common`, `bubblewrap/common`, `wslc/common`, `seatbelt/common` |
| `ffi/` | Foreign-function-interface crates (C ABI for language bindings) | `mxc_ffi/` |
| `host/` | Host-side utilities | `wxc_host_prep/`, `wxc_winhttp_proxy_shim/` |
| `testing/` | Test infrastructure crates | `wxc_e2e_tests/`, `wxc_test_driver/`, `wxc_test_proxy/`, `linux_test_proxy/`, `wxc_ui_probe/`, `fuzz/` |
| `testing/` | Test infrastructure crates | `wxc_e2e_tests/`, `wxc_test_driver/`, `wxc_test_proxy/`, `unix_test_proxy/`, `wxc_ui_probe/`, `fuzz/` |
| `tools/` | Developer/diagnostic tools | `mxc_diagnostic_console/` |

- `wxc_common` is the **cross-platform foundation**: config parsing, models, errors, logger, `ScriptRunner` / `StatefulSandboxBackend` traits, state-aware dispatch helpers, validators, ids, ui-policy, encoding. Plus a few thin Windows API helpers shared by host tools and backends (`process_util`, `string_util`, `filesystem_dacl`, `diagnostic`). It must not depend on any `backends/*` crate.
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/Build.Linux.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,18 @@ jobs:
working-directory: src
run: cargo test --locked --release --target ${{ matrix.target }} -p plm

# linux_test_proxy is a separate workspace member, not a dep of lxc.
- name: Build linux-test-proxy
# unix_test_proxy is a separate workspace member, not a dep of lxc.
- name: Build unix-test-proxy
working-directory: src
run: cargo build --locked --release --target ${{ matrix.target }}
-p linux_test_proxy
-p unix_test_proxy

- name: Upload binaries
uses: actions/upload-artifact@v4
with:
name: lxc-binaries-${{ matrix.target }}
path: |
src/target/${{ matrix.target }}/release/lxc-exec
src/target/${{ matrix.target }}/release/linux-test-proxy
src/target/${{ matrix.target }}/release/unix-test-proxy
if-no-files-found: error
retention-days: 1
2 changes: 1 addition & 1 deletion .github/workflows/Package.NpmSdk.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
for name in lxc-exec linux-test-proxy mxc-exec-mac; do
for name in lxc-exec unix-test-proxy mxc-exec-mac; do
find bin -name "$name" -exec chmod +x {} +
done

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ cargo clippy --workspace --all-targets -- -D warnings

# Linux Rust (from src/; matches build.sh's platform-compatible crate set)

cargo clippy -p lxc -p lxc_common -p wxc_common -p bwrap_common -p linux_test_proxy --all-targets -- -D warnings
cargo clippy -p lxc -p lxc_common -p wxc_common -p bwrap_common -p unix_test_proxy --all-targets -- -D warnings

# macOS Rust (from src/)

Expand Down
14 changes: 7 additions & 7 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ echo "=== Building Rust binaries ($BUILD_TYPE) ==="
cd "$SRC_DIR"

# Packages to build and lint β€” kept in one place so build and clippy stay in sync.
LXC_PACKAGES=(-p lxc -p lxc_common -p wxc_common -p bwrap_common -p linux_test_proxy)
LXC_PACKAGES=(-p lxc -p lxc_common -p wxc_common -p bwrap_common -p unix_test_proxy)

CARGO_FEATURES=()
FEATURES_LIST=()
Expand Down Expand Up @@ -126,16 +126,16 @@ if [ -n "$TARGET_TRIPLE" ]; then
cp "$SRC_DIR/target/release/lxc-exec" "$BIN_DIR/" 2>/dev/null || \
cp "$SRC_DIR/target/$TARGET_TRIPLE/release/lxc-exec" "$BIN_DIR/" 2>/dev/null || \
echo "Warning: Could not find lxc-exec binary to copy"
cp "$SRC_DIR/target/release/linux-test-proxy" "$BIN_DIR/" 2>/dev/null || \
cp "$SRC_DIR/target/$TARGET_TRIPLE/release/linux-test-proxy" "$BIN_DIR/" 2>/dev/null || \
echo "Warning: Could not find linux-test-proxy binary to copy"
cp "$SRC_DIR/target/release/unix-test-proxy" "$BIN_DIR/" 2>/dev/null || \
cp "$SRC_DIR/target/$TARGET_TRIPLE/release/unix-test-proxy" "$BIN_DIR/" 2>/dev/null || \
echo "Warning: Could not find unix-test-proxy binary to copy"
else
cp "$SRC_DIR/target/debug/lxc-exec" "$BIN_DIR/" 2>/dev/null || \
cp "$SRC_DIR/target/$TARGET_TRIPLE/debug/lxc-exec" "$BIN_DIR/" 2>/dev/null || \
echo "Warning: Could not find lxc-exec binary to copy"
cp "$SRC_DIR/target/debug/linux-test-proxy" "$BIN_DIR/" 2>/dev/null || \
cp "$SRC_DIR/target/$TARGET_TRIPLE/debug/linux-test-proxy" "$BIN_DIR/" 2>/dev/null || \
echo "Warning: Could not find linux-test-proxy binary to copy"
cp "$SRC_DIR/target/debug/unix-test-proxy" "$BIN_DIR/" 2>/dev/null || \
cp "$SRC_DIR/target/$TARGET_TRIPLE/debug/unix-test-proxy" "$BIN_DIR/" 2>/dev/null || \
echo "Warning: Could not find unix-test-proxy binary to copy"
fi
fi

Expand Down
2 changes: 1 addition & 1 deletion docs/bwrap-support/bubblewrap-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ Bubblewrap because it requires **no root and no `CAP_NET_ADMIN`**.

1. When `network.proxy` is set, the runner launches an unprivileged HTTP
proxy on loopback (`127.0.0.1:N`). For tests, the bundled
`linux-test-proxy` binary is used (`builtinTestServer: true`,
`unix-test-proxy` binary is used (`builtinTestServer: true`,
testing-only and gated behind `--allow-testing-features`); in production callers
supply their own proxy via `localhost: <port>` or `url: <url>`.
2. The sandbox is then started **without** `--unshare-net` so the sandbox
Expand Down
6 changes: 3 additions & 3 deletions sdk/node/tests/integration/linux-bubblewrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
isLinuxBubblewrap,
debugSpawnOptions,
spawnFromConfigAsync,
startLinuxTestProxy,
startUnixTestProxy,
} from './test-helpers.js';
import type { ChildProcess } from 'node:child_process';

Expand Down Expand Up @@ -102,8 +102,8 @@ describe('Linux Bubblewrap network proxy (schema 0.6.0-alpha)', {
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
});

it('should route HTTPS traffic through an externally launched linux-test-proxy', async () => {
const { port, proxyProcess } = startLinuxTestProxy(tmpDir);
it('should route HTTPS traffic through an externally launched unix-test-proxy', async () => {
const { port, proxyProcess } = startUnixTestProxy(tmpDir);
proxies.push(proxyProcess);

const config = sdk.createConfigFromPolicy(
Expand Down
34 changes: 20 additions & 14 deletions sdk/node/tests/integration/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const EXPECTED_WINDOWS_BINARIES = [

export const EXPECTED_LINUX_BINARIES = [
'lxc-exec',
'linux-test-proxy',
'unix-test-proxy',
];

export const EXPECTED_MACOS_BINARIES = [
Expand Down Expand Up @@ -383,31 +383,34 @@ export function startTestProxy(dir: string): { port: number; proxyProcess: Child
return { port, proxyProcess };
}

// Linux-only: linux-test-proxy helpers
// Linux-only: unix-test-proxy helpers

/** Locate linux-test-proxy in the SDK package bin directory. */
function findLinuxTestProxyBinary(): string {
/** Locate unix-test-proxy in the SDK package bin directory. */
function findUnixTestProxyBinary(): string {
const binDir = getSdkBinDir();
const proxyPath = path.join(binDir, 'linux-test-proxy');
const proxyPath = path.join(binDir, 'unix-test-proxy');
if (fs.existsSync(proxyPath)) {
return proxyPath;
}
throw new Error(`linux-test-proxy not found at expected SDK package location: ${proxyPath}`);
throw new Error(`unix-test-proxy not found at expected SDK package location: ${proxyPath}`);
}

/**
* Start linux-test-proxy in a child process.
* Start unix-test-proxy in a child process.
*
* Binds to an OS-assigned port on `127.0.0.1` and writes it atomically to a
* ready file. The proxy uses `PR_SET_PDEATHSIG` so it dies when the test
* process exits β€” no cleanup-event mechanism is needed on Linux.
* ready file. The proxy watches its stdin for EOF as a cross-platform
* parent-death signal, so it must be spawned with a piped stdin that this
* process keeps open: when the test process exits the pipe closes, the proxy
* reads EOF and shuts down. An ignored/inherited `/dev/null` stdin would
* signal EOF immediately and make the proxy exit right after binding.
*/
export function startLinuxTestProxy(
export function startUnixTestProxy(
dir: string,
opts: { allowHosts?: string[]; blockHosts?: string[] } = {},
): { port: number; proxyProcess: ChildProcess } {
const proxyPath = findLinuxTestProxyBinary();
const readyFile = path.join(dir, 'linux-proxy-ready.txt');
const proxyPath = findUnixTestProxyBinary();
const readyFile = path.join(dir, 'unix-proxy-ready.txt');

const args: string[] = ['--ready-file', readyFile, '--bind-address', '127.0.0.1'];
for (const host of opts.allowHosts ?? []) {
Expand All @@ -417,7 +420,10 @@ export function startLinuxTestProxy(
args.push('--block-host', host);
}

const proxyProcess = spawn(proxyPath, args, { stdio: 'ignore' });
// stdin must stay open (piped, held by this process) so the proxy's
// stdin-EOF parent-death watcher only fires when the test process exits;
// `stdio: 'ignore'` would give it a `/dev/null` stdin that EOFs instantly.
const proxyProcess = spawn(proxyPath, args, { stdio: ['pipe', 'ignore', 'ignore'] });

const deadline = Date.now() + 15000;
while (!fs.existsSync(readyFile) && Date.now() < deadline) {
Expand All @@ -426,7 +432,7 @@ export function startLinuxTestProxy(

if (!fs.existsSync(readyFile)) {
proxyProcess.kill('SIGTERM');
throw new Error('linux-test-proxy did not write ready file within 15 seconds');
throw new Error('unix-test-proxy did not write ready file within 15 seconds');
}

const portStr = fs.readFileSync(readyFile, 'utf-8').trim();
Expand Down
26 changes: 13 additions & 13 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ members = [
"testing/wxc_e2e_tests",
"testing/wxc_test_driver",
"testing/wxc_test_proxy",
"testing/linux_test_proxy",
"testing/unix_test_proxy",
"testing/wxc_ui_probe",
"tools/mxc_diagnostic_console",
"tools/mxc_schema_gen",
Expand Down
8 changes: 4 additions & 4 deletions src/backends/bubblewrap/common/src/bwrap_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//! supports two paths:
//! - **Cooperative env-var proxy** (default, no privilege required): when
//! `network.proxy` is configured the runner launches an unprivileged HTTP
//! proxy via [`wxc_common::linux_proxy_coordinator::LinuxProxyCoordinator`]
//! proxy via [`wxc_common::unix_proxy_coordinator::UnixProxyCoordinator`]
//! and the command builder injects `HTTP_PROXY` / `HTTPS_PROXY` /
//! `NO_PROXY` env vars into the sandbox.
//! - **iptables firewall** (requires `CAP_NET_ADMIN` / root): when
Expand All @@ -34,14 +34,14 @@ use std::time::Duration;

use lxc_common::network_iptables::NetworkIptablesManager;
use wxc_common::interruptible_reader::{wrap_pipe, InterruptibleReader, ReadCanceller};
use wxc_common::linux_proxy_coordinator::LinuxProxyCoordinator;
use wxc_common::logger::Logger;
use wxc_common::models::{ExecutionRequest, NetworkEnforcementMode, ScriptResponse};
use wxc_common::sandbox_process::{
boxed_closer, cancel_and_join_discard, group_kill, spawn_discard, take_boxed_read,
take_boxed_write, wait_with_timeout, SandboxBackend, SandboxProcess, StdioMode, StreamCloser,
WaitError,
};
use wxc_common::unix_proxy_coordinator::UnixProxyCoordinator;
use wxc_common::validator::validate_common;

use crate::bwrap_command;
Expand Down Expand Up @@ -171,7 +171,7 @@ impl BubblewrapScriptRunner {
// 1. Start the network proxy if configured. Must happen before
// arg-building so the proxy's loopback address can be injected as
// HTTP_PROXY / HTTPS_PROXY into the sandbox environment.
let mut proxy = LinuxProxyCoordinator::new();
let mut proxy = UnixProxyCoordinator::new();
if request.policy.network_proxy.is_enabled() {
if let Err(err) = proxy.start(
&request.policy.network_proxy,
Expand Down Expand Up @@ -340,7 +340,7 @@ struct BwrapChild {
/// termination signals the whole group; `false` for `Inherit` mode, where
/// killing bwrap (pid 1 of the namespace) alone tears the sandbox down.
group: bool,
proxy: LinuxProxyCoordinator,
proxy: UnixProxyCoordinator,
fw_manager: Option<NetworkIptablesManager>,
timeout: Option<Duration>,
}
Expand Down
7 changes: 4 additions & 3 deletions src/core/wxc_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ pub mod string_util;
#[cfg(unix)]
pub mod interruptible_reader;

// Linux-specific modules
#[cfg(target_os = "linux")]
pub mod linux_proxy_coordinator;
// Unix cooperative network proxy coordinator, used by the Bubblewrap (Linux)
// and Seatbelt (macOS) backends.
#[cfg(unix)]
pub mod unix_proxy_coordinator;

/// Test-only helper for env-var serialization within this crate's
/// `filesystem_dacl` tests. The same shape lives in
Expand Down
Loading
Loading