diff --git a/.gitignore b/.gitignore index 3875e01..79a8a58 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target/ *.lock +!Cargo.lock diff --git a/spec/porthole-spec.md b/spec/porthole-spec.md index 02a85bc..4767ac7 100644 --- a/spec/porthole-spec.md +++ b/spec/porthole-spec.md @@ -105,10 +105,18 @@ re-exec of its own binary: supervisor loop inline, attached to the current session. 4. The supervisor's loop: spawn `ssh` with the flags in §3.1, wait on it, classify the exit per §4, sleep/backoff or give up accordingly, and - rewrite the instance file after every state change. `close` sends - SIGTERM to the *supervisor* pid (not raw `ssh`), which forwards it to - its `ssh` child, waits briefly, then exits and removes its lock and - instance file. `--force` skips the wait and SIGKILLs both immediately. + rewrite the instance file after every state change. A signal handler + installed on the supervisor (not exit-code inference on `ssh` itself, + which is unreliable) is what distinguishes an intentional `close` from + a dropped connection: `close` sends SIGTERM to the *supervisor* pid, + whose handler kills its `ssh` child, waits briefly, deletes its lock and + instance file, and exits — any *other* way the `ssh` child ends (any + exit status) is treated as a failure to reconnect from, per §4. Because + `step 2`'s `setsid()` makes the supervisor its own process group leader + and `ssh` inherits that group, `--force` sends SIGKILL to the whole + group (`kill(-pid, SIGKILL)`) rather than just the supervisor pid — a + plain single-pid SIGKILL would leave `ssh` running, orphaned and + untracked, since a killed process can't forward anything to its child. A supervisor that exits on its own due to a fatal failure (§4.2) leaves the instance file in place with `state: error` rather than deleting it, so the failure stays visible to `status`/`list` until the user acts. @@ -124,11 +132,24 @@ regardless of the user's own `~/.ssh/config`: | `-o ExitOnForwardFailure=yes` | Makes `ssh` exit non-zero immediately if the requested forward can't be bound, instead of staying up as a plain (forward-less) session that *looks* healthy. | | `-o ConnectTimeout=10` | Bounds how long one connection attempt can hang before porthole's own backoff logic (§4.1) gets a turn. | | `-N` | No remote command — porthole only ever wants the forward, never a shell. | -| `-J ` | Only when `--via` is set. The comma-joined hop list is passed to `ssh -J` verbatim — porthole does not re-implement jump-host chaining itself (see §5.1). | +| `-J ` + positional target | See below — `--via`'s *last* hop is the actual connection target, not another jump. | `ServerAliveInterval` comes from the profile's `keepalive` field (not hardcoded), so it stays user-tunable. +**`--via` → `ssh` argument translation:** `ssh -J a,b,c` is not itself a +valid invocation — `-J` only ever carries jump hosts *before* the final +hop; `ssh` still needs a positional `destination` to actually connect (and +run the forward from). So porthole splits `--via`'s comma list at the last +entry: everything before it becomes `-J`'s value (omitted entirely if +`--via` has only one hop), and the last entry becomes `ssh`'s positional +target argument. `--via jumpbox` → `ssh ... jumpbox` (no `-J`). `--via +bastion1,bastion2` → `ssh -J bastion1 ... bastion2` (connect through +bastion1, forward runs from bastion2). This is also why `--via` is +**required**, not optional (see §5.1) — there is no other field +representing "the host `ssh` actually connects to"; `--via`'s last hop +*is* that field. + --- ## 4. Reconnect & failure handling @@ -183,7 +204,7 @@ Saves a new profile. Does **not** open it. | `-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` | `[user@]host[:port][,...]` | no | — | SSH jump-host chain, comma-separated; passed to `ssh -J` verbatim (§3.1) | +| `--via` | `[user@]host[:port][,...]` | **yes** | — | Comma-separated hop chain; the *last* hop is the actual `ssh` connection target, any before it are `-J` jumps (§3.1) | | `-u, --user` | `user` | no | current user / ssh_config | Default user for the final target and any `--via` hop that doesn't specify its own | | `-i, --identity` | `path` | no | ssh_config default | | | `-p, --port` | `port` | no | `22` | SSH port on the final target only — a `--via` hop needs its own inline `:port` if it isn't 22 | @@ -202,8 +223,9 @@ error. stored verbatim as whichever `-l/-r/-d` payload was given (without the flag itself) — `kind` records which one it was, so re-deriving the right `-L`/`-R`/`-D` flag at `open` time is a lookup, not a re-parse. -- `--via` hosts resolved/checked against `~/.ssh/config` if present, but - not required to exist there. +- `--via` requires at least one hop (its last entry is the mandatory + connection target, see §3.1); hosts are resolved/checked against + `~/.ssh/config` if present, but not required to exist there. **Examples:** ```