@@ -0,0 +1,278 @@
|
||||
# Architecture
|
||||
|
||||
Holesail Browser is a two-layer system: a browser extension and a native messaging host. Together they let the browser navigate `hs://` P2P URLs as if they were ordinary HTTPS sites, expose local services as Holesail tunnels, provide SSH access, connect to remote desktops (VNC/RDP), and manage backups — all through the browser.
|
||||
|
||||
## Layer Overview
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Browser Extension (Manifest V3) │
|
||||
│ │
|
||||
│ content.js ──────► background.js (service worker) │
|
||||
│ (event relay) │ chrome.runtime.connectNative │
|
||||
│ │ PAC proxy script (*.hole.sail) │
|
||||
└────────────────────────┼─────────────────────────────────────────┘
|
||||
│ stdin / stdout (4-byte LE + JSON)
|
||||
┌────────────────────────▼─────────────────────────────────────────┐
|
||||
│ Native Host (Bare runtime) │
|
||||
│ │
|
||||
│ index.mjs → messenger.js → host.js │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ holesail-manager.js │ │
|
||||
│ │ ├── Server tunnels (expose local port as hs:// URL) │ │
|
||||
│ │ ├── Virtual hosts (hs:// URL → 127.0.0.1:19000+) │ │
|
||||
│ │ └── Service tunnels (hs:// URL → user-chosen port) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ https-proxy.js 127.0.0.1:8443 (TLS termination) │
|
||||
│ connect-proxy.js 127.0.0.1:8442 (HTTP CONNECT, PAC target) │
|
||||
│ certificate-authority.js (Holesail Browser CA, *.hole.sail) │
|
||||
│ ssh-manager.js (Holesail tunnel → PTY → WebSocket) │
|
||||
│ rdp-manager.js (Holesail tunnel → VNC/RDP → WebSocket) │
|
||||
│ backup-manager.js (tar.gz backup/restore of state + certs) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Extension Components
|
||||
|
||||
### `manifest.json`
|
||||
|
||||
Manifest V3. Key permissions:
|
||||
|
||||
| Permission | Purpose |
|
||||
|-----------|---------|
|
||||
| `nativeMessaging` | Connect to `com.holesail.browser` native host |
|
||||
| `proxy` | Set PAC script to route `*.hole.sail` traffic |
|
||||
| `declarativeNetRequest` | Redirect `*.host.test` typos to an error page |
|
||||
| `scripting` | Inject scripts into pages if needed |
|
||||
| `storage` | Persist proxy port state for fast PAC startup |
|
||||
| `tabs` | Track tab lifecycle |
|
||||
| `notifications` | Notify on native host disconnect |
|
||||
|
||||
Host permissions cover `*://*.hole.sail/*` and `*://*.host.local/*`.
|
||||
|
||||
### `background.js` — Service Worker
|
||||
|
||||
The central hub of the extension. Responsibilities:
|
||||
|
||||
- **Native messaging** — opens a port to `com.holesail.browser` via `chrome.runtime.connectNative`. Reconnects with exponential backoff (100 ms → 30 s) on disconnect.
|
||||
- **PAC proxy** — sets a Proxy Auto-Config script that routes all `*.hole.sail` traffic through `127.0.0.1:8442` (the CONNECT proxy). Re-applies if another extension overrides it.
|
||||
- **Message routing** — forwards requests from the dashboard to the native host and routes events back to the correct tab.
|
||||
- **State relay** — propagates `settings` and `sshConnections`/`rdpConnections` from the native host's `getState` response to the dashboard.
|
||||
|
||||
### `content.js` — Content Script
|
||||
|
||||
Injected at `document_start` on every page. Subscribes to native host events and relays `holesail-host-disconnect` to the page via a `CustomEvent`.
|
||||
|
||||
### `dashboard.html` / `dashboard.js`
|
||||
|
||||
Full management UI opened when the extension icon is clicked. Pages:
|
||||
|
||||
| Page | Description |
|
||||
|------|-------------|
|
||||
| **Overview** | Host connection status, active connections, uptime |
|
||||
| **Virtual Hosts** | Add/remove `hs://` URL → `*.hole.sail` hostname mappings |
|
||||
| **Server Tunnels** | Expose local ports as `hs://` URLs (TCP or UDP) |
|
||||
| **Service Tunnels** | Forward `hs://` URLs to local TCP ports |
|
||||
| **Proxy & CA** | Proxy status, CA installation |
|
||||
| **SSH** | Manage saved SSH connections, launch xterm.js terminal sessions |
|
||||
| **Remote Desktop** | Manage saved VNC/RDP connections, launch viewer sessions |
|
||||
| **Backups** | Take, restore, and delete tar.gz backups |
|
||||
| **Logs** | Live log stream from the native host |
|
||||
| **Settings** | Extension options (proxy ports, debug, backup retention, etc.) |
|
||||
|
||||
---
|
||||
|
||||
## Native Host Components
|
||||
|
||||
The native host runs under the [Bare](https://github.com/nicolo-ribaudo/bare) runtime — a minimal JavaScript runtime for native addons, distinct from Node.js.
|
||||
|
||||
### `index.mjs` — Entry Point
|
||||
|
||||
Sets up `bare-process` globals, creates the messenger, and calls `handleMessage` (from `host.js`) for each incoming message. Handles `SIGTERM`/`SIGINT` for graceful shutdown (closes all tunnels, SSH sessions, and RDP sessions).
|
||||
|
||||
### `messenger.js` — Native Messaging Protocol
|
||||
|
||||
Implements the Chrome/Firefox native messaging framing: each message is a 4-byte little-endian length prefix followed by a UTF-8 JSON body. Maximum message size is 1 MB. Reads from `process.stdin`, writes to `process.stdout`.
|
||||
|
||||
### `host.js` — Command Handler
|
||||
|
||||
Handles all message types from the extension. On startup it:
|
||||
|
||||
1. Starts the HTTPS proxy on port 8443
|
||||
2. Starts the CONNECT proxy on port 8442
|
||||
3. Restores persisted tunnels from `holesail-browser-storage/state.json`
|
||||
|
||||
Full command reference: see [NATIVE-HOST.md](NATIVE-HOST.md).
|
||||
|
||||
### `holesail-manager.js` — Tunnel Manager + State
|
||||
|
||||
Manages three tunnel types and owns all persistent state:
|
||||
|
||||
| Type | Direction | Port allocation |
|
||||
|------|-----------|----------------|
|
||||
| Server tunnel | local port → `hs://` URL | none (uses the local port you specify) |
|
||||
| Virtual host | `hs://` URL → `127.0.0.1:19000+` | auto-allocated from 19000 |
|
||||
| Service tunnel | `hs://` URL → user-chosen local port | user-specified |
|
||||
|
||||
Also manages:
|
||||
- **Settings** — `getSettings()` / `updateSettings(patch)` with defaults and persistence
|
||||
- **SSH connections** — `getSshConnections()` / `setSshConnections(list)` (saved connection metadata, not active sessions)
|
||||
- **RDP connections** — `getRdpConnections()` / `setRdpConnections(list)` (saved connection metadata)
|
||||
|
||||
State is persisted to `holesail-browser-storage/state.json` after every change and restored on startup.
|
||||
|
||||
### `https-proxy.js` — HTTPS Reverse Proxy
|
||||
|
||||
Listens on `127.0.0.1:8443` with a wildcard `*.hole.sail` TLS certificate signed by the local CA. For each request:
|
||||
|
||||
1. Reads the `Host` header
|
||||
2. Calls `holesailManager.getLocalBackend(hostname)` → `{ host, port }`
|
||||
3. Proxies the HTTP request to that backend (the Holesail client tunnel)
|
||||
|
||||
### `connect-proxy.js` — HTTP CONNECT Proxy
|
||||
|
||||
Listens on `127.0.0.1:8442`. Accepts browser `CONNECT hostname:443` requests, replies `200 Connection established`, then pipes the raw TCP stream to `127.0.0.1:8443`. This is what the PAC script points to, because browsers send CONNECT for HTTPS targets.
|
||||
|
||||
### `certificate-authority.js` — CA and Certificate Management
|
||||
|
||||
On startup, generates (or loads) a 2048-bit RSA root CA named `Holesail Browser CA` with a 10-year validity, stored in `holesail-browser-certs/`. Signs a wildcard `*.hole.sail` certificate (1-year validity) for the HTTPS proxy. Can install the root CA into:
|
||||
|
||||
- **macOS** — login keychain via `security add-trusted-cert` (prompts for password via osascript)
|
||||
- **Linux** — `/usr/local/share/ca-certificates/` + `update-ca-certificates`
|
||||
- **Windows** — ROOT store via `certutil -addstore`
|
||||
|
||||
### `ssh-manager.js` — SSH Session Manager
|
||||
|
||||
For each SSH session:
|
||||
|
||||
1. Allocates a port from 20000+ and starts a Holesail client tunnel
|
||||
2. Spawns `ssh` with a real PTY via `tt-native` (`forkpty(3)`)
|
||||
3. Starts a `bare-ws` WebSocket server on a port from 21000+
|
||||
4. Bridges PTY ↔ WebSocket so the xterm.js terminal in the dashboard can connect
|
||||
|
||||
PTY resize (`pty.resize(cols, rows)`) calls `ioctl(TIOCSWINSZ)`, which SSH forwards as an SSH window-change request to the remote sshd.
|
||||
|
||||
### `rdp-manager.js` — Remote Desktop Session Manager
|
||||
|
||||
For each remote desktop session:
|
||||
|
||||
1. Allocates a tunnel port from 22000+ and starts a Holesail client tunnel
|
||||
2. Starts a `bare-ws` WebSocket server on a port from 23000+
|
||||
3. Dispatches based on protocol:
|
||||
- **VNC** — connects a `bare-tcp` socket to the tunnel, pipes raw bytes between the TCP socket and the WebSocket. The browser uses noVNC's RFB client for all protocol handling and rendering.
|
||||
- **RDP** — creates a `node-rdpjs-2` RDP client connecting to the tunnel. Converts RDP bitmap updates to JSON and sends them to the browser. Receives mouse/keyboard input as JSON from the browser.
|
||||
|
||||
See [REMOTE-DESKTOP.md](REMOTE-DESKTOP.md) for full documentation.
|
||||
|
||||
### `backup-manager.js` — Backup Manager
|
||||
|
||||
Creates and manages `tar.gz` backups of both the storage directory and the certificates directory:
|
||||
|
||||
- Backups are stored in `holesail-browser-storage/backups/`
|
||||
- Each archive contains a `storage/` prefix (state data) and a `certs/` prefix (CA and domain certificates)
|
||||
- Supports create, list, restore, delete, and prune-by-retention operations
|
||||
- Uses the system `tar` binary via `child_process.spawn`
|
||||
|
||||
See [BACKUP.md](BACKUP.md) for full documentation.
|
||||
|
||||
---
|
||||
|
||||
## Message Flow
|
||||
|
||||
### Dashboard → Native Host (request)
|
||||
|
||||
```
|
||||
Dashboard: fetch state / add virtual host / start tunnel
|
||||
→ chrome.runtime.sendMessage({ action: 'getState' / 'send', payload })
|
||||
→ background.js: onMessage → send() → port.postMessage (native messaging)
|
||||
→ messenger.js: 4-byte length + JSON → process.stdout
|
||||
→ host.js: handleMessage → holesailManager / sshManager / rdpManager / backupManager
|
||||
→ messenger.js: response → process.stdout
|
||||
→ background.js: port.onMessage → pending.get(id).resolve(payload)
|
||||
→ dashboard: receives response
|
||||
```
|
||||
|
||||
### Native Host → Extension (tunnel events)
|
||||
|
||||
```
|
||||
Holesail tunnel connects
|
||||
→ holesail-manager.js: emit('tunnelReady', { hostname, localPort })
|
||||
→ host.js: send({ type: 'event', event: 'tunnelReady', payload })
|
||||
→ messenger.js: stdout
|
||||
→ background.js: port.onMessage → tabs.sendMessage(tabId, { type: 'holesail-event' })
|
||||
→ dashboard: updates UI
|
||||
```
|
||||
|
||||
### Browser Navigating to `myapp.hole.sail`
|
||||
|
||||
```
|
||||
Browser: DNS lookup for myapp.hole.sail
|
||||
→ PAC script: return "PROXY 127.0.0.1:8442"
|
||||
→ Browser: CONNECT myapp.hole.sail:443 → 127.0.0.1:8442
|
||||
→ connect-proxy: "200 Connection established" → pipe to 127.0.0.1:8443
|
||||
→ https-proxy: TLS handshake with *.hole.sail cert
|
||||
→ https-proxy: Host: myapp.hole.sail → getLocalBackend() → 127.0.0.1:19000
|
||||
→ https-proxy: HTTP request → 127.0.0.1:19000
|
||||
→ Holesail client tunnel: P2P connection to remote peer
|
||||
→ Remote peer: HTTP response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Port Assignments
|
||||
|
||||
| Port range | Component | Purpose |
|
||||
|------------|-----------|---------|
|
||||
| 8443 | `https-proxy.js` | TLS-terminating HTTPS reverse proxy |
|
||||
| 8442 | `connect-proxy.js` | HTTP CONNECT proxy (PAC target) |
|
||||
| 19000+ | `holesail-manager.js` | Virtual host Holesail client tunnels |
|
||||
| 20000–20999 | `ssh-manager.js` | Holesail client tunnels for SSH sessions |
|
||||
| 21000–21999 | `ssh-manager.js` | WebSocket servers for xterm.js |
|
||||
| 22000–22999 | `rdp-manager.js` | Holesail client tunnels for RDP/VNC sessions |
|
||||
| 23000–23999 | `rdp-manager.js` | WebSocket servers for noVNC / RDP viewer |
|
||||
|
||||
---
|
||||
|
||||
## Data Persistence
|
||||
|
||||
`native-host/holesail-browser-storage/state.json` stores all application state:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"settings": {
|
||||
"proxyPort": 8443,
|
||||
"connectProxyPort": 8442,
|
||||
"readyTimeoutMs": 0,
|
||||
"notifyOnDisconnect": true,
|
||||
"debug": false,
|
||||
"disableOnFileUrls": false,
|
||||
"backupRetention": 5
|
||||
},
|
||||
"nextServerId": 3,
|
||||
"nextServiceTunnelId": 2,
|
||||
"servers": [
|
||||
{ "id": "server_1", "port": 3000, "host": "127.0.0.1", "secure": true, "udp": false }
|
||||
],
|
||||
"virtualHosts": [
|
||||
{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." }
|
||||
],
|
||||
"serviceTunnels": [
|
||||
{ "id": "svc-1", "label": "Postgres", "hsUrl": "hs://def456...", "localPort": 5432 }
|
||||
],
|
||||
"sshConnections": [
|
||||
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://xyz...", "username": "admin" }
|
||||
],
|
||||
"rdpConnections": [
|
||||
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://xyz...", "type": "vnc", "port": 5900 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All state is owned by the native host. The extension reads state via `getState` / `getSettings` / `getSshConnections` / `getRdpConnections` and writes it via `updateSettings` / `setSshConnections` / `setRdpConnections`.
|
||||
|
||||
On first run after upgrading from a previous version, `holesail-persist.json` is automatically migrated to `state.json` and the old file is removed.
|
||||
Reference in New Issue
Block a user