8.0 KiB
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 usingSSH_ASKPASSand 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
- Open the dashboard → SSH
- Click Add Connection
- Enter a label, the
hs://key for the remote peer, and the username - Optionally enter a password (leave blank to type it each time, or to use a public key)
- Click Save — the connection definition is stored in
state.json; the password is kept in memory only and is not persisted
Connecting
- Click Connect next to a saved connection
- An xterm.js terminal opens in the dashboard and is automatically focused — you can start typing immediately
- The native host starts a Holesail tunnel to the remote peer, then spawns
ssh - 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:
{
"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:
{
"ok": true,
"sessionId": "ssh-session-1",
"wsPort": 21000,
"tunnelPort": 20000
}
stopSshSession
Stop a running SSH session.
Request payload:
{ "sessionId": "ssh-session-1" }
Response payload:
{ "ok": true }
resizeSshSession
Resize the terminal.
Request payload:
{ "sessionId": "ssh-session-1", "cols": 120, "rows": 40 }
Response payload:
{ "ok": true }
getSshSessions
List active SSH sessions.
Response payload:
{
"ok": true,
"sessions": [
{ "sessionId": "ssh-session-1", "connectionId": "ssh-abc123", "wsPort": 21000 }
]
}
setSshConnections
Save SSH connection definitions to state.json.
Request payload:
{
"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
{ "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.
{
"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
sshbinary 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.