INTEGRITY Документация

Tunnels

sandbox.tunnels пространство имён публикует сервис, работающий внутри песочницы, в публичном интернете через Cloudflare Tunnel. SDK запускает cloudflared внутри контейнера и открывает постоянное соединение QUIC с периферийной сетью Cloudflare.

Доступны два варианта:

Требования

Оба варианта туннеля требуют:

Для именованных туннелей также требуются API-токен Cloudflare, аккаунт и зона, подробнее см. Именованные туннели: предварительные требования.

Методы

tunnels.get()

Возвращает запись туннеля для port. SDK создаёт новый cloudflared процесс внутри контейнера, если он ещё не запущен. Метод идемпотентен: повторные вызовы с одним и тем же (port, options) возвращают ту же запись.

const tunnel = await sandbox.tunnels.get(
  port: number,
  options?: { name?: string }
): Promise<TunnelInfo>

Параметры:

Возвращает: Promise<TunnelInfo> : запись туннеля. См. TunnelInfo.

Вызов get(port) с разными options на порту, для которого уже создан туннель, вызывает исключение. Вызовите 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

		// Repeated calls for the same port return the same record.
		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

    // Repeated calls for the same port return the same record.
    const same = await sandbox.tunnels.get(8080);
    console.log(same.url === tunnel.url); // true

    return Response.json({ url: tunnel.url });

},
};

tunnels.list()

Возвращает все туннели, отслеживаемые в данный момент для этой песочницы.

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()

Закрывает туннель. Принимает номер порта или TunnelInfo запись, возвращённая get(). Идемпотентно: удаление неизвестного порта завершается успешно.

await sandbox.tunnels.destroy(portOrInfo: number | TunnelInfo): Promise<void>

Параметры:

const tunnel = await sandbox.tunnels.get(8080);

// Tear down by port number...
await sandbox.tunnels.destroy(8080);

// ...or by the record.
await sandbox.tunnels.destroy(tunnel);
const tunnel = await sandbox.tunnels.get(8080);

// Tear down by port number...
await sandbox.tunnels.destroy(8080);

// ...or by the record.
await sandbox.tunnels.destroy(tunnel);

Типы

TunnelInfo

Быстрые туннели не включают name; именованные туннели несут метку, переданную через options.name.

Поле Тип Описание
id string Идентификатор туннеля. quick-<random> для быстрых туннелей и UUID Cloudflare Tunnel для именованных туннелей.
port number Номер порта внутри sandbox, на который проксирует туннель.
url string Публичный URL: https://<random>.trycloudflare.com (быстро) или https://<name>.<your-zone> (именованная).
hostname string Часть hostname в url.
createdAt string Метка времени в формате ISO-8601, указывающая момент создания tunnel.
name string Только именованные туннели. Метка, переданная через options.name. Отсутствует для Quick Tunnels.
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 и проксированный CNAME запись в вашей зоне. В отличие от Quick Tunnels, URL сохраняется при перезапуске контейнера и используется во всех песочницах которые вызывают get(port, { name }) с тем же name.

Чем они отличаются от quick tunnels

Аспект Quick tunnel Именованный туннель
Имя хоста Случайный *.trycloudflare.com, присвоенное Cloudflare <name>.<your-zone>, выбранный вами
Стабильность Изменяется при каждом перезапуске контейнера Стабильно; сохраняется при перезапусках и в течение всего жизненного цикла sandbox
Аккаунт Cloudflare Не требуется Требуется (API-токен + зона)
Ресурсы на стороне Cloudflare None Managed Cloudflare Tunnel + проксируемый DNS CNAME
Гарантия аптайма Нет (вспомогательное средство отладки) Обеспечивается стандартным SLA Cloudflare для вашей зоны
Сертификат TLS Wildcard, принадлежащий Cloudflare Universal SSL для <name>.<your-zone> (только одна метка DNS)
Server-Sent Events Не поддерживается (edge-сервер буферизует text/event-stream) Поддерживается

Предварительные требования

Чтобы создать именованный туннель, вам потребуется:

  1. A Аккаунт Cloudflare с зона (домен, которым вы управляете в Cloudflare DNS).
  2. A API-токен Cloudflare с необходимыми правами доступа.
  3. ID аккаунта и ID зоны : SDK может определить оба значения из токена, если токен ограничен ровно одним значением каждого типа.

Создайте API-токен

Создайте токен из My Profile > API-токены > Create Token > Custom token со следующими разрешениями:

Область действия Используется для
Аккаунт · Cloudflare Tunnel · Изменить Создавайте, находите и удаляйте туннели.
Zone · DNS · Изменить Добавление, обновление и удаление проксируемых CNAME для <name>.<your-zone>.
Zone · Zone · Чтение Найдите имя зоны, чтобы определить <name>.<your-zone>.
Аккаунт · Account Settings · Чтение (необязательно) Позволяет SDK определять ID аккаунта по токену, если он не задан явно.

В разделе Account Resources, ограничьте токен учётной записью, которой будет принадлежать туннель. В разделе Zone Resources, ограничьте его конкретной зоной, к которой вы хотите привязаться.

Оба User API Tokens и Токены API аккаунта (секрет с префиксом cfat_) поддерживаются, при этом SDK определяет тип токена и использует соответствующую конечную точку интроспекции.

Создайте токен с помощью REST API

Токен можно создать без использования дашборда. Идентификаторы групп разрешений постоянны: в примере ниже используются заполнители, актуальные идентификаторы можно получить из GET /user/tokens/permission_groups.

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>" }
        ]
      }
    ]
  }'

Привяжите токен и идентификаторы к Worker

SDK считывает CLOUDFLARE_API_TOKEN из окружения Worker и пытается автоматически определить account ID и zone ID на основе токена. Если токен связан с несколькими аккаунтами или зонами, SDK не может однозначно выбрать нужный, и вам нужно задать CLOUDFLARE_ACCOUNT_ID и/или CLOUDFLARE_ZONE_ID явно.

Переменная Обязательно? Примечания
CLOUDFLARE_API_TOKEN Да Сохраните как секрет с помощью wrangler secret put.
CLOUDFLARE_ACCOUNT_ID Только если токену доступно несколько аккаунтов В противном случае определяется по токену.
CLOUDFLARE_ZONE_ID Только если токену доступно несколько зон В противном случае определяется по токену.
npx wrangler secret put CLOUDFLARE_API_TOKEN

Для локальной разработки поместите переменные в .dev.vars (добавлено в gitignore). Для продакшн-окружения задайте несекретные ID (если нужно) в vars в конфигурации Wrangler:

{
  "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");

		// Reuse an existing app process across container restarts, or start it.
		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");
			}
		}

		// Provision (or reuse) https://app.example.com pointing at port 8080.
		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");

    // Reuse an existing app process across container restarts, or start it.
    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');
      }
    }

    // Provision (or reuse) https://app.example.com pointing at port 8080.
    const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
    console.log(tunnel.url); // → https://app.example.com

    return Response.json({ url: tunnel.url });
  },
};

Жизненный цикл

Именованные туннели рассчитаны на то, чтобы продолжать работу дольше контейнера, который их создал:

  1. Первый вызов к sandbox.tunnels.get(port, { name }):
    • Разрешается в <name>.<your-zone> из настроенного zone ID.
    • Создает Cloudflare Tunnel ресурс с именем sandbox-<sandbox-id>-<name> с меткой в виде ID sandbox.
    • Добавляет или обновляет проксируемый CNAME от <name>.<your-zone> к <tunnel-id>.cfargotunnel.com.
    • Запускает cloudflared внутри контейнера с токеном туннеля.
  2. Последующие вызовы с тем же (port, name) возвращают закэшированную запись без обращения к Cloudflare.
  3. Перезапуск контейнера (вытеснение Durable Object, развёртывание, сбой):
    • cloudflared прекращает существование вместе с контейнером, но Cloudflare Tunnel и запись DNS сохраняются.
    • При следующем get(port, { name }), SDK заново обнаруживает помеченный туннель через API Cloudflare и перезапускает cloudflared. Имя хоста не изменяется.
  4. Явное завершение работы с sandbox.tunnels.destroy(port):
    • Останавливает cloudflared внутри контейнера.
    • Удаляет ресурс Cloudflare Tunnel.
    • Удаляет проксируемый CNAME запись.
  5. Уничтожение песочницы с sandbox.destroy() перед остановкой контейнера закрывает все туннели, созданные sandbox, включая ресурсы на стороне Cloudflare.

Если destroy() не удаётся обратиться к Cloudflare API (например, токен был отозван между get() и destroy()), SDK записывает предупреждение с именем осиротевшего tunnelId и dnsRecordId чтобы вы могли удалить их вручную через дашборд.

Ресурсы Cloudflare и их тегирование

Именованные туннели создают ресурсы в вашем аккаунте Cloudflare, вне контейнера песочницы. Они не хранятся в хранилище Durable Object и не учитываются в квотах песочницы, однако отображаются в панели управления Cloudflare и расходуют квоты туннелей и DNS вашего аккаунта.

Для каждого (sandbox, name) пары SDK создаёт:

Ресурс Имя / расположение Идентификатор
Cloudflare Tunnel Сеть > Tunnels sandbox-<sandbox-id>-<name>
Проксируемая DNS-запись Ваша зона, DNS > Записи CNAME <name>.<zone> → <tunnel-id>.cfargotunnel.com

Оба ресурса помечены тегами, поэтому их можно проверять, запрашивать и массово удалять через дашборд или API:

На тарифах, отличных от Enterprise, Cloudflare отклоняет теги ресурсов: SDK обнаруживает это и повторяет запрос без тегов. DNS-комментарий и метаданные туннеля при этом сохраняются, поэтому ресурс всегда можно отследить до его песочницы.

Чтобы вывести список всех туннелей, созданных SDK для данного аккаунта:

curl "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/cfd_tunnel?name=sandbox-" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"

Ограничения

Оба варианта туннеля:

Только для быстрых туннелей:

Только именованные туннели: