# 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`. ```sh cargo build --release ``` The binary is written to `target/release/porthole`. Place it on `PATH`, e.g.: ```sh 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 ```sh # 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/.toml` - Runtime state, lock, and log for each open forward: `$XDG_STATE_HOME/porthole/.{json,lock,log}`. State locations can be overridden with the `PORTHOLE_STATE_DIR_OVERRIDE` environment variable. --- ## Shell completions ```sh porthole completions bash > /etc/bash_completion.d/porthole porthole completions zsh > "${fpath[1]}/_porthole" porthole completions fish > ~/.config/fish/completions/porthole.fish ```