# 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) ```json { "id": 42, "type": "", "payload": { ... } } ``` ### Response (native host → extension) ```json { "id": 42, "type": "response", "payload": { ... } } ``` Responses always echo the `id` from the request. If an error occurred, the payload contains an `error` field: ```json { "id": 42, "type": "response", "payload": { "error": "Tunnel failed to connect" } } ``` ### Event (native host → extension, unsolicited) ```json { "type": "event", "event": "", "payload": { ... } } ``` --- ## Commands ### `getState` Returns the full current state of the native host. The extension calls this on startup and after reconnecting. **Response payload:** ```json { "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:** ```json { "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." } ``` Custom TLD examples: ```json { "hostname": "api.haha.wooo", "hsUrl": "hs://abc123..." } { "hostname": "i.love.hole.sail", "hsUrl": "hs://abc123..." } ``` **Response payload:** ```json { "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:** ```json { "hostname": "myapp.hole.sail" } ``` **Response payload:** ```json { "ok": true } ``` --- ### `startServer` Start a server tunnel (expose a local port as an `hs://` key). **Request payload:** ```json { "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:** ```json { "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:** ```json { "serverId": "server_1" } ``` **Response payload:** ```json { "ok": true } ``` --- ### `startServiceTunnel` Start a service tunnel (forward a remote `hs://` peer to a local TCP port). **Request payload:** ```json { "tunnelId": "svc-1", "label": "Postgres", "hsUrl": "hs://abc123...", "localPort": 5432 } ``` **Response payload:** ```json { "ok": true, "tunnelId": "svc-1", "localPort": 5432, "state": "ready" } ``` --- ### `stopServiceTunnel` Stop a service tunnel. **Request payload:** ```json { "tunnelId": "svc-1" } ``` **Response payload:** ```json { "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:** ```json { "tunnelId": "svc-1", "label": "Postgres", "hsUrl": "hs://abc123...", "localPort": 5432 } ``` **Response payload:** Same as `startServiceTunnel`. ```json { "ok": true, "tunnelId": "svc-1", "localPort": 5432, "state": "ready" } ``` --- ### `lookupTunnel` Check whether an `hs://` key is reachable on the DHT. **Request payload:** ```json { "hsUrl": "hs://abc123..." } ``` **Response payload:** ```json { "ok": true, "reachable": true } ``` --- ### `startSshSession` Start an 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)))`). 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:** ```json { "ok": true, "sessionId": "ssh-session-1", "wsPort": 21000, "tunnelPort": 20000 } ``` --- ### `stopSshSession` Stop an SSH session. **Request payload:** ```json { "sessionId": "ssh-session-1" } ``` **Response payload:** ```json { "ok": true } ``` --- ### `resizeSshSession` Resize the SSH 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. **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 stored in `state.json` and delivered to SSH via `SSH_ASKPASS` + a named FIFO — never via the command line or environment. **Response payload:** ```json { "ok": true } ``` --- ### `startRdpSession` Start a VNC or RDP session. **Request payload:** ```json { "connectionId": "rdp-abc123", "hsUrl": "hs://abc123...", "type": "vnc", "port": 5900, "width": 1280, "height": 720, "username": "" } ``` **Response payload:** ```json { "ok": true, "sessionId": "rdp-session-1", "wsPort": 23000, "tunnelPort": 22000 } ``` --- ### `stopRdpSession` Stop a VNC or RDP session. **Request payload:** ```json { "sessionId": "rdp-session-1" } ``` **Response payload:** ```json { "ok": true } ``` --- ### `getRdpSessions` List active VNC/RDP sessions. **Response payload:** ```json { "ok": true, "sessions": [ { "sessionId": "rdp-session-1", "connectionId": "rdp-abc123", "type": "vnc", "wsPort": 23000 } ] } ``` --- ### `setRdpConnections` Save RDP/VNC connection definitions. **Request payload:** ```json { "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:** ```json { "ok": true } ``` --- ### `installRootCA` Install the root CA into the OS trust store. **Request payload:** `{}` **Response payload:** ```json { "ok": true } ``` On failure: ```json { "ok": false, "error": "Could not install CA: ..." } ``` --- ### `updateSettings` Update one or more settings. **Request payload:** ```json { "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:** ```json { "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:** ```json { "ok": true, "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz" } ``` --- ### `listBackups` List available backups. **Response payload:** ```json { "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:** ```json { "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz" } ``` **Response payload:** ```json { "ok": true } ``` --- ### `deleteBackup` Delete a backup. **Request payload:** ```json { "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz" } ``` **Response payload:** ```json { "ok": true } ``` --- ### `getLogs` Get recent log lines from the native host log file. **Request payload:** ```json { "lines": 100 } ``` **Response payload:** ```json { "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. ```json { "type": "event", "event": "tunnelReady", "payload": { "hostname": "myapp.hole.sail", "hsUrl": "hs://...", "localHost": "127.0.0.1", "localPort": 19000 } } ``` ### `tunnelClosed` A tunnel disconnected. ```json { "type": "event", "event": "tunnelClosed", "payload": { "hostname": "myapp.hole.sail" } } ``` ### `tunnelError` A tunnel failed to connect. ```json { "type": "event", "event": "tunnelError", "payload": { "hostname": "myapp.hole.sail", "error": "Tunnel ready timeout after 30000ms" } } ``` ### `connection` A peer connected to a server tunnel. ```json { "type": "event", "event": "connection", "payload": { "connId": "conn-1", "swarmId": "server_1", "peerInfo": { "publicKey": "abc123..." } } } ``` ### `error` A connection error occurred. ```json { "type": "event", "event": "error", "payload": { "connId": "conn-1", "message": "Connection reset" } } ``` ### `end` A connection ended. ```json { "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) |