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 ssh call 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 --export does 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 ssh command for the profile (-N -T plus the right -L/-R/-D flag), holding an advisory file lock for its whole lifetime so status/list can 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.
  • Resets the backoff delay once a connection has stayed up long enough to be considered stable again.
  • Logs ssh output 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
Description
Create and manage named SSH port fowards easily.
Readme AGPL-3.0 306 KiB
2026-08-14 18:55:05 +00:00
Languages
Rust 74.1%
Shell 25.9%