Compare commits
1 Commits
develop
...
11302572e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 11302572e0 |
275
spec/porthole-spec.md
Normal file
275
spec/porthole-spec.md
Normal file
@@ -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/<name>.toml`
|
||||
- Runtime state: `~/.local/state/porthole/<name>.json` (written by supervisor, not hand-edited)
|
||||
- Logs: `~/.local/state/porthole/<name>.log`
|
||||
|
||||
---
|
||||
|
||||
## 3. Commands
|
||||
|
||||
### 3.1 `porthole add <name> [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 <name> [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 <name> [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 <name> [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 `<name>` 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 <name>`
|
||||
|
||||
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 <name>`
|
||||
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 <shell>`
|
||||
|
||||
Generates a shell completion script. `<shell>` ∈ `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.
|
||||
Reference in New Issue
Block a user