7.0 KiB
porthole
Create and manage named SSH port forwards easily.
Porthole wraps ssh to turn tunnel commands into named profiles: define a
forward once, then open, close, and inspect it by name. Each open forward
runs under a small supervisor process that keeps it alive and reconnects
automatically if the connection drops.
Features
- Named profiles for local, remote, and dynamic/SOCKS forwards, saved to disk instead of retyped each time.
- Multi-hop jump chains, built on
ssh -J. - Auto-reconnect with exponential backoff, unless a failure looks permanent (bad auth, host key mismatch, port already bound).
- Status and listing with live state, uptime, and reconnect counts, as text or JSON.
- Import/export of profiles as a single TOML file, for moving a setup to another machine.
- Shell completions for bash, zsh, fish, and others.
- Hardened by default: every spawned
sshcall ignores the caller's own config, runs in batch mode with no interactive prompts, and applies the same hardening to every hop in a jump chain, not just the final target.
Installation
Requires a Rust toolchain and an ssh binary on PATH.
cargo build --release
The binary is written to target/release/porthole. Place it on PATH,
e.g.:
install -Dm755 target/release/porthole ~/.local/bin/porthole
Commands
| Command | Aliases | Description |
|---|---|---|
add |
mk, create |
Save a new forward profile. |
open |
start |
Start a saved forward as a supervised background process. |
close |
stop |
Stop a running forward. |
edit |
change |
Update a saved profile. |
status |
Show detailed status for one forward. | |
list |
ls |
List all saved profiles with live status. |
remove |
rm, delete |
Delete a saved profile. |
wipe |
reset |
Close and delete every forward, tracked or not. |
transfer |
data |
Export saved profiles to a file, or import them from one. |
completions |
Generate a shell completion script. |
Run porthole or porthole --help for the full flag reference.
add / edit flags
One of -l/--local, -r/--remote, or -d/--dynamic selects the forward
kind (required for add, optional for edit):
| Flag | Value | Meaning | Default |
|---|---|---|---|
-l, --local |
[BIND:]PORT:HOST:PORT |
Local forward (this machine -> remote machine). | - |
-r, --remote |
[BIND:]PORT:HOST:PORT |
Remote forward (remote machine -> this machine). | - |
-d, --dynamic |
[BIND:]PORT |
Dynamic forward (SOCKS proxy). | - |
--via |
[USER@]HOST[:PORT] |
Jump-host chain, ending at the connection target. Required. | - |
-u, --user |
USER |
Default user for the target and any hop without one. | - |
-i, --identity |
PATH |
Identity file override. | - |
-p, --port |
PORT |
SSH port of the final target. | 22 |
--reconnect |
BOOL |
Auto-reconnect on connection drop. | true |
--retry-interval |
SECONDS |
Base delay between reconnection attempts. | 5 |
--backoff-max |
SECONDS |
Cap on the doubling reconnection delay. | 60 |
--keepalive |
SECONDS |
SSH ServerAliveInterval. |
15 |
edit only touches the fields given on the command line; everything else
stays as-is.
Examples
# Remote forward, exposing this machine's port 3000 to the remote host.
porthole add expose-app -r 3000:localhost:3000 --via ops@server.example.com
# Dynamic SOCKS proxy.
porthole add socks -d 1080 --via user@gateway
# Two-hop jump chain: bastion1, then bastion2, ending at db-host.
porthole add mydb -l 5432:internal-db:5432 --via bastion1 --via bastion2 -u ops
# Open every profile with reconnect enabled that isn't already running.
porthole open --all
# Run attached in the current shell instead of detaching.
porthole open mydb --foreground
# Force-kill instead of a graceful SIGTERM-then-wait.
porthole close mydb --force
# Machine-readable status/listing.
porthole status mydb --json
porthole list --json
# Back up all profiles, then restore them elsewhere.
porthole transfer --export backup.toml
porthole transfer --import backup.toml
ⓘ Note:
transfer --exportdoes not include identity file contents, only their configured paths; key files need to be copied to the target machine separately.
How a forward stays open
open spawns a detached copy of the porthole binary running an internal
supervisor loop for that one profile. The supervisor:
- Builds and runs the
sshcommand for the profile (-N -Tplus the right-L/-R/-Dflag), holding an advisory file lock for its whole lifetime sostatus/listcan reliably tell if it's still alive. - Watches the connection; once it survives a short grace period it's
reported as
up. - On a dropped or failed connection, classifies the failure:
- Fatal (bad auth, host key mismatch, port already in use): gives up
immediately, state becomes
error. - Known transient (connection refused, DNS failure, timeout): reconnects with exponential backoff.
- Unrecognized: also retries, but gives up after too many consecutive unrecognized failures in a row.
- Fatal (bad auth, host key mismatch, port already in use): gives up
immediately, state becomes
- Resets the backoff delay once a connection has stayed up long enough to be considered stable again.
- Logs
sshoutput to a per-profile log file, rotating it once it grows past 10 MB. - Exits cleanly and removes its own state on
close(SIGTERM) or Ctrl-C in foreground mode (SIGINT).
State on disk
- Profiles:
$XDG_CONFIG_HOME/porthole/profiles/<name>.toml - Runtime state, lock, and log for each open forward:
$XDG_STATE_HOME/porthole/<name>.{json,lock,log}.
State locations can be overridden with the PORTHOLE_STATE_DIR_OVERRIDE environment variable.
Shell completions
porthole completions bash > /etc/bash_completion.d/porthole
porthole completions zsh > "${fpath[1]}/_porthole"
porthole completions fish > ~/.config/fish/completions/porthole.fish