一个轻量级沙箱工具,用于在操作系统层面强制执行任意进程的文件系统和网络限制,而无需使用容器。
# Anthropic Sandbox Runtime (srt)
一款轻量级沙箱工具,可在操作系统层面为任意进程强制执行文件系统和网络限制,无需依赖容器。
`srt` 使用操作系统原生的沙箱原语(macOS 上的 `sandbox-exec`、Linux 上的 `bubblewrap`)以及基于代理的网络过滤。它可用于对智能体、本地 MCP 服务器、bash 命令和任意进程的行为进行沙箱隔离。
> **Beta 研究预览版**
>
> Sandbox Runtime 是为 [Claude Code](https://www.claude.com/product/claude-code) 开发的研究预览版,旨在实现更安全的 AI 智能体。它作为早期开源预览版发布,以帮助更广泛的生态系统构建更安全的智能体系统。由于这是早期研究预览版,API 和配置格式可能会发生变化。我们欢迎反馈和贡献,让 AI 智能体默认更安全!
## 安装
```bash
npm install -g @anthropic-ai/sandbox-runtime
```
## 基本用法
```bash
# Network restrictions
$ srt "curl anthropic.com"
Running: curl anthropic.com
<html>...</html> # Request succeeds
$ srt "curl example.com"
Running: curl example.com
Connection blocked by network allowlist # Request blocked
# Filesystem restrictions
$ srt "cat README.md"
Running: cat README.md
# Anthropic Sandb... # Current directory access allowed
$ srt "cat ~/.ssh/id_rsa"
Running: cat ~/.ssh/id_rsa
cat: /Users/ollie/.ssh/id_rsa: Operation not permitted # Specific file blocked
```
## 概述
此包提供了一个独立的沙箱实现,既可作为 CLI 工具使用,也可作为库使用。它遵循**默认安全**的设计理念,针对常见的开发者使用场景量身定制:进程以最小权限启动,你只需显式地打开所需的权限缺口。
**主要能力:**
- **网络限制**:控制可通过 HTTP/HTTPS 及其他协议访问的主机/域名
- **文件系统限制**:控制可读取/写入的文件/目录
- **Unix 套接字限制**:控制对本地 IPC 套接字的访问
- **违规监控**:在 macOS 上,接入系统的沙箱违规日志存储以获取实时警报
### 示例用例:沙箱化 MCP 服务器
一个关键用例是对 Model Context Protocol (MCP) 服务器进行沙箱化,以限制其能力。例如,对文件系统 MCP 服务器进行沙箱化:
**不使用沙箱**(`.mcp.json`):
```json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
```
**启用沙箱**(`.mcp.json`):
```json
{
"mcpServers": {
"filesystem": {
"command": "srt",
"args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
```
然后在 `~/.srt-settings.json` 中配置限制:
```json
{
"filesystem": {
"denyRead": [],
"allowWrite": ["."],
"denyWrite": ["~/sensitive-folder"]
},
"network": {
"allowedDomains": [],
"deniedDomains": []
}
}
```
现在 MCP 服务器将被阻止写入被拒绝的路径:
```
> Write a file to ~/sensitive-folder
✗ Error: EPERM: operation not permitted, open '/Users/ollie/sensitive-folder/test.txt'
```
## 工作原理
该沙箱使用操作系统级原语来强制执行适用于整个进程树的限制:
- **macOS**:使用 `sandbox-exec` 配合动态生成的 [Seatbelt 配置文件](https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf)
- **Linux**:使用 [bubblewrap](https://github.com/containers/bubblewrap) 进行容器化,并实现网络命名空间隔离
- **Windows**:在专用的 `srt-sandbox` 本地用户账户下运行沙箱进程,使用以该账户 SID 为键的 [Windows 筛选平台](https://learn.microsoft.com/en-us/windows/win32/fwp/windows-filtering-platform-start-page) 出站防护、写入受限令牌,以及工作树上的显式 ACE
0d1c612947c798aef48e6ab4beb7e8544da9d41a-4096x2305
### 双重隔离模型
文件系统隔离和网络隔离对于有效的沙箱化都是必需的。没有文件隔离,被攻陷的进程可能窃取 SSH 密钥或其他敏感文件。没有网络隔离,进程可能逃逸沙箱并获得不受限制的网络访问。
**文件系统隔离**强制执行读和写限制:
- **读**(先拒绝后允许模式):默认情况下,所有位置都允许读取。你可以拒绝较大的区域(例如 `/Users`),然后重新允许其中的特定路径(例如 `.`)。`allowRead` 优先于 `denyRead`——这与写相反,写操作中 `denyWrite` 优先于 `allowWrite`。如果 `denyRead` 条目比其所在的 `allowRead` 区域更具体(例如 `denyRead: ["**/.env"]` 或 `["./secrets"]` 配合 `allowRead: ["."]`),则仍然保持拒绝。
- **写**(仅允许模式):默认情况下,所有位置都拒绝写入。你必须显式允许路径(例如 `.`、`/tmp`)。空的允许列表意味着没有写入权限。
**网络隔离**(仅允许模式):默认情况下,所有网络访问都被拒绝。你必须显式允许域名。空的 allowedDomains 列表意味着没有网络访问。网络流量通过运行在主机上的代理服务器进行路由:
- **Linux**:请求通过 Unix 域套接字经由文件系统进行路由。沙箱进程的网络命名空间被完全移除,因此所有网络流量都必须经过运行在主机上的代理(监听绑定挂载到沙箱中的 Unix 套接字)
- **macOS**:Seatbelt 配置文件仅允许与特定 localhost 端口通信。代理监听该端口,为所有网络访问创建受控通道
- **Windows**:机器范围的 WFP 筛选器集阻止以 `srt-sandbox` 账户运行的进程发起的出站连接,但到代理端口范围的环回连接除外。代理在该范围内监听,为所有网络访问创建受控通道。这些筛选器不会跟随在创建套接字之前将不同令牌放到线程上的沙箱进程,也不会跟随系统服务代表其承载的流量;请参阅 Windows [已知限制](#known-limitations)
HTTP/HTTPS(通过 HTTP 代理)和其他 TCP 流量(通过 SOCKS5 代理)都由这些代理进行中介,代理会强制执行你的域名允许列表和拒绝列表。
有关 Claude Code 中沙箱化的更多详细信息,请参阅:
- [Claude Code 沙箱化文档](https://docs.claude.com/en/docs/claude-code/sandboxing)
- [超越权限提示:让 Claude Code 更安全、更自主](https://www.anthropic.com/engineering/claude-code-sandboxing)
## 架构
```
src/
├── index.ts # Library exports
├── cli.ts # CLI entrypoint (srt command)
├── utils/ # Shared utilities
│ ├── debug.ts # Debug logging
│ ├── settings.ts # Settings reader (permissions + sandbox config)
│ ├── platform.ts # Platform detection
│ └── exec.ts # Command execution utilities
└── sandbox/ # Sandbox implementation
├── sandbox-manager.ts # Main sandbox manager
├── sandbox-schemas.ts # Zod schemas for validation
├── sandbox-violation-store.ts # Violation tracking
├── sandbox-utils.ts # Shared sandbox utilities
├── http-proxy.ts # HTTP/HTTPS proxy for network filtering
├── socks-proxy.ts # SOCKS5 proxy for network filtering
├── linux-sandbox-utils.ts # Linux bubblewrap sandboxing
├── macos-sandbox-utils.ts # macOS sandbox-exec sandboxing
└── windows-sandbox-utils.ts # Windows srt-win sandboxing
```
## 用法
### 作为 CLI 工具
`srt` 命令(Anthropic Sandbox Runtime)会用安全边界封装任意命令:
```bash
# Run a command in the sandbox
srt echo "hello world"
# With debug logging
srt --debug curl https://example.com
# Specify custom settings file
srt --settings /path/to/srt-settings.json npm install
```
设置文件是可选的——如果 `~/.srt-settings.json` 处没有文件,`srt` 会使用内置默认值运行:无网络访问、不在默认写入路径之外写入,且读取不受限制。如果设置文件_确实_存在但为空、无法读取或未通过校验,则视为错误:`srt` 会报告该错误并退出,而不是回退到那些默认值,因为默认值是一种不同的配置,而非更弱的配置——回退会丢弃文件中的 `denyRead`、`allowRead` 和凭据规则以及它声明的其他所有内容。通过 `--settings` 指定的文件同样如此,该文件也必须存在。
#### 在命令运行期间更新配置:`--control-fd`
`--control-fd <fd>` 从调用方已打开的描述符读取配置更新,每行一个 JSON 对象,其结构与设置文件相同。每一行都会替换整个配置,并按照相同的 schema 进行校验,因此一行是一个完整配置而非补丁:`network` 和 `filesystem` 为必填项,某行省略的键会恢复为其默认值。
```bash
# fd 3 is the read end of a pipe the caller writes lines to
srt --control-fd 3 -- npm test
```
命令运行后,被接受的配置行会改变以下内容:
- 网络列表——`network.allowedDomains`、`deniedDomains`(含
`deniedDomainReasons`)和 `deniedResolvedAddresses`——会在下一次连接时
生效,且在所有平台上均如此:代理会按请求查询这些列表,解析地址检查也会
根据同一配置行重建。`network.mitmProxy` 和
`network.tlsTerminate.excludeDomains` 同样如此,它们也是按请求读取的。
- `credentials.sigv4` 会在下一次请求时生效,前提是 srt 启动时带有
`credentials` 块:签名钩子仅在这种情况下才会安装,并且会按请求读取策略,
而不是在安装时捕获策略。
- 其他内容不会改变。文件系统规则已经写入 seatbelt 配置文件、bubblewrap
argv 或 Windows ACE 中,凭据掩码在命令被包装时就已经构建完成,而正在运行的
代理服务器在启动时就已经捕获了 `network.parentProxy` 以及是否终止 TLS 的
设置。
- 在 Windows 上,如果某配置行的文件访问集合(`filesystem.*` 与
`credentials.files` 的并集)与启动时应用的集合不同,srt 会注意到这一点,
但仅在 `SRT_DEBUG` 下才会提示:ACL 授权是会话级的,因此启动时应用的集合
会继续生效。配置行的其余部分按上述方式应用。
srt 如何读取通道,以及它会拒绝什么:
- 描述符必须是 **3 或以上** 的整数且可读——`0`-`2` 是标准流。当 srt 无法
读取给定的描述符时,它会报错退出而不是运行命令,因此失效的通道永远不会被
当作有效通道。如果在传递任何更新之前通道就失效,命令会随之终止;如果在
传递更新之后失效,srt 会报告该情况,并让命令在最后应用的配置下继续运行。
- 不是有效配置的行会在 stderr 上报告并被丢弃;之前的配置继续生效。该行本身
会写入调试日志而不是终端,空行会被静默忽略。
- srt 仅在沙箱启动后才开始读取,因此在此之前写入的更新会在通道中等待而不会
丢失——也不会被沙箱启动时使用的配置覆盖。这样的行可能会在命令仍在被包装
时被读取,此时它就成为包装命令所用的配置,包括文件系统规则。是否会被那么早
读取取决于包装耗时(在 Linux 上,对大型目录树执行 deny glob 就足够长),
因此命令必须在其下启动的规则应放在设置文件中。
- srt **会随被包装的命令一起退出**,不会等待写入方关闭描述符。输入结束也不是
错误:命令会在最后应用的配置下继续运行。
- 给 srt 一个**专用的、只读的端**。srt 会将管道或套接字置于非阻塞模式,而该
标志存在于打开的文件描述上,因此任何其他持有同一描述的对象——shell 的
`exec 3<fifo`、父进程继续使用的描述符的 `pass_fds`——从此以后其自身的阻塞
读取都会得到 `EAGAIN`。
- 在 macOS 和 Linux 上,沙箱内的命令不会获得该描述符:srt 会将该槽位指向
`/dev/null`,因此沙箱内的任何内容都无法读取更新或写入自己的配置。在
Windows 上,命令仅以三个标准流启动,因此控制描述符不在传递给它的描述符
之中。
### 作为带外部决策器的独立代理:`srt proxy`
`srt proxy`(或由 `bun run build:srt-proxy` 构建的单文件 `srt-proxy` 可执行
文件)仅运行 SRT 的 HTTP 代理,不包含沙箱子进程,适用于在别处运行工作负载的
主机。它在主机传入的监听套接字上接受连接,在进程内终止每个 CONNECT,并且仅当
通过另一个继承描述符访问的独立决策器进程允许时,才转发请求。有关命令行、决策器
协议和安全模型,请参阅 [docs/srt-proxy.md](https://github.com/anthropic-experimental/sandbox-runtime/blob/main/docs/srt-proxy.md)。
### 作为库使用
```typescript
import {
SandboxManager,
type SandboxRuntimeConfig,
} from '@anthropic-ai/sandbox-runtime'
import { spawn } from 'child_process'
// Define your sandbox configuration
const config: SandboxRuntimeConfig = {
network: {
allowedDomains: ['example.com', 'api.github.com'],
deniedDomains: [],
},
filesystem: {
denyRead: ['~/.ssh'],
allowWrite: ['.', '/tmp'],
denyWrite: ['.env'],
},
}
// Initialize the sandbox (starts proxy servers, etc.)
await SandboxManager.initialize(config)
// Wrap a command with sandbox restrictions
const sandboxedCommand = await SandboxManager.wrapWithSandbox(
'curl https://example.com',
)
// Execute the sandboxed command
const child = spawn(sandboxedCommand, { shell: true, stdio: 'inherit' })
// Handle exit and cleanup after child process completes
child.on('exit', async code => {
console.log(`Command exited with code ${code}`)
// Cleanup when done (optional, happens automatically on process exit)
await SandboxManager.reset()
})
```
**违规归因(`commandId` / `commandText`)。** 在被包装命令运行期间观察到的违规(seatbelt 日志行、seccomp 事件、代理拒绝)会存储在一个归因键下,而 `annotateStderrWithSandboxFailures(key, stderr)` / `getViolationsForCommand(key)` 会通过同一个键查找它们。默认情况下,该键就是被包装的字符串本身。传入一个不透明的、每次调用唯一的 `commandId`(例如工具使用 id)来改为以它作为键——推荐这样做:键只比较前 100 个字符,因此共享前缀的长命令否则会交叉归因,而同一文本的重新运行会继承先前运行的事件。如果你_执行_的字符串并不是该调用_所代表_的命令(例如你包装了一个拼接而成的 `source <snapshot> && eval '<cmd>'`),还要传入 `commandText: '<cmd>'`:它是 `ignoreViolations` 命令模式所匹配的对象,也是每个违规所报告为其 `command` 的内容。你传给 `wrapWithSandbox` 的 `commandId` 必须与你随后传给 `annotateStderrWithSandboxFailures` / `getViolationsForCommand` 的是同一个非空字符串;空字符串会被视为完全没有 `commandId`,因此键就是该命令。
只有键会被截断到 100 个字符。自 v0.0.76 起,所报告的 `command`——以及 `ignoreViolations` 命令模式所匹配的文本——对于未带 `commandId` 包装的调用而言是完整命令,而不是其前 100 个字符;因此某个模式只可能比以前抑制得更多,绝不会更少。本进程的任何调用都未注册的归因键(这些载体可从沙箱内部写入)会被以净化后的形式报告,并截断到相同的键长度。
```typescript
const wrapped = await SandboxManager.wrapWithSandbox(
assembledCommand, // what actually runs
undefined,
undefined,
undefined,
{ commandId: invocationId, commandText: rawCommand },
)
// ... run it ...
const annotated = SandboxManager.annotateStderrWithSandboxFailures(
invocationId,
stderr,
)
```
**询问一个没有规则决定的主机(ask 回调)。** `SandboxManager.initialize` 的第二个参数是一个可选的 `SandboxAskCallback`。当某个目标既未被 `network.deniedDomains` 决定,也未被 `network.allowedDomains` 决定,也未被按命令的允许列表(下一节)决定时,就会询问它;而在 `network.strictAllowlist` 下则永远不会询问它。没有回调时,这样的目标会被拒绝。该回调解析为以下之一:
- `true` - 允许该连接。只有值 `true` 才允许。
- `false` - 拒绝它。违规行报告通用原因 `user denied`。
- `{ allow: false, reason: '...' }` - 拒绝它,并在违规行中用 `reason` 代替通用文本进行报告,以便任何读取 `<sandbox_violations>` 块的人(包括模型)了解原因以及应该改做什么。沙箱内的客户端也会读取它,作为应答其 CONNECT 的 `403` 的状态短语。
任何其他答案都会拒绝,无论它是否为真值,并报告通用原因。这包括一个带有 `reason` 却没有声明 `allow: false` 的对象,例如 `{ allow: true, reason: '...' }`:它的原因不会被报告。
原因在存储之前会像违规行的其余部分一样被清理。每一段控制字符(包括换行符和制表符)或不可见字符(零宽字符,包括连接符,以及双向控制字符)都会变成一个空格;`<` 和 `>` 会被移除,因此 `re-run with <host> listed` 会被存储为 `re-run with host listed`;两端会被修剪。结果会被截断到 500 个字符,按 `String.prototype.length` 的方式以 UTF-16 代码单元计数。如果清理后什么都不剩,原因会回退为 `user denied`。请将其写成一行纯文本。
**0.0.79 之后首个版本中的行为变更:** 该过滤器过去会在任何真值答案上允许,因此一个解析为 `true` 以外的真值(`1`、字符串、任意对象)的回调会允许该连接。现在它会拒绝。出于同样的原因,`{ allow: false, reason }` 若返回到较旧的版本,在那里会被解读为允许,因为对象是真值。`SandboxManager.askCallbackDenyReason` 在理解该对象的版本上为 `true`:在返回一个对象之前先检查它,在它不存在的地方用普通的 `false` 拒绝。
```typescript
const deny = (reason: string) =>
SandboxManager.askCallbackDenyReason
? { allow: false as const, reason }
: false
await SandboxManager.initialize(config, async ({ host, port }) => {
if (await userApproves(host, port)) return true
return deny(`${host} was not approved for this session`)
})
```
**按命令的网络允许列表(`registerCommandNetworkLists`)。** 一次调用可以在其运行期间,在已配置的允许列表之外,额外携带自己的允许列表:
```typescript
import { randomBytes } from 'node:crypto'
// 128 bits of randomness, 22 characters. See below for why nothing less will do.
const commandId = randomBytes(16).toString('base64url')
const wrapped = await SandboxManager.wrapWithSandbox(
command,
undefined,
undefined,
undefined,
{ commandId },
)
SandboxManager.registerCommandNetworkLists(commandId, {
allowedDomains: ['registry.example.org', '*.cdn.example.org:443'],
})
const child = spawn(wrapped, { shell: true })
const unregister = () => SandboxManager.unregisterCommandNetworkLists(commandId)
child.once('exit', unregister)
child.once('error', unregister) // the child never started
```
条目使用 `network.allowedDomains` 的语法和匹配器,并获得相同的校验。`registerCommandNetworkLists` 在条目无效时抛出异常,在 `commandId` 短于 22 个字符时也会抛出异常(未配对的代理项不计入)。再次注册同一个 id 会替换其列表;`unregisterCommandNetworkLists` 对没有列表的 id 是空操作;`reset()` 会移除所有注册。
连接的评估顺序如下:
1. 匹配 `network.deniedDomains` 则拒绝;
2. 匹配 `network.allowedDomains` 则允许;
3. `network.strictAllowlist` 则拒绝;
4. 连接所呈现的 id 对应的已注册列表中有匹配项则允许;
5. 若存在询问回调则由其决定,否则拒绝该连接。
因此,按命令的条目永远不会覆盖已配置的 `deniedDomains` 条目,并且在 `strictAllowlist` 下会被完全忽略。它也永远不会进入 `network.*`:`getConfig()` 和 `getNetworkRestrictionConfig()` 不会显示它,被掩码凭据的默认 `injectHosts` 作用域(即 `network.allowedDomains`)也不会扩大。以此方式允许的主机在拨号时仍会经过解析地址检查,按命令列表中的 IP 字面量不会在此处增加任何豁免。
id 必须是什么,以及此功能不是什么:代理通过代理用户名得知连接属于哪次调用,而用户名由沙箱内的客户端呈现。因此,id 是**唯一**将连接绑定到允许列表的东西。它**必须**不可猜测:至少 128 位随机性,绝不能是计数器、时间戳或任何由命令派生的内容。呈现另一个活动调用 id 的沙箱进程会获得该调用的允许,所以这是归属,而不是同一会话中并发命令之间的边界。在生成被包装命令之前立即注册,并在子进程退出或从未启动时注销:比其命令存活更久的注册会扩大其 id 值得被呈现的时间窗口。与所有归属键一样,只有 id 的前 100 个字符参与。保持 id 为 ASCII:非 ASCII id 的编码形式若无法放入代理用户名(255 字节),其列表会注册但永远不会生效。
当 `network.httpProxyPort` 指定外部代理时,不会应用任何列表:包装不会给该代理提供用户名,因此没有连接携带 id。`registerCommandNetworkLists` 仍然成功(并在 `SRT_DEBUG` 下用一行说明),并且每个连接都按不存在列表的方式决定。
#### 可用的导出
```typescript
// Main sandbox manager
export { SandboxManager } from '@anthropic-ai/sandbox-runtime'
// Violation tracking
export { SandboxViolationStore } from '@anthropic-ai/sandbox-runtime'
// TypeScript types
export type {
SandboxRuntimeConfig,
NetworkConfig,
FilesystemConfig,
IgnoreViolationsConfig,
SandboxAskCallback,
FsReadRestrictionConfig,
FsWriteRestrictionConfig,
NetworkRestrictionConfig,
} from '@anthropic-ai/sandbox-runtime'
```
## 配置
### 设置文件位置
默认情况下,沙箱运行时会在 `~/.srt-settings.json` 处查找配置。你可以使用 `--settings` 标志指定自定义路径:
```bash
srt --settings /path/to/srt-settings.json <command>
```
### 完整配置示例
```json
{
"network": {
"allowedDomains": [
"github.com",
"*.github.com",
"lfs.github.com",
"api.github.com",
"npmjs.org",
"*.npmjs.org"
],
"deniedDomains": ["malicious.com"],
"allowUnixSockets": ["/var/run/docker.sock"],
"allowLocalBinding": false
},
"filesystem": {
"denyRead": ["~/.ssh"],
"allowRead": [],
"allowWrite": [".", "src/", "test/", "/tmp"],
"denyWrite": [".env", "config/production.json"]
},
"ignoreViolations": {
"*": ["/usr/bin", "/System"],
"git push": ["/usr/bin/nc"],
"npm": ["/private/tmp"]
},
"enableWeakerNestedSandbox": false,
"allowNestedUserNamespaces": false,
"enableWeakerNetworkIsolation": false,
"allowAppleEvents": false
}
```
### 配置选项
#### 网络配置
采用**仅允许模式**——默认拒绝所有网络访问。
- `network.allowedDomains` - 允许的域名数组(支持 `*.example.com` 等通配符)。空数组 = 无网络访问。可选的 `:port` 后缀(`api.example.com:443`、`*.example.com:8443`)将该条目限制为仅匹配该目标端口;不带端口的条目匹配任意端口。
- IPv6 字面量必须按 RFC 3986 风格加方括号:`[::1]`、`[2001:db8::1]:443`。未加方括号的多冒号条目会因歧义而被拒绝(`2001:db8::1:443` 本身就是一个合法地址)。
- `network.deniedDomains` - 拒绝的域名数组(优先检查,优先于 allowedDomains)。同样支持 `:port` 后缀,并且接受裸 `*`(或 `*:22`)表示全部拒绝。
- `network.deniedDomainReasons` - 可选映射,将 `deniedDomains` 条目(按精确字符串匹配)映射到面向模型的理由,当该条目拒绝连接时,该理由会出现在 `<sandbox_violations>` 行中——说明被阻止的内容以及获准的替代方案(例如 `{"github.com:22": "SSH pushes to GitHub are blocked; use an https:// remote"}`)。没有理由的条目会报告一个通用理由。对于 SSH 目标(端口 22),该理由还会带内传递:通过无认证 SOCKS ProxyCommand(例如 BSD `nc -X 5`)隧道的 SSH 客户端会收到一个密钥交换前的 SSH 断开连接,其描述即为该理由,OpenSSH 会原样打印——请将此类理由控制在约 400 个 ASCII 字符以内,以祈使句开头,因为 OpenSSH 会截断并转义非 ASCII 字符。确实进行认证的客户端(如 srt 注入的 `GIT_SSH_COMMAND`)会在任意端口上收到该理由,作为应答其 CONNECT 的 `403` 的状态短语,这正是该命令以及许多 HTTP 客户端所打印的内容。在那里,理由会像违规行中一样被清理和截断,并且每个非 ASCII 字符都会变成 `?`,因为客户端对状态行中非 ASCII 字符的读取方式并不一致。
- `network.allowLocalBinding` - 允许沙箱化命令直接使用本地端口(布尔值,默认:false)。macOS:可在任意本地端口上绑定和接受连接,并连接到任意端口的 `localhost`。Windows:可连接到任意 TCP 或 UDP 端口的回环地址;它属于机器级安装的一部分,参见 [Windows 上的本地服务器](#local-servers-on-windows)。Linux:忽略——沙箱拥有自己的网络命名空间,其回环始终可用,且不是主机的回环。
**解析地址检查。** 允许/拒绝列表按_名称_匹配,但控制某个被允许名称的 DNS(或被允许通配符下任意标签的 DNS)的人,就控制了它解析到什么地址。因此,在直接拨号某个被允许的**主机名**之前,代理会先解析一次,丢弃任何位于拒绝集合中的地址,并连接到存活的地址(通过检查的地址就是被拨号的地址——不会进行第二次查询)。如果没有地址存活,连接会像其他策略拒绝一样被拒绝:HTTP/CONNECT 得到 `403`(`X-Proxy-Error: blocked-by-sandbox-runtime`,理由在响应体中,对于 CONNECT 则作为状态短语),SOCKS 得到 "connection not allowed by ruleset",并且违规存储中会记录一行 `deny network-outbound host:port (resolved to a loopback address)`——指明地址类别(回环、链路本地、本机、云元数据、拒绝列表、允许列表……),而非地址本身,地址本身只出现在调试日志中。
拒绝集合包括:回环地址(`127.0.0.0/8`、`::1`)、未指定地址(`0.0.0.0/8`、`::`)、链路本地地址(`169.254.0.0/16`、`fe80::/10`)、组播地址(`224.0.0.0/4`、`ff00::/8`)、广播地址、位于链路本地之外的云实例元数据/平台端点(`100.100.100.200`、`168.63.129.16`、`192.0.0.192`、`fd00:ec2::/32`、`fd20:ce::254`、`fd00:c1::a9fe:a9fe`、`fd00:42::42`)、当前分配给本机任一网络接口的所有地址(绑定到 `0.0.0.0` 的服务在局域网或全局地址上的应答与在回环地址上完全相同)、`deniedDomains` 中列出的每个 IP 字面量(若带有 `:port` 则遵循该端口),以及 `deniedResolvedAddresses` 中的任何内容。IPv4 条目也会匹配携带 IPv4 地址的 IPv6 形式——IPv4 映射地址、IPv4 兼容地址和 IPv4 转换地址、NAT64 知名前缀(`64:ff9b::/96`)以及 6to4(`2002::/16`)均按其内嵌的 IPv4 地址进行判定。本地使用的 NAT64 前缀 `64:ff9b:1::/48` 和网络特定前缀不会被解码——其布局(RFC 6052 允许 IPv4 出现在多个位置)无法仅从地址本身识别;在此类网络上,请列出所拒绝范围在该前缀下的转换形式(例如 `10.0.0.0/8` 对应 `<prefix>::a00:0/104`)。未经分配即可到达本机的地址——云实例的 1:1-NAT 公网地址、路由器端口转发、容器或虚拟机主机网关别名——不会被自动覆盖;请将它们列入 `deniedResolvedAddresses`。
该检查不会处理的情况:**本身即为** IP 字面量的允许列表条目(将 `127.0.0.1:3000` 加入允许列表是一种明确选择)——同理,当某个 IP 字面量(在该端口上)本身位于 `allowedDomains` 中时,主机名可以解析到原本被拒绝的地址,因为通过名称访问它并不会获得字面量条目所不具备的任何权限(`deniedDomains` 中的 IP 字面量仍然优先,与字面量请求的情况完全一致)。因此,在通过 `/etc/hosts` 将 `myapp.test` 映射到本地服务器的开发环境中,允许列表为 `["myapp.test", "127.0.0.1:3000"]`;不存在单独的例外列表。`localhost` 以及 `.localhost` 下的名称只会解析到回环地址(或允许列表中的字面量),不会解析到其他任何地址。对于经由 `parentProxy`(包括从 srt 自身环境中的 `HTTP_PROXY` / `HTTPS_PROXY` 获取的代理)或 `mitmProxy` 路由的连接,不会执行该检查——该跳负责解析名称并拥有自己的地址策略——并且该检查仅约束代理所拨号的地址:在 macOS 上,`allowLocalBinding` 单独允许沙箱进程完全不经过代理直接连接回环端口。
- `network.deniedResolvedAddresses` - 允许的主机名不得解析到的额外 IP 地址 / CIDR 范围(IPv4 或 IPv6,不带方括号,任意端口)。私有用途地址空间默认不会被拒绝,因为将内网主机名加入允许列表是合法的;当允许列表中的名称必须避开这些地址时,请在此处列出,例如 `["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10", "fc00::/7"]`。请分别列出 IPv4 和 IPv6 范围——覆盖 IPv4 映射块(`::ffff:0:0/96`)的 IPv6 范围(例如 `::/0`)在某些运行时上会匹配 IPv4 应答,而在其他运行时上则不会,因此不要依赖它来拒绝 IPv4。
**TLS 终止**(`network.tlsTerminate`,实验性):设置后,HTTPS CONNECT 会在进程内终止,以便 SRT 能够看到(并通过 `network.filterRequest` 过滤)解密后的请求。沙箱进程会被指向一个信任包,其中包含 MITM CA(`caCertPath`/`caKeyPath`,若省略则使用临时 CA)以及主机的常规根证书,因此代理签发的证书和真实的上游证书都能通过验证。它需要 Node,或 Bun 1.4 或更高版本;在较旧版本的 Bun 上,`SandboxManager.initialize` 会抛出异常。有关其工作原理、限制以及它所取代的按隧道监听器的变更,请参阅 [TLS Termination](#tls-termination)。
- `network.tlsTerminate.excludeDomains` - 域模式(语法与 `allowedDomains` 相同),这些域**不会**被终止。匹配的 CONNECT 请求会以不透明方式隧道传输:它们仍然受域允许列表约束,但沙箱内的客户端会与真实上游完成自己的 TLS 握手,并且 `filterRequest` / 凭据注入不会应用于其 HTTPS 流量。用于 TLS 终止从根本上会破坏的两种情况:
- **mTLS 上游** - 只有沙箱内的客户端持有客户端证书,因此代理无法代表其重新发起连接。
- **证书固定客户端** - 自行验证上游身份的客户端(自定义 CA、SAN 固定)会拒绝 MITM 证书。
- `network.tlsTerminate.extraCaCertPaths` - 追加到该信任包中的 PEM CA 证书文件路径,位于 MITM CA 和主机常规根证书之后。被排除(未终止)的主机由沙箱内的客户端验证,而 SRT 设置的信任环境变量(`SSL_CERT_FILE`、`GIT_SSL_CAINFO`、...)会_替换_每个工具自身的信任配置,因此站点本地根证书(例如内部 mTLS CA)必须位于该包中,否则这些主机将永远无法被验证。每个文件中只有 `CERTIFICATE` 块会被复制到包中(其他任何内容,例如组合 PEM 中的私钥,绝不会暴露给沙箱);缺失、不可读或不包含 PEM `CERTIFICATE` 块的文件会被跳过,因此列出仅存在于部分主机上的路径是安全的。
---
[Read more](https://github.com/anthropic-experimental/sandbox-runtime)