Skip to content
Login

Firepuncher

Every tunnel described elsewhere in these docs is inbound: the internet reaches a web app on your network. Firepuncher inverts that. Your laptop dials the relay, the relay pairs it with one of your agents, and a port on your laptop becomes a port inside your network.

That makes it work for things a browser can’t reach: SSH, Postgres, Redis, RDP, VNC — anything that speaks TCP.

Terminal window
hle fp --agent rpi --to 22 --port 9922
Forwarding 127.0.0.1:9922 → rpi → localhost:22
Agent allows: localhost:*
Try: ssh -p 9922 <user>@127.0.0.1
Ctrl+C to stop.

Then, in another terminal:

Terminal window
ssh -p 9922 root@localhost

Which machine is which

This is the one genuinely confusing part, so it’s worth being blunt about it. The word localhost appears on both sides of a firepuncher session and means a different machine each time.

WhereResolved byExample
--tothe agent--to 22 is the agent’s own sshd
--tothe agent--to 192.168.1.50:5432 is a box on the agent’s LAN
--port / --bindyour laptop--port 9922 opens 127.0.0.1:9922 locally
ssh … root@localhostyour laptopconnects to the port you just opened

Read --to as “as the agent would type it”. You are naming a target from inside your network, from a terminal outside it.

Requirements

An enrolled, running agent on the far end — see Agent — and an API key for yourself (hle auth login). The agent needs no firepuncher-specific setup.

Targets

--to accepts a bare port or host:port:

--toMeans
22port 22 on the agent’s own loopback
localhost:22the same thing, written out
192.168.1.50:5432port 5432 on that host, as resolved by the agent
nas:8096a hostname the agent can resolve
[::1]:22IPv6 literals must be bracketed

The local port

--port is optional. Left out, the local port is the target port + 9000 (so 229022, 543214432), which keeps the mapping guessable and off privileged ports.

--bind defaults to 127.0.0.1. Widening it exposes the forward to your whole local network, which throws away much of the point — think before changing it.

The allowlist

An agent only forwards to targets it has been told to allow. The allowlist is per agent and stored on the server — there is nothing to configure on the agent’s machine. It is pushed down the agent’s control connection when it connects and again whenever you change it.

A fresh agent with no rules configured allows its own loopback on any port. That is why SSH-ing to the agent’s own machine needs no setup at all.

Manage rules in the dashboard under Connections → Firepuncher.

A rule is a host plus an optional port. Omitting the port allows any port on that host.

RuleAllows
localhost:22only SSH on the agent itself
localhostany port on the agent itself
192.168.1.50:5432only Postgres on that host
192.168.1.50any port on that host

localhost, 127.0.0.1, ::1, and ip6-localhost are treated as the same target, so a rule for one can’t be side-stepped by spelling it another way.

When a target isn’t allowed, the connection is refused and the CLI tells you what is allowed:

Refused: 192.168.1.50:5432 is not allowed. Allowed: localhost:*

hle fp also prints the agent’s allowlist in its start-up banner, so you can see what you can reach before you try it.

Keeping a forward up

hle fp reconnects on its own. The local port is bound once and held for the lifetime of the command, so a relay restart or a dropped network doesn’t take the port away — connections in flight when the link drops are closed (the far end is genuinely gone), but the listener stays ready.

For a forward that should always be there, install it as a service:

Terminal window
hle service install --fp --agent-name rpi --to 22 --port 9922

The API key is never written into the service file; it is read at runtime from ~/.config/hle/config.toml or HLE_API_KEY.

Using it with SSH

Add the forwarded port to ~/.ssh/config and the indirection disappears:

Host rpi
HostName localhost
Port 9922
User root
Terminal window
ssh rpi

Security

  • Nothing listens on the public internet. Both ends dial out to the relay: your laptop with an API key, the agent with its agent token. There is no inbound port on either side, and nothing to firewall.
  • The agent decides what is reachable, not the client. A caller who asks for a target that isn’t allowed is refused by the agent itself.
  • The local listener is loopback-only by default, so other machines on your network can’t use your forward.
  • Traffic is carried over the same encrypted WebSocket transport as the rest of HLE. Firepuncher does not terminate or inspect the stream — SSH’s own end-to-end encryption is unaffected.

The forward log

Every forward is recorded when it closes: what it reached, when it opened, how long it lasted, how many bytes went each way, and how it ended — a clean close, the client going away, or the agent dropping. It is on the Firepuncher page under the allowlist, one log per agent.

Only the payload is counted, and only its size. Firepuncher does not inspect the stream, so the log can say how much went to 10.0.0.9:22 and never what.

A forward still running is not in the log yet — it is in the rules table above, which shows live sessions. The log is kept for the same window as a tunnel’s access log, and is bounded by the same retention setting.

Flags

FlagDefaultMeaning
--agentrequiredAgent name or id to forward through
--torequiredTarget as the agent sees it: HOST:PORT, or a bare port
--porttarget port + 9000Local port to listen on
--bind127.0.0.1Local address to bind
--api-keyenv / configAPI key, if not already saved
--relay-hosthle.worldRelay host
--relay-port443Relay port

Troubleshooting

Missing option '--agent' — both --agent and --to are required.

Refused: … is not allowed — the target isn’t in that agent’s allowlist. Add it in Connections → Firepuncher, and remember that your first rule replaces the loopback default.

No API key — run hle auth login, or pass --api-key.

“Waiting for it to come back…” — the agent is offline. hle fp holds the local port and reconnects by itself once the agent returns.

could not bind 127.0.0.1:9922 — something else already has that local port. Pick another with --port.