feat(linux-sandbox): implement proxy-only egress via TCP-UDS-TCP bridge (#11293)

## Summary
- Implement Linux proxy-only routing in `codex-rs/linux-sandbox` with a
two-stage bridge: host namespace `loopback TCP proxy endpoint -> UDS`,
then bwrap netns `loopback TCP listener -> host UDS`.
- Add hidden `--proxy-route-spec` plumbing for outer-to-inner stage
handoff.
- Fail closed in proxy mode when no valid loopback proxy endpoints can
be routed.
- Introduce explicit network seccomp modes: `Restricted` (legacy
restricted networking) and `ProxyRouted` (allow INET/INET6 for routed
proxy access, deny `AF_UNIX` and `socketpair`).
- Enforce that proxy bridge/routing is bwrap-only by validating
`--apply-seccomp-then-exec` requires `--use-bwrap-sandbox`.
- Keep landlock-only flows unchanged (no proxy bridge behavior outside
bwrap).

---------

Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
This commit is contained in:
viyatb-oai
2026-02-21 10:16:34 -08:00
committed by GitHub
parent e7b6f38b58
commit b3202cbd58
11 changed files with 1501 additions and 149 deletions

View File

@@ -10,6 +10,8 @@ use crate::bwrap::BwrapNetworkMode;
use crate::bwrap::BwrapOptions;
use crate::bwrap::create_bwrap_command_args;
use crate::landlock::apply_sandbox_policy_to_current_thread;
use crate::proxy_routing::activate_proxy_routes_in_netns;
use crate::proxy_routing::prepare_host_proxy_route_spec;
use crate::vendored_bwrap::exec_vendored_bwrap;
use crate::vendored_bwrap::run_vendored_bwrap_main;
@@ -44,11 +46,15 @@ pub struct LandlockCommand {
/// Internal compatibility flag.
///
/// By default, restricted-network sandboxing uses isolated networking.
/// If set, sandbox setup switches to proxy-only network mode
/// (currently enforced the same as isolated networking).
/// If set, sandbox setup switches to proxy-only network mode with
/// managed routing bridges.
#[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)]
pub allow_network_for_proxy: bool,
/// Internal route spec used for managed proxy routing in bwrap mode.
#[arg(long = "proxy-route-spec", hide = true)]
pub proxy_route_spec: Option<String>,
/// When set, skip mounting a fresh `/proc` even though PID isolation is
/// still enabled. This is primarily intended for restrictive container
/// environments that deny `--proc /proc`.
@@ -74,6 +80,7 @@ pub fn run_main() -> ! {
use_bwrap_sandbox,
apply_seccomp_then_exec,
allow_network_for_proxy,
proxy_route_spec,
no_proc,
command,
} = LandlockCommand::parse();
@@ -81,15 +88,26 @@ pub fn run_main() -> ! {
if command.is_empty() {
panic!("No command specified to execute.");
}
ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_bwrap_sandbox);
// Inner stage: apply seccomp/no_new_privs after bubblewrap has already
// established the filesystem view.
if apply_seccomp_then_exec {
if allow_network_for_proxy {
let spec = proxy_route_spec
.as_deref()
.unwrap_or_else(|| panic!("managed proxy mode requires --proxy-route-spec"));
if let Err(err) = activate_proxy_routes_in_netns(spec) {
panic!("error activating Linux proxy routing bridge: {err}");
}
}
let proxy_routing_active = allow_network_for_proxy;
if let Err(e) = apply_sandbox_policy_to_current_thread(
&sandbox_policy,
&sandbox_policy_cwd,
false,
allow_network_for_proxy,
proxy_routing_active,
) {
panic!("error applying Linux sandbox restrictions: {e:?}");
}
@@ -102,6 +120,7 @@ pub fn run_main() -> ! {
&sandbox_policy_cwd,
false,
allow_network_for_proxy,
false,
) {
panic!("error applying Linux sandbox restrictions: {e:?}");
}
@@ -112,11 +131,20 @@ pub fn run_main() -> ! {
// Outer stage: bubblewrap first, then re-enter this binary in the
// sandboxed environment to apply seccomp. This path never falls back
// to legacy Landlock on failure.
let proxy_route_spec =
if allow_network_for_proxy {
Some(prepare_host_proxy_route_spec().unwrap_or_else(|err| {
panic!("failed to prepare host proxy routing bridge: {err}")
}))
} else {
None
};
let inner = build_inner_seccomp_command(
&sandbox_policy_cwd,
&sandbox_policy,
use_bwrap_sandbox,
allow_network_for_proxy,
proxy_route_spec,
command,
);
run_bwrap_with_proc_fallback(
@@ -134,12 +162,19 @@ pub fn run_main() -> ! {
&sandbox_policy_cwd,
true,
allow_network_for_proxy,
false,
) {
panic!("error applying legacy Linux sandbox restrictions: {e:?}");
}
exec_or_panic(command);
}
fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_bwrap_sandbox: bool) {
if apply_seccomp_then_exec && !use_bwrap_sandbox {
panic!("--apply-seccomp-then-exec requires --use-bwrap-sandbox");
}
}
fn run_bwrap_with_proc_fallback(
sandbox_policy_cwd: &Path,
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
@@ -147,14 +182,15 @@ fn run_bwrap_with_proc_fallback(
mount_proc: bool,
allow_network_for_proxy: bool,
) -> ! {
let network_mode = bwrap_network_mode(sandbox_policy, allow_network_for_proxy);
let mut mount_proc = mount_proc;
if mount_proc && !preflight_proc_mount_support(sandbox_policy_cwd, sandbox_policy) {
if mount_proc && !preflight_proc_mount_support(sandbox_policy_cwd, sandbox_policy, network_mode)
{
eprintln!("codex-linux-sandbox: bwrap could not mount /proc; retrying with --no-proc");
mount_proc = false;
}
let network_mode = bwrap_network_mode(sandbox_policy, allow_network_for_proxy);
let options = BwrapOptions {
mount_proc,
network_mode,
@@ -202,19 +238,29 @@ fn build_bwrap_argv(
fn preflight_proc_mount_support(
sandbox_policy_cwd: &Path,
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
network_mode: BwrapNetworkMode,
) -> bool {
let preflight_argv =
build_preflight_bwrap_argv(sandbox_policy_cwd, sandbox_policy, network_mode);
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
!is_proc_mount_failure(stderr.as_str())
}
fn build_preflight_bwrap_argv(
sandbox_policy_cwd: &Path,
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
network_mode: BwrapNetworkMode,
) -> Vec<String> {
let preflight_command = vec![resolve_true_command()];
let preflight_argv = build_bwrap_argv(
build_bwrap_argv(
preflight_command,
sandbox_policy,
sandbox_policy_cwd,
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
network_mode,
},
);
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
!is_proc_mount_failure(stderr.as_str())
)
}
fn resolve_true_command() -> String {
@@ -318,6 +364,7 @@ fn build_inner_seccomp_command(
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
use_bwrap_sandbox: bool,
allow_network_for_proxy: bool,
proxy_route_spec: Option<String>,
command: Vec<String>,
) -> Vec<String> {
let current_exe = match std::env::current_exe() {
@@ -342,6 +389,10 @@ fn build_inner_seccomp_command(
}
if allow_network_for_proxy {
inner.push("--allow-network-for-proxy".to_string());
let proxy_route_spec = proxy_route_spec
.unwrap_or_else(|| panic!("managed proxy mode requires a proxy route spec"));
inner.push("--proxy-route-spec".to_string());
inner.push(proxy_route_spec);
}
inner.push("--".to_string());
inner.extend(command);
@@ -371,100 +422,5 @@ fn exec_or_panic(command: Vec<String>) -> ! {
panic!("Failed to execvp {}: {err}", command[0].as_str());
}
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
#[test]
fn detects_proc_mount_invalid_argument_failure() {
let stderr = "bwrap: Can't mount proc on /newroot/proc: Invalid argument";
assert_eq!(is_proc_mount_failure(stderr), true);
}
#[test]
fn detects_proc_mount_operation_not_permitted_failure() {
let stderr = "bwrap: Can't mount proc on /newroot/proc: Operation not permitted";
assert_eq!(is_proc_mount_failure(stderr), true);
}
#[test]
fn detects_proc_mount_permission_denied_failure() {
let stderr = "bwrap: Can't mount proc on /newroot/proc: Permission denied";
assert_eq!(is_proc_mount_failure(stderr), true);
}
#[test]
fn ignores_non_proc_mount_errors() {
let stderr = "bwrap: Can't bind mount /dev/null: Operation not permitted";
assert_eq!(is_proc_mount_failure(stderr), false);
}
#[test]
fn inserts_bwrap_argv0_before_command_separator() {
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::new_read_only_policy(),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::FullAccess,
},
);
assert_eq!(
argv,
vec![
"bwrap".to_string(),
"--new-session".to_string(),
"--die-with-parent".to_string(),
"--ro-bind".to_string(),
"/".to_string(),
"/".to_string(),
"--dev".to_string(),
"/dev".to_string(),
"--unshare-pid".to_string(),
"--proc".to_string(),
"/proc".to_string(),
"--argv0".to_string(),
"codex-linux-sandbox".to_string(),
"--".to_string(),
"/bin/true".to_string(),
]
);
}
#[test]
fn inserts_unshare_net_when_network_isolation_requested() {
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::new_read_only_policy(),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::Isolated,
},
);
assert_eq!(argv.contains(&"--unshare-net".to_string()), true);
}
#[test]
fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
let argv = build_bwrap_argv(
vec!["/bin/true".to_string()],
&SandboxPolicy::new_read_only_policy(),
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode: BwrapNetworkMode::ProxyOnly,
},
);
assert_eq!(argv.contains(&"--unshare-net".to_string()), true);
}
#[test]
fn proxy_only_mode_takes_precedence_over_full_network_policy() {
let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true);
assert_eq!(mode, BwrapNetworkMode::ProxyOnly);
}
}
#[path = "linux_run_main_tests.rs"]
mod tests;