在 Sandbox SDK 0.12 中,ContainerProxy 将你的 Sandbox 类的出站字段应用到容器发送的每个请求。在 1.0 中,你的 Durable Object 为来自容器的 HTTP 和 HTTPS 请求注册自己的 WorkerEntrypoint,入口点按相同顺序应用相同规则。
你的 1.0 类已替换 0.12 Sandbox 类,并具有 替换 Sandbox 类 中的 container getter、ensureRunning() 和 startContainer() 方法,以及 ENV 和 INACTIVITY_TIMEOUT_MS 常量。入口点使用 this.ctx.exports,需要兼容日期 2025-11-17 或更晚,或 enable_ctx_exports 标志。0.12 出站规则也需要它。
本页替换 enableInternet、interceptHttps、allowedHosts、deniedHosts、outbound、outboundByHost、outboundHandlers、它们的别名 outboundProxy 和 outboundProxies、运行时 setter(例如 allowHost() 和 setOutboundByHost()),以及 gitCheckout()。从导入和 Worker 的导出中移除 ContainerProxy。对于 0.12 为存储桶挂载添加的 handler,请参阅 迁移存储桶挂载。
-
将 0.12 类的出站字段复制到常量中,并为每个 handler 命名:
src/index.tsts type Handler = { method: string; params?: unknown }; type OutboundHandler = ( request: Request, env: Env, params: unknown, ) => Response | Promise<Response>; // The outbound fields of your 0.12 class. const ENABLE_INTERNET = false; const ALLOWED_HOSTS: string[] | undefined = [ "github.com", "registry.npmjs.org", "api.example.com", ]; const DENIED_HOSTS: string[] | undefined = undefined; const OUTBOUND_BY_HOST: Record<string, Handler> = { "api.example.com": { method: "withToken" }, }; const OUTBOUND: Handler | undefined = undefined; // Your 0.12 handlers, by name. const HANDLERS: Record<string, OutboundHandler> = { withToken: (request, env) => { const headers = new Headers(request.headers); headers.set("Authorization", `Bearer ${env.API_TOKEN}`); return fetch(new Request(request, { headers })); }, };如果你的 0.12 类未设置
enableInternet,将ENABLE_INTERNET设为true(0.12 默认值)。对于类未设置的字段,对OUTBOUND_BY_HOST使用{},对其余常量使用undefined。将
outboundHandlers、outboundByHost和outbound中的每个函数放入HANDLERS,并在OUTBOUND_BY_HOST和OUTBOUND中按名称引用它们。0.12 handler 接受(request, env, ctx)。将其改为接受(request, env, params),并在原先读取ctx.params的地方读取params。 -
将
WorkerEntrypoint添加到cloudflare:workers导入,并添加入口点;它按 0.12 使用的顺序检查每个请求:src/index.tsts import { WorkerEntrypoint } from "cloudflare:workers"; // The shape that 0.12 stored under OUTBOUND_CONFIGURATION. type StoredRules = { outboundByHostOverrides?: Record<string, Handler>; outboundHandlerOverride?: Handler; allowedHosts?: string[]; deniedHosts?: string[]; }; // "*" matches any run of characters, as in 0.12. function matches(pattern: string, hostname: string): boolean { const escaped = pattern .split("*") .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")); return new RegExp(`^${escaped.join(".*")}$`).test(hostname); } function lookup( handlers: Record<string, Handler> | undefined, hostname: string, ): Handler | undefined { return ( handlers?.[hostname] ?? Object.entries(handlers ?? {}).find(([pattern]) => matches(pattern, hostname), )?.[1] ); } export class Outbound extends WorkerEntrypoint<Env, StoredRules> { async fetch(request: Request): Promise<Response> { const rules = this.ctx.props; const allowed = rules.allowedHosts ?? ALLOWED_HOSTS; const denied = rules.deniedHosts ?? DENIED_HOSTS; const url = new URL(request.url); const hostname = url.hostname.replace(/\.+$/, ""); const listed = (patterns: string[]) => patterns.some((pattern) => matches(pattern, hostname)); const blocked = new Response("Origin is disallowed", { status: 520, }); if (denied && listed(denied)) { return blocked; } if (allowed && !listed(allowed)) { return blocked; } const handler = [ lookup(rules.outboundByHostOverrides, hostname), lookup(OUTBOUND_BY_HOST, hostname), rules.outboundHandlerOverride, OUTBOUND, ].find((handler) => handler && HANDLERS[handler.method]); if (handler) { const run = HANDLERS[handler.method]; return run(request, this.env, handler.params); } if (allowed || ENABLE_INTERNET) { return fetch(request); } return blocked; } }入口点先拦截被拒绝的主机名,再拦截不在允许列表中的主机名。然后按此顺序运行第一个匹配的 handler:sandbox 在运行时设置的主机名 handler、来自你的类的主机名 handler、运行时设置的兜底 handler,以及你的类的兜底 handler。没有 handler 时,如果你设置了允许列表或允许 Internet,则将请求发送到 Internet。被拦截的请求获得状态
520,正文为Origin is disallowed,与 0.12 相同。props 携带 sandbox 在运行时设置的规则,形状与 0.12 存储的相同。与 0.12 一样,入口点会跳过名称不在
HANDLERS中的 handler,例如 0.12 为存储桶挂载存储的 handler。 -
将证书变量添加到
ENV,并在startContainer()中注册入口点:src/index.tsts const RULES = "OUTBOUND_CONFIGURATION"; const CA = "/etc/cloudflare/certs/cloudflare-containers-ca.crt"; const BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; const ENV = { NODE_ENV: "test", NODE_EXTRA_CA_CERTS: CA, REQUESTS_CA_BUNDLE: BUNDLE, }; // Keep a copy of the system bundle, wait up to 10 seconds for the // certificate, then write the bundle from the copy and the certificate. const SYSTEM = "/etc/ssl/certs/ca-certificates.system.crt"; const CA_WAIT = `until [ -s ${CA} ]; do sleep 0.1; done`; const CA_READY = `timeout 10 sh -c '${CA_WAIT}'`; const TRUST = [ "sh", "-c", `[ -e ${SYSTEM} ] || { cp ${BUNDLE} ${SYSTEM}.tmp && mv ${SYSTEM}.tmp ${SYSTEM}; } ${CA_READY} && cat ${SYSTEM} ${CA} >${BUNDLE}.tmp && mv ${BUNDLE}.tmp ${BUNDLE}`, ]; export class MySandbox extends DurableObject<Env> { // ... private rules(): StoredRules { return this.ctx.storage.kv.get<StoredRules>(RULES) ?? {}; } private async intercept(): Promise<void> { const props = this.rules(); const outbound = this.ctx.exports.Outbound({ props }); await this.container.interceptAllOutboundHttp(outbound); await this.container.interceptOutboundHttps("*", outbound); } private async trust(): Promise<void> { const trust = await this.container.exec(TRUST); if ((await trust.exitCode) !== 0) { throw new Error("The container did not trust the intercept CA"); } } private async startContainer(): Promise<void> { const newContainer = !this.container.running; if (newContainer) { this.container.start({ image: this.container.images.sandbox, instance: "standard-1", env: ENV, enableInternet: ENABLE_INTERNET, }); } try { if (newContainer) { // Replaces onStart(). await this.ctx.storage.put("startedAt", Date.now()); } await this.intercept(); await this.trust(); await this.container.setInactivityTimeout( INACTIVITY_TIMEOUT_MS, ); } catch (error) { // The next request starts a new container. await this.container.destroy(); throw error; } } }// ...代表构造函数、containergetter 以及你的其他方法。rules()读取 0.12 存储每个 sandbox 在运行时设置的规则的键,因此切换后这些规则继续生效。如果你 并排迁移 sandbox,copyFrom0x()会将该键复制到 1.0 类。拦截将端口
80上的每个请求,以及端口443上的每个 HTTPS 请求,发送到入口点。enableInternet保留 0.12 的值,并仍决定其他端口上的连接。拦截持续到容器停止。startContainer()为每个新容器注册它,并在重启的 Durable Object 发现容器正在运行时再次注册。再次注册会替换较早的注册,因此部署中断了设置的容器仍会在下一条命令之前获得其拦截。在
intercept()之前注册其他拦截,例如存储桶挂载。在intercept()之后注册的主机名拦截永远收不到请求,因为Outbound入口点会收到它们。更多信息请参阅 如果你迁移了出站规则。如果你的类使用 迁移备份 中的
DirectoryBackup,在try块中于你的intercept()之前调用this.backups.intercept()。否则每次备份和还原都会因BACKUP_TRANSFER失败,因为你的Outbound入口点会收到来自容器的备份请求。仅在startContainer()中调用它,不要在每个请求上调用。HTTPS 拦截用容器默认不信任的证书签署响应。与 0.12 一样,
TRUST命令将该证书添加到系统捆绑包,curl 和 Git 会读取它。ENV为 Node.js 和 Pythonrequests设置NODE_EXTRA_CA_CERTS和REQUESTS_CA_BUNDLE,它们默认不读取捆绑包。保留你的其他ENV值,并将ENV传给每次exec()调用。镜像需要提供捆绑包的ca-certificates包。证书在 HTTPS 拦截注册后出现。
TRUST最多等待 10 秒。0.12 最多等待 5 秒。如果证书未出现,或追加失败,trust()会抛出,startContainer()会停止容器。每个容器获得新证书。快照 会保存来自该容器的证书捆绑包,因此追加到捆绑包会保留较早容器的证书。因此TRUST会在添加任何证书之前保留系统捆绑包的副本,并从该副本和当前证书再次写入捆绑包。对同一容器再次运行会写入相同捆绑包。
添加更新已存储规则的方法,并用它们再次注册入口点:
export class MySandbox extends DurableObject<Env> {
// ...
private async updateRules(change: StoredRules): Promise<void> {
this.ctx.storage.kv.put(RULES, { ...this.rules(), ...change });
if (this.container.running) {
await this.intercept();
}
}
async allowHost(hostname: string): Promise<void> {
const hosts = this.rules().allowedHosts ?? ALLOWED_HOSTS ?? [];
await this.updateRules({ allowedHosts: [...hosts, hostname] });
}
async setOutboundByHost(
hostname: string,
method: string,
params?: unknown,
): Promise<void> {
const hosts = { ...this.rules().outboundByHostOverrides };
hosts[hostname] = { method, params };
await this.updateRules({ outboundByHostOverrides: hosts });
}
}再次注册入口点会替换正在运行的容器的规则,打开的连接会获取新规则而不会被断开。以相同方式编写其他 setter。每个 setter 将新值传给 updateRules():
| 0.12 | 要传递的值 |
|---|---|
setAllowedHosts(hosts)、setDeniedHosts(hosts) |
allowedHosts 或 deniedHosts,设为 hosts |
denyHost()、removeAllowedHost()、removeDeniedHost() |
deniedHosts 或 allowedHosts,添加或移除主机名 |
setOutboundByHosts()、removeOutboundByHost() |
outboundByHostOverrides,添加或移除主机名 |
setOutboundHandler(method, params) |
outboundHandlerOverride,设为 { method, params } |
与 0.12 一样,sandbox 设置的每个列表都会替换该类对该 sandbox 的列表。
0.12 仅在你的类设置 interceptHttps = true 时检查 HTTPS 请求。没有它时,当 enableInternet 为 false 时,对允许主机名的 HTTPS 请求会失败;当为 true 时,对被拒绝主机名的 HTTPS 请求会成功。入口点检查每个 HTTPS 请求,因此依赖任一行为的命令现在都会遵循你的规则。
在 0.12 中,当你的类将 handler 声明为静态字段(例如 static outboundByHost = { … })时,如果 TypeScript 将类字段编译为定义,handler 永远不会运行。这是目标 ES2022 及以后的默认行为。该字段跳过了注册 handler 的 setter,因此对这些主机名的请求会进入 Internet 或获得 520。在 1.0 中,handler 会运行。
对 IP 地址的 HTTPS 请求(例如 https://203.0.113.10/)在拦截 * 且 enableInternet 为 false 时会失败。请使用主机名。
在 0.12 中,gitCheckout() 将仓库克隆到 /workspace:
const repo = "https://github.com/octocat/Hello-World";
await sandbox.gitCheckout(repo, { depth: 1 });在 1.0 中,在 Durable Object 方法中运行 git clone:
export class MySandbox extends DurableObject<Env> {
// ...
async clone(url: string, dir: string): Promise<void> {
await this.ensureRunning();
const git = ["git", "clone", "--filter=blob:none", "--depth", "1"];
const clone = await this.container.exec(
["timeout", "-k", "5", "600", ...git, "--", url, dir],
{ cwd: "/workspace", env: ENV },
);
const output = await clone.output();
if (output.exitCode !== 0) {
throw new Error(new TextDecoder().decode(output.stderr));
}
}
}0.12 使用 --filter=blob:none 克隆到 /workspace/<repository name>,并在 600 秒后停止克隆。为 branch 选项传递 --branch。要读取 0.12 返回的分支,以克隆目录为 cwd 运行 git branch --show-current。镜像需要 Git,你的规则必须允许 Git 服务器的主机名,例如 github.com。要在不向 sandbox 提供令牌的情况下克隆私有仓库,请参阅 克隆私有仓库。
部署切换后,在镜像具有 curl 和 Git 的 sandbox 中运行这些命令。你的规则拦截的主机名会返回 0.12 响应:
curl -s -w ' %{http_code}\n' https://example.com/Origin is disallowed 520允许的主机名可通过 HTTPS 工作,命令上无需证书选项:
git ls-remote https://github.com/octocat/Hello-World HEAD7fd1a60b01f91b314f59955a4e4d4e80d8edf11d HEAD在 0.12 中调用过 allowHost() 的 sandbox 中,对该主机名的请求在切换后仍然成功。