diff --git a/Cargo.lock b/Cargo.lock index 6b91db4..e9b94cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,7 +220,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "porthole" -version = "0.1.0" +version = "1.0.0" dependencies = [ "clap", "clap_complete", diff --git a/Cargo.toml b/Cargo.toml index 1475415..a66db8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "porthole" -version = "0.1.0" +version = "1.0.0" edition = "2021" description = "Create and manage named SSH port forwards." license = "AGPL-3+" diff --git a/README.md b/README.md index 52dbbb0..cf8323a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,166 @@ # porthole -Create and manage named SSH port fowards easily. \ No newline at end of file +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 +``` diff --git a/src/cli.rs b/src/cli.rs index 9ee444b..e0a794c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -12,7 +12,7 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { /// Save a new forward profile. - #[command(visible_alias = "create", visible_alias = "new")] + #[command(visible_alias = "create", visible_alias = "mk")] Add(AddArgs), /// Start a saved forward as a supervised background process. @@ -42,6 +42,7 @@ pub enum Commands { Wipe(WipeArgs), /// Export saved profiles to a file, or import them from one. + #[command(visible_alias = "data")] Transfer(TransferArgs), /// Generate a shell completion script.