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

11 KiB

Native Host Protocol

The native host communicates with the browser extension via Chrome's native messaging protocol: each message is a UTF-8 JSON object prefixed with a 4-byte little-endian length. Maximum message size is 1 MB.

The extension sends requests; the native host sends responses and events.

Message format

Request (extension → native host)

{ "id": 42, "type": "<command>", "payload": { ... } }

Response (native host → extension)

{ "id": 42, "type": "response", "payload": { ... } }

Responses always echo the id from the request. If an error occurred, the payload contains an error field:

{ "id": 42, "type": "response", "payload": { "error": "Tunnel failed to connect" } }

Event (native host → extension, unsolicited)

{ "type": "event", "event": "<eventName>", "payload": { ... } }

Commands

getState

Returns the full current state of the native host. The extension calls this on startup and after reconnecting.

Response payload:

{
  "ok": true,
  "servers": [ { "id": "server_1", "port": 3000, "hsUrl": "hs://...", "state": "ready" } ],
  "virtualHosts": [ { "hostname": "myapp.hole.sail", "hsUrl": "hs://...", "state": "ready", "localPort": 19000 } ],
  "serviceTunnels": [ { "id": "svc-1", "label": "Postgres", "hsUrl": "hs://...", "localPort": 5432, "state": "ready" } ],
  "sshConnections": [ { "id": "ssh-abc", "label": "My Server", "hsUrl": "hs://...", "username": "root" } ],
  "rdpConnections": [ { "id": "rdp-abc", "label": "Work PC", "hsUrl": "hs://...", "type": "vnc", "port": 5900 } ],
  "settings": { "proxyPort": 8443, "connectProxyPort": 8442, "readyTimeoutMs": 0, "notifyOnDisconnect": true, "debug": false, "disableOnFileUrls": false, "backupRetention": 5 },
  "proxyPort": 8443,
  "connectProxyPort": 8442,
  "caInstalled": true
}

setVirtualHost

Add or update a virtual host. If the hostname already exists, the old tunnel is closed and a new one is started.

Hostnames can use any private TLD — not just .hole.sail. The hostname must have at least 3 labels and must not use a real public TLD. Any depth is supported (e.g. i.love.hole.sail, api.v2.my.internal).

After a successful response, the background service worker automatically updates the PAC script to include the new TLD and requests host permissions for it.

Request payload:

{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." }

Custom TLD examples:

{ "hostname": "api.haha.wooo", "hsUrl": "hs://abc123..." }
{ "hostname": "i.love.hole.sail", "hsUrl": "hs://abc123..." }

Response payload:

{ "ok": true, "hostname": "myapp.hole.sail", "localHost": "127.0.0.1", "localPort": 19000, "state": "ready" }

removeVirtualHost

Remove a virtual host and close its tunnel.

Request payload:

{ "hostname": "myapp.hole.sail" }

Response payload:

{ "ok": true }

startServer

Start a server tunnel (expose a local port as an hs:// key).

Request payload:

{
  "port": 3000,
  "host": "127.0.0.1",
  "secure": true,
  "udp": false,
  "label": "My Web App"
}

label is optional. When provided it is shown in the dashboard and persisted in state.json.

Response payload:

{ "ok": true, "serverId": "server_1", "url": "hs://abc123...", "port": 3000, "host": "127.0.0.1", "secure": true, "udp": false, "label": "My Web App" }

stopServer

Stop a server tunnel.

Request payload:

{ "serverId": "server_1" }

Response payload:

{ "ok": true }

startServiceTunnel

Start a service tunnel (forward a remote hs:// peer to a local TCP port).

Request payload:

{
  "tunnelId": "svc-1",
  "label": "Postgres",
  "hsUrl": "hs://abc123...",
  "localPort": 5432
}

Response payload:

{ "ok": true, "tunnelId": "svc-1", "localPort": 5432, "state": "ready" }

stopServiceTunnel

Stop a service tunnel.

Request payload:

{ "tunnelId": "svc-1" }

Response payload:

{ "ok": true }

updateServiceTunnel

Stop and restart a service tunnel with the same (or updated) parameters. Used by the dashboard Reconnect button for tunnels in error or closed state.

Request payload:

{
  "tunnelId": "svc-1",
  "label": "Postgres",
  "hsUrl": "hs://abc123...",
  "localPort": 5432
}

Response payload: Same as startServiceTunnel.

{ "ok": true, "tunnelId": "svc-1", "localPort": 5432, "state": "ready" }

lookupTunnel

Check whether an hs:// key is reachable on the DHT.

Request payload:

{ "hsUrl": "hs://abc123..." }

Response payload:

{ "ok": true, "reachable": true }

startSshSession

Start an 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)))). It is delivered to SSH via SSH_ASKPASS and a named FIFO — never written to disk separately or passed on the command line. If omitted, SSH tries public key auth first; if that fails, a password prompt appears in the xterm.js terminal.

Response payload:

{ "ok": true, "sessionId": "ssh-session-1", "wsPort": 21000, "tunnelPort": 20000 }

stopSshSession

Stop an SSH session.

Request payload:

{ "sessionId": "ssh-session-1" }

Response payload:

{ "ok": true }

resizeSshSession

Resize the SSH 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.

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 stored in state.json and delivered to SSH via SSH_ASKPASS + a named FIFO — never via the command line or environment.

Response payload:

{ "ok": true }

startRdpSession

Start a VNC or RDP session.

Request payload:

{
  "connectionId": "rdp-abc123",
  "hsUrl": "hs://abc123...",
  "type": "vnc",
  "port": 5900,
  "width": 1280,
  "height": 720,
  "username": ""
}

Response payload:

{ "ok": true, "sessionId": "rdp-session-1", "wsPort": 23000, "tunnelPort": 22000 }

stopRdpSession

Stop a VNC or RDP session.

Request payload:

{ "sessionId": "rdp-session-1" }

Response payload:

{ "ok": true }

getRdpSessions

List active VNC/RDP sessions.

Response payload:

{
  "ok": true,
  "sessions": [ { "sessionId": "rdp-session-1", "connectionId": "rdp-abc123", "type": "vnc", "wsPort": 23000 } ]
}

setRdpConnections

Save RDP/VNC connection definitions.

Request payload:

{
  "connections": [
    {
      "id": "rdp-abc123",
      "label": "Work PC",
      "hsUrl": "hs://abc...",
      "type": "vnc",
      "port": 5900,
      "width": 1280,
      "height": 720,
      "username": "",
      "passwordB64": "cGFzc3dvcmQ="
    }
  ]
}

passwordB64 is optional. When present it is a base64-encoded UTF-8 password string. It is stored in state.json and used automatically when starting a session.

Response payload:

{ "ok": true }

installRootCA

Install the root CA into the OS trust store.

Request payload: {}

Response payload:

{ "ok": true }

On failure:

{ "ok": false, "error": "Could not install CA: ..." }

updateSettings

Update one or more settings.

Request payload:

{
  "proxyPort": 8443,
  "connectProxyPort": 8442,
  "readyTimeoutMs": 30000,
  "notifyOnDisconnect": true,
  "debug": false,
  "disableOnFileUrls": false,
  "backupRetention": 5
}

All fields are optional — only the provided fields are updated.

Response payload:

{ "ok": true, "settings": { ... }, "requiresRestart": false }

requiresRestart is true if proxyPort or connectProxyPort were changed — the dashboard displays a warning in this case.


createBackup

Create a backup archive.

Response payload:

{ "ok": true, "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz" }

listBackups

List available backups.

Response payload:

{
  "ok": true,
  "backups": [
    { "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz", "size": 12345, "createdAt": "2026-02-28T12:00:00.000Z" }
  ]
}

restoreBackup

Restore a backup.

Request payload:

{ "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz" }

Response payload:

{ "ok": true }

deleteBackup

Delete a backup.

Request payload:

{ "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz" }

Response payload:

{ "ok": true }

getLogs

Get recent log lines from the native host log file.

Request payload:

{ "lines": 100 }

Response payload:

{ "ok": true, "logs": "...\n[2026-02-28T12:00:00.000Z] ..." }

Events

Events are sent by the native host without a corresponding request. The extension broadcasts them to subscribed tabs.

tunnelReady

A tunnel connected successfully.

{
  "type": "event",
  "event": "tunnelReady",
  "payload": { "hostname": "myapp.hole.sail", "hsUrl": "hs://...", "localHost": "127.0.0.1", "localPort": 19000 }
}

tunnelClosed

A tunnel disconnected.

{
  "type": "event",
  "event": "tunnelClosed",
  "payload": { "hostname": "myapp.hole.sail" }
}

tunnelError

A tunnel failed to connect.

{
  "type": "event",
  "event": "tunnelError",
  "payload": { "hostname": "myapp.hole.sail", "error": "Tunnel ready timeout after 30000ms" }
}

connection

A peer connected to a server tunnel.

{
  "type": "event",
  "event": "connection",
  "payload": { "connId": "conn-1", "swarmId": "server_1", "peerInfo": { "publicKey": "abc123..." } }
}

error

A connection error occurred.

{
  "type": "event",
  "event": "error",
  "payload": { "connId": "conn-1", "message": "Connection reset" }
}

end

A connection ended.

{
  "type": "event",
  "event": "end",
  "payload": { "connId": "conn-1" }
}

Tunnel states

State Description
connecting Tunnel is being established
ready Tunnel is connected and serving requests
error Tunnel failed to connect (see tunnelError event)
closed Tunnel was closed (removed or native host restarted)