跳转到内容
搜索文档

Tunnel (隧道)

最后更新 查看 MarkdownAgent 设置

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。

要求

两种隧道变体都需要:

命名隧道另外需要 Cloudflare API 令牌、账户和 Zone — 请参阅命名隧道:先决条件

方法 (Methods)

tunnels.get()

返回 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 });

},
};

tunnels.list()

返回当前针对该 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}`);
}

tunnels.destroy()

拆除隧道。接受端口号或 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);

类型 (Types)

TunnelInfo

快捷隧道省略 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;
}

命名隧道 (Named tunnels)

命名隧道绑定一个由用户控制的主机名 — <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) 支持

先决条件

要配置命名隧道,你需要:

  1. 一个拥有 ZoneCloudflare 账户(你在 Cloudflare DNS 上控制的域名)
  2. 一个具有正确作用域的 Cloudflare API 令牌
  3. 账户 ID (Account ID)Zone ID — 当令牌精确定位到各自恰好一个时,SDK 可以从令牌推导两者。

创建 API 令牌

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 TokensAccount API Tokens (前缀为 cfat_ 的密钥) 均受支持 — SDK 会检测令牌种类并使用适当的自省端点。

使用 REST API 创建令牌

你可以在不使用仪表板的情况下创建令牌。权限组 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>" }
        ]
      }
    ]
  }'

将令牌和 ID 绑定到 Worker

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 });
  },
};

生命周期 (Lifecycle)

命名隧道旨在比配置它们的容器存续时间更长:

  1. 首次调用 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
  2. 带有相同 (port, name)后续调用 会返回缓存的记录,而无需联系 Cloudflare。
  3. 容器重启 (Durable Object 驱逐、部署、崩溃):
    • cloudflared 随容器死亡,但 Cloudflare Tunnel 和 DNS 记录保留。
    • 在下次调用 get(port, { name }) 时,SDK 通过 Cloudflare API 重新发现已打标记的隧道,并重新生成 cloudflared。主机名保持不变。
  4. 使用 sandbox.tunnels.destroy(port) 进行 显式拆除
    • 停止容器内部的 cloudflared
    • 删除 Cloudflare Tunnel 资源。
    • 删除代理的 CNAME 记录。
  5. 使用 sandbox.destroy() 进行 Sandbox 销毁 会在停止容器之前,拆除该 Sandbox 配置的每个隧道(包括 Cloudflare 端的资源)。

如果 destroy() 无法连接 Cloudflare API(例如在 get()destroy() 之间撤销了令牌),SDK 会记录一条命名孤立 tunnelIddnsRecordId 的警告,以便你在仪表板中手动清理。

Cloudflare 资源与标记

命名隧道**在你的 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"

限制 (Limitations)

两种隧道变体:

  • 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.com Edge 会缓冲 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 APIexposePort() 以及 Worker 前端的预览 URL 流程。
  • 公开服务指南 — 在生产环境中公开服务的端到端指南。
  • 传输配置 — RPC 与基于路由的传输。sport configuration](/sandbox/configuration/transport/) — RPC vs. route-based transport.

这篇文档对您有帮助吗?