Files
2026-08-21 16:00:13 +08:00
..
2026-08-21 16:00:13 +08:00
2026-08-21 16:00:13 +08:00
2026-08-21 16:00:13 +08:00
2026-08-21 16:00:13 +08:00

乾坤 · 魂镜安全沙箱服务(hunjing-sandbox / Sandbox Engine)

文化命名「乾坤」(Sandbox Engine),百工枢的隔离执行后端——为器用百工提供一个自成天地、越界即止的封闭执行世界。命名登记见 docs/contracts/contract-glossary.md §2.1。

Rust + axum 实现的 HTTP 微服务,为「百工 Tool Runtime」提供本地隔离的命令执行层。在系统中的定位:

flowchart LR
    Backend["后端 FastAPI"] -->|POST /v1/exec| Sandbox["hunjing-sandbox (Rust)"]
    Sandbox --> Child["受限子进程<br/>进程组 / 临时工作目录 / 超时"]
    Child --> System["shell / python / 外部命令"]
    Sandbox -->|SIGKILL 进程组| Cleanup["超时或取消时清理子进程"]

安全边界:沙箱只执行已授权任务,不做风险判定(由司命 Safety 负责);调用方应在发起前完成权限与风险审核。设计依据见 docs/architecture/service-boundaries.md 与 docs/architecture/service-boundaries.md。

司命授权

/v1/exec 受 HMAC 授权中间件保护(/healthz 不受保护)。调用方(后端百工客户端)需经司命审核后签发短期凭证,沙箱端验签通过才执行。

  • 凭证格式:<payload_b64url>.<sig_b64url>,payload 含 exp / trace_id / risk_level / tool / character_id / run_id / body_hash / rlimit_overrides / file_op_hash,sig 为 HMAC-SHA256(SANDBOX_AUTH_SECRET, payload_b64url)。
  • rlimit_overrides:司命按 tool policy 授予的资源配额上限(key: fsize/as/cpu/nproc/nofile),随 HMAC 不可伪造;sandbox 验签后 clamp 到全局天花板 SANDBOX_RLIMIT_*_MAX。属授权属性,不进 body_hash。
  • 凭证轮换:SANDBOX_AUTH_SECRET_PREVIOUS(逗号分隔旧 secret)支持多 secret 并存的无中断轮换——sandbox 先加 previous 认新旧 → backend 切新 secret → TTL 后清 previous。metric sandbox_auth_secret_used{slot=primary|previous} 指示轮换进度。
  • body_hash 为 SHA256(canonical JSON {argv, stdin}),把凭证绑死到具体执行内容——凭证在 TTL 内无法被重放去执行任意其他命令;两端字节级一致(共享固定向量回归测试守护,见 sandbox/src/auth.rs 与 backend/.../sandbox/auth.py)。
  • file_op_hash 为 SHA256(canonical JSON {op, path}),把文件 IO 凭证绑死到具体操作(read/write/list/stat)与路径——TTL 内无法重放去读写任意其他文件;与 body_hash 互斥(exec 凭证此字段为 null),两端字节级一致(同样由固定向量回归守护)。
  • HTTP 头:Authorization: Bearer <token>。
  • 校验失败统一返回 401 {"error":{"code":"sandbox_unauthorized", ...}}。
  • SANDBOX_AUTH_ENABLED=false 时跳过校验(仅本地联调,生产禁用)。

后端客户端实现见 backend/app/integrations/sandbox/(签发 + 调用 + 一炁事件回写,两端 HMAC 字节级一致,已通过跨语言闭环验证)。

隔离措施

维度 措施
进程 独立进程组(process_group(0)),超时/取消对整组 SIGKILL,清理派生子进程
环境 默认 env_clear,仅注入 SANDBOX_ALLOWED_ENV 白名单 + 请求指定变量
工作目录 默认每次执行用 base 下临时目录(执行后回收);可指定 base 内子目录,拒绝路径逃逸;文件类工具由百工 runtime 按 run/trace 隔离为 workspace/<run_key>(key 经 sanitize,防路径注入)
超时 min(请求 timeout_ms, 全局 SANDBOX_MAX_TIMEOUT_MS),到点强制结束
客户端断开 流式执行(/v1/exec/stream)感知接收端关闭(rx drop / SSE 断连)后主动对进程组 SIGKILL 取消并收尸,避免已离开用户的长任务(如 sleep 30)占满并发闸;覆盖无输出命令(capture_streaming 的 send-err 检测对无输出命令失效)
输出 stdout / stderr 各自截断至 max_output_bytes,超出标记 truncated 并继续 drain 防管道阻塞
残留清理 启动时清理 base 下 .tmp* 临时目录(tempfile 异常退出残留),不触及 workspace/ 持久目录
资源(进程) fork 后、exec 前 setrlimit 限制单文件写入 / 虚拟内存 / CPU / 子进程数 / 打开文件数(软=硬),与容器限额形成纵深;配额分层(全局天花板 → 凭证授予 → 请求收紧)见下文
资源(容器) Docker 侧 deploy.resources.limits 约束 memory / cpus / pids,/tmp 走 tmpfs 限大小
网络(容器) sandbox 仅接入 internal 专属网络 hunjing-sandbox-net,无公网出站,防数据外泄;后端经该网络以服务名访问
权限(容器) 只读根文件系统(仅 /tmp tmpfs 与 sandbox-work bind mount 可写)、no-new-privileges、cap_drop: ALL、Docker 默认 seccomp profile

HTTP API

GET /healthz

{ "status": "ok", "service": "hunjing-sandbox", "version": "0.1.0" }

GET /metrics

Prometheus 指标(text/plain; version=0.0.4),不鉴权,供抓取:

指标 类型 含义
sandbox_exec_total{outcome} counter 同步执行次数(completed/timeout/cancelled/failed)
sandbox_exec_duration_seconds histogram 同步执行耗时分布(含 _bucket/_sum/_count)
sandbox_concurrent gauge 当前并发执行数
sandbox_concurrent_limit gauge 并发上限(SANDBOX_MAX_CONCURRENT)
sandbox_stream_total{outcome} counter 流式执行次数
sandbox_auth_secret_used{slot} counter 验签命中 secret 分布(primary/previous),指示轮换进度
sandbox_auth_enabled gauge 鉴权是否开启(1/0);值为 0 时应告警(生产环境鉴权被关)

POST /v1/exec

请求体:

{
  "argv": ["python3", "-c", "print(1 + 1)"],
  "stdin": "可选标准输入",
  "timeout_ms": 5000,
  "workdir": "可选,base 内的相对子目录",
  "env": { "GREETING": "hi" },
  "max_output_bytes": 1048576,
  "trace_id": "可选,透传用"
}

响应体:

{
  "ok": true,
  "exit_code": 0,
  "stdout": "2\n",
  "stderr": "",
  "duration_ms": 42,
  "timeout": false,
  "cancelled": false,
  "truncated": false,
  "trace_id": "sbx_18f3a1...",
  "isolation_mode": "process"
}

支持两种互斥的执行形态:

  • 命令模式:argv(首个元素为可执行文件)。
  • 代码模式:code + language(默认 python,P0 仅支持 python)。沙箱转译为 argv(写临时脚本文件 + interpreter)后走执行路径;凭证 body_hash 按 SHA256(canonical JSON {language, code, stdin}) 绑定。

argv 与 code 同时或都不给出返回 400。响应体 isolation_mode 字段(process / container)回显实际生效的隔离模式:P0 期间凭证下发 container 一律降级 process 并 warn(L3-L4 真正走容器留待 P1)。

错误统一格式(见跨语言契约):

{ "error": { "code": "exec_timeout", "message": "执行超时" } }

超时不计入错误码:响应 HTTP 200 且 timeout: true(执行被 SIGKILL 中止,exit_code 为 null),调用方据 timeout 字段判断。

code HTTP 含义
bad_request 400 argv/code 互斥冲突、argv 为空、language 不支持等参数错误
sandbox_unauthorized 401 授权凭证缺失 / 过期 / 签名无效 / body_hash 不匹配
not_found 404 cancel 的 trace_id 不存在或已完成
service_unavailable 503 并发闸耗尽
exec_failed 500 spawn / 等待失败等

POST /v1/cancel

按 trace_id 取消运行中的任务(对进程组 SIGKILL)。请求体 { "trace_id": "..." },命中返回 { "cancelled": true, "trace_id": "..." };任务不存在或已完成返回 404 not_found。被取消的 /v1/exec 调用会以 cancelled: true、ok: false 返回。

安全权衡:cancel 仅中止任务、不执行新命令,危害有限,故凭证验签 + 过期即可,不绑定 trace_id(与 exec 的 body_hash 强绑定不同)。调用方需自行记录 trace_id——exec 未传 trace_id 时由沙箱生成,客户端无从获知,无法取消。

POST /v1/exec/stream

流式执行(SSE):请求体同 /v1/exec,响应为 text/event-stream,实时推送:

  • event: stdout / event: stderr:data 为 base64(urlsafe-no-pad) 编码的原始字节增量(保真,前端解码为 UTF-8)
  • event: done:data 为 JSON {exit_code, duration_ms, timeout, cancelled, truncated, trace_id},标志执行结束
  • 心跳:每 15s 一个注释行(KeepAlive),防代理空闲断开

后端 POST /tools/{name}/invoke/stream(sse-starlette)转发为 tool.stdout / tool.stderr / tool.completed / tool.requires_confirmation / tool.failed 事件;前端用 fetch+ReadableStream 消费(EventSource 不支持 POST body)。started/completed 落一炁事件存储,stdout/stderr 增量只透传不落库。

文件 IO(/v1/file/read · /write · /list · /stat)

不经 shell、二进制安全的 base 内文件读写(与 /v1/exec 的 shell 路径互补,适合需要精确字节控制或无 shell 语义的场景)。各端点独立校验 file_op_hash——凭证的 {op, path} 必须与请求一致,否则 401(防重放:read 凭证不能用于读写别的路径)。

端点 请求体 响应
POST /v1/file/read {path, offset?, limit?} {content_b64, size, offset, returned, truncated}(content_b64 为 base64url-no-pad,支持 offset/limit 分页)
POST /v1/file/write {path, content_b64} {bytes_written}(覆盖写)
POST /v1/file/list {path?}(默认 .) {entries:[{name, kind, size}]}(按名排序,kind 为 file/dir/other)
POST /v1/file/stat {path} {kind, size, modified_unix}
  • path 为 base 内相对路径;词法预检查 + canonicalize + starts_with(base) 三重校验拒绝路径逃逸(.. 不得使深度为负,且 create_dir_all 前先拒,防越界副作用);符号链接覆盖由 canonicalize 兜底。
  • 单次读写受 SANDBOX_MAX_FILE_BYTES(默认 1 MiB)约束,超出 read 标 truncated、write 直接 400。文件操作在主进程运行(非 exec 子进程),不受 RLIMIT_* 约束,故用此上限替代。
  • 后端 SandboxClient.file_read/write/list/stat 自动签发 file_op_hash 凭证并回写 tool.started/tool.completed/tool.failed 一炁事件(argv 记 [file:<op>, <path>] 供审计 input_summary)。
  • 无 delete 端点(设计上不开放删除,防误删;需清理时由运维直接操作 base 目录)。

配置(环境变量)

变量 默认 说明
SANDBOX_HOST 0.0.0.0 监听地址
SANDBOX_PORT 8090 监听端口
SANDBOX_BASE_WORKDIR ../sandbox-work 沙箱工作根目录(相对二进制所在目录)
SANDBOX_LOG_DIR ../logs tracing 文件日志目录(相对二进制所在目录,与后端对齐到项目根 logs/);容器内为 /app/logs
SANDBOX_MAX_TIMEOUT_MS 30000 单次执行全局超时上限(ms)
SANDBOX_MAX_CONCURRENT 8 最大并发执行数
SANDBOX_MAX_OUTPUT_BYTES 1048576 单流最大捕获字节数(1 MiB)
SANDBOX_MAX_FILE_BYTES 1048576 单次文件读写最大字节数(1 MiB);文件 IO 在主进程,不受 RLIMIT_* 约束,故用此上限
SANDBOX_ALLOWED_ENV PATH,HOME,TZ,LANG 默认注入子进程的环境变量白名单(逗号分隔)
SANDBOX_AUTH_ENABLED true 是否校验司命授权凭证;生产必须为 true
SANDBOX_ISOLATION_DEFAULT process 凭证未下发 isolation_mode 时的兜底隔离模式(process/container;P0 期间一律按 process)
SANDBOX_AUTH_SECRET — 授权 HMAC 共享密钥(与后端客户端一致),AUTH_ENABLED=true 时必填
SANDBOX_AUTH_TTL_SECONDS 30 授权凭证有效期(秒)
SANDBOX_AUTH_SECRET_PREVIOUS —(空) 旧 secret(逗号分隔),凭证轮换并存窗口;verify 时与主 secret 一并尝试
SANDBOX_RLIMIT_FSIZE_BYTES 67108864 单文件写入上限(64 MiB,普通工具默认)
SANDBOX_RLIMIT_AS_BYTES 536870912 虚拟内存上限(512 MiB;仅 Linux 生效——macOS 的 RLIMIT_AS 实为 RLIMIT_RSS 别名,不真正限制虚拟地址空间,且 server 自身 AS 常超此值会使 setrlimit 返回 EINVAL 导致 spawn 失败,故 macOS 跳过)
SANDBOX_RLIMIT_CPU_SECS max_timeout 秒 CPU 秒数上限(对齐全局超时)
SANDBOX_RLIMIT_NPROC 64 子进程数上限(防 fork bomb;root 运行时自动跳过)
SANDBOX_RLIMIT_NOFILE 256 打开文件数上限
SANDBOX_RLIMIT_*_MAX = 对应 *_BYTES/SECS 可选,分级场景的全局硬天花板;未设则不允许上探(默认部署等价单一限额)
SANDBOX_RLIMIT_DISABLE —(空) 显式禁用维度(逗号分隔,如 nproc)
RUST_LOG info tracing 日志级别;日志同时写 stdout(彩色)与 <SANDBOX_LOG_DIR>/sandbox.log(按日轮转,当天文件名 sandbox.log.YYYY-MM-DD)

后端连接沙箱用 .env 中的 RUST_SANDBOX_URL(例如 http://hunjing-sandbox:8090);本服务的自身配置用上面的 SANDBOX_*。

本地开发

cd sandbox
cargo run                       # 监听 0.0.0.0:8090
# 或指定配置
SANDBOX_PORT=8090 SANDBOX_BASE_WORKDIR=./tmp-work cargo run

# 手动测试
curl -s http://127.0.0.1:8090/healthz
curl -s -X POST http://127.0.0.1:8090/v1/exec \
  -H 'Content-Type: application/json' \
  -d '{"argv":["python3","-c","print(6*7)"]}'

# 验证超时清理
curl -s -X POST http://127.0.0.1:8090/v1/exec \
  -H 'Content-Type: application/json' \
  -d '{"argv":["sh","-c","sleep 30"],"timeout_ms":300}'

# 验证工作目录隔离与逃逸拒绝
curl -s -X POST http://127.0.0.1:8090/v1/exec \
  -H 'Content-Type: application/json' \
  -d '{"argv":["sh","-c","pwd"],"workdir":"../../etc"}'

cargo test                      # 执行内核单元测试(echo / 超时 / 截断 / 路径逃逸 / stdin+env)
cargo build --release           # 产出 release 二进制

Docker

镜像构建见 docker/sandbox/Dockerfile,多阶段(rust 构建 → debian-slim 运行),监听 8090。通过 docker-compose.app.yml 的 --profile sandbox 启用。

容器级隔离:sandbox 仅接入 internal 网络 hunjing-sandbox-net(无公网出站,后端经该网络以服务名 hunjing-sandbox 访问),并受 deploy.resources.limits(memory / cpus / pids)与 /tmp tmpfs 约束。网络拓扑与资源定义见 docker/docker-compose.app.yml。容器日志既可经 docker compose logs hunjing-sandbox(stdout)查看,也持久化到挂载的项目根 logs/sandbox.log.YYYY-MM-DD(与后端 app.log 同目录,便于统一收集)。

docker compose -f docker/docker-compose.infra.yml up -d   # 先建网络(如未建)
docker compose -f docker/docker-compose.app.yml --profile sandbox up -d --build hunjing-sandbox
docker compose -f docker/docker-compose.app.yml logs -f hunjing-sandbox

后续加固(按需,非当前阶段)

  • 自定义 seccomp 白名单(当前用 Docker 默认 profile,已阻止约 50 个危险系统调用;自定义白名单维护成本高且易误伤正常命令)。
  • workspace 容量配额与按 run 的自动 GC(当前已实现按 run 隔离 + 启动清理 .tmp* 残留)。
  • 更强隔离形态:nsjail / gVisor / firecracker microVM(远期)。

IoT / 边缘设备部署

乾坤沙箱可在 IoT 硬件上以进程模式运行(无需 Docker),单静态二进制即可部署。

交叉编译

# ARM64 静态编译(零运行时依赖)
rustup target add aarch64-unknown-linux-musl
cargo build --release --target aarch64-unknown-linux-musl

# 产物大小约 5-8 MB
ls -lh target/aarch64-unknown-linux-musl/release/hunjing-sandbox

# 部署
scp target/aarch64-unknown-linux-musl/release/hunjing-sandbox \
    root@device:/usr/local/bin/

资源占用

模式 RSS 磁盘
进程模式闲置 ~12 MB ~5 MB
进程模式执行中 ~25 MB -
容器模式(需 Docker) ~200 MB+ 镜像额外 ~100 MB

启动(进程模式)

ssh root@device \
  'SANDBOX_DOCKER_ENABLED=false \
   SANDBOX_PORT=8090 \
   RUST_LOG=info \
   /usr/local/bin/hunjing-sandbox'

systemd 托管

[Unit]
Description=hunjing-sandbox (乾坤安全沙箱)
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/hunjing-sandbox
Environment=SANDBOX_DOCKER_ENABLED=false
Environment=SANDBOX_PORT=8090
Environment=SANDBOX_BASE_WORKDIR=/var/lib/hunjing-sandbox
Environment=SANDBOX_LOG_DIR=/var/log/hunjing-sandbox
Environment=RUST_LOG=info
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

硬件兼容

硬件 架构 进程模式 容器模式
Raspberry Pi 4/5 ARM64 ✅ ✅(需 4GB+)
Jetson Orin ARM64 ✅ ✅
RK3588 / 全志 H6 ARM64 ✅ ⚠️(需 4GB+)
x86 工控机 / NUC x86-64 ✅ ✅
OpenWRT 路由器 MIPS/ARM ⚠️(需确认内核支持) ❌
ESP32 / MCU - ❌ ❌