sandbox_runtime/sandbox/linux/
bwrap.rs

1//! Bubblewrap command generation for Linux sandbox.
2
3use std::path::Path;
4
5use crate::config::SandboxRuntimeConfig;
6use crate::error::SandboxError;
7use crate::sandbox::linux::bridge::SocatBridge;
8use crate::sandbox::linux::filesystem::{generate_bind_mounts, BindMount};
9use crate::sandbox::linux::seccomp::{get_apply_seccomp_path, get_bpf_path};
10use crate::utils::quote;
11
12/// Check if bubblewrap is available.
13pub fn check_bwrap() -> bool {
14    std::process::Command::new("bwrap")
15        .arg("--version")
16        .output()
17        .map(|o| o.status.success())
18        .unwrap_or(false)
19}
20
21/// Generate the bubblewrap command for sandboxed execution.
22pub fn generate_bwrap_command(
23    command: &str,
24    config: &SandboxRuntimeConfig,
25    cwd: &Path,
26    http_socket_path: Option<&str>,
27    socks_socket_path: Option<&str>,
28    http_proxy_port: u16,
29    socks_proxy_port: u16,
30    shell: Option<&str>,
31) -> Result<(String, Vec<String>), SandboxError> {
32    let shell = shell.unwrap_or("/bin/bash");
33
34    // Generate filesystem mounts
35    let (mounts, warnings) = generate_bind_mounts(
36        &config.filesystem,
37        cwd,
38        config.ripgrep.as_ref(),
39        config.mandatory_deny_search_depth,
40    )?;
41
42    // Build bwrap arguments
43    let mut bwrap_args = vec![
44        "bwrap".to_string(),
45        "--unshare-net".to_string(), // Network isolation
46        "--dev".to_string(),
47        "/dev".to_string(),
48        "--proc".to_string(),
49        "/proc".to_string(),
50        "--tmpfs".to_string(),
51        "/tmp".to_string(),
52        "--tmpfs".to_string(),
53        "/run".to_string(),
54    ];
55
56    // Start with read-only root filesystem
57    bwrap_args.push("--ro-bind".to_string());
58    bwrap_args.push("/".to_string());
59    bwrap_args.push("/".to_string());
60
61    // Add writable mounts
62    for mount in &mounts {
63        if !mount.readonly {
64            bwrap_args.extend(mount.to_bwrap_args());
65        }
66    }
67
68    // Add read-only (deny) mounts to override writable ones
69    for mount in &mounts {
70        if mount.readonly {
71            bwrap_args.extend(mount.to_bwrap_args());
72        }
73    }
74
75    // Set working directory
76    bwrap_args.push("--chdir".to_string());
77    bwrap_args.push(cwd.display().to_string());
78
79    // Build the inner command with socat bridges and seccomp
80    let inner_command = build_inner_command(
81        command,
82        config,
83        http_socket_path,
84        socks_socket_path,
85        http_proxy_port,
86        socks_proxy_port,
87        shell,
88    )?;
89
90    // Add the command
91    bwrap_args.push("--".to_string());
92    bwrap_args.push(shell.to_string());
93    bwrap_args.push("-c".to_string());
94    bwrap_args.push(inner_command);
95
96    // Join into a single command string
97    let wrapped = bwrap_args
98        .iter()
99        .map(|s| quote(s))
100        .collect::<Vec<_>>()
101        .join(" ");
102
103    Ok((wrapped, warnings))
104}
105
106/// Build the inner command to run inside bubblewrap.
107/// This sets up socat bridges and applies seccomp before running the user command.
108fn build_inner_command(
109    command: &str,
110    config: &SandboxRuntimeConfig,
111    http_socket_path: Option<&str>,
112    socks_socket_path: Option<&str>,
113    http_proxy_port: u16,
114    socks_proxy_port: u16,
115    shell: &str,
116) -> Result<String, SandboxError> {
117    let mut parts = Vec::new();
118
119    // Set up socat bridges for proxy access
120    if let Some(http_sock) = http_socket_path {
121        let bridge_cmd = SocatBridge::tcp_to_unix_command(http_proxy_port, http_sock);
122        parts.push(format!("{} &", bridge_cmd));
123    }
124
125    if let Some(socks_sock) = socks_socket_path {
126        let bridge_cmd = SocatBridge::tcp_to_unix_command(socks_proxy_port, socks_sock);
127        parts.push(format!("{} &", bridge_cmd));
128    }
129
130    // Small delay to let socat bridges start
131    if http_socket_path.is_some() || socks_socket_path.is_some() {
132        parts.push("sleep 0.1".to_string());
133    }
134
135    // Apply seccomp filter and execute command
136    if !config.network.allow_all_unix_sockets.unwrap_or(false) {
137        // Try to use seccomp to block Unix socket creation
138        if let (Ok(bpf_path), Ok(apply_path)) = (
139            get_bpf_path(config.seccomp.as_ref()),
140            get_apply_seccomp_path(config.seccomp.as_ref()),
141        ) {
142            // Export proxy environment variables before applying seccomp
143            let env_vars = generate_proxy_env_string(http_proxy_port, socks_proxy_port);
144            parts.push(env_vars);
145
146            // Use apply-seccomp to apply the filter and exec the command
147            parts.push(format!(
148                "{} {} {} -c {}",
149                apply_path.display(),
150                bpf_path.display(),
151                shell,
152                quote(command)
153            ));
154        } else {
155            // Seccomp not available, just run the command with warning
156            tracing::warn!(
157                "Seccomp not available - Unix socket creation will not be blocked"
158            );
159            let env_vars = generate_proxy_env_string(http_proxy_port, socks_proxy_port);
160            parts.push(format!("{} {} -c {}", env_vars, shell, quote(command)));
161        }
162    } else {
163        // Unix sockets allowed, just run the command
164        let env_vars = generate_proxy_env_string(http_proxy_port, socks_proxy_port);
165        parts.push(format!("{} {} -c {}", env_vars, shell, quote(command)));
166    }
167
168    Ok(parts.join(" ; "))
169}
170
171/// Generate proxy environment variable exports.
172fn generate_proxy_env_string(http_port: u16, socks_port: u16) -> String {
173    format!(
174        "export http_proxy='http://localhost:{}' https_proxy='http://localhost:{}' \
175         HTTP_PROXY='http://localhost:{}' HTTPS_PROXY='http://localhost:{}' \
176         ALL_PROXY='socks5://localhost:{}' all_proxy='socks5://localhost:{}' ;",
177        http_port, http_port, http_port, http_port, socks_port, socks_port
178    )
179}
180
181/// Generate proxy environment variables.
182pub fn generate_proxy_env(http_port: u16, socks_port: u16) -> Vec<(String, String)> {
183    let http_proxy = format!("http://localhost:{}", http_port);
184    let socks_proxy = format!("socks5://localhost:{}", socks_port);
185
186    vec![
187        ("http_proxy".to_string(), http_proxy.clone()),
188        ("HTTP_PROXY".to_string(), http_proxy.clone()),
189        ("https_proxy".to_string(), http_proxy.clone()),
190        ("HTTPS_PROXY".to_string(), http_proxy),
191        ("ALL_PROXY".to_string(), socks_proxy.clone()),
192        ("all_proxy".to_string(), socks_proxy),
193    ]
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_generate_proxy_env_string() {
202        let env = generate_proxy_env_string(3128, 1080);
203        assert!(env.contains("http_proxy='http://localhost:3128'"));
204        assert!(env.contains("ALL_PROXY='socks5://localhost:1080'"));
205    }
206
207    #[test]
208    fn test_check_bwrap() {
209        // This test will pass/fail based on system configuration
210        let available = check_bwrap();
211        println!("Bubblewrap available: {}", available);
212    }
213}