How a request from the public internet reaches your local upstream: the SSH connection to sish, the tunnel daemon, Caddy, and your process. Plus the diagnostic commands for each layer.
A tunnel is a reverse SSH connection from your machine to a
tunnel server (sish-compatible). The server takes incoming HTTPS,
routes by subdomain, and forwards down the SSH connection to your
local Caddy. This page is the layer-by-layer view.
For the operator-facing flow (asd net apply, asd expose),
start at cli/net and
cli/expose.
| Layer | Where it runs | What it does |
|---|---|---|
| Browser | The user's machine | DNS resolves <subdomain>-<client-id>.<tunnel-host> to the tunnel server's IP. Initiates HTTPS. |
| Tunnel server | Wherever you/we host it (sish-compatible) | Terminates TLS. Maps incoming <subdomain>-<client-id> to the right reverse-SSH connection. Forwards plain HTTP down the connection. |
| Tunnel daemon | Your machine, in the asd binary | Holds the SSH connection to the tunnel server. One process per registered service (multiplexed onto one SSH connection per destination). |
| Caddy | Your machine, in the asd binary | Local reverse proxy. Matches the Host: header to a route, applies headers / auth / compression, forwards to upstream. |
| Upstream | Your machine (or container) | Your actual process — Vite dev server, Rails app, whatever. Sees a plain HTTP request from 127.0.0.1. |
No port forwarding. Your machine doesn't accept inbound — it
initiates a single outbound SSH connection. Works behind any
NAT, corporate firewall, or CGNAT.
TLS terminates at the tunnel server. Your local Caddy speaks
HTTP to the upstream and to the tunnel server. The tunnel server
has the public cert (Let's Encrypt or your own CA). Two
consequences:
X-Forwarded-Proto: "https" (set by Caddy)reference/net-manifest § Rules.One SSH connection per destination, multiplexed. If three
services on your machine are all on tunnel.public: true to the
same tunnel server, they share one SSH connection. The tunnel
server demultiplexes by subdomain.
| Property | Value |
|---|---|
| Binary | asd-tunnel (bundled inside the asd distribution) |
| Lifecycle | Started by asd net apply (one per service with tunnel.public: true). Stopped by asd net expose stop --id=<id> or asd net expose reset. |
| State | Tracked in registry.json (PID, started-at, healthy/unhealthy). |
| Restart on .env change | Yes — fingerprint-based: if the tunnel's parameters change, the daemon restarts automatically (safe for tunnels, unlike Caddy). |
| Communication | Speaks the sish-compatible reverse-SSH protocol. Default tunnel server: an asd-managed sish instance; set ASD_TUNNEL_HOST to override. |
Tunnel daemons are independent processes (you can kill -9 one
without affecting the others). The wedged-daemon recovery is
asd net expose start --id=<id> (or --reset for everything).
| Component | Owner | What happens if it fails |
|---|---|---|
DNS for *.<tunnel-host> |
The tunnel server's operator | Browsers can't find the tunnel. Nothing on your machine can fix this. |
| Tunnel server (sish) | Whoever hosts it (you, the asd team, a third party) | All tunnels to it go down. Switching servers requires asd server set + re-apply. |
| Reverse-SSH connection | asd tunnel daemon | Browser sees connection refused. asd net expose start --id=<id> to recover. |
| Caddy route | asd, applied from manifests | Browser sees 404 / 502. asd net apply to re-apply. |
| Upstream process | You | Caddy returns 502. Start the process. |
# Are credentials valid for the tunnel server?
asd auth status
# Which tunnels are configured?
asd expose list
# Detailed state for one service
asd net discover | jq '.runtime["<id>"]'
# Verify everything end-to-end (returns non-zero on failure)
asd net verify
# Force-restart a wedged tunnel
asd net expose start --id=<id>
# Restart all tunnels (last resort)
asd net expose reset
For the full decision-tree-style debug walkthrough, see
cookbook/debug-a-broken-route.
asd's tunnel daemons speak the sish protocol. To run your own
tunnel server:
ASD_TUNNEL_HOST=your.tunnel.example.com in your machine's.env.asd auth refresh to get credentials from the new server.asd net apply to bring tunnels up against the new destination.Switching tunnel servers is a low-friction operation — manifests
don't reference the server by name (the ${{ macro.tunnelEndpoint() }}
macro reads it from your credentials).
architecture — where tunnels fit in the whole.registry — where tunnel state is recorded.credential-model — what authenticates the SSH connection.cli/expose + cli/net — the operator surface.