Tunnels
A tunnel makes a loopback-bound container port on a machine reachable from
your machine, over iroh. It opens no port on the machine's network
interfaces and needs no firewall change: the only listener it creates is on
your side, bound to 127.0.0.1.
It is a local forward: the ssh -L shape, not a reverse tunnel. Every
connection is dialed local → remote; the machine's worker never dials back
to you.
ant nest tunnel --machine prod --container app-local --port 8080
# Forwarding 127.0.0.1:8080 -> prod container app-local:8080 over iroh; Ctrl-C to stop
curl http://127.0.0.1:8080
How it works
your machine (CLI or ant ui) remote machine (ant-worker)
┌───────────────────────────────┐ ┌──────────────────────────────────────────┐
│ curl 127.0.0.1:18080 │ │ │
│ │ │ iroh │ net.Dial("127.0.0.1:8080") │
│ ▼ │ stream │ ▲ │
│ listener on 127.0.0.1:18080 ─┼──────────────►│ worker ─┴─► app container │
│ ▲ │◄──────────────┼─── bytes both ways │
└────────┴──────────────────────┘ └──────────────────────────────────────────┘
- Your side (the
antCLI process, or theant uiprocess for the dashboard's Tunnel button) binds127.0.0.1:<local>, the only port opened. - Something on your machine connects to that port. For each connection, ant
dials the worker over iroh (a raw stream on the
ant-tunnel/1ALPN, separate from RPC framing) and writes the target linecontainer:port. - The worker authorizes the caller against the machine's roster, resolves the target to a port ant published on the machine's loopback for a container ant manages, and dials it locally.
- Bytes are piped both ways, your local connection ↔ iroh stream ↔ the container. Responses retrace the same path.
The container port is the target, not the published host port. A service
published as 127.0.0.1:18080->80/tcp is reached with --port 80.
CLI
ant nest tunnel --machine prod --container app-local --port 8080
ant nest tunnel --machine prod --container app-local --port 8080 --local 18080
| Flag | Meaning |
|---|---|
--machine M | The machine to tunnel to. Required; the local machine is refused (reach it directly). |
--container NAME | An ant-managed container name, as shown by ant nest containers list --machine M. |
--port P | The container port to reach (not the host port). |
--local L | Local port to listen on. Defaults to --port. |
Ctrl-C closes the listener and every live connection. The tunnel lives as long
as the process does; an established connection is not cut for being quiet.
Dashboard
On a remote machine's page (Nest → machine → Containers), a running container's row has a Tunnel action:
- the container port is prefilled from the published ports; a local port is optional (a free one is picked when empty);
- open tunnels for that machine are listed with a clickable
http://127.0.0.1:<port>link and a Stop button; - the listener is created by the
ant uiprocess, so the URL resolves only on the machine running the dashboard, and creation is refused for a non-loopback caller (a browser on another device gets a clear error rather than an unreachable port).
What can be the target
- A registered remote machine. Local-machine tunnels are refused.
- An ant-managed container, running and publishing the requested port on
the machine's loopback. Deploys publish loopback-only by default
(
deploy.publish: loopback), so this is the normal case. - A container published on every interface (
deploy.publish: all, or--publish-all) has no loopback binding, so the worker refuses it. That is not a loss: reach it directly over the network instead. - Tunneling to a stopped container fails; the dashboard only offers the action on a running container's row.
The worker refuses anything else. A tunnel can never reach the docker socket, Caddy's admin API, SSH, or another local listener; see Security model.
Security
- Opening a tunnel needs the deploy capability on that machine
(
deployer,ci,admin, orowner), the same trust as running a container there. The worker authorizes every stream; a denied one is recorded in the audit log, and a successful open is recorded astunnel.openwith thecontainer:portdetail. - The caller authenticates with their account NodeID; there is no password and no inbound port.
- The local listener binds
127.0.0.1only, so other devices cannot use the tunnel even if they can reach your machine.
Limits and troubleshooting
| Symptom | Cause / fix |
|---|---|
invalid container name | Use the name from ant nest containers list --machine M. |
container "x" does not publish port N on loopback | Wrong port (pass the container port, not the host port), the container is stopped, or it was published on all interfaces (connect directly instead). |
local port 18080: ... address already in use | Another process (or tunnel) owns the port; pick another or omit --local. |
| Tunnel opens, requests fail immediately | The port is published but the app inside is not listening on it; check the container's logs. |
| No Tunnel button in the dashboard | The machine is the local one, the container is stopped, or the dashboard binary predates the feature (ant ui logs the version). |
| 403 "tunnels are managed from the machine running this dashboard" | The dashboard is bound non-loopback and you are using it from another device; run ant nest tunnel on that device, or use the dashboard on its host. |
transport: unsupported / "iroh unavailable" | The binary was built without the transport (CGO_ENABLED=0); remote features need the Linux + CGO build (see Transport). |
Each accepted connection opens one tunnel connection to the worker, closed when either end ends. Interactive use is fine; a workload that opens hundreds of concurrent connections is better served by deploying the service normally.
Compared with ssh -L
ant nest tunnel | ssh -L L:localhost:P host | |
|---|---|---|
| Listener | Local 127.0.0.1 only | Local only (by default) |
| Initiator | Local | Local |
| Authentication | Account NodeID + machine roster role | SSH keys/accounts |
| Target | An ant-managed container's published loopback port | Any host/port the SSH server can dial |
| Machine-side agent | ant-worker (no SSH required) | sshd |
There is no reverse mode: to expose a service on your machine to the remote
host, ant has no equivalent of ssh -R.