← Sandbox SDK / sandbox / api
Tunnels
sandbox.tunnels пространство имён публикует сервис, работающий внутри песочницы, в публичном интернете через Cloudflare Tunnel. SDK запускает cloudflared внутри контейнера и открывает постоянное соединение QUIC с периферийной сетью Cloudflare.
Доступны два варианта:
- Quick tunnels (
sandbox.tunnels.get(port)) : Не требует настройки. Cloudflare назначает случайный*.trycloudflare.comhostname для каждого новогоcloudflaredпроцесс. Не требуется ни учётная запись Cloudflare, ни API-токен, ни DNS-запись, ни собственный домен. URL меняются при каждом перезапуске контейнера. - Именованные туннели (
sandbox.tunnels.get(port, { name })) : Привяжите стабильное имя хоста<name>.<your-zone>в зоне, которой вы управляете. Имя хоста сохраняется при перезапуске контейнера и используется совместно всеми песочницами, запрашивающими тот жеname. Требуется API-токен Cloudflare, аккаунт и зона.
Требования
Оба варианта туннеля требуют:
- RPC-транспорт. Вызов
sandbox.tunnelsдля транспортов HTTP/Websocket вызывает исключение"RPC transport required". См. Настройка транспорта.
Для именованных туннелей также требуются API-токен Cloudflare, аккаунт и зона, подробнее см. Именованные туннели: предварительные требования.
Методы
tunnels.get()
Возвращает запись туннеля для port. SDK создаёт новый cloudflared процесс внутри контейнера, если он ещё не запущен. Метод идемпотентен: повторные вызовы с одним и тем же (port, options) возвращают ту же запись.
const tunnel = await sandbox.tunnels.get(
port: number,
options?: { name?: string }
): Promise<TunnelInfo>Параметры:
port: номер порта внутри песочницы для открытия (1024-65535, за исключением зарезервированных портов). Сервис, к которому создаётся туннель, уже должен прослушивать0.0.0.0:<port>внутри контейнера.options.name(необязательно) : Одна метка DNS (строчные буквы, цифры, внутренние дефисы; 1-63 символов; без точек). При указании обеспечивает именованный туннель привязан к<name>.<your-zone>. Если параметр не указан, создаётся quick tunnel.
Возвращает: 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>Параметры:
portOrInfo: либо номер порта, либоTunnelInfoзапись, возвращённаяget().
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) |
Поддерживается |
Предварительные требования
Чтобы создать именованный туннель, вам потребуется:
- A Аккаунт Cloudflare с зона (домен, которым вы управляете в Cloudflare DNS).
- A API-токен Cloudflare с необходимыми правами доступа.
- 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 });
},
};Жизненный цикл
Именованные туннели рассчитаны на то, чтобы продолжать работу дольше контейнера, который их создал:
- Первый вызов к
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внутри контейнера с токеном туннеля.
- Разрешается в
- Последующие вызовы с тем же
(port, name)возвращают закэшированную запись без обращения к Cloudflare. - Перезапуск контейнера (вытеснение Durable Object, развёртывание, сбой):
cloudflaredпрекращает существование вместе с контейнером, но Cloudflare Tunnel и запись DNS сохраняются.- При следующем
get(port, { name }), SDK заново обнаруживает помеченный туннель через API Cloudflare и перезапускаетcloudflared. Имя хоста не изменяется.
- Явное завершение работы с
sandbox.tunnels.destroy(port):- Останавливает
cloudflaredвнутри контейнера. - Удаляет ресурс Cloudflare Tunnel.
- Удаляет проксируемый
CNAMEзапись.
- Останавливает
- Уничтожение песочницы с
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:
- Метаданные туннеля:
{ sandboxId, createdBy: 'sandbox-sdk', name, port } - Комментарий к DNS-записи:
sandbox-<sandbox-id> - Тег ресурса (только для планов Enterprise):
sandboxId:<sandbox-id>
На тарифах, отличных от 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"Ограничения
Оба варианта туннеля:
- Исходящий трафик через WARP / Zero Trust. Если на вашем локальном компьютере запущен Cloudflare WARP или другая политика исходящего трафика Zero Trust, исходящий трафик к
api.trycloudflare.comи edge cloudflared может быть заблокирован. В этом случаеtunnels.get()зависает на этапе edge-хендшейка и со временем завершается по тайм-ауту. Отключите WARP или добавьте исключение для исходящего трафика для этих адресов. - Непродолжительный прогрев DNS. Первый запрос по только что созданному URL может занять пару секунд, пока распространяется DNS, даже после
get()разрешается.
Только для быстрых туннелей:
- URL-адреса не сохраняются при перезапуске контейнера. Cloudflare назначает имя хоста во время
cloudflaredполучает при запуске handshake, из-за которого каждый перезапуск создаёт новый URL. При старте контейнера SDK очищает свой кеш туннелей, поэтому следующийtunnels.get(port)возвращает свежую запись. Используйте именованный туннель для получения стабильного имени хоста. - Гарантия доступности не предоставляется. Cloudflare позиционирует
trycloudflare.comкак вспомогательное средство отладки, а не как цель для продакшна. - Без Server-Sent Events. Заголовок
trycloudflare.comedge буферизуетtext/event-streamответы, поэтому события SSE никогда не доходят до клиента. WebSocket работает нормально. Используйте именованный туннель если ваша служба передает данные через SSE.
Только именованные туннели:
- Одна метка DNS.
nameне должен содержать точек. Universal SSL покрывает только<name>.<your-zone>. - Учитывается в квотах вашей зоны. Каждый именованный туннель создает Cloudflare Tunnel и запись DNS в вашем аккаунте. См. Ограничения Cloudflare Tunnel.
- Для очистки требуется API-токен. Если
destroy()выполняется уже после отзыва токена, ресурсы на стороне Cloudflare остаются бесхозными. SDK записывает их идентификаторы в лог, чтобы вы могли удалить их вручную.
Дополнительные материалы
- Концепция Preview URL : Preview URL через Worker и их отличия от быстрых туннелей.
- API портов,
exposePort()и сценарий с preview URL через Worker. - Руководство по публикации сервисов : полное пошаговое руководство по публикации сервисов в продакшене.
- Настройка транспорта : RPC против транспорта на основе маршрутов.