multikernel/sandlock

GitHub: multikernel/sandlock

一款基于 Rust 开发的轻量级 Linux 进程沙箱,利用 Landlock 和 seccomp 无需 root 权限即可隔离不受信任的代码。

Stars: 215 | Forks: 25

# Sandlock 适用于 Linux 的轻量级进程沙箱。通过以下机制限制不受信任的代码: **Landlock**(文件系统 + 网络 + IPC)、**seccomp-bpf**(syscall 过滤) 以及 **seccomp user notification**(资源限制、IP 强制执行、/proc 虚拟化)。无需 root、无需 cgroups、无需容器。 ``` sandlock run -w /tmp -r /usr -r /lib -m 512M -- python3 untrusted.py ``` ## 为什么选择 Sandlock? 容器和虚拟机功能强大但过于沉重。Sandlock 旨在填补这一空白:无需镜像构建或 root 权限即可实现严格隔离。内置 COW 文件系统可自动保护您的工作目录。 | 功能 | Sandlock | 容器 | MicroVM (Firecracker) | |---|---|---|---| | 需要 Root | 否 | 是* | 是 (KVM) | | 镜像构建 | 否 | 是 | 是 | | 启动时间 | ~5 ms | ~200 ms | ~100 ms | | 内核 | 共享 | 共享 | 独立客户机 | | 文件系统隔离 | Landlock + seccomp COW | Overlay | 块级别 | | 网络隔离 | Landlock + seccomp notif | Network namespace | TAP device | | HTTP 级别 ACL | Method + host + path 规则 | 不适用 | 不适用 | | Syscall 过滤 | seccomp-bpf | seccomp | 不适用 | | 资源限制 | seccomp notif + SIGSTOP | cgroup v2 | 虚拟机配置 | \* 虽然存在 rootless 容器,但需要 user namespace 支持以及配置 `/etc/subuid`。 ## 架构 Sandlock 使用 **Rust** 实现,以确保性能与安全性: - **sandlock-core** — Rust 库:Landlock、seccomp、supervisor、COW、pipeline - **sandlock-cli** — Rust CLI 二进制文件 (`sandlock run ...`) - **sandlock-ffi** — C ABI 共享库 (`libsandlock_ffi.so`) - **Python SDK** — 指向 FFI 库的 ctypes 绑定 ``` ┌─────────────┐ │ Python SDK │ ctypes FFI │ (sandlock) │──────────────┐ └─────────────┘ │ ▼ ┌──────────────┐ ┌──────────────────────────────┐ │ sandlock CLI │───>│ libsandlock_ffi.so │ └──────────────┘ └──────────────┬───────────────┘ │ ┌──────────────▼───────────────┐ │ sandlock-core │ │ Landlock · seccomp · COW · │ │ pipeline · policy_fn · vDSO │ └──────────────────────────────┘ ``` ## 环境要求 - **Linux 6.12+**(Landlock ABI v6),**Rust 1.70+**(用于构建) - **Python 3.8+**(可选,用于 Python SDK) - 无需 root,无需 cgroups | 功能 | 最低内核版本 | |---|---| | seccomp user notification | 5.6 | | Landlock 文件系统规则 | 5.13 | | Landlock TCP 端口规则 | 6.7 (ABI v4) | | Landlock IPC 作用域 | 6.12 (ABI v6) | 如有需要,保护机制可以根据策略选择性放弃 —— 参见 [`docs/sandbox-reference.md#protection-opt-out`](docs/sandbox-reference.md#protection-opt-out)。 ## 安装说明 ### 从源码构建 ``` # 构建 Rust 二进制文件和共享库 cargo build --release # 安装 Python SDK(自动构建 Rust FFI 库) cd python && pip install -e . ``` ### 仅安装 CLI ``` cargo install --path crates/sandlock-cli ``` ## 快速开始 ### CLI ``` # 基本限制 sandlock run -r /usr -r /lib -w /tmp -- ls /tmp # 交互式 shell sandlock run -i -r /usr -r /lib -r /lib64 -r /bin -r /etc -w /tmp -- /bin/sh # 资源限制 + 超时 sandlock run -m 512M -P 20 -t 30 -- ./compute.sh # 出站 allowlist — 限制为单个主机上的单个端口 sandlock run --net-allow api.openai.com:443 -r /usr -r /lib -r /etc -- python3 agent.py # 单个主机的多个端口,加上一个单独的任意 IP 端口 sandlock run --net-allow github.com:22,443 --net-allow :8080 \ -r /usr -r /lib -r /etc -- python3 agent.py # 通配符端口(可选):一个纯粹的主机(或 `host:*`)允许所有端口 sandlock run --net-allow github.com -r /usr -r /lib -r /etc -- ssh user@github.com # 以 IP、CIDR 范围或 IPv6 字面量作为目标(通过包含关系匹配, # 无需 DNS);语法与 --net-deny 相同 sandlock run --net-allow 10.0.0.0/8:443 --net-allow '[2606:4700::/32]:443' \ -r /usr -r /lib -r /etc -- python3 agent.py # 不受限制的出站:`*` 开启任何主机和任何 TCP 端口(`:*` / `*:*` # 是等价的)。要实现完整的出站访问,请添加 UDP 通配符 `udp://*`。 sandlock run --net-allow '*' --net-allow 'udp://*' \ -r /usr -r /lib -r /etc -- ./client # UDP — scheme 前缀控制协议并限定目标范围 # (例如:DNS 到 1.1.1.1,加上 TCP HTTPS 到任何地方) sandlock run --net-allow udp://1.1.1.1:53 --net-allow :443 \ -r /usr -r /lib -r /etc -- ./client # Ping — 由 net.ipv4.ping_group_range 控制的内核 ping socket (SOCK_DGRAM) sandlock run --net-allow icmp://github.com -r /usr -r /lib -r /etc -- ping github.com # Denylist:默认允许网络,阻止特定的 IP/CIDR/端口 # (--net-allow 的反向操作;与其互斥)。端口为可选。 sandlock run --net-deny 169.254.169.254 --net-deny 10.0.0.0/8 \ -r /usr -r /lib -r /etc -- python3 agent.py # HTTP 级别 ACL(通过透明代理实现 method + host + path 规则) # 带有具体主机的 HTTP 规则会自动将 host:80,443 扩展至 --net-allow sandlock run \ --http-allow "GET docs.python.org/*" \ --http-allow "POST api.openai.com/v1/chat/completions" \ --http-deny "* */admin/*" \ -r /usr -r /lib -r /etc -- python3 agent.py # HTTPS MITM,零配置:sandlock 会生成一个临时 CA 并将其注入 # 到您指定的 trust bundle 中。无需 openssl,无需手动安装。 sandlock run \ --http-allow "POST api.openai.com/v1/*" \ --http-inject-ca /etc/ssl/certs/ca-certificates.crt \ -r /usr -r /lib -r /etc -- python3 agent.py # 对于 Node 和其他内置 CA 列表的运行时:导出证书并 # 设置运行时自身的环境变量。 sandlock run \ --http-allow "POST api.example.com/*" \ --http-inject-ca /etc/ssl/certs/ca-certificates.crt \ --http-ca-out /tmp/sandlock-ca.pem \ --env NODE_EXTRA_CA_CERTS=/tmp/sandlock-ca.pem \ -r /usr -r /lib -r /etc -- node agent.js # 使用您自己的 CA 进行 HTTPS MITM(仍然支持) sandlock run \ --http-allow "POST api.openai.com/v1/*" \ --http-ca ca.pem --http-key ca-key.pem \ -r /usr -r /lib -r /etc -- python3 agent.py # 监听端口的服务器(Landlock --net-allow-bind,与 --net-allow 分开; # 接受逗号分隔的端口和 lo-hi 范围,可重复) sandlock run --net-allow-bind 8080,9000-9005 -r /usr -r /lib -r /etc -- python3 server.py # 干净的环境 sandlock run --clean-env --env CC=gcc \ -r /usr -r /lib -w /tmp -- make # 确定性执行(冻结时间 + 设定种子的随机性) sandlock run --time-start "2000-01-01T00:00:00Z" --random-seed 42 -- ./build.sh # 端口虚拟化(多个 sandbox 可以绑定同一个端口) sandlock run --port-remap --net-allow-bind 6379 -r /usr -r /lib -r /etc -- redis-server --port 6379 # 带有命名 sandbox 的端口虚拟化(启用网络发现) sandlock run --name api.local --port-remap --net-allow-bind 8080 -r /usr -r /lib -r /etc -- python3 server.py sandlock run --name web.local --port-remap --net-allow-bind 8080 -r /usr -r /lib -r /etc -- python3 server.py # 列出所有正在运行的 sandbox sandlock list # 通过名称终止正在运行的 sandbox sandlock kill web.local # 带有每个 sandbox 独立挂载的 Chroot(无需内核 bind mount) sandlock run --chroot ./rootfs --fs-mount /work:/tmp/sandbox/work -- /bin/sh # COW 文件系统(捕获写入,成功时提交) sandlock run --workdir /opt/project -r /usr -r /lib -- python3 task.py # Dry-run(查看哪些文件会更改,然后丢弃) sandlock run --dry-run --workdir . -w . -r /usr -r /lib -r /bin -r /etc -- make build # 使用已保存的 profile sandlock run -p build -- make -j4 # 无 supervisor 模式(Landlock + deny-only seccomp,无 supervisor 进程) sandlock run --no-supervisor -r /usr -r /lib -r /lib64 -r /bin -w /tmp -- ./script.sh # 嵌套沙盒化:限制 sandlock 自身的 supervisor sandlock run --no-supervisor -r /proc -r /usr -r /lib -r /lib64 -r /bin -r /etc -w /tmp -- \ sandlock run -r /usr -w /tmp -- untrusted-command ``` ### Python API ``` from sandlock import Sandbox, confine sandbox = Sandbox( fs_writable=["/tmp/sandbox"], fs_readable=["/usr", "/lib", "/etc"], max_memory="256M", max_processes=10, clean_env=True, ) # 运行命令(可选以秒为单位的超时时间) result = sandbox.run(["python3", "-c", "print('hello')"], timeout=30) assert result.success assert b"hello" in result.stdout # HTTP ACL:仅允许特定的 API 调用 agent = Sandbox( fs_readable=["/usr", "/lib", "/etc"], http_allow=["POST api.openai.com/v1/chat/completions"], http_deny=["* */admin/*"], ) result = agent.run(["python3", "agent.py"]) # 带有每个 sandbox 独立挂载的 Chroot(Docker 风格的 -v,无需 root) chrooted = Sandbox( chroot="/opt/rootfs", fs_mount={"/work": "/tmp/sandbox-1/work"}, # maps /work inside chroot fs_readable=["/usr", "/bin", "/lib", "/etc"], cwd="/work", ) result = chrooted.run(["python3", "task.py"]) # 端口虚拟化:在 sandbox 运行时查询端口映射 sb = Sandbox(port_remap=True, fs_readable=["/usr", "/lib", "/etc"], name="api.local") # 运行时 sb.ports() 返回 {virtual_port: real_port} # 限制当前进程(仅限 Landlock 文件系统,不可逆) confine(Sandbox(fs_readable=["/usr", "/lib"], fs_writable=["/tmp"])) # Dry-run:查看哪些文件会更改,然后丢弃 sandbox = Sandbox(fs_writable=["."], workdir=".", fs_readable=["/usr", "/lib", "/bin", "/etc"]) result = sandbox.dry_run(["make", "build"]) for c in result.changes: print(f"{c.kind} {c.path}") # A=added, M=modified, D=deleted ``` ### Pipeline 使用 `|` 操作符链式组合沙箱化阶段 —— 每个阶段都有各自独立的 沙箱配置。数据通过内核管道流动。 ``` from sandlock import Sandbox trusted = Sandbox(fs_readable=["/usr", "/lib", "/bin", "/etc", "/opt/data"]) restricted = Sandbox(fs_readable=["/usr", "/lib", "/bin", "/etc"]) # Reader 可以访问数据,processor 不能 result = ( trusted.cmd(["cat", "/opt/data/secret.csv"]) | restricted.cmd(["tr", "a-z", "A-Z"]) ).run() assert b"SECRET" in result.stdout ``` **XOA 模式** (eXecute Only Agents):规划器生成代码, 执行器在具有数据访问权限但无网络访问权限的环境下运行它: ``` planner = Sandbox(fs_readable=["/usr", "/lib", "/bin", "/etc"]) executor = Sandbox(fs_readable=["/usr", "/lib", "/bin", "/etc", "/data"]) result = ( planner.cmd(["python3", "-c", "print('cat /data/input.txt')"]) | executor.cmd(["sh"]) ).run() ``` ### 动态策略 (policy_fn) 在运行时检查 syscall 事件并动态调整权限。 事件包含 syscall 名称、类别、PID、网络目标(用于 `connect`/`sendto`/`bind`)以及 `argv`(用于 `execve`)。回调函数 返回裁决结果,允许、拒绝或审计。 ``` from sandlock import Sandbox import errno def on_event(event, ctx): # Block download tools by argv if event.syscall == "execve" and event.argv_contains("curl"): return True # deny # Deny connections to a specific IP if event.syscall == "connect" and event.host == "10.0.0.5": return errno.EACCES # Lock down once the program has finished starting up if event.syscall == "execve": ctx.restrict_network([]) # block all network ctx.deny_path("/etc/shadow") # dynamic fs deny # Audit every file access (allow but flag) if event.category == "file": return "audit" return 0 # allow sandbox = Sandbox( fs_readable=["/usr", "/lib", "/etc"], net_allow=["api.example.com:443"], policy_fn=on_event, ) result = sandbox.run(["python3", "agent.py"]) ``` **裁决 (Verdicts):** `0`/`False` = 允许,`True`/`-1` = 拒绝 (EPERM), 正整数 = 拒绝并返回 errno,`"audit"`/`-2` = 允许 + 标记。 **事件字段:** `syscall`、`category` (file/network/process/memory)、 `pid`、`parent_pid`、`host`、`port`、`argv`、`denied`。 **上下文方法:** - `ctx.restrict_network(ips)` / `ctx.grant_network(ips)` — 网络控制 - `ctx.restrict_max_memory(bytes)` / `ctx.restrict_max_processes(n)` — 资源限制 - `ctx.deny_path(path)` / `ctx.allow_path(path)` — 动态文件系统限制 - `ctx.restrict_pid_network(pid, ips)` — 基于 PID 的网络覆盖 **被挂起的 syscalls**(子进程阻塞直到回调返回):`execve`、 `connect`、`sendto`、`bind`、`openat`。 ### Rust API ``` use sandlock_core::{confine, Confinement, Sandbox, Stage}; use sandlock_core::sandbox::ByteSize; use sandlock_core::policy_fn::Verdict; // Basic run let mut sandbox = Sandbox::builder() .fs_read("/usr").fs_read("/lib") .fs_write("/tmp") .max_memory(ByteSize::mib(256)) .name("hello-box") .build()?; let result = sandbox.run(&["echo", "hello"]).await?; assert!(result.success()); // HTTP ACL: restrict API access at the HTTP level let mut agent = Sandbox::builder() .fs_read("/usr").fs_read("/lib").fs_read("/etc") .http_allow("POST api.openai.com/v1/chat/completions") .http_deny("* */admin/*") .name("agent-box") .build()?; let result = agent.run(&["python3", "agent.py"]).await?; // Confine the current process (Landlock filesystem only, irreversible) let confinement = Confinement::builder() .fs_read("/usr").fs_read("/lib") .fs_write("/tmp") .build(); confine(&confinement)?; // Pipeline let producer = Sandbox::builder() .fs_read("/usr").fs_read("/lib").fs_read("/bin") .build()?; let consumer = producer.clone(); let result = ( Stage::new(&producer, &["echo", "hello"]) | Stage::new(&consumer, &["tr", "a-z", "A-Z"]) ).run(None).await?; // Dynamic policy let mut dynamic = Sandbox::builder() .fs_read("/usr").fs_read("/lib") .policy_fn(|event, ctx| { if event.argv_contains("curl") { return Verdict::Deny; } if event.syscall == "execve" { ctx.restrict_network(&[]); ctx.deny_path("/etc/shadow"); } Verdict::Allow }) .build()?; let result = dynamic.run(&["python3", "agent.py"]).await?; ``` ## 配置文件 将可复用的沙箱配置文件作为 TOML 文件保存在 `~/.config/sandlock/profiles/` 中。配置文件采用分段的 schema;顶层的 扁平键(例如 `fs_readable = [...]`)将被拒绝。当您需要一个稳定的 虚拟主机名时,请使用 `--name` 传入沙箱实例名称。 ``` # ~/.config/sandlock/profiles/build.toml [program] exec = "make" args = ["-j4"] clean_env = true env = { CC = "gcc", LANG = "C.UTF-8" } [filesystem] read = ["/usr", "/lib", "/lib64", "/bin", "/etc"] write = ["/tmp/work"] [limits] memory = "512M" processes = 50 [syscalls] extra_deny = [] ``` ``` sandlock profile list sandlock profile show build sandlock run -p build # uses [program].exec + args sandlock run -p build -- make test # trailing command overrides [program] ``` ## 工作原理 Sandlock 在 `fork()` 之后按顺序应用限制机制: ``` Parent Child │ fork() │ │──────────────────────────────────>│ │ ├─ 1. setpgid(0,0) │ ├─ 2. Optional: chdir(cwd) │ ├─ 3. NO_NEW_PRIVS │ ├─ 4. Landlock (fs + net + IPC) │ ├─ 5. seccomp filter (deny + notif) │ │ └─ send notif fd ──> Parent │ receive notif fd ├─ 6. Wait for "ready" signal │ start supervisor (tokio) ├─ 7. Close fds 3+ │ optional: vDSO patching └─ 8. exec(cmd) │ optional: policy_fn thread │ optional: CPU throttle task ``` ### Seccomp Supervisor 异步通知 supervisor (tokio) 负责处理被拦截的 syscalls: | Syscall | 处理程序 | |---|---| | `clone/fork/vfork` | 进程计数强制执行 | | `mmap/munmap/brk/mremap` | 内存限制跟踪 | | `connect/sendto/sendmsg` | IP 允许列表 + 代理执行 + HTTP ACL 重定向 | | `bind` | 代理绑定 + 端口重映射 | | `openat` | /proc 虚拟化,COW 拦截 | | `unlinkat/mkdirat/renameat2` | COW 写入拦截 | | `execve/execveat` | policy_fn 挂起 + vDSO 重新打补丁 | | `getrandom` | 确定性 PRNG 注入 | | `clock_nanosleep/timer_settime` | 针对冻结时间的定时器调整 | | `getdents64` | PID 过滤,COW 目录合并 | | `getsockname` | 端口重映射转换 | ### 自定义处理器 下游 Rust crate 可以将其自己的 seccomp-notification 处理器附加到 supervisor 链中,与内置处理器并列,并通过 `Handler` trait 和 `Sandbox::run_with_handlers` 注册它们关心的任何 syscall。内置链会 首先运行,因此用户处理器无法破坏隔离机制;注册步骤也会 拒绝针对默认黑名单或 `extra_deny_syscalls` 中 syscalls 的处理器。有关完整的 API、顺序语义和状态模式,请参见 [`docs/extension-handlers.md`](docs/extension-handlers.md)。 ### COW 文件系统 通过 seccomp notification 实现写时复制 (Copy-on-write) 文件系统隔离:当设置了 `workdir` 时,sandlock 会拦截文件系统 syscalls 并将写入暂存到 一个 upper 目录中;读取操作则按从上到下的顺序解析。无需 mount namespace,无需 user namespace,无需 root。正常退出时提交,发生 错误时中止。 **试运行模式 (Dry-run mode)**:`--dry-run` 会运行命令,检查 COW 层 的更改(添加/修改/删除的文件),打印摘要,然后 中止 —— 保持工作目录完全不受影响。这有助于在提交前预览 命令将要执行的操作。 ### COW Fork & Map-Reduce 只需初始化一次高昂的状态,然后 fork 共享内存的 COW 克隆。 每个克隆使用带有共享写时复制页面的原始 `fork(2)`。在约 530ms 内 完成 1000 个克隆,速度约为 ~1,900 forks/sec。 每个克隆的标准输出通过其各自的管道捕获。`reduce()` 读取所有 管道,并将合并后的输出提供给 reducer 的标准输入 —— 完全基于管道的 数据流,无需临时文件。 ``` from sandlock import Sandbox def init(): global model, data model = load_model() # 2 GB, loaded once data = preprocess_dataset() def work(clone_id): shard = data[clone_id::4] print(sum(shard)) # stdout → per-clone pipe # Map:fork 4 个带有独立 sandbox 配置的克隆 mapper = Sandbox( fs_readable=["/usr", "/lib", "/bin", "/etc", "/data"], init_fn=init, work_fn=work, ) clones = mapper.fork(4) # Reduce:通过管道将克隆输出传递到 reducer stdin reducer = Sandbox(fs_readable=["/usr", "/lib", "/bin", "/etc"]) result = reducer.reduce( ["python3", "-c", "import sys; print(sum(int(l) for l in sys.stdin))"], clones, ) print(result.stdout) # b"total\n" ``` ``` let mut mapper = Sandbox::builder() .fs_read("/usr").fs_read("/lib").fs_read("/bin").fs_read("/etc") .fs_read("/data") .name("mapper") .init_fn(|| { load_data(); }) .work_fn(|id| { println!("{}", compute(id)); }) .build()?; let mut clones = mapper.fork(4).await?; let reducer = Sandbox::builder() .fs_read("/usr").fs_read("/lib").fs_read("/bin").fs_read("/etc") .name("reducer") .build()?; let result = reducer.reduce( &["python3", "-c", "import sys; print(sum(int(l) for l in sys.stdin))"], &mut clones, ).await?; ``` Map 和 reduce 在具有独立配置的独立沙箱中运行 —— mapper 具有数据访问权限,而 reducer 没有。每个克隆都继承 Landlock + seccomp 限制。`CLONE_ID=0..N-1` 将被自动设置。 ### 网络模型 出站流量受由 **协议 × 目标** 组成的端点列表控制。 `--net-allow`(允许列表)和 `--net-deny`(拒绝列表)共享一套 语法,并且是互斥的: ``` repeatable; the port is optional (a bare target = all ports) target host | | | * (`*` or empty target = any IP) forms target[:port[,port,...]] · :port · host:* · :* · *:* []:port (bracket IPv6 when a port follows) scheme tcp:// (default) · udp:// (`udp://*` = any UDP) · icmp:// (no port) --net-allow target may also be a hostname, resolved via DNS at start --net-deny target must be a literal IP/CIDR (no hostnames; use --http-deny) ``` 逗号用于在单个规范内对端口进行分组(`host:80,443`);如果要传递多个 规则,请重复使用该标志。IP 和 CIDR 目标通过包含关系进行匹配, 无需 DNS(字面 IP 被视为 `/32` 或 `/128`);只有主机名会被解析。 多个规则之间是“或”的关系。只有当某个规则匹配与 socket 相同的 **协议** 以及目标 IP 和端口时(ICMP 的端口不适用),目标才被允许。 **协议管控**源于各协议规则的存在情况: * 没有 UDP 规则 → UDP socket 的创建将在 seccomp 层被拒绝。 * 没有 ICMP 规则 → 内核 ping socket 的创建 (SOCK_DGRAM + IPPROTO_ICMP) 将在 seccomp 层被拒绝。 * Raw ICMP (SOCK_RAW + IPPROTO_ICMP) **永远不会被暴露** — 数据包 构造不在讨论范围内。需要 ping 的工作负载应依赖 宿主机的 `net.ipv4.ping_group_range` 并使用上述的 dgram 路径 (`--net-allow icmp://...`)。 * TCP 在 syscall 级别始终被允许;其目标受 Landlock 和/或代理路径控制。 **默认情况。** 如果没有 `--net-allow` 和任何 HTTP ACL 标志,Landlock 将拒绝所有的 TCP `connect()`,UDP / ICMP / raw socket 的创建 将在 seccomp 层被拒绝,并且不会激活任何代理路径。 如需不受限制的 TCP 出站,请使用 `--net-allow '*'` 显式开启;对于任何 UDP 流量,请添加 `--net-allow 'udp://*'`。 **拒绝列表 (`--net-deny`)。** 允许列表的反向操作:网络默认为 允许,列出的目标将被阻止。它使用与上述 `--net-allow` 相同的 语法,唯一的区别在于其目标 必须是字面 IP/CIDR(主机名将被拒绝;针对域名请使用 `--http-deny`)。 示例: ``` --net-deny 10.0.0.0/8 # all ports on a CIDR (all protocols) --net-deny 169.254.169.254:80 # one IP, one port --net-deny 169.254.169.254:80,443 # comma-separated ports in one rule --net-deny '*' # any IP, all ports (TCP) --net-deny 'udp://192.168.0.0/16' # any UDP to a CIDR ``` **解析。** 只有主机名目标会触及 DNS:它们在沙箱启动时 被解析一次,并固定在一个合成的 `/etc/hosts` 中(适用于所有 协议)。IP 和 CIDR 目标直接通过包含关系匹配,因此 它们从不被解析,也从不出现在 `/etc/hosts` 中。只有当至少有一个 规则具有具体的主机名时,该合成文件 才会替换真实的文件;纯粹由 IP/CIDR、`:port`、`udp://*` 或 `icmp://*` 组成的规则会保留真实的 `/etc/hosts` 和可见的 DNS。 **通配符。** 主机名按字面意义匹配 — `--net-allow *.example.com:443` 是 **不被** 支持的,请列出您需要的每个域名(或者 针对地址范围使用 CIDR/IP 目标)。`*` 标记可以作为 目标(空的别名:`*:port` ≡ `:port`),也可以作为 TCP/UDP 规则的端口(`host:*`、`:*`、`*:*`)。 端口是可选的:省略端口意味着所有端口,因此 `host` ≡ `host:*` 且 `*` ≡ `:*` ≡ `*:*`(并且 `udp://*` ≡ `udp://*:*`)。将 `*` 与具体端口混合使用(`host:80,*`)会被拒绝。当任何 TCP 规则 使用全端口通配符时,Landlock 将不再 在内核级别过滤 TCP connect(如果不枚举 65535 条规则,它无法表达 “每一个端口”);代理路径 成为唯一的执行者,并且对于 `:*`,它会直接短路至 允许全部。 **实现细节。** 两条执行路径: * **直接路径** — 纯 `:port` TCP 策略(任意 IP,无具体 host/IP/CIDR)且无 HTTP ACL。Landlock 在内核 级别执行 TCP 端口允许列表;没有每次 syscall 的开销。UDP 和 ICMP 不被 Landlock 覆盖,在允许时 始终使用代理路径。 * **代理路径** — 任何主机、IP 或 CIDR标,任何 HTTP ACL 规则,或任何 UDP / ICMP 规则(必须检查 目标 IP,而 Landlock 无法做到)。Seccomp 会捕获 `connect()`、`sendto()`、 `sendmsg()` 和 `sendmmsg()`;supervisor 会复制子进程的 fd,查询 `getsockopt(SOL_SOCKET, SO_PROTOCOL)` 以确认该 socket 是 TCP / UDP / ICMP,然后在执行 syscall 之前,针对该 协议已解析的允许列表检查目标。 HTTP/HTTPS 代理重定向(如果已配置)也会在这里发生。 **HTTP / HTTPS 拦截。** `--http-allow` / `--http-deny` 会将匹配的端口 通过透明代理进行路由。每一个带有具体 主机的规则都会使用 `host:80`(如果设置了 `--http-ca`,则还包括 `host:443`)自动扩展 `--net-allow`,以便代理的拦截端口 可达;通配符主机 会自动添加 `:80` / `:443`(任意 IP)。所有自动添加的 条目都是 TCP。HTTPS MITM 通过两种方式启用:传递 `--http-ca ` 和 `--http-key ` 以使用您自带的 CA,或者传递 `--http-inject-ca ` 让 sandlock 生成一个临时 CA(私钥仅 保留在内存中),并在 open 时将其公共证书拼接到每个指定的信任束中, 这样工作负载就会信任代理,而无需手动安装。对于具有编译内置 CA 存储 的运行时(如 Node),`--http-ca-out ` 会写入公共证书,以便您可以将运行时自身的 env var 指向它(例如 `NODE_EXTRA_CA_CERTS`)。如果不使用这些选项,端口 443 将不会被拦截:`--net-allow host:443` 允许原始 TLS 连接到 主机,而不进行内容检查。 **绑定。** `--net-allow-bind ` 独立于 `--net-allow`,并且 作为默认拒绝的允许列表控制服务器端的 `bind()`。每个值都是 一个以逗号分隔的列表,包含单个端口或包含 `lo-hi` 的范围(例如 `--net-allow-bind 8080,9000-9005`),并且该标志可以重复。Landlock 执行此操作(仅限 TCP);`--port-remap` 为绑定 添加代理虚拟化。 `--net-deny-bind ` 是反向操作:默认允许绑定,拒绝 列出的 TCP 端口(相同的端口语法,与 `--net-allow-bind` 互斥)。由于 Landlock 仅支持允许列表,deny-bind 会放宽 Landlock `BIND_TCP` 钩子,并转而在代理 seccomp `bind()` 路径上执行拒绝列表。 **AF_UNIX sockets** 受 Landlock 的 `LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET` 控制,独立于 `--net-allow`。 ### 端口虚拟化 每个沙箱都会获得一个完整的虚拟端口空间。多个沙箱可以绑定 同一个端口而不会发生冲突。Supervisor 会通过 `pidfd_getfd`(TOCTOU 安全)代表 子进程执行 `bind()`。当端口发生冲突时,会透明地 分配另一个真实的端口。`/proc/net/tcp` 会被过滤, 只显示沙箱自身的端口。 当启用 `--port-remap` 时,沙箱会将其状态注册到 共享注册表 (`/dev/shm`) 中。使用 `sandlock list` 查看所有正在运行的 沙箱,并使用 `sandlock kill` 停止它们: ``` $ sandlock list NAME PID PORTS api.local 12345 8080 web.local 12346 8080 -> 35299 $ sandlock kill web.local Killed sandbox 'web.local' (PID 12346) ``` 这使得外部反向代理(nginx、envoy)能够按名称将流量 路由到正确的真实端口。 ## 性能 在典型的 Linux 工作站上进行基准测试: | 工作负载 | 裸金属 | Sandlock | Docker | Sandlock 开销 | |---|---|---|---|---| | `/bin/echo` 启动 | 2 ms | 7 ms | 307 ms | 5 ms(比 Docker 快 44 倍) | | Redis SET (100K ops) | 82K rps | 80K rps | 52K rps | 裸金属的 97.1% | | Redis GET (100K ops) | 79K rps | 77K rps | 53K rps | 裸金属的 97.1% | | Redis p99 延迟 | 0.5 ms | 0.6 ms | 1.5 ms | 比 Docker 低 ~2.5 倍 | | COW fork ×1000 | — | 530 ms | — | 530μs/fork,~1,900 forks/sec | ## 测试 ``` # Rust tests cargo test --release # Python tests cd python && pip install -e . && pytest tests/ ``` ## 沙箱参考 完整的 `Sandbox` 配置参考 —— 包含每个字段、默认值 和分组 —— 位于 [`docs/sandbox-reference.md`](docs/sandbox-reference.md) 中。
标签:AI安全, Chat Copilot, Landlock, Rust, Seccomp, 可视化界面, 沙箱, 网络流量审计, 进程隔离, 逆向工具, 通知系统