在 Sandbox SDK 0.12 中,tunnels.get() 在容器中运行 cloudflared 并返回公共 URL。快速隧道获得随机的 trycloudflare.com URL,命名隧道获得账户中的主机名,例如 ada.example.com。在 1.0 中,你的 Durable Object 用 exec() 启动 cloudflared。0.12 创建的命名隧道会留在账户中,一旦 cloudflared 再次运行就会保留其主机名。
Worker 已经将请求路由到容器,因此大多数隧道可以改为预览 URL。那些请参阅 迁移预览 URL。
你的 1.0 类已替换 0.12 Sandbox 类,并具有 替换 Sandbox 类 中的 container getter 以及 ensureRunning() 和 startContainer() 方法。src/index.ts 具有 在后台运行服务器 第 1 步中的脚本和函数,它们启动并停止 cloudflared。
startContainer() 必须以 enableInternet: true 启动容器,因为 cloudflared 会向外连接到 Cloudflare。sandbox 中的代码随后也可以到达 Internet。这允许什么,请参阅 Sandbox 安全。
本页替换 tunnels.get()、tunnels.list() 和 tunnels.destroy()。移除 0.12 隧道所需的 SANDBOX_TRANSPORT 变量。
-
将
cloudflared复制到镜像中。在FROM行之后将此行添加到Dockerfile:Dockerfiledockerfile COPY --from=docker.io/cloudflare/cloudflared:2026.10.2 \ /usr/local/bin/cloudflared /usr/local/bin/cloudflared0.12.14 也运行 2026.10.2 版本。
cloudflared需要ca-certificates软件包;替换 Sandbox 类的Dockerfile会安装该软件包。 -
将检查隧道的脚本添加到
src/index.ts的顶层:src/index.tsts const TUNNEL_ROOT = "/var/lib/tunnels"; const QUICK_URL = "https://[a-z0-9-]+\\.trycloudflare\\.com"; // Prints "connected" and any quick tunnel URL while cloudflared runs // and serves requests. const TUNNEL_STATUS = `${CURRENT} dir=$1 current "$dir" && kill -0 "$pid" 2>/dev/null || exit 0 if grep -q 'Registered tunnel connection' "$dir/log" then echo connected $(grep -o -m1 -E '${QUICK_URL}' "$dir/log") fi`; // Prints the port and URL of each connected quick tunnel. const LIST = `status=$1 for dir in ${TUNNEL_ROOT}/*/; do [ -d "$dir" ] || continue set -- $(sh -c "$status" sh "$dir") [ "$1" = connected ] && [ -n "$2" ] && echo "$(basename "$dir") $2" done`; async function capture( container: Container, argv: string[], ): Promise<string> { const process = await container.exec(argv); const output = await process.output(); return new TextDecoder().decode(output.stdout).trim(); }每个隧道都作为以其端口命名的目录中的服务器运行。
cloudflared在隧道开始服务请求时记录Registered tunnel connection。 -
向 Durable Object 类添加启动
cloudflared并等待其连接的方法,以及打开快速隧道的方法:src/index.tsts export class MySandbox extends DurableObject<Env> { // ... private async startTunnel( port: number, args: string[], env: Record<string, string> = {}, ): Promise<string> { const dir = `${TUNNEL_ROOT}/${port}`; const argv = ["cloudflared", "tunnel", "--no-autoupdate", ...args]; await stopServer(this.container, dir); await startServer(this.container, dir, argv, { env }); const deadline = Date.now() + 30_000; while (Date.now() < deadline) { const status = await capture(this.container, [ "sh", "-c", TUNNEL_STATUS, "sh", dir, ]); if (status.startsWith("connected")) { return status.slice("connected".length).trim(); } const log = await serverExited(this.container, dir); if (log !== undefined) { throw new Error(`cloudflared exited: ${log}`); } await scheduler.wait(500); } await stopServer(this.container, dir); throw new Error("The tunnel did not connect within 30 seconds"); } async openTunnel(port: number): Promise<string> { await this.ensureRunning(); const url = `http://localhost:${port}`; return this.startTunnel(port, ["--url", url]); } }openTunnel()替换tunnels.get(port),并返回trycloudflare.comURL。该方法返回后,URL 可能需要数秒才能解析。快速隧道会随容器结束,与 0.12 一样。每次调用都会停止已在该端口上服务的任何
cloudflared,并返回新 URL。0.12 会返回已在该端口上服务的隧道。要保留 URL,请在调用openTunnel()之前用下一步的listTunnels()查找它。 -
添加列出和关闭快速隧道的方法:
src/index.tsts export class MySandbox extends DurableObject<Env> { // ... async listTunnels(): Promise<{ port: number; url: string }[]> { if (!this.container.running) { return []; } const output = await capture(this.container, [ "sh", "-c", LIST, "sh", TUNNEL_STATUS, ]); return output .split("\n") .filter((line) => line !== "") .map((line) => { const [port, url] = line.split(" "); return { port: Number(port), url }; }); } async closeTunnel(port: number): Promise<void> { if (!this.container.running) { return; } await stopServer(this.container, `${TUNNEL_ROOT}/${port}`); } }listTunnels()为快速隧道替换tunnels.list(),closeTunnel()替换tunnels.destroy()。closeTunnel()之后,该 URL 返回530。
0.12 在 Durable Object 存储中记录每个命名隧道,键为 tunnels 和 tunnels:meta。这些键保存隧道 ID、主机名、你的账户和 zone,以及其 DNS 记录。就地部署切换时它们会保留,因此 1.0 可以再次运行同一隧道。如果你 并排迁移 sandbox,copyFrom0x() 会将这些键复制到 1.0 类。
-
保留 0.12 使用的
CLOUDFLARE_API_TOKEN密钥,并具有 Cloudflare Tunnel Edit 和 DNS Edit 权限。代码用它获取每个隧道的 run token,以及删除隧道和 DNS 记录。为了让wrangler types将该密钥添加到Env,也请在.dev.vars中设置它。代码读取 0.12 存储的账户和 zone,不需要
CLOUDFLARE_ACCOUNT_ID、CLOUDFLARE_TUNNEL_ACCOUNT_ID或CLOUDFLARE_ZONE_ID。 -
将类型和调用 Cloudflare API 的函数添加到
src/index.ts的顶层:src/index.tsts type Tunnels = Record<string, { id: string; hostname: string }>; type TunnelsMeta = Record< string, { accountId?: string; zoneId?: string; dnsRecordId?: string } >; type ApiBody<T> = { result: T; errors: unknown[] }; async function cloudflare<T>( env: Env, path: string, init: RequestInit = {}, ): Promise<T> { const url = `https://api.cloudflare.com/client/v4${path}`; const response = await fetch(url, { ...init, headers: { Authorization: `Bearer ${env.CLOUDFLARE_API_TOKEN}`, }, }); const body = await response.json<ApiBody<T>>(); if (!response.ok) { const errors = JSON.stringify(body.errors); throw new Error(`Cloudflare API: ${errors}`); } return body.result; } -
向 Durable Object 类添加运行和删除命名隧道的方法:
src/index.tsts export class MySandbox extends DurableObject<Env> { // ... private async namedTunnel(port: number) { const { storage } = this.ctx; const tunnels = await storage.get<Tunnels>("tunnels"); const meta = await storage.get<TunnelsMeta>("tunnels:meta"); const tunnel = tunnels?.[port]; return tunnel && { ...tunnel, ...meta?.[port] }; } async resumeTunnel(port: number): Promise<string | undefined> { const tunnel = await this.namedTunnel(port); if (!tunnel?.accountId) { return undefined; } const { accountId, id } = tunnel; const token = await cloudflare<string>( this.env, `/accounts/${accountId}/cfd_tunnel/${id}/token`, ); await this.ensureRunning(); // TUNNEL_TOKEN keeps the token out of the process list. await this.startTunnel( port, ["run", "--url", `http://localhost:${port}`], { TUNNEL_TOKEN: token }, ); return `https://${tunnel.hostname}`; } async deleteTunnel(port: number): Promise<void> { const tunnel = await this.namedTunnel(port); if (!tunnel?.accountId) { return; } const { accountId, id, zoneId, dnsRecordId } = tunnel; await this.closeTunnel(port); await cloudflare( this.env, `/accounts/${accountId}/cfd_tunnel/${id}`, { method: "DELETE" }, ); if (zoneId && dnsRecordId) { await cloudflare( this.env, `/zones/${zoneId}/dns_records/${dnsRecordId}`, { method: "DELETE" }, ); } await this.ctx.storage.transaction(async (txn) => { const tunnels = (await txn.get<Tunnels>("tunnels")) ?? {}; const meta = (await txn.get<TunnelsMeta>("tunnels:meta")) ?? {}; delete tunnels[port]; delete meta[port]; await txn.put({ tunnels, "tunnels:meta": meta }); }); } }两个方法都会跳过已存储隧道为快速隧道的端口,因为 0.12 仅为命名隧道存储账户。对该端口,
resumeTunnel()返回undefined。在每次
start()之后,为应用通过命名隧道服务的每个端口调用resumeTunnel()。0.12 仅在下一次对该端口调用tunnels.get()时再次启动命名隧道。在cloudflared运行之前,主机名会返回 错误 1033。deleteTunnel()为命名隧道替换tunnels.destroy()。它删除隧道、其 DNS 记录及其存储条目。0.12destroy()会对 sandbox 的每个隧道这样做,因此结束 sandbox 时请对每个已存储端口调用deleteTunnel()。
就地切换后,不会运行 0.12 destroy(),因此已不存在的 sandbox 的隧道和 DNS 记录会留在账户中。0.12 将每个隧道命名为 sandbox-<DURABLE_OBJECT_ID>-<NAME>,并用 createdBy: "sandbox-sdk" 元数据标记它。列出这些隧道:
API=https://api.cloudflare.com/client/v4
curl --get "$API/accounts/$ACCOUNT_ID/cfd_tunnel" \
--data is_deleted=false --data per_page=100 \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" |
jq -r '.result[]
| select(.metadata.createdBy == "sandbox-sdk")
| "\(.id) \(.name) \(.status)"'0.12 给每条 DNS 记录加上注释 sandbox-<DURABLE_OBJECT_ID>。列出这些记录:
curl --get "$API/zones/$ZONE_ID/dns_records" \
--data comment.startswith=sandbox- \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" |
jq -r '.result[] | "\(.id) \(.name) \(.comment)"'状态为 down 的隧道没有正在运行的 cloudflared。对于你不再保留的每个 sandbox,删除其隧道 和 其 DNS 记录。
如果你 并排迁移 sandbox,copyFrom0x() 迁移的隧道会在名称和注释中保留 0.12 Durable Object 的 ID,并且仍为 1.0 类服务。请按主机名决定删除哪些隧道,而不是按 Durable Object ID。
要给新 sandbox 自己的主机名,请从 Worker 服务这些主机名,如 在自己的主机名上提供预览 所述。这些主机名具有与 0.12 命名隧道相同的 <NAME>.<ZONE> 形状。sandbox 不运行 cloudflared,也不持有令牌。要继续创建隧道,请用 Cloudflare Tunnel API 创建每个隧道,并像 0.12 那样为其添加 DNS 记录。
在端口 8080 上有服务器监听时,调用 openTunnel(8080),并请求它返回的 URL。响应来自你的服务器。
部署切换后,请求 0.12 创建的命名隧道的主机名:
curl https://ada.example.com/ --write-out "%{http_code}\n"error code: 1033
530调用 resumeTunnel(8080),然后再次发送请求。响应来自你的服务器。deleteTunnel(8080) 之后,隧道及其 DNS 记录不再出现在上一节的列表中。