Skip to content

feat(server): Managed Tunnels (Cloudflare + ngrok) for webhook ingress behind NAT - #45

Merged
dvcdsys merged 2 commits into
developfrom
feat/managed-tunnels
May 20, 2026
Merged

feat(server): Managed Tunnels (Cloudflare + ngrok) for webhook ingress behind NAT#45
dvcdsys merged 2 commits into
developfrom
feat/managed-tunnels

Conversation

@dvcdsys

@dvcdsys dvcdsys commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

Built-in, dashboard-managed outbound tunnel so a NAT'd server (the RTX 3090 box) gets a public URL for GitHub webhook delivery — no inbound ports, no env feature flag. We rejected gh webhook forward (dev/test only) in favour of production-grade agents. Provider-pluggable: Cloudflare Tunnel and ngrok both implemented.

What's included

Tunnel engine (server/internal/tunnels)

  • Provider abstraction + Manager with runtime Apply (start/stop/reconfigure), mirroring the llama-server sidecar supervisor (exec, exit-watcher, bounded crash-restart, graceful SIGTERM→SIGKILL).
  • Cloudflare (named/quick) and ngrok (reserved/ephemeral — authtoken always required; public URL parsed from the agent's JSON log).
  • Webhook reconciler: re-points webhook_mode=auto repos at the live tunnel URL on boot and whenever it changes; new githubapi.UpdateWebhook (PATCH). buildWebhookURL now prefers the live tunnel URL over CIX_PUBLIC_URL.

Config in DB, managed from the dashboard (no env flag)

  • tunnel_config single-row table (migration Bump actions/download-artifact from 4 to 8 #8); connector token encrypted via secrets.
  • tunnelcfg service + GET/PUT /api/v1/tunnels/config (admin); provider-aware validation. Only deployment infra stays in env (binary paths, metrics addr, startup timeout).

Binary management

  • GET /api/v1/tunnels/binaries reports installed/path/version/managed.
  • Missing locally → dashboard shows manual install instructions (brew/linux) with a note that Docker bundles them automatically.
  • CIX_TUNNEL_BIN_MANAGED (true in the Docker images) enables Install/Update — the Go server downloads agents (raw or .tgz, no shell, distroless-safe) into a writable /data/tunnel-bin, atomic rename. POST /api/v1/tunnels/binaries/{provider}/{install,update}.

Dashboard

  • New admin-only Managed Tunnels section: provider select, mode, hostname, token, live status, Test connection / Restart, binary install/update.
  • GitHub Tokens → GitHub Integration with Tokens + Webhook Integrations tabs.

Docker — both images bundle cloudflared + ngrok and enable managed updates.

Security notes (for reviewers)

  • A tunnel to localhost:<port> exposes the whole server, not just the webhook path. Webhook is HMAC-protected; the rest is not. Recommend Cloudflare named mode with an ingress rule restricting to /api/v1/webhooks/github/*; quick/ephemeral modes are dev-only.
  • cloudflared/ngrok add binary + CVE surface → run make scout-cpu / make scout-cuda (0 HIGH/CRITICAL gate) before any prod push.

Test plan

  • go build/vet/test ./... green; unit tests for UpdateWebhook, manager Apply, reconciler, ngrok URL parse, installer (tgz/raw/asset URLs)
  • dashboard tsc -b && vite build green; dist/.gitkeep preserved
  • boot smoke: config CRUD, provider switch, validation (400), missing-binary → clean failed (not 500), managed vs unmanaged binaries (409 when disabled)
  • Live: cloudflared/ngrok actually establish a tunnel and the /health round-trip succeeds (needs the agents installed + network)
  • Docker: image builds (confirm bundled cloudflared version pin + ngrok channel) and make scout-* clean
  • Named-mode: reserved domain / Cloudflare account path verified manually

🤖 Generated with Claude Code

dvcdsys and others added 2 commits May 20, 2026 13:31
…s behind NAT

Adds a built-in, dashboard-managed outbound tunnel so a NAT'd server gets a
public URL for GitHub webhook delivery — no inbound ports, no env feature flag.

Backend (internal/tunnels):
- Provider abstraction + Manager with runtime Apply (start/stop/reconfigure),
  mirroring the llama-server sidecar supervisor (exec, exit-watcher, bounded
  crash-restart, graceful SIGTERM->SIGKILL).
- Cloudflare provider (named/quick) and ngrok provider (reserved/ephemeral;
  authtoken always required; URL parsed from the agent JSON log).
- Webhook reconciler: re-points webhook_mode=auto repos at the live tunnel URL
  on boot and on URL change; new githubapi.UpdateWebhook (PATCH).
- buildWebhookURL prefers the live tunnel URL over CIX_PUBLIC_URL.

Config in DB, managed from the dashboard (no env flag):
- tunnel_config single-row table (migration #8); token encrypted via secrets.
- tunnelcfg service + GET/PUT /api/v1/tunnels/config (admin); provider-aware
  validation. Only deployment infra stays in env (binary paths, metrics addr).

Binary management:
- GET /api/v1/tunnels/binaries reports installed/path/version/managed.
- When missing locally, the dashboard shows manual install instructions
  (brew/linux) noting Docker bundles them automatically.
- CIX_TUNNEL_BIN_MANAGED (true in the Docker images) enables Install/Update:
  the server downloads agents (raw or .tgz, no shell) into a writable
  /data/tunnel-bin via POST /api/v1/tunnels/binaries/{provider}/{install,update}.

Dashboard:
- New admin-only "Managed Tunnels" section (provider select, mode, hostname,
  token, status, Test/Restart, binary install/update).
- "GitHub Tokens" -> "GitHub Integration" with Tokens + Webhook Integrations tabs.

Docker: both images bundle cloudflared + ngrok and enable managed updates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…, races

- High: require admin on TestTunnel/RestartTunnel/ReconcileWebhooks — these
  decrypt PATs, register GitHub webhooks, restart the subprocess, or probe
  outbound, and were only behind requireAuth (any authenticated user).
  Hide the dashboard "Re-register webhooks" button from non-admins.
- Medium: pass the Cloudflare/ngrok tokens via env (TUNNEL_TOKEN /
  NGROK_AUTHTOKEN) instead of argv so they don't show in ps/proc cmdline.
- Medium: installer no longer tracks "latest" — pin cloudflared version
  (synced with the Dockerfile arg); compute the download SHA-256, verify it
  against a pinned map when present and warn (with the sum) when not; cap the
  download/extraction size to guard against a decompression bomb.
- Low: serialize Reconcile (mutex) so the boot double-reconcile can't create
  duplicate GitHub hooks; serialize Manager.Apply so concurrent applies can't
  orphan a provider; RestartTunnel uses a background-derived ctx so a client
  disconnect doesn't abort the spawn (matching UpdateTunnelConfig); capture
  readySignal under the lock before close to avoid a restart-time race.
- Nits: drop stale CIX_TUNNEL_ENABLED wording from handler messages/comments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@dvcdsys
dvcdsys merged commit 09dfd2e into develop May 20, 2026
1 check passed
@dvcdsys
dvcdsys deleted the feat/managed-tunnels branch May 20, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant