# quick-tunnel

> Manage expiring durable leases on authorized cloudflared Quick Tunnel child processes.

| field | value |
|-------|-------|
| version | 0.1.1 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=quick-tunnel@0.1.1
```

## configuration

```yaml
- cloudflared: cloudflared
  max_lease_seconds: 2592000
  max_leases: 1024
  max_retries: 3
  retry_initial_ms: 1000
  retry_max_ms: 30000
  startup_timeout_ms: 30000
  state_path: data/quick-tunnel/leases.json
  targets:
    webhooks: http://127.0.0.1:3112
```

## dependencies

- `configuration` @ `latest`

## readme

# quick-tunnel

Worker binário Rust do iii que gerencia **processos filhos cloudflared Quick Tunnel** por leases independentes e persistidas. Não implementa servidor HTTP, não substitui os workers HTTP/queue e não instala nem baixa cloudflared.

## Pré-requisito e segurança

O operador deve fornecer um executável cloudflared confiável, com suporte a `tunnel --output json` (contrato de logs conferido no upstream 2025.11.1 e no `master` consultado em 2026-09-19). Configure `cloudflared` com um caminho absoluto, ou o nome: um nome solto é procurado **somente** em `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin` e `/bin` (inclui o Homebrew em Apple silicon), e o filho é iniciado pelo caminho absoluto encontrado. O `PATH` herdado do worker não é consultado: uma entrada gravável pelo usuário (`~/.local/bin`, shims) poderia substituir o binário que publica um serviço local na Internet. Para qualquer outro local, configure o caminho absoluto. A interface registra mesmo sem o executável: somente a primeira lease inicia um filho; executável ausente produz `failed` com `cloudflared not found (...); install it from <página de downloads da Cloudflare>`. `quick-tunnel::status` informa `prerequisites.cloudflared` antes de qualquer lease, para que o operador instale o binário antes de habilitar webhooks.

**Acquire publica o serviço HTTP autorizado na Internet.** Configure o destino HTTP de webhooks com validação HMAC e sem rotas administrativas. O worker não autentica HTTP público, não cria webhooks GitHub e não fornece SLA: Quick Tunnels são efêmeros e destinados a desenvolvimento. Não exponha o engine iii como target.

Callers não fornecem origin, porta, argumentos, ambiente ou caminho executável. `targets` é uma allowlist exclusiva do operador; aceita apenas HTTP(S) com IP literal loopback, sem credenciais, caminho, query ou fragmento. `localhost` é recusado para evitar ambiguidade DNS. Cada target expõe **o serviço inteiro**, não um prefixo de rota. O worker HTTP e o processo cloudflared precisam compartilhar o mesmo namespace de rede para que o destino loopback seja alcançável; containers com redes isoladas precisam ser configurados pelo operador antes de habilitar a integração.

O filho usa `env_clear`, HOME temporário privado, configuração YAML vazia explícita, stdin fechado, `--no-autoupdate`, `--metrics 127.0.0.1:0`, `--retries 0`, `--grace-period 0s`, saída limitada a linhas de 16 KiB. Não herda `TUNNEL_*`, tokens, proxies ou configuração cloudflared do host. Métricas do próprio cloudflared ficam somente em loopback. Logs crus, segredos e origin não são retornados nos snapshots. IDs de consumers devem ser identificadores opacos, nunca secrets.

## Configuração

O worker usa a configuração central (`configuration`), via `iii-config-client`, com ID `III_CONFIG_NAME` ou `quick-tunnel`. `--config arquivo.yaml` é seed somente se o registro ainda não tiver valor; falhas da configuração central não viram defaults silenciosos. Mudanças exigem **reinício**: reconfiguração ao vivo de destinos/executável não é suportada para evitar trocar a autorização de leases ativas. `--local-config` é alternativa explícita para desenvolvimento/testes isolados.

```yaml
targets:
  webhooks: http://127.0.0.1:3112
cloudflared: cloudflared
state_path: data/quick-tunnel/leases.json
startup_timeout_ms: 30000
max_retries: 3
failed_cooldown_ms: 300000
retry_budget_reset_ms: 600000
retry_initial_ms: 1000
retry_max_ms: 30000
max_leases: 1024
max_lease_seconds: 2592000
```

Mantenha `state_path` em volume persistente privado e exclusivo por instância. Caminhos relativos são resolvidos no diretório do worker. Um lock de arquivo impede duas instâncias sobre o mesmo estado. Gravação usa arquivo temporário privado, fsync e rename atômico; erro/corrupção falha fechado. Não há estado distribuído: não execute réplicas com arquivos diferentes para a mesma identidade de túnel. Se remover um target com leases persistidas, o próximo boot recusa essas leases; libere-as antes da alteração.

## Contratos

Todos os handlers registram descrição e schemas tipados via serde/schemars, SDK **`=0.22.1-alpha.25`**.

### `quick-tunnel::acquire`

```json
{"consumer_id":"github:owner.repo","tunnel_id":"webhooks","expires_at":"2030-01-01T00:00:00Z"}
```

A identidade deve ser um identificador opaco (caracteres aceitos: alfanuméricos, `-_.:`; máximo 128 bytes). Use uma data futura dentro de `max_lease_seconds`.

```json
{"tunnel_id":"webhooks","lease_id":"uuid","status":"starting","public_url":null,"generation":"uuid","error":null}
```

`tunnel_id` omitido é `webhooks`. Campos desconhecidos, inclusive `origin`, são recusados. Retorno imediato; não bloqueia esperando DNS/conexão. Idempotência por `(consumer_id,tunnel_id)`: retorna a mesma lease e estende a expiração pelo máximo entre a existente e a solicitada (nunca encurta uma renovação concorrente). Consumers diferentes possuem leases independentes. A lease é gravada antes do início do processo. `error` é um campo aditivo do snapshot comum.

### `quick-tunnel::release`

Entrada `{"lease_id":"uuid"}`; saída `{"released":true}`. Repetição/ID desconhecido retorna `false`. A última lease encerra e coleta o filho antes da resposta. Não há autorização por consumer dentro do worker: callers confiáveis dependem da política iii; leases não são credenciais de segurança.

### `quick-tunnel::status`

Entrada `{}` ou `{"tunnel_id":"webhooks"}`. Retorna snapshot acima **sem `lease_id` singular**, mais `leases:[{lease_id,consumer_id,tunnel_id,expires_at}]` e `prerequisites:{cloudflared:{found,path,version,error,install_url}}` (sondado com `cloudflared --version`, limitado a 5 s, fora da task dona das leases; o resultado de um binário inalterado — mesmo caminho e mtime — é reutilizado por 60 s, e um binário ausente é reavaliado a cada leitura sem executar processo). Nunca inclui secrets do cloudflared. O worker nunca baixa nem instala o cloudflared: `install_url` aponta a página oficial da Cloudflare.

### `quick-tunnel::changed`

Config `{}` (todos os targets) ou `{"tunnel_id":"webhooks"}`. Evento `{tunnel_id,generation,status,public_url,error}`. Estados: `starting`, `ready`, `reconnecting`, `failed`, `stopped`. `public_url` só é não-nulo em `ready`; HTTPS estrito, exatamente um label DNS antes de `.trycloudflare.com`, sem credenciais, porta, caminho, query ou fragmento. Metadata e namespace resolvido do `TriggerConfig` são preservados no disparo; namespace legado ausente significa `default`, não o namespace do provider.

**Integração GitHub: assine o trigger ANTES de acquire e leia status DEPOIS.** Mudanças podem ocorrer antes da resposta do acquire. Geração é UUID opaco, não ordenável; em dúvida reconcilie status. Nova tentativa de processo/restart sempre ganha nova geração e obtém uma URL nova do serviço (não há garantia de que o serviço nunca reutilize um hostname). Reconexão interna do mesmo filho conserva geração. Eventos são best-effort, não uma fila durável: bindings/entregas podem falhar, consumidores devem reconciliar status após reconectar; overflow de 256 eventos provoca reenvio dos snapshots atuais. Callback lento não bloqueia leases/expiração, mas atrasa outros callbacks (timeout de 5 s por entrega).

## Ciclo de vida

- Um task proprietário serializa aquisições/liberações, vencimentos, saídas e retries.
- Expirações são checadas pelo runtime a cada 50 ms, sem agente ou chamadas externas. O relógio UTC é a autoridade; atrasos de scheduling/IO são possíveis.
- A última lease termina o processo. Shutdown preserva leases válidas; o próximo boot as reconstrói e inicia nova geração, nunca restaura URL anterior como pronta.
- Startup e reconexão têm deadline monotônico; repetidas mensagens de retry não prolongam o deadline. Backoff exponencial limitado; no máximo `1 + max_retries` processos rápidos por orçamento (depois, um por `failed_cooldown_ms`). O orçamento só é restaurado depois que uma geração fica `ready` continuamente por `retry_budget_reset_ms` (padrão 10 min), então loops de crash logo após conectar continuam consumindo o orçamento. Esgotado o orçamento, o status fica `failed` (observável), mas enquanto houver leases o worker tenta um novo processo a cada `failed_cooldown_ms` (padrão 5 min): uma queda transitória do trycloudflare se recupera sozinha, sem loop rápido. `failed_cooldown_ms: 0` restaura o comportamento terminal (libere/expire todas as leases ou reinicie o worker); `retry_budget_reset_ms: 0` nunca restaura o orçamento.
- SIGTERM e CTRL-C encerram/recolhem os filhos, abortam leitores e desligam o SDK. `kill_on_drop(true)` protege cancelamento/panic; encerramento normal usa kill + wait, inclusive se o filho ignorar SIGTERM. Requisições em andamento no serviço público são interrompidas, sem drain.
- SIGKILL/queda do host não executa destructors; use o supervisor/container com grupo de processos/cgroup para limpeza nesse cenário. Não se promete cleanup sob SIGKILL nem de descendentes criados por executável malicioso. O prerequisite deve ser cloudflared oficial sem auto-update, não um wrapper que daemonize.

## Contrato real de JSON

[try.cloudflare.com](https://try.cloudflare.com/) anuncia hostname/edge/health em JSON no stdout. Porém as fontes oficiais consultadas ainda mostram **logs JSON no stderr**, ativados por **`--output json`**, e URL em linha ASCII dentro de `message`; não encontramos ali flag dedicada `--json` ou campo estável `public_url`. Não inventamos esse contrato.

O parser faz primeiro `serde_json` para registros tipados. Somente uma mensagem inteira contendo URL (opcionalmente envolvida em `| ... |`) pode anunciar hostname. Não procura URLs arbitrárias com regex em texto. `ready` exige também `message == "Registered tunnel connection"`, `level == "info"`, `connIndex == 0`, UUID `connection` e protocolo `quic`/`http2`, emitidos após registro efetivo no edge. URL impressa sozinha **não** basta; não se faz probe HTTP ao hostname público. Isso confirma edge conectado, não saúde do origin ou propagação DNS global. Mudanças upstream de formato podem causar timeout; são detectáveis, não presumidas como sucesso.

Fontes:
- [SDK Rust](https://iii.dev/docs/reference/sdk-rust.md)
- [quick_tunnel.go 2025.11.1](https://raw.githubusercontent.com/cloudflare/cloudflared/2025.11.1/cmd/cloudflared/tunnel/quick_tunnel.go)
- [quick_tunnel.go master consultado](https://raw.githubusercontent.com/cloudflare/cloudflared/master/cmd/cloudflared/tunnel/quick_tunnel.go)
- [flags.go: output=json](https://raw.githubusercontent.com/cloudflare/cloudflared/master/cmd/cloudflared/flags/flags.go)
- [logger/create.go: stderr JSON](https://raw.githubusercontent.com/cloudflare/cloudflared/master/logger/create.go)
- [connection/observer.go: registro efetivo](https://raw.githubusercontent.com/cloudflare/cloudflared/2025.11.1/connection/observer.go)
- [supervisor/tunnel.go: desconexão/retry](https://raw.githubusercontent.com/cloudflare/cloudflared/2025.11.1/supervisor/tunnel.go)
- [cmd.go: flags, config e métricas](https://raw.githubusercontent.com/cloudflare/cloudflared/2025.11.1/cmd/cloudflared/tunnel/cmd.go)

## Validação local, sem túnel real

```bash
CARGO_TARGET_DIR="$PWD/target/isolated" cargo fmt --all -- --check
CARGO_TARGET_DIR="$PWD/target/isolated" cargo clippy --locked --all-targets --all-features -- -D warnings
CARGO_TARGET_DIR="$PWD/target/isolated" cargo test --locked --all-features
```

Os testes de processo Unix requerem Python 3 em `/usr/bin/python3` e `/bin/kill`; usam somente fake cloudflared, sem sockets externos. O teste de contratos usa engine WebSocket simulado em loopback. Incluem concorrência/idempotência, leases independentes, expiração autônoma, timeout/limite de retries, crash, reconexão, restart, exclusão de estado, cleanup e sinais. SDK/lockfile usam somente a versão acordada; regenerar lockfile sem pin transitivo pode selecionar duas versões via config-client: mantenha o lockfile entregue.

O worker está registrado no catálogo privado `.deploy/workers.yaml` e nas permissões agregadas. A publicação inicial é para Unix; o ambiente do processo filho e seus testes ainda não suportam Windows. `iii-permissions.yaml` permite somente status sem aprovação, deixando acquire/release no default de aprovação obrigatória. Não há install script que finja prover cloudflared.

## skills

This version ships 1 skill file for agents, also browsable on the worker page:

- [`SKILL.md`](https://workers.iii.dev/workers/quick-tunnel?tab=skills&skill=SKILL.md&version=0.1.1)

### SKILL.md

# Quick Tunnel

Use this worker only when a development webhook service needs a public ephemeral HTTPS URL. Acquiring exposes the entire configured local service publicly: require user approval. Do not run cloudflared or download binaries yourself.

1. Discover and inspect `quick-tunnel::acquire`, `quick-tunnel::release`, `quick-tunnel::status`, and trigger type `quick-tunnel::changed`.
2. Register a bounded changed binding **before** acquiring, filtered by `tunnel_id` (default `webhooks`). Preserve callback metadata and namespace; use the engine's wake primitive for future work.
3. Acquire with a stable opaque `consumer_id`, authorized `tunnel_id`, and future RFC3339 `expires_at`. No origin, port, executable, arguments or credentials are accepted. Repeated acquire renews the same lease, never shortens expiry.
4. Read status once after acquiring to recover races. Use `public_url` only for `ready`. `starting` and `reconnecting` are not failures but are not ready; `failed` includes sanitized diagnostics. Do not poll: react to changed events.
5. New process generations are opaque UUIDs and can change URLs. GitHub integration must update its webhook when the ready generation changes; validate webhook signatures in the HTTP receiver.
6. Release your lease when finished. Never release another consumer's lease. Last lease stops the child; expiration is automatic without an agent. Unregister unneeded bindings.

Prerequisite cloudflared is supplied by the operator, not this worker. Missing prerequisite is a real observable failure. Quick Tunnels have no SLA and are not production endpoints. Status exposes no Cloudflare secrets, but consumer IDs must not contain secrets either.

After engine reconnection reconcile status; events are best-effort and callbacks are not a durable queue. Configuration is central and changes apply on worker restart. Never expose engine ports or broaden the target allowlist autonomously.

## api reference

```json
{
  "functions": [
    {
      "description": "Acquire or extend an expiring durable lease for an operator-authorized local target; returns immediately, possibly starting",
      "metadata": {},
      "name": "quick-tunnel::acquire",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "properties": {
          "consumer_id": {
            "description": "Stable opaque consumer identity; not a credential.",
            "type": "string"
          },
          "expires_at": {
            "description": "Absolute lease expiry in RFC3339, strictly in the future.",
            "format": "date-time",
            "type": "string"
          },
          "tunnel_id": {
            "default": "webhooks",
            "type": "string"
          }
        },
        "required": [
          "consumer_id",
          "expires_at"
        ],
        "title": "AcquireRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Status": {
            "enum": [
              "starting",
              "ready",
              "reconnecting",
              "failed",
              "stopped"
            ],
            "type": "string"
          }
        },
        "description": "A generation is opaque. Compare equality, never lexical ordering.",
        "properties": {
          "error": {
            "description": "Sanitized lifecycle failure, never raw cloudflared output.",
            "type": [
              "string",
              "null"
            ]
          },
          "generation": {
            "type": "string"
          },
          "lease_id": {
            "type": "string"
          },
          "public_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "$ref": "#/definitions/Status"
          },
          "tunnel_id": {
            "type": "string"
          }
        },
        "required": [
          "generation",
          "lease_id",
          "status",
          "tunnel_id"
        ],
        "title": "AcquireResponse",
        "type": "object"
      }
    },
    {
      "description": "Release one lease idempotently; the last lease stops and reaps the tunnel child",
      "metadata": {},
      "name": "quick-tunnel::release",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "properties": {
          "lease_id": {
            "type": "string"
          }
        },
        "required": [
          "lease_id"
        ],
        "title": "ReleaseRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "released": {
            "type": "boolean"
          }
        },
        "required": [
          "released"
        ],
        "title": "ReleaseResponse",
        "type": "object"
      }
    },
    {
      "description": "Read the latest tunnel generation, connection state, public URL and non-secret leases",
      "metadata": {},
      "name": "quick-tunnel::status",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "properties": {
          "tunnel_id": {
            "default": "webhooks",
            "type": "string"
          }
        },
        "title": "StatusRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "CloudflaredCheck": {
            "properties": {
              "error": {
                "description": "Why it is not usable, when `found` is false.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "found": {
                "description": "True when an executable was found and answered `--version`.",
                "type": "boolean"
              },
              "install_url": {
                "description": "Where to install it from; quick-tunnel never installs it.",
                "type": "string"
              },
              "path": {
                "description": "Absolute path quick-tunnel runs.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "version": {
                "description": "First line of `cloudflared --version`.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "found",
              "install_url"
            ],
            "type": "object"
          },
          "Lease": {
            "additionalProperties": false,
            "properties": {
              "consumer_id": {
                "type": "string"
              },
              "expires_at": {
                "format": "date-time",
                "type": "string"
              },
              "lease_id": {
                "type": "string"
              },
              "tunnel_id": {
                "type": "string"
              }
            },
            "required": [
              "consumer_id",
              "expires_at",
              "lease_id",
              "tunnel_id"
            ],
            "type": "object"
          },
          "Prerequisites": {
            "properties": {
              "cloudflared": {
                "$ref": "#/definitions/CloudflaredCheck"
              }
            },
            "required": [
              "cloudflared"
            ],
            "type": "object"
          },
          "Status": {
            "enum": [
              "starting",
              "ready",
              "reconnecting",
              "failed",
              "stopped"
            ],
            "type": "string"
          }
        },
        "description": "A generation is opaque. Compare equality, never lexical ordering.",
        "properties": {
          "error": {
            "description": "Sanitized lifecycle failure, never raw cloudflared output.",
            "type": [
              "string",
              "null"
            ]
          },
          "generation": {
            "type": "string"
          },
          "leases": {
            "items": {
              "$ref": "#/definitions/Lease"
            },
            "type": "array"
          },
          "prerequisites": {
            "anyOf": [
              {
                "$ref": "#/definitions/Prerequisites"
              },
              {
                "type": "null"
              }
            ],
            "description": "Whether the operator-provided cloudflared is usable, and where to get it."
          },
          "public_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "$ref": "#/definitions/Status"
          },
          "tunnel_id": {
            "type": "string"
          }
        },
        "required": [
          "generation",
          "leases",
          "status",
          "tunnel_id"
        ],
        "title": "StatusResponse",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "Tunnel lifecycle snapshot changed; subscribe before acquire, then read status to close races",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "properties": {
          "tunnel_id": {
            "description": "Omit to subscribe to all authorized targets.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "ChangedConfig",
        "type": "object"
      },
      "metadata": {},
      "name": "quick-tunnel::changed",
      "return_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Status": {
            "enum": [
              "starting",
              "ready",
              "reconnecting",
              "failed",
              "stopped"
            ],
            "type": "string"
          }
        },
        "description": "A generation is opaque. Compare equality, never lexical ordering.",
        "properties": {
          "error": {
            "description": "Sanitized lifecycle failure, never raw cloudflared output.",
            "type": [
              "string",
              "null"
            ]
          },
          "generation": {
            "type": "string"
          },
          "public_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "$ref": "#/definitions/Status"
          },
          "tunnel_id": {
            "type": "string"
          }
        },
        "required": [
          "generation",
          "status",
          "tunnel_id"
        ],
        "title": "Snapshot",
        "type": "object"
      }
    }
  ]
}
```
