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

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 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:

{
  "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 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.