sandbox.tunnels 命名空间通过 Cloudflare Tunnel 在公共互联网上公开运行在 Sandbox 内部的服务。SDK 在容器内部运行 cloudflared,并与 Cloudflare 的 Edge 网络建立持久的 QUIC 连接。
提供两种变体:
- 快捷隧道 (Quick tunnels) (
sandbox.tunnels.get(port)) — 零配置。Cloudflare 为每个新的cloudflared进程分配一个随机的*.trycloudflare.com主机名。不需要 Cloudflare 账户、API 令牌、DNS 记录或自定义域名。URL 会在每次容器重启时改变。 - 命名隧道 (Named tunnels) (
sandbox.tunnels.get(port, { name })) — 在你控制的 Zone 上绑定一个稳定的主机名<name>.<your-zone>。该主机名能在容器重启后存续,并在请求相同name的 Sandbox 之间共享。需要 Cloudflare API 令牌、账户和 Zone。
两种隧道变体都需要:
- RPC 传输。 在 HTTP/Websocket 传输上调用
sandbox.tunnels会抛出"RPC transport required"。请参阅 传输配置 (Transport configuration)。
命名隧道另外需要 Cloudflare API 令牌、账户和 Zone — 请参阅命名隧道:先决条件。
返回 port 的隧道记录。如果尚未运行,SDK 会在容器内生成一个新的 cloudflared 进程。该方法是幂等的:使用相同的 (port, options) 重复调用会返回相同的记录。
const tunnel = await sandbox.tunnels.get(
port: number,
options?: { name?: string }
): Promise<TunnelInfo>参数:
port— Sandbox 内部要公开的端口号(1024-65535,排除保留端口)。要隧道传输的服务必须已经在容器内部监听0.0.0.0:<port>。options.name(可选) — 单个 DNS 标签(小写字母、数字、内部连字符;1–63 个字符;不能有句点)。设置时,将配置一个绑定到<name>.<your-zone>的命名隧道。省略时,将配置一个快捷隧道。
返回值:Promise<TunnelInfo> — 隧道记录。请参阅 TunnelInfo。
在已经拥有隧道的端口上使用不同的 options 调用 get(port) 会抛出异常。请先调用 destroy(port)。
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request, env) {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
await sandbox.startProcess("python -m http.server 8080");
const tunnel = await sandbox.tunnels.get(8080);
console.log(tunnel.url);
// → https://random-words-here.trycloudflare.com
// 针对相同端口的重复调用会返回相同的记录。
const same = await sandbox.tunnels.get(8080);
console.log(same.url === tunnel.url); // true
return Response.json({ url: tunnel.url });
},
};import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
await sandbox.startProcess("python -m http.server 8080");
const tunnel = await sandbox.tunnels.get(8080);
console.log(tunnel.url);
// → https://random-words-here.trycloudflare.com
// 针对相同端口的重复调用会返回相同的记录。
const same = await sandbox.tunnels.get(8080);
console.log(same.url === tunnel.url); // true
return Response.json({ url: tunnel.url });
},
};返回当前针对该 Sandbox 跟踪的每个隧道。
const tunnels = await sandbox.tunnels.list(): Promise<TunnelInfo[]>返回值:Promise<TunnelInfo[]> — TunnelInfo 记录数组。当没有活动隧道时为空。
const tunnels = await sandbox.tunnels.list();
for (const tunnel of tunnels) {
console.log(`port ${tunnel.port} → ${tunnel.url}`);
}const tunnels = await sandbox.tunnels.list();
for (const tunnel of tunnels) {
console.log(`port ${tunnel.port} → ${tunnel.url}`);
}拆除隧道。接受端口号或 get() 返回的 TunnelInfo 记录。幂等 — 销毁未知端口将顺利完成。
await sandbox.tunnels.destroy(portOrInfo: number | TunnelInfo): Promise<void>参数:
portOrInfo— 端口号或get()返回的TunnelInfo记录。
const tunnel = await sandbox.tunnels.get(8080);
// 通过端口号拆除...
await sandbox.tunnels.destroy(8080);
// ...或通过记录拆除。
await sandbox.tunnels.destroy(tunnel);const tunnel = await sandbox.tunnels.get(8080);
// 通过端口号拆除...
await sandbox.tunnels.destroy(8080);
// ...或通过记录拆除。
await sandbox.tunnels.destroy(tunnel);快捷隧道省略 name;命名隧道包含通过 options.name 传递的标签。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
隧道标识符。快捷隧道为 quick-<random>,命名隧道为 Cloudflare Tunnel UUID。 |
port |
number |
隧道代理到 Sandbox 内部的端口号。 |
url |
string |
公共 URL — https://<random>.trycloudflare.com (快捷隧道) 或 https://<name>.<your-zone> (命名隧道)。 |
hostname |
string |
url 的主机名部分。 |
createdAt |
string |
创建隧道的 ISO-8601 时间戳。 |
name |
string |
仅限命名隧道。 通过 options.name 传递的标签。快捷隧道中不存在。 |
type TunnelInfo = QuickTunnelInfo | NamedTunnelInfo;
interface QuickTunnelInfo {
id: string;
port: number;
url: string;
hostname: string;
createdAt: string;
name?: never;
}
interface NamedTunnelInfo {
id: string;
port: number;
url: string;
hostname: string;
createdAt: string;
name: string;
}命名隧道绑定一个由用户控制的主机名 — <name>.<your-zone> — 由托管的 Cloudflare Tunnel 和 Zone 上的代理 CNAME 记录支持。与快捷隧道不同,该 URL 在容器重启时保持稳定,并在使用相同 name 调用 get(port, { name }) 的 Sandbox 之间共享。
| 方面 | 快捷隧道 (Quick tunnel) | 命名隧道 (Named tunnel) |
|---|---|---|
| 主机名 | 随机 *.trycloudflare.com,由 Cloudflare 分配 |
<name>.<your-zone>,由你选择 |
| 稳定性 | 每次容器重启都会改变 | 稳定;在重启和 Sandbox 生命周期中持久存在 |
| Cloudflare 账户 | 不需要 | 需要 (API 令牌 + zone) |
| Cloudflare 端资源 | 无 | 托管的 Cloudflare Tunnel + 代理的 DNS CNAME |
| SLA 保证 | 无(调试辅助工具) | 由你 Zone 的标准 Cloudflare SLA 支持 |
| TLS 证书 | Cloudflare 拥有的通配符证书 | <name>.<your-zone> 上的 Universal SSL (仅限单 DNS 标签) |
| Server-Sent Events | 不支持 (Edge 会缓冲 text/event-stream) |
支持 |
要配置命名隧道,你需要:
- 一个拥有 Zone 的 Cloudflare 账户(你在 Cloudflare DNS 上控制的域名)。
- 一个具有正确作用域的 Cloudflare API 令牌。
- 账户 ID (Account ID) 和 Zone ID — 当令牌精确定位到各自恰好一个时,SDK 可以从令牌推导两者。
在 My Profile(我的个人资料) > API Tokens(API 令牌) > Create Token(创建令牌) > Custom token(自定义令牌) 中创建一个具有以下权限的令牌:
| 作用域 | 用于 |
|---|---|
| Account(账户) · Cloudflare Tunnel · Edit(编辑) | 创建、查找和删除隧道。 |
| Zone · DNS · Edit(编辑) | 为 <name>.<your-zone> 更新/插入并删除代理的 CNAME。 |
| Zone · Zone · Read(读取) | 查找 Zone 的名称以派生 <name>.<your-zone>。 |
| Account(账户) · Account Settings(账户设置) · Read(读取) (可选) | 当没有显式设置时,允许 SDK 从令牌推导账户 ID。 |
在 Account Resources(账户资源) 下,将令牌限制在将拥有隧道的账户。在 Zone Resources(区域资源) 下,将其限制在你想要绑定的特定 Zone。
User API Tokens 和 Account API Tokens (前缀为 cfat_ 的密钥) 均受支持 — SDK 会检测令牌种类并使用适当的自省端点。
你可以在不使用仪表板的情况下创建令牌。权限组 ID 是稳定的;下面的代码片段使用了占位符 — 请从 GET /user/tokens/permission_groups 获取当前 ID。
curl -X POST "https://api.cloudflare.com/client/v4/user/tokens" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "sandbox-named-tunnels",
"policies": [
{
"effect": "allow",
"resources": { "com.cloudflare.api.account.<ACCOUNT_ID>": "*" },
"permission_groups": [{ "id": "<TUNNEL_EDIT_GROUP_ID>" }]
},
{
"effect": "allow",
"resources": { "com.cloudflare.api.account.zone.<ZONE_ID>": "*" },
"permission_groups": [
{ "id": "<DNS_EDIT_GROUP_ID>" },
{ "id": "<ZONE_READ_GROUP_ID>" }
]
}
]
}'SDK 从 Worker 环境中读取 CLOUDFLARE_API_TOKEN,并尝试从令牌自动推导账户 ID 和 Zone ID。如果令牌与多个账户或 Zone 相关联,SDK 无法明确选择一个,你就必须显式设置 CLOUDFLARE_ACCOUNT_ID 和/或 CLOUDFLARE_ZONE_ID。
| 变量 | 必须? | 备注 |
|---|---|---|
CLOUDFLARE_API_TOKEN |
是 | 使用 wrangler secret put 存储为 secret。 |
CLOUDFLARE_ACCOUNT_ID |
仅当令牌能看到多个账户时 | 否则从令牌中推导。 |
CLOUDFLARE_ZONE_ID |
仅当令牌能看到多个 Zone 时 | 否则从令牌中推导。 |
npx wrangler secret put CLOUDFLARE_API_TOKEN对于本地开发,将变量放在 .dev.vars 中(在 gitignore 中)。对于生产环境,在 Wrangler 配置的 vars 下设置非 secret ID(在需要时):
{
"vars": {
"CLOUDFLARE_ACCOUNT_ID": "<account-id>",
"CLOUDFLARE_ZONE_ID": "<zone-id>"
}
}当推导失败时,SDK 会抛出一个明确的错误,指出需要设置的变量。
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request, env) {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
// 重用跨容器重启的现有应用进程,或启动它。
let proc = await sandbox.getProcess("app");
if (!proc) {
try {
proc = await sandbox.startProcess("python -m http.server 8080", {
processId: "app",
});
} catch (err) {
if (err?.code !== "PROCESS_ALREADY_EXISTS") throw err;
proc = await sandbox.getProcess("app");
}
}
// 配置(或重用)指向端口 8080 的 https://app.example.com。
const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
console.log(tunnel.url); // → https://app.example.com
return Response.json({ url: tunnel.url });
},
};import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
// 重用跨容器重启的现有应用进程,或启动它。
let proc = await sandbox.getProcess('app');
if (!proc) {
try {
proc = await sandbox.startProcess('python -m http.server 8080', { processId: 'app' });
} catch (err) {
if ((err as { code?: string })?.code !== 'PROCESS_ALREADY_EXISTS') throw err;
proc = await sandbox.getProcess('app');
}
}
// 配置(或重用)指向端口 8080 的 https://app.example.com。
const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
console.log(tunnel.url); // → https://app.example.com
return Response.json({ url: tunnel.url });
},
};命名隧道旨在比配置它们的容器存续时间更长:
- 首次调用
sandbox.tunnels.get(port, { name }):- 从配置的 Zone ID 解析
<name>.<your-zone>。 - 创建名为
sandbox-<sandbox-id>-<name>的 Cloudflare Tunnel 资源,并用 Sandbox ID 打上标记。 - 更新/插入从
<name>.<your-zone>到<tunnel-id>.cfargotunnel.com的代理CNAME。 - 使用隧道的令牌在容器内部生成
cloudflared。
- 从配置的 Zone ID 解析
- 带有相同
(port, name)的 后续调用 会返回缓存的记录,而无需联系 Cloudflare。 - 容器重启 (Durable Object 驱逐、部署、崩溃):
cloudflared随容器死亡,但 Cloudflare Tunnel 和 DNS 记录保留。- 在下次调用
get(port, { name })时,SDK 通过 Cloudflare API 重新发现已打标记的隧道,并重新生成cloudflared。主机名保持不变。
- 使用
sandbox.tunnels.destroy(port)进行 显式拆除:- 停止容器内部的
cloudflared。 - 删除 Cloudflare Tunnel 资源。
- 删除代理的
CNAME记录。
- 停止容器内部的
- 使用
sandbox.destroy()进行 Sandbox 销毁 会在停止容器之前,拆除该 Sandbox 配置的每个隧道(包括 Cloudflare 端的资源)。
如果 destroy() 无法连接 Cloudflare API(例如在 get() 和 destroy() 之间撤销了令牌),SDK 会记录一条命名孤立 tunnelId 和 dnsRecordId 的警告,以便你在仪表板中手动清理。
命名隧道**在你的 Cloudflare 账户上(Sandbox 容器外部)**创建资源。它们不存储在 Durable Object 存储中,也不计入 Sandbox 配额,但它们会在 Cloudflare 仪表板中显示并消耗你账户的隧道和 DNS 配额。
对于每个 (sandbox, name) 对,SDK 会创建:
| 资源 | 名称 / 位置 | 标识符 |
|---|---|---|
| Cloudflare Tunnel | Networking(网络) > Tunnels(隧道) | sandbox-<sandbox-id>-<name> |
| 代理 DNS 记录 | 你的 Zone,DNS > Records | CNAME <name>.<zone> → <tunnel-id>.cfargotunnel.com |
两个资源都被打上了标记,因此你可以从仪表板或 API 对它们进行审计、查询和批量清理:
- 隧道元数据:
{ sandboxId, createdBy: 'sandbox-sdk', name, port } - DNS 记录注释:
sandbox-<sandbox-id> - 资源标签 (仅限 Enterprise 计划):
sandboxId:<sandbox-id>
在非 Enterprise 计划中,Cloudflare 会拒绝资源标签;SDK 会检测到这一点,并在没有标签的情况下重试请求。DNS 注释和隧道元数据仍然适用,因此你始终可以将资源追溯到其 Sandbox。
要列出 SDK 为给定账户创建的每个隧道:
curl "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/cfd_tunnel?name=sandbox-" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"两种隧道变体:
- WARP / Zero Trust 出站。 如果你的本地机器运行 Cloudflare WARP 或另一个 Zero Trust 出站策略,到
api.trycloudflare.com和 cloudflared Edge 的出站流量可能会被阻塞。当发生这种情况时,tunnels.get()会卡在 Edge 握手上并最终超时。请禁用 WARP 或为这些目的地添加出站例外。 - 短暂的 DNS 预热。 即便在
get()解析后,通过全新 URL 发起的第一个请求也可能需要几秒钟的时间来进行 DNS 传播。
仅限快捷隧道:
- URL 无法在容器重启后存续。 Cloudflare 在
cloudflared的启动握手期间分配主机名,因此每次重启都会产生一个新的 URL。当容器启动时,SDK 会清除其隧道缓存,因此下一次tunnels.get(port)会返回一个全新的记录。要获得稳定主机名,请使用命名隧道。 - 无 SLA 保证。 Cloudflare 将
trycloudflare.com定位为调试辅助工具,而不是生产目标。 - 无 Server-Sent Events。
trycloudflare.comEdge 会缓冲text/event-stream响应,因此 SSE 事件永远无法到达客户端。WebSocket 正常工作。如果你的服务传输 SSE,请使用命名隧道。
仅限命名隧道:
- 单句 DNS 标签。
name不能包含句点。Universal SSL 仅覆盖<name>.<your-zone>。 - 计入你 Zone 的配额。 每个命名隧道都会在你的账户上创建一个 Cloudflare Tunnel 和一个 DNS 记录。请参阅 Cloudflare Tunnel 限制。
- 清理需要 API 令牌。 如果在撤销令牌后运行
destroy(),Cloudflare 端的资源将被孤立。SDK 会记录孤立 ID,以便你手动移除它们。
- Preview URLs 概念 — Worker 前端的预览 URL 以及它们与快捷隧道的不同之处。
- Ports API —
exposePort()以及 Worker 前端的预览 URL 流程。 - 公开服务指南 — 在生产环境中公开服务的端到端指南。
- 传输配置 — RPC 与基于路由的传输。sport configuration](/sandbox/configuration/transport/) — RPC vs. route-based transport.