Remote

Tunnels

Reach a managed container port from your machine: no inbound port on either side.

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                       │
 └────────┴──────────────────────┘               └──────────────────────────────────────────┘
  1. Your side (the ant CLI process, or the ant ui process for the dashboard's Tunnel button) binds 127.0.0.1:<local>, the only port opened.
  2. Something on your machine connects to that port. For each connection, ant dials the worker over iroh (a raw stream on the ant-tunnel/1 ALPN, separate from RPC framing) and writes the target line container:port.
  3. 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.
  4. 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
FlagMeaning
--machine MThe machine to tunnel to. Required; the local machine is refused (reach it directly).
--container NAMEAn ant-managed container name, as shown by ant nest containers list --machine M.
--port PThe container port to reach (not the host port).
--local LLocal 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 ui process, 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, or owner), 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 as tunnel.open with the container:port detail.
  • The caller authenticates with their account NodeID; there is no password and no inbound port.
  • The local listener binds 127.0.0.1 only, so other devices cannot use the tunnel even if they can reach your machine.

Limits and troubleshooting

SymptomCause / fix
invalid container nameUse the name from ant nest containers list --machine M.
container "x" does not publish port N on loopbackWrong 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 useAnother process (or tunnel) owns the port; pick another or omit --local.
Tunnel opens, requests fail immediatelyThe port is published but the app inside is not listening on it; check the container's logs.
No Tunnel button in the dashboardThe 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 tunnelssh -L L:localhost:P host
ListenerLocal 127.0.0.1 onlyLocal only (by default)
InitiatorLocalLocal
AuthenticationAccount NodeID + machine roster roleSSH keys/accounts
TargetAn ant-managed container's published loopback portAny host/port the SSH server can dial
Machine-side agentant-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.

Copyright © 2026