Files
holesail-browser/docs/SSH.md
T
Raven Scott f5c349a4a3
CI / Build & Test (push) Successful in 2m56s
docs
2026-02-28 22:29:56 -05:00

263 lines
8.0 KiB
Markdown

# SSH
Holesail Browser includes a full in-browser SSH terminal. It uses a Holesail tunnel to reach the remote host, spawns a real PTY via `tt-native`, and bridges it to an xterm.js terminal in the dashboard over WebSocket.
## Architecture
```
Dashboard (xterm.js)
│ WebSocket ws://127.0.0.1:21000+
ssh-manager.js (WebSocket server)
│ PTY I/O (tt-native / forkpty)
ssh binary (system ssh)
│ TCP
127.0.0.1:20000+ (Holesail client tunnel)
│ P2P / Noise protocol
Remote SSH daemon
```
The `ssh` binary is the system's own SSH client — all its features (key auth, agent forwarding, etc.) work as normal. The Holesail tunnel provides the transport; `ssh` sees it as a plain TCP connection to `127.0.0.1`.
---
## Authentication
SSH authentication is handled entirely by the system `ssh` binary. The following methods are supported, tried in order:
### 1. Public key (automatic, no prompt)
If you have a key in `~/.ssh/` (e.g. `id_rsa`, `id_ed25519`) or loaded in `ssh-agent`, it is tried first. If the remote server accepts it, you are logged in with no prompt at all.
### 2. Password — saved on the connection
If you enter a password when saving the connection, it is stored as base64 (`passwordB64`) in `state.json` and survives native host restarts. When connecting, the password is delivered to SSH automatically via `SSH_ASKPASS` and a named FIFO — no prompt appears and the password is never passed on the command line or in the environment.
### 3. Password — typed interactively
If no public key is available and no password is saved, the terminal displays a password prompt (`[email protected]'s password:`). Type your password and press Enter. The input is masked (no echo).
> **Note:** The native messaging host launched by Chrome has no controlling terminal (`/dev/tty`), so SSH cannot read passwords directly. Holesail Browser handles this transparently using `SSH_ASKPASS` and a named FIFO pipe — the password prompt appears in xterm.js and the typed password is securely forwarded to SSH without exposing it in the process environment or on disk.
### Authentication order summary
| Scenario | What happens |
|----------|-------------|
| Public key in `~/.ssh/` or `ssh-agent` | Logged in silently, no prompt |
| Password saved on connection | Password delivered automatically, no prompt |
| No key, no saved password | Password prompt appears in xterm.js |
---
## Usage
### Saving a connection
1. Open the dashboard → **SSH**
2. Click **Add Connection**
3. Enter a label, the `hs://` key for the remote peer, and the username
4. Optionally enter a password (leave blank to type it each time, or to use a public key)
5. Click **Save** — the connection definition is stored in `state.json`; the password is kept in memory only and is not persisted
### Connecting
1. Click **Connect** next to a saved connection
2. An xterm.js terminal opens in the dashboard and is automatically focused — you can start typing immediately
3. The native host starts a Holesail tunnel to the remote peer, then spawns `ssh`
4. If authentication requires a password and none is saved, a prompt appears in the terminal
### Terminal resize
The terminal resizes automatically when you resize the dashboard window. Resize events are sent to the native host, which calls `ioctl(TIOCSWINSZ)` on the PTY and sends an SSH window-change request to the remote sshd.
### Disconnecting
Click **Disconnect** or close the terminal tab. The PTY and Holesail tunnel are cleaned up.
---
## Native host commands
### `startSshSession`
Start a new SSH session.
**Request payload:**
```json
{
"connectionId": "ssh-abc123",
"hsUrl": "hs://abc123...",
"username": "root",
"passwordB64": "cGFzc3dvcmQ=",
"cols": 80,
"rows": 24,
"label": "My Server"
}
```
`passwordB64` is optional. When present it is a base64-encoded UTF-8 password (`btoa(unescape(encodeURIComponent(password)))`). If omitted or empty, SSH will use public key auth or prompt interactively in the terminal.
**Response payload:**
```json
{
"ok": true,
"sessionId": "ssh-session-1",
"wsPort": 21000,
"tunnelPort": 20000
}
```
### `stopSshSession`
Stop a running SSH session.
**Request payload:**
```json
{ "sessionId": "ssh-session-1" }
```
**Response payload:**
```json
{ "ok": true }
```
### `resizeSshSession`
Resize the terminal.
**Request payload:**
```json
{ "sessionId": "ssh-session-1", "cols": 120, "rows": 40 }
```
**Response payload:**
```json
{ "ok": true }
```
### `getSshSessions`
List active SSH sessions.
**Response payload:**
```json
{
"ok": true,
"sessions": [
{ "sessionId": "ssh-session-1", "connectionId": "ssh-abc123", "wsPort": 21000 }
]
}
```
### `setSshConnections`
Save SSH connection definitions to `state.json`.
**Request payload:**
```json
{
"connections": [
{
"id": "ssh-abc123",
"label": "My Server",
"hsUrl": "hs://abc...",
"username": "root",
"passwordB64": "cGFzc3dvcmQ="
}
]
}
```
`passwordB64` is optional. When present it is a base64-encoded UTF-8 password string (`btoa(unescape(encodeURIComponent(password)))`). It is persisted in `state.json` and delivered to SSH via `SSH_ASKPASS` + a named FIFO on each connection — never via the command line or environment.
---
## WebSocket protocol
The dashboard connects to `ws://127.0.0.1:<wsPort>` after receiving the `startSshSession` response.
SSH is not spawned until the WebSocket client connects and sends a ready-signal (`\x00`). This ensures the terminal is live before SSH starts, so any prompts (password, host key) are visible immediately.
**Browser → native host:**
- First byte `\x00`: ready-signal (triggers SSH spawn)
- Binary frames: raw terminal input (keystrokes, paste)
- Text frames: JSON control messages
```json
{ "type": "resize", "cols": 120, "rows": 40 }
```
**Native host → browser:**
- Binary frames: raw PTY output (terminal data)
---
## Port allocation
| Range | Usage |
|-------|-------|
| 20000+ | Holesail client tunnel (one per session) |
| 21000+ | WebSocket server (one per session) |
Ports are allocated sequentially and released when the session ends.
---
## Saved connections
SSH connections are stored in `state.json` under `sshConnections`. If a password was saved, it is stored as `passwordB64` (base64-encoded) and survives native host restarts.
```json
{
"sshConnections": [
{
"id": "ssh-abc123",
"label": "My Server",
"hsUrl": "hs://abc123...",
"username": "root",
"passwordB64": "cGFzc3dvcmQ="
}
]
}
```
`passwordB64` is omitted when no password was saved for the connection.
---
## Requirements
- The `ssh` binary must be installed on the machine running the native host
- macOS: included with the OS
- Linux: install `openssh-client`
- Windows: install OpenSSH via Windows Optional Features or Git for Windows
- The remote host must be running an SSH daemon accessible via a Holesail `hs://` key
---
## Troubleshooting
**Connection times out**
The remote peer may be offline or the `hs://` key may be wrong. Check `~/.holesail-browser/holesail-browser.log` for tunnel errors.
**`ssh: command not found`**
The `ssh` binary is not in the PATH seen by the native host. Install OpenSSH or ensure `ssh` is in `/usr/bin/ssh` (macOS/Linux) or available in `PATH` (Windows).
**Permission denied (publickey,password)**
- If you have a public key but it is not accepted by the server, SSH falls back to password auth and shows a prompt in the terminal
- If you enter the wrong password, SSH exits with "Permission denied" — reconnect and try again
- If no authentication method works, check that the username is correct and that the server allows password auth
**Password prompt does not appear**
This should not happen with the current implementation. If it does, check `~/.holesail-browser/holesail-browser.log` for `WARNING: askpass setup failed` — this would indicate that `mkfifo` is not available on the system.
**Terminal display is garbled**
Try resizing the dashboard window to trigger a resize event, or disconnect and reconnect.