Skip to main content

kz_proxy/
enforce.rs

1//! Platform-specific enforcement of "force traffic through proxy" via the [`Enforce`] trait.
2//! When force is true, only Firecracker (Linux) or Docker (macOS) backends are used; no fallback to unshare/sandbox-exec.
3
4use std::path::Path;
5
6use crate::backend::{default_backend_for_os, EnvOnlyEnforcer, SandboxBackend};
7use crate::{ConnectionPolicy, SecretMapping, StringMapping};
8
9/// Platform-specific enforcement: run the child in a sandbox so all traffic is forced through the proxy.
10pub trait Enforce: Send + Sync {
11    /// If this platform enforces by spawning a runner process (e.g. Linux network namespace),
12    /// spawn it and return `Ok(Some(status))`. Otherwise return `Ok(None)` and the caller
13    /// will run proxy+child in-process.
14    fn maybe_spawn_runner(
15        &self,
16        cmd: &str,
17        secret_mappings: &[SecretMapping],
18        string_mappings: &[StringMapping],
19        allow_private_connect: bool,
20        upstream_ca: &Option<std::path::PathBuf>,
21        connection_policies: &[ConnectionPolicy],
22    ) -> Result<Option<std::process::ExitStatus>, Box<dyn std::error::Error + Send + Sync>>;
23
24    /// Run the child process with proxy env vars. When `force` is true, wrap with platform
25    /// sandbox (e.g. sandbox-exec on macOS). Otherwise run normally (e.g. duct).
26    fn run_child(
27        &self,
28        cmd: &str,
29        proxy_url: &str,
30        env_vars_with_masked: &[(String, String)],
31        ssl_cert_file: &Path,
32        force: bool,
33    ) -> Result<std::process::ExitStatus, Box<dyn std::error::Error + Send + Sync>>;
34}
35
36/// No-op enforcer for platforms without kernel-level enforcement (e.g. Windows).
37/// Only sets proxy env vars; child can bypass by ignoring them.
38#[allow(dead_code)] // used on non-Linux, non-macOS in platform_enforcer()
39pub struct NoOpEnforcer;
40
41impl Enforce for NoOpEnforcer {
42    fn maybe_spawn_runner(
43        &self,
44        _cmd: &str,
45        _secret_mappings: &[SecretMapping],
46        _string_mappings: &[StringMapping],
47        _allow_private_connect: bool,
48        _upstream_ca: &Option<std::path::PathBuf>,
49        _connection_policies: &[ConnectionPolicy],
50    ) -> Result<Option<std::process::ExitStatus>, Box<dyn std::error::Error + Send + Sync>> {
51        Ok(None)
52    }
53
54    fn run_child(
55        &self,
56        cmd: &str,
57        proxy_url: &str,
58        env_vars_with_masked: &[(String, String)],
59        ssl_cert_file: &Path,
60        _force: bool,
61    ) -> Result<std::process::ExitStatus, Box<dyn std::error::Error + Send + Sync>> {
62        run_child_duct(cmd, proxy_url, env_vars_with_masked, ssl_cert_file)
63    }
64}
65
66/// Shared helper: run child with proxy env vars via duct (no sandbox wrapping).
67pub(crate) fn run_child_duct(
68    cmd: &str,
69    proxy_url: &str,
70    env_vars_with_masked: &[(String, String)],
71    ssl_cert_file: &Path,
72) -> Result<std::process::ExitStatus, Box<dyn std::error::Error + Send + Sync>> {
73    let mut env: Vec<(String, String)> = vec![
74        ("HTTP_PROXY".into(), proxy_url.to_string()),
75        ("HTTPS_PROXY".into(), proxy_url.to_string()),
76        ("NO_PROXY".into(), String::new()),
77        (
78            "SSL_CERT_FILE".into(),
79            ssl_cert_file.to_string_lossy().into_owned(),
80        ),
81        (
82            "NODE_EXTRA_CA_CERTS".into(),
83            ssl_cert_file.to_string_lossy().into_owned(),
84        ),
85    ];
86    env.extend(env_vars_with_masked.iter().cloned());
87
88    let mut run = duct_sh::sh_dangerous(cmd);
89    for (k, v) in &env {
90        run = run.env(k, v);
91    }
92    run.unchecked().run().map(|o| o.status).map_err(Into::into)
93}
94
95/// Returns the enforcer for the given sandbox backend and force flag. When force is false, returns
96/// an env-only enforcer. When force is true, returns the backend's enforcer (Firecracker on Linux,
97/// Docker on macOS); errors if the backend is not available or not valid for this OS.
98pub fn enforcer_for(
99    backend: Option<SandboxBackend>,
100    force_traffic_through_proxy: bool,
101) -> Result<&'static dyn Enforce, Box<dyn std::error::Error + Send + Sync>> {
102    if !force_traffic_through_proxy {
103        return Ok(&ENV_ONLY_ENFORCER);
104    }
105    let backend = backend.unwrap_or_else(default_backend_for_os);
106
107    #[cfg(target_os = "linux")]
108    {
109        match backend {
110            SandboxBackend::Firecracker => {
111                Err("Firecracker backend is not yet implemented; use --no-force-proxy".into())
112            }
113            SandboxBackend::Docker => Err(
114                "Docker backend is not available on Linux; use firecracker or --no-force-proxy"
115                    .into(),
116            ),
117        }
118    }
119
120    #[cfg(target_os = "macos")]
121    {
122        match backend {
123            SandboxBackend::Docker => Ok(crate::enforce_docker::docker_enforcer()),
124            SandboxBackend::Firecracker => Err(
125                "Firecracker backend is not available on macOS; use docker or --no-force-proxy"
126                    .into(),
127            ),
128        }
129    }
130
131    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
132    {
133        let _ = backend;
134        Err("force_traffic_through_proxy requires a sandbox backend (Firecracker on Linux, Docker on macOS); this OS has no backend. Use --no-force-proxy.".into())
135    }
136}
137
138pub(crate) static ENV_ONLY_ENFORCER: EnvOnlyEnforcer = EnvOnlyEnforcer;