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