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, 可视化界面, 沙箱, 网络流量审计, 进程隔离, 逆向工具, 通知系统