Skip to main content

aster/sandbox/
executor.rs

1//! 沙箱执行器
2//!
3//! 提供统一的沙箱执行接口,自动选择最佳沙箱类型
4
5use super::config::{SandboxConfig, SandboxType};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::process::Stdio;
9use std::time::Duration;
10use tokio::process::Command;
11
12/// 执行结果
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ExecutorResult {
15    /// 退出码
16    pub exit_code: i32,
17    /// 标准输出
18    pub stdout: String,
19    /// 标准错误
20    pub stderr: String,
21    /// 是否在沙箱中执行
22    pub sandboxed: bool,
23    /// 沙箱类型
24    pub sandbox_type: SandboxType,
25    /// 执行时长(毫秒)
26    pub duration: Option<u64>,
27}
28
29/// 执行选项
30#[derive(Debug, Clone)]
31pub struct ExecutorOptions {
32    /// 命令
33    pub command: String,
34    /// 参数
35    pub args: Vec<String>,
36    /// 超时时间(毫秒)
37    pub timeout: Option<u64>,
38    /// 环境变量
39    pub env: HashMap<String, String>,
40    /// 工作目录
41    pub working_dir: Option<String>,
42}
43
44/// 沙箱能力
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SandboxCapabilities {
47    /// Bubblewrap 可用
48    pub bubblewrap: bool,
49    /// Seatbelt 可用 (macOS)
50    pub seatbelt: bool,
51    /// Docker 可用
52    pub docker: bool,
53    /// 资源限制可用
54    pub resource_limits: bool,
55}
56
57/// 在沙箱中执行命令
58pub async fn execute_in_sandbox(
59    command: &str,
60    args: &[String],
61    config: &SandboxConfig,
62) -> anyhow::Result<ExecutorResult> {
63    let start_time = std::time::Instant::now();
64
65    // 禁用沙箱或类型为 None
66    if !config.enabled || config.sandbox_type == SandboxType::None {
67        return execute_unsandboxed(command, args, config).await;
68    }
69
70    // 根据沙箱类型执行
71    let result = match config.sandbox_type {
72        SandboxType::Docker => execute_in_docker(command, args, config).await,
73        SandboxType::Bubblewrap => {
74            #[cfg(target_os = "linux")]
75            {
76                execute_in_bubblewrap(command, args, config).await
77            }
78            #[cfg(not(target_os = "linux"))]
79            {
80                tracing::warn!("Bubblewrap 仅在 Linux 上可用,回退到无沙箱执行");
81                execute_unsandboxed(command, args, config).await
82            }
83        }
84        SandboxType::Seatbelt => {
85            #[cfg(target_os = "macos")]
86            {
87                execute_in_seatbelt(command, args, config).await
88            }
89            #[cfg(not(target_os = "macos"))]
90            {
91                tracing::warn!("Seatbelt 仅在 macOS 上可用,回退到无沙箱执行");
92                execute_unsandboxed(command, args, config).await
93            }
94        }
95        SandboxType::Firejail => {
96            #[cfg(target_os = "linux")]
97            {
98                execute_in_firejail(command, args, config).await
99            }
100            #[cfg(not(target_os = "linux"))]
101            {
102                tracing::warn!("Firejail 仅在 Linux 上可用,回退到无沙箱执行");
103                execute_unsandboxed(command, args, config).await
104            }
105        }
106        SandboxType::None => execute_unsandboxed(command, args, config).await,
107    };
108
109    result.map(|mut r| {
110        r.duration = Some(start_time.elapsed().as_millis() as u64);
111        r
112    })
113}
114
115/// 无沙箱执行
116async fn execute_unsandboxed(
117    command: &str,
118    args: &[String],
119    config: &SandboxConfig,
120) -> anyhow::Result<ExecutorResult> {
121    let mut cmd = Command::new(command);
122    cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
123
124    // 设置环境变量
125    for (key, value) in &config.environment_variables {
126        cmd.env(key, value);
127    }
128
129    let timeout = config
130        .resource_limits
131        .as_ref()
132        .and_then(|l| l.max_execution_time)
133        .map(Duration::from_millis);
134
135    let output = if let Some(timeout) = timeout {
136        tokio::time::timeout(timeout, cmd.output()).await??
137    } else {
138        cmd.output().await?
139    };
140
141    Ok(ExecutorResult {
142        exit_code: output.status.code().unwrap_or(1),
143        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
144        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
145        sandboxed: false,
146        sandbox_type: SandboxType::None,
147        duration: None,
148    })
149}
150
151/// Docker 沙箱执行
152async fn execute_in_docker(
153    command: &str,
154    args: &[String],
155    config: &SandboxConfig,
156) -> anyhow::Result<ExecutorResult> {
157    let docker_config = config.docker.as_ref();
158    let image = docker_config
159        .and_then(|d| d.image.as_ref())
160        .map(|s| s.as_str())
161        .unwrap_or("alpine:latest");
162
163    let mut docker_args = vec!["run", "--rm"];
164
165    // 资源限制
166    if let Some(ref limits) = config.resource_limits {
167        if let Some(max_memory) = limits.max_memory {
168            let mem_str = format!("{}m", max_memory / 1024 / 1024);
169            docker_args.push("-m");
170            docker_args.push(Box::leak(mem_str.into_boxed_str()));
171        }
172    }
173
174    // 网络
175    if !config.network_access {
176        docker_args.push("--network=none");
177    }
178
179    docker_args.push(image);
180    docker_args.push(command);
181    for arg in args {
182        docker_args.push(arg);
183    }
184
185    let mut cmd = Command::new("docker");
186    cmd.args(&docker_args)
187        .stdout(Stdio::piped())
188        .stderr(Stdio::piped());
189
190    let output = cmd.output().await?;
191
192    Ok(ExecutorResult {
193        exit_code: output.status.code().unwrap_or(1),
194        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
195        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
196        sandboxed: true,
197        sandbox_type: SandboxType::Docker,
198        duration: None,
199    })
200}
201
202/// Bubblewrap 沙箱执行 (Linux)
203#[cfg(target_os = "linux")]
204async fn execute_in_bubblewrap(
205    command: &str,
206    args: &[String],
207    config: &SandboxConfig,
208) -> anyhow::Result<ExecutorResult> {
209    let mut bwrap_args = vec!["--unshare-all".to_string()];
210
211    // 只读路径
212    for path in &config.read_only_paths {
213        bwrap_args.push("--ro-bind".to_string());
214        bwrap_args.push(path.to_string_lossy().to_string());
215        bwrap_args.push(path.to_string_lossy().to_string());
216    }
217
218    // 可写路径
219    for path in &config.writable_paths {
220        bwrap_args.push("--bind".to_string());
221        bwrap_args.push(path.to_string_lossy().to_string());
222        bwrap_args.push(path.to_string_lossy().to_string());
223    }
224
225    // /dev 访问
226    if config.allow_dev_access {
227        bwrap_args.push("--dev".to_string());
228        bwrap_args.push("/dev".to_string());
229    }
230
231    // /proc 访问
232    if config.allow_proc_access {
233        bwrap_args.push("--proc".to_string());
234        bwrap_args.push("/proc".to_string());
235    }
236
237    // 随父进程退出
238    if config.die_with_parent {
239        bwrap_args.push("--die-with-parent".to_string());
240    }
241
242    // 新会话
243    if config.new_session {
244        bwrap_args.push("--new-session".to_string());
245    }
246
247    bwrap_args.push("--".to_string());
248    bwrap_args.push(command.to_string());
249    bwrap_args.extend(args.iter().cloned());
250
251    let mut cmd = Command::new("bwrap");
252    cmd.args(&bwrap_args)
253        .stdout(Stdio::piped())
254        .stderr(Stdio::piped());
255
256    let output = cmd.output().await?;
257
258    Ok(ExecutorResult {
259        exit_code: output.status.code().unwrap_or(1),
260        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
261        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
262        sandboxed: true,
263        sandbox_type: SandboxType::Bubblewrap,
264        duration: None,
265    })
266}
267
268/// Seatbelt 沙箱执行 (macOS)
269#[cfg(target_os = "macos")]
270async fn execute_in_seatbelt(
271    command: &str,
272    args: &[String],
273    config: &SandboxConfig,
274) -> anyhow::Result<ExecutorResult> {
275    // 构建 sandbox profile
276    let mut profile = String::from("(version 1)\n(deny default)\n");
277
278    // 允许执行
279    profile.push_str("(allow process-exec)\n");
280
281    // 只读路径
282    for path in &config.read_only_paths {
283        profile.push_str(&format!(
284            "(allow file-read* (subpath \"{}\"))\n",
285            path.display()
286        ));
287    }
288
289    // 可写路径
290    for path in &config.writable_paths {
291        profile.push_str(&format!(
292            "(allow file-write* (subpath \"{}\"))\n",
293            path.display()
294        ));
295    }
296
297    // 网络访问
298    if config.network_access {
299        profile.push_str("(allow network*)\n");
300    }
301
302    let mut cmd = Command::new("sandbox-exec");
303    cmd.args(["-p", &profile, command])
304        .args(args)
305        .stdout(Stdio::piped())
306        .stderr(Stdio::piped());
307
308    let output = cmd.output().await?;
309
310    Ok(ExecutorResult {
311        exit_code: output.status.code().unwrap_or(1),
312        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
313        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
314        sandboxed: true,
315        sandbox_type: SandboxType::Seatbelt,
316        duration: None,
317    })
318}
319
320/// Firejail 沙箱执行 (Linux)
321#[cfg(target_os = "linux")]
322async fn execute_in_firejail(
323    command: &str,
324    args: &[String],
325    config: &SandboxConfig,
326) -> anyhow::Result<ExecutorResult> {
327    let mut firejail_args = vec!["--quiet".to_string()];
328
329    // 网络隔离
330    if !config.network_access {
331        firejail_args.push("--net=none".to_string());
332    }
333
334    // 私有 /tmp
335    firejail_args.push("--private-tmp".to_string());
336
337    // 只读路径
338    for path in &config.read_only_paths {
339        firejail_args.push(format!("--read-only={}", path.display()));
340    }
341
342    firejail_args.push("--".to_string());
343    firejail_args.push(command.to_string());
344    firejail_args.extend(args.iter().cloned());
345
346    let mut cmd = Command::new("firejail");
347    cmd.args(&firejail_args)
348        .stdout(Stdio::piped())
349        .stderr(Stdio::piped());
350
351    let output = cmd.output().await?;
352
353    Ok(ExecutorResult {
354        exit_code: output.status.code().unwrap_or(1),
355        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
356        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
357        sandboxed: true,
358        sandbox_type: SandboxType::Firejail,
359        duration: None,
360    })
361}
362
363/// 检测最佳沙箱类型
364pub fn detect_best_sandbox() -> SandboxType {
365    #[cfg(target_os = "linux")]
366    {
367        // 检查 bwrap
368        if std::process::Command::new("which")
369            .arg("bwrap")
370            .output()
371            .map(|o| o.status.success())
372            .unwrap_or(false)
373        {
374            return SandboxType::Bubblewrap;
375        }
376    }
377
378    #[cfg(target_os = "macos")]
379    {
380        // macOS 默认有 sandbox-exec
381        if std::process::Command::new("which")
382            .arg("sandbox-exec")
383            .output()
384            .map(|o| o.status.success())
385            .unwrap_or(false)
386        {
387            return SandboxType::Seatbelt;
388        }
389    }
390
391    // 检查 Docker
392    if std::process::Command::new("docker")
393        .arg("version")
394        .output()
395        .map(|o| o.status.success())
396        .unwrap_or(false)
397    {
398        return SandboxType::Docker;
399    }
400
401    SandboxType::None
402}
403
404/// 获取沙箱能力
405pub fn get_sandbox_capabilities() -> SandboxCapabilities {
406    let mut caps = SandboxCapabilities {
407        bubblewrap: false,
408        seatbelt: false,
409        docker: false,
410        resource_limits: false,
411    };
412
413    #[cfg(target_os = "linux")]
414    {
415        caps.bubblewrap = std::process::Command::new("which")
416            .arg("bwrap")
417            .output()
418            .map(|o| o.status.success())
419            .unwrap_or(false);
420        caps.resource_limits = true;
421    }
422
423    #[cfg(target_os = "macos")]
424    {
425        caps.seatbelt = std::process::Command::new("which")
426            .arg("sandbox-exec")
427            .output()
428            .map(|o| o.status.success())
429            .unwrap_or(false);
430        caps.resource_limits = true;
431    }
432
433    caps.docker = std::process::Command::new("docker")
434        .arg("version")
435        .output()
436        .map(|o| o.status.success())
437        .unwrap_or(false);
438
439    caps
440}
441
442/// 沙箱执行器
443pub struct SandboxExecutor {
444    config: SandboxConfig,
445}
446
447impl SandboxExecutor {
448    /// 创建新的执行器
449    pub fn new(config: SandboxConfig) -> Self {
450        Self { config }
451    }
452
453    /// 执行命令
454    pub async fn execute(&self, command: &str, args: &[String]) -> anyhow::Result<ExecutorResult> {
455        execute_in_sandbox(command, args, &self.config).await
456    }
457
458    /// 顺序执行多个命令
459    pub async fn execute_sequence(
460        &self,
461        commands: &[(String, Vec<String>)],
462    ) -> anyhow::Result<Vec<ExecutorResult>> {
463        let mut results = Vec::new();
464
465        for (command, args) in commands {
466            let result = self.execute(command, args).await?;
467            let failed = result.exit_code != 0;
468            results.push(result);
469
470            if failed {
471                break;
472            }
473        }
474
475        Ok(results)
476    }
477
478    /// 并行执行多个命令
479    pub async fn execute_parallel(
480        &self,
481        commands: &[(String, Vec<String>)],
482    ) -> anyhow::Result<Vec<ExecutorResult>> {
483        let futures: Vec<_> = commands
484            .iter()
485            .map(|(cmd, args)| self.execute(cmd, args))
486            .collect();
487
488        let results = futures::future::try_join_all(futures).await?;
489        Ok(results)
490    }
491
492    /// 更新配置
493    pub fn update_config(&mut self, config: SandboxConfig) {
494        self.config = config;
495    }
496
497    /// 获取当前配置
498    pub fn get_config(&self) -> &SandboxConfig {
499        &self.config
500    }
501}