跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

迁移隧道

最后更新 查看 MarkdownAgent 设置

在 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 变量。

从 Durable Object 运行 cloudflared

  1. 将 cloudflared 复制到镜像中。在 FROM 行之后将此行添加到 Dockerfile:

    Dockerfiledockerfile
    COPY --from=docker.io/cloudflare/cloudflared:2026.10.2 \
        /usr/local/bin/cloudflared /usr/local/bin/cloudflared

    0.12.14 也运行 2026.10.2 版本。cloudflared 需要 ca-certificates 软件包;替换 Sandbox 类的 Dockerfile 会安装该软件包。

  2. 将检查隧道的脚本添加到 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。

  3. 向 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.com URL。该方法返回后,URL 可能需要数秒才能解析。快速隧道会随容器结束,与 0.12 一样。

    每次调用都会停止已在该端口上服务的任何 cloudflared,并返回新 URL。0.12 会返回已在该端口上服务的隧道。要保留 URL,请在调用 openTunnel() 之前用下一步的 listTunnels() 查找它。

  4. 添加列出和关闭快速隧道的方法:

    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 创建的命名隧道

0.12 在 Durable Object 存储中记录每个命名隧道,键为 tunnels 和 tunnels:meta。这些键保存隧道 ID、主机名、你的账户和 zone,以及其 DNS 记录。就地部署切换时它们会保留,因此 1.0 可以再次运行同一隧道。如果你 并排迁移 sandbox,copyFrom0x() 会将这些键复制到 1.0 类。

  1. 保留 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。

  2. 将类型和调用 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;
    }
  3. 向 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.12 destroy() 会对 sandbox 的每个隧道这样做,因此结束 sandbox 时请对每个已存储端口调用 deleteTunnel()。

查找 0.12 留下的隧道

就地切换后,不会运行 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 记录不再出现在上一节的列表中。

相关资源

这篇文档对您有帮助吗?