Skip to content
Registry

quick-tunnel

v0.1.1

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

license: Apache-2.0cloudflareleasesquick-tunneltunnelwebhooks

Install

bash
iii trigger compose::add worker=quick-tunnel

Run in your project with the iii engine running. Exact versions are immutable and digest-pinned.

Agent-ready brief for v0.1.1
Install, config, dependencies, readme, skills and API reference, all in one place. Fetch as agent-context.md for an LLM to consume.
The same content rendered as discrete blocks below is exposed as a single markdown document at /workers/quick-tunnel.md. Paste it into an LLM prompt or pipe it through curl from a worker.

Install

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

Configuration

iii-config.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

Dependency · 1Version

Readme

README.md

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 . 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.

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

{"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.

{"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 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:

Validação local, sem túnel real

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

API reference (JSON)

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

Search the registry

Find workers, kits and executables, or jump to a page.