refactor(sandbox): 长期容器 + docker exec 替代一次性 docker run

容器策略从"每次 docker run 一次性容器"改为"长期容器 + docker exec":

- 统一产物目录 tools/bin/:build.sh 和 tool_manager 编译后复制产物到
  此目录,registry 解析路径优先用 tools/bin/<tool>。
- 长期容器 ias-sandbox:daemon 启动时 ensure(已运行则复用),
  tools/bin 只读挂载到 /tools,无网络/只读根/cap-drop ALL/资源限制。
- docker exec 调用:工具执行改为 docker exec -i ias-sandbox
  timeout -k 5 <secs> /tools/<name>,省去容器创建开销。
  timeout -k 5 确保超时 SIGTERM→SIGKILL,无孤儿进程。
- 新增 src/tools/sandbox.rs:ensure_container + build_exec_cmd。
- executor 删除 build_docker_cmd/sandbox_enabled,docker 分支改用
  sandbox::build_exec_cmd。
- env 按 spec 白名单 -e 注入(容器启动时仅有 PATH)。

验证:本地执行 hostname=debian,docker exec hostname=容器ID,
证明确实走长期容器 exec;25 测试通过。
This commit is contained in:
2026-06-22 17:11:05 +08:00
parent 092f3b78f2
commit 142efb070f
9 changed files with 232 additions and 84 deletions
+5 -63
View File
@@ -145,11 +145,12 @@ pub async fn execute_loop(
}
// ── spawn ──
// 执行隔离边界:启用沙箱时按 sandbox.profile 选择一次性 Docker 容器,
// 否则本地 spawnenv_clear + 白名单注入)
let use_docker = sandbox_enabled() && spec.sandbox.profile != "local";
// 执行隔离边界:启用沙箱且工具 profile 非 local 时,通过 docker exec
// 在长期容器内执行;否则本地 spawnenv_clear + 白名单注入)
let use_docker =
crate::tools::sandbox::enabled() && spec.sandbox.profile != "local";
let mut cmd = if use_docker {
build_docker_cmd(spec, binary_path)
crate::tools::sandbox::build_exec_cmd(spec)
} else {
build_local_cmd(spec, binary_path)
};
@@ -347,14 +348,6 @@ pub async fn execute_loop(
}
}
/// 全局沙箱开关:`IAS_DOCKER_SANDBOX=1` 启用一次性容器隔离
fn sandbox_enabled() -> bool {
std::env::var("IAS_DOCKER_SANDBOX")
.ok()
.as_deref()
== Some("1")
}
/// 本地执行:env_clear + PATH + spec.env 白名单注入
fn build_local_cmd(spec: &ToolSpec, binary_path: &str) -> Command {
let mut cmd = Command::new(std::path::Path::new(binary_path));
@@ -377,54 +370,3 @@ fn build_local_cmd(spec: &ToolSpec, binary_path: &str) -> Command {
}
cmd
}
/// Docker 隔离执行:一次性容器,默认无网络、只读根、cap-drop ALL、
/// no-new-privileges,仅挂载工具二进制(ro+ tmpfs /tmp。
/// 供 tool_manager 产出的不受信任工具使用(profile: stdio)。
fn build_docker_cmd(spec: &ToolSpec, binary_path: &str) -> Command {
let image = match spec.sandbox.profile.as_str() {
"builder" => "ias-tool-builder",
_ => "ias-stdio-runner",
};
let mut cmd = Command::new("docker");
cmd.arg("run")
.arg("--rm")
.arg("--network")
.arg(if spec.sandbox.network == "default" {
"default"
} else {
"none"
})
.arg("--memory")
.arg(&spec.sandbox.memory)
.arg("--cpus")
.arg(&spec.sandbox.cpus)
.arg("--pids-limit")
.arg(spec.sandbox.pids_limit.to_string())
.arg("--cap-drop")
.arg("ALL")
.arg("--security-opt")
.arg("no-new-privileges");
if spec.sandbox.read_only {
cmd.arg("--read-only");
}
// 工具二进制只读挂载到 /tool
cmd.arg("--mount")
.arg(format!("type=bind,source={binary_path},target=/tool,readonly"));
// 可写临时目录
cmd.arg("--mount")
.arg("type=tmpfs,target=/tmp");
// env:容器内固定 PATH + spec 声明的白名单
cmd.arg("--env")
.arg("PATH=/usr/local/bin:/usr/bin:/bin");
for key in &spec.env {
if let Ok(v) = std::env::var(key) {
cmd.arg("--env").arg(format!("{key}={v}"));
}
}
cmd.arg(image).arg("/tool");
if let Some(c) = &spec.command {
cmd.arg("--command").arg(c);
}
cmd
}