diff --git a/spec/porthole-spec.md b/spec/porthole-spec.md new file mode 100644 index 0000000..0115869 --- /dev/null +++ b/spec/porthole-spec.md @@ -0,0 +1,275 @@ +# `porthole` — Spec v0.1 + +Named, managed SSH port forwards. Wraps `ssh -L/-R/-D` so forwards are +addressable by name instead of by PID, terminal tab, or shell history. + +--- + +## 1. Overview + +**Problem:** SSH forwards are anonymous and ephemeral. They die when a +terminal closes, when a laptop sleeps, or when a network blips — silently, +with no notification. There's no built-in way to list what's currently +forwarded, and multi-hop / reverse forwards have enough flag surface that +people end up hand-rolling shell aliases per-tunnel. + +**Solution:** Persist forward definitions as named profiles. Run forwards as +a supervised background process (not tied to a shell session), with +auto-reconnect, health status, and a single command to see everything that's +open. + +**Non-goals:** Not a replacement for a VPN or a full SOCKS/proxy manager. +Not a secrets manager — SSH auth still comes from your existing SSH config, +agent, or identity files. No GUI. + +--- + +## 2. Data model + +### 2.1 Profile + +A saved definition. Does not imply anything is running. + +| Field | Type | Notes | +|------------------|-----------|------------------------------------------| +| `name` | string | Unique key. `[a-z0-9_-]+`. | +| `kind` | enum | `local` \| `remote` \| `dynamic` | +| `mapping` | string | Raw `ssh -L/-R/-D`-style spec, see §3.1 | +| `via` | string[] | Ordered list of hops for multi-hop jumps | +| `user` | string? | Defaults to current user / `ssh_config` | +| `identity` | path? | Identity file override | +| `ssh_port` | int | Default `22` | +| `reconnect` | bool | Default `true` | +| `retry_interval` | int (sec) | Default `5` | +| `keepalive` | int (sec) | `ServerAliveInterval`, default `15` | +| `created_at` | timestamp | | +| `updated_at` | timestamp | | + +### 2.2 Instance (runtime state) + +Exists only while a profile is open. Tracked separately from the profile so +`list`/`status` can report live data without touching the saved definition. + +| Field | Type | Notes | +|---------------------|------------|---------------------------------------------| +| `name` | string | FK to profile | +| `pid` | int | Supervisor process PID, not raw `ssh` PID | +| `state` | enum | `up` \| `down` \| `reconnecting` \| `error` | +| `opened_at` | timestamp | | +| `last_error` | string? | Most recent failure message, if any | +| `reconnect_count` | int | Since last manual `open` | +| `last_reconnect_at` | timestamp? | | + +### 2.3 Storage + +- Profiles: `~/.config/porthole/profiles/.toml` +- Runtime state: `~/.local/state/porthole/.json` (written by supervisor, not hand-edited) +- Logs: `~/.local/state/porthole/.log` + +--- + +## 3. Commands + +### 3.1 `porthole add [flags]` +Aliases: `create`, `new` + +Saves a new profile. Does **not** open it. + +| Flag | Arg | Required | Default | Description | +|--------------------|-----------------------------|-------------------|---------------------------|---------------------------------------| +| `-l, --local` | `[bind:]port:host:hostport` | one of `-l/-r/-d` | — | Local forward: your machine → remote | +| `-r, --remote` | `[bind:]port:host:hostport` | one of `-l/-r/-d` | — | Remote forward: remote → your machine | +| `-d, --dynamic` | `[bind:]port` | one of `-l/-r/-d` | — | Dynamic forward (SOCKS proxy) | +| `--via` | `host` | yes | — | SSH target; multiple for multi-hop | +| `-u, --user` | `user` | no | current user / ssh_config | | +| `-i, --identity` | `path` | no | ssh_config default | | +| `-p, --port` | `port` | no | `22` | SSH port on final target | +| `--reconnect` | `bool` | no | `true` | Auto-reconnect on drop | +| `--retry-interval` | `seconds` | no | `5` | Delay between reconnect attempts | +| `--keepalive` | `seconds` | no | `15` | `ServerAliveInterval` | + +Exactly one of `-l`/`-r`/`-d` is required. Providing more than one is an +error. + +**Validation:** +- `name` must not already exist (use `edit` to modify). +- `mapping` port syntax validated against the same grammar `ssh` accepts. +- `--via` hosts resolved/checked against `~/.ssh/config` if present, but not + required to exist there. + +**Examples:** +``` +porthole add db --local 5432:db.internal:5432 --via jumpbox +porthole add admin-ui --local 8080:localhost:8080 --via bastion1,bastion2 --user ops +porthole add webhook --remote 9000:localhost:3000 --via public-vps +porthole add proxy --dynamic 1080 --via edge-host +``` + +--- + +### 3.2 `porthole open [flags]` +Alias: `start` + +Starts a saved forward as a background-supervised process. + +| Flag | Description | +|--------------------|-------------------------------------------------------------------------| +| `-f, --foreground` | Run attached in current shell instead of daemonizing. Ctrl-C closes it. | +| `--once` | Open without auto-reconnect, regardless of profile setting. | + +**Behavior:** +- If already open: no-op, print current status, exit 0. +- If port bind fails (already in use): exit non-zero with the conflicting + process info if discoverable (`lsof`-style lookup), don't silently retry. +- Spawns a supervisor process that owns the underlying `ssh` subprocess, + watches for exit, and reconnects per profile settings. + +**Examples:** +``` +porthole open db +porthole open db --foreground +porthole open proxy --once +``` + +--- + +### 3.3 `porthole close [flags]` +Alias: `stop` + +Stops a running forward. Profile definition is untouched. + +| Flag | Description | +|-----------|--------------------------------------------------------| +| `--force` | SIGKILL immediately instead of graceful SIGTERM + wait | + +**Examples:** +``` +porthole close db +porthole close db --force +``` + +--- + +### 3.4 `porthole edit [flags]` + +Updates a saved profile. Accepts the same flags as `add` (all optional — +only provided flags are changed). Does not restart a running instance +automatically; changes apply on next `open`. + +**Examples:** +``` +porthole edit db --retry-interval 10 +porthole edit db --local 5433:db.internal:5432 +``` + +If `` is currently running, print a warning that changes won't take +effect until the next `open` (or offer `--restart` — see §6 open questions). + +--- + +### 3.5 `porthole status ` + +Deep-dive health for one forward. + +**Output includes:** +- Profile summary (kind, mapping, via, user) +- Current state (`up` / `down` / `reconnecting` / `error`) +- Uptime since last successful connect +- Reconnect count and timestamp of last reconnect +- Last error message, if any +- Path to log file + +**Example:** +``` +porthole status db +``` + +--- + +### 3.6 `porthole list` +Alias: `ls` + +All saved profiles with live status. Fast, scannable — no deep diagnostics +(use `status` for that). + +**Columns:** `NAME KIND MAPPING VIA STATE UPTIME` + +**Flags:** + +| Flag | Description | +|-------------|-----------------------------------| +| `--running` | Show only currently-open forwards | +| `--json` | Machine-readable output | + +**Example:** +``` +porthole list +porthole list --running +``` + +--- + +### 3.7 `porthole remove ` +Aliases: `rm`, `delete` + +Deletes a saved profile. Closes it first if running. + +| Flag | Description | +|------------------|-------------------------------------------------------------------| +| `--keep-running` | Delete the profile but leave an active instance running untracked | + +--- + +### 3.8 `porthole wipe` +Alias: `reset` + +Closes and deletes **every** forward, including any `ssh` processes matching +porthole's supervisor signature that aren't in the current profile store +(e.g. orphaned after a crash). Confirmation prompt unless `--yes`. + +| Flag | Description | +|-------------|--------------------------| +| `-y, --yes` | Skip confirmation prompt | + +--- + +### 3.9 `porthole completions ` + +Generates a shell completion script. `` ∈ `bash`, `zsh`, `fish`. + +--- + +## 4. Global options + +| Flag | Description | +|-----------------|---------------| +| `-h, --help` | Print help | +| `-V, --version` | Print version | + +--- + +## 5. Exit codes + +| Code | Meaning | +|------|-----------------------------------------------| +| `0` | Success | +| `1` | Generic error | +| `2` | Profile not found | +| `3` | Profile already exists (`add` without `edit`) | +| `4` | Port bind conflict on `open` | +| `5` | SSH auth/connection failure | +// TODO: fix these up, just use 0/1/2 and output a good error code +// ERR_IDENT (ERR_NUM): Err_Msg + +--- + +## 6. Open questions for v0.2 + +- Should `edit` on a running profile support `--restart` to apply + immediately, or stay explicit (`edit` then `close`/`open`)? +- Multi-hop `--via` — do we shell out to `ssh -J`, or manage a chain of + supervised hops ourselves for finer-grained per-hop status? +- Templating (`dbtun`-style): saved "kind" templates (e.g. `--template + postgres` implies port 5432) — worth adding as sugar over `add`, or scope + creep? +- Config export/import for moving profiles between machines.