first commit
CI / Build & Test (push) Has been cancelled

This commit is contained in:
Raven Scott
2026-02-27 18:13:59 -05:00
commit d58a0b6e2d
64 changed files with 30550 additions and 0 deletions
+278
View File
@@ -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 |
| 2000020999 | `ssh-manager.js` | Holesail client tunnels for SSH sessions |
| 2100021999 | `ssh-manager.js` | WebSocket servers for xterm.js |
| 2200022999 | `rdp-manager.js` | Holesail client tunnels for RDP/VNC sessions |
| 2300023999 | `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.
+196
View File
@@ -0,0 +1,196 @@
# Backup
Holesail Browser includes a built-in backup system that creates `tar.gz` archives of all persistent state and certificates. Backups can be taken manually from the Dashboard or triggered programmatically. Old backups are automatically pruned based on a configurable retention count.
---
## What Is Backed Up
Each backup archive contains two directories:
| Archive path | Source | Contents |
|-------------|--------|----------|
| `storage/` | `holesail-browser-storage/` | `state.json` (tunnels, settings, connections), excluding the `backups/` subdirectory itself |
| `certs/` | `holesail-browser-certs/` | Root CA key and certificate, wildcard `*.hole.sail` key and certificate |
Restoring a backup replaces both the state data and the certificates. If the CA certificate changes after a restore, you will need to reinstall it via Dashboard → Proxy & CA → Install CA.
---
## Backup Storage
Backups are stored in:
```
native-host/holesail-browser-storage/backups/
```
Each backup file is named with a timestamp:
```
holesail-backup-2026-02-27T14-30-00-000Z.tar.gz
```
---
## Using Backups via the Dashboard
### Taking a Backup
1. Dashboard → **Backups**
2. Click **Take Backup**
3. The new backup appears in the table with its filename, size, and creation date
### Restoring a Backup
1. Dashboard → **Backups**
2. Find the backup you want to restore and click **Restore**
3. Confirm the restore in the dialog — this will overwrite current state and certificates
4. After a successful restore, the native host reloads `state.json` automatically
5. If certificates were restored, go to Dashboard → Proxy & CA → Install CA to reinstall the CA
### Deleting a Backup
1. Dashboard → **Backups**
2. Find the backup you want to delete and click **Delete**
3. Confirm the deletion in the dialog
---
## Backup Retention
By default, Holesail Browser keeps the **5 most recent backups**. Older backups are automatically deleted after each new backup is created.
To change the retention count:
1. Dashboard → **Settings**
2. Change **Backup Retention** (minimum: 1)
3. Click **Save Settings**
The new retention count takes effect on the next backup creation.
---
## Native Host Commands
### `createBackup`
Create a new backup and prune old backups based on the current `backupRetention` setting.
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"filename": "holesail-backup-2026-02-27T14-30-00-000Z.tar.gz",
"path": "/path/to/backups/holesail-backup-2026-02-27T14-30-00-000Z.tar.gz",
"size": 12345,
"createdAt": 1700000000000
}
```
---
### `listBackups`
List all available backup files, sorted newest first.
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"backups": [
{
"filename": "holesail-backup-2026-02-27T14-30-00-000Z.tar.gz",
"path": "/path/to/backups/holesail-backup-2026-02-27T14-30-00-000Z.tar.gz",
"size": 12345,
"createdAt": 1700000000000
}
]
}
```
---
### `restoreBackup`
Restore state and certificates from a backup file. The native host reloads `state.json` after a successful restore.
**Payload:** `{ "filename": "holesail-backup-2026-02-27T14-30-00-000Z.tar.gz" }`
**Response:**
```json
{
"ok": true,
"restoredStorage": true,
"restoredCerts": true
}
```
`restoredCerts` is `false` if the backup did not contain a `certs/` directory (legacy backups) or if the certs directory could not be written.
---
### `deleteBackup`
Delete a specific backup file.
**Payload:** `{ "filename": "holesail-backup-2026-02-27T14-30-00-000Z.tar.gz" }`
**Response:** `{ "ok": true }`
---
## Implementation Details
The backup manager (`native-host/backup-manager.js`) uses the system `tar` binary via `child_process.spawn`. It does not use any Bare-specific compression libraries.
### Backup Process
1. A temporary staging directory (`.staging-<timestamp>`) is created inside the `backups/` directory
2. The contents of `holesail-browser-storage/` (excluding `backups/`) are copied into `staging/storage/`
3. The contents of `holesail-browser-certs/` are copied into `staging/certs/` (non-fatal if missing)
4. `tar -czf <output.tar.gz> -C <staging> .` creates the archive
5. The staging directory is removed
### Restore Process
1. The archive is inspected to detect whether it uses the new `storage/`+`certs/` layout or the legacy flat layout
2. For new-style archives: content is extracted to a temporary `.restore-<timestamp>` directory, then copied into the real `storageDir` and `certsDir`
3. For legacy archives: content is extracted directly into `storageDir`
4. Temporary directories are cleaned up
### Archive Layout
```
holesail-backup-<timestamp>.tar.gz
├── storage/
│ └── state.json
└── certs/
├── ca.key
├── ca.crt
├── wildcard.key
└── wildcard.crt
```
---
## Requirements
- `tar` must be available on `PATH` (standard on macOS and Linux; available via WSL or Git Bash on Windows)
- The `backups/` directory is created automatically on first backup
---
## Troubleshooting
**"tar failed"** — Ensure `tar` is available on `PATH`. On Windows, use WSL or install Git Bash.
**"Backup directory not found"** — The `holesail-browser-storage/` directory must exist. Start the native host at least once before taking a backup.
**"Restore failed: file not found"** — The backup file may have been deleted manually. Use `listBackups` to see available backups.
**After restore, `*.hole.sail` sites show certificate errors** — The CA certificate was restored from backup. Go to Dashboard → Proxy & CA → Install CA to reinstall it in the OS trust store.
+248
View File
@@ -0,0 +1,248 @@
# Installation
## Prerequisites
### Option A: Standalone Distributable (recommended)
Download a pre-built standalone binary from the [releases page](https://github.com/holesail/holesail-browser/releases). No runtime dependencies are required — the binary is fully self-contained.
### Option B: Build from Source
You need:
- **[Bare](https://github.com/nicolo-ribaudo/bare)** runtime (for running the native host via launcher script)
- **Node.js** (for build and install scripts only, not used at runtime)
```bash
# macOS / Linux (via npm)
npm install -g bare
# Verify
bare --version
```
For Windows, download the Bare binary from the [Bare releases page](https://github.com/nicolo-ribaudo/bare/releases) and add it to your `PATH`.
---
## Install from Distributable Binary
1. Download the binary for your platform from the [releases page](https://github.com/holesail/holesail-browser/releases):
- `holesail-browser-host-darwin-arm64.zip` — macOS Apple Silicon
- `holesail-browser-host-darwin-x64.zip` — macOS Intel
- `holesail-browser-host-linux-arm64.zip` — Linux ARM64
- `holesail-browser-host-linux-x64.zip` — Linux x64
- `holesail-browser-host-win32-x64.zip` — Windows x64
2. Extract the archive and place the binary in the `releases/` directory of the repository (or any permanent location).
3. Run the install script — it will detect the binary automatically:
```bash
./scripts/install-host.sh # macOS / Linux
.\scripts\install-host.ps1 # Windows
```
---
## Install from Source
### macOS / Linux
```bash
git clone https://github.com/holesail/holesail-browser.git
cd holesail-browser
# Install dependencies and build
./scripts/install.sh
```
The script:
1. Runs `npm install` in the root and `native-host/` directories
2. Builds the native host launcher script (`npm run build:host`)
3. Registers the native messaging manifest at the correct system path
4. Packages the extension as a `.zip` (Chrome) and `.xpi` (Firefox)
### Building Standalone Binaries
To build self-contained distributable binaries (no Bare runtime required on the target machine):
```bash
npm run build:dist # Current platform only
npm run build:dist:all # All platforms (mac + linux + windows)
npm run build:dist:package # All platforms + zip archives for distribution
```
Binaries are written to `releases/`. The install script automatically uses the distributable binary if one is present.
### Windows
```powershell
git clone https://github.com/holesail/holesail-browser.git
cd holesail-browser
.\scripts\install.ps1
```
---
## Manual Steps
If you prefer to run steps individually:
```bash
# 1. Install dependencies
npm install
cd native-host && npm install && cd ..
# 2. Build everything
npm run build
# 3. Register the native messaging manifest
./scripts/install-host.sh # macOS / Linux
.\scripts\install-host.ps1 # Windows
# 4. Package the extension
npm run pack
```
---
## Native Messaging Manifest
The file `com.holesail.browser.json` tells the browser where to find the native host binary and which extension IDs are allowed to connect to it.
```json
{
"name": "com.holesail.browser",
"description": "Native messaging host for Holesail Browser extension (Bare runtime)",
"path": "/absolute/path/to/native-host/holesail-browser-host",
"type": "stdio",
"allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID/"],
"allowed_extensions": ["[email protected]"]
}
```
The install script writes the absolute path and copies the manifest to the correct system location:
| Browser | Platform | Manifest location |
|---------|----------|-------------------|
| Chrome / Edge | macOS | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/` |
| Chrome / Edge | Linux | `~/.config/google-chrome/NativeMessagingHosts/` |
| Chrome / Edge | Windows | Registry: `HKCU\Software\Google\Chrome\NativeMessagingHosts\com.holesail.browser` |
| Firefox | macOS | `~/Library/Application Support/Mozilla/NativeMessagingHosts/` |
| Firefox | Linux | `~/.mozilla/native-messaging-hosts/` |
| Firefox | Windows | Registry: `HKCU\Software\Mozilla\NativeMessagingHosts\com.holesail.browser` |
If you load the extension and get a different extension ID than the one in the manifest, update it:
```bash
./scripts/update-native-manifest-extension-id.sh YOUR_EXTENSION_ID
```
---
## Loading the Extension
### Chrome / Edge
1. Open `chrome://extensions` (or `edge://extensions`)
2. Enable **Developer mode** (toggle in the top-right)
3. Click **Load unpacked**
4. Select the `extension/` folder from the repository
5. Note the **Extension ID** shown on the card — you may need it to update the native messaging manifest
### Firefox
1. Open `about:debugging#/runtime/this-firefox`
2. Click **Load Temporary Add-on**
3. Select `extension/manifest.json`
> **Note:** Firefox requires the extension to be re-loaded after each browser restart when loaded as a temporary add-on. For permanent installation, the extension must be signed by Mozilla or loaded via an enterprise policy.
---
## Installing the CA Certificate
Holesail Browser generates a local root CA (`Holesail Browser CA`) and uses it to sign a wildcard `*.hole.sail` certificate for the HTTPS proxy. Without installing this CA, the browser will show certificate errors when navigating to `*.hole.sail` sites.
### Via the Dashboard (recommended)
1. Click the Holesail Browser extension icon
2. Go to **Proxy & CA**
3. Click **Install CA**
4. Follow the OS prompt (macOS will ask for your password)
### Manual Installation
The CA certificate is at `holesail-browser-certs/ca.cert.pem`.
**macOS:**
```bash
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain \
holesail-browser-certs/ca.cert.pem
```
**Linux:**
```bash
sudo cp holesail-browser-certs/ca.cert.pem \
/usr/local/share/ca-certificates/holesail-browser-ca.crt
sudo update-ca-certificates
```
**Windows (PowerShell as Administrator):**
```powershell
certutil -addstore -f "ROOT" holesail-browser-certs\ca.cert.pem
```
> **Important:** After installing the CA, you must restart the browser for the change to take effect.
---
## Updating the Extension ID
When you load the extension in Chrome, it generates a deterministic extension ID from the public key in `manifest.json`. The `key` field in `manifest.json` is already set, so the ID should be stable across reloads.
If the ID changes (e.g. after re-generating the key), update the native messaging manifest:
```bash
./scripts/update-native-manifest-extension-id.sh NEW_EXTENSION_ID
```
For Firefox, the extension ID is set in `manifest.json` under `browser_specific_settings.gecko.id` and does not change.
---
## Verifying the Installation
1. Load the extension and click the icon — the Dashboard should open
2. Check **Overview**: the host status should show **Connected**
3. Go to **Proxy & CA**: the proxy status should show **Active** and the CA should show **Installed**
4. Try navigating to a `*.hole.sail` hostname — it should load without certificate errors
---
## Uninstalling
```bash
# macOS / Linux
./scripts/uninstall.sh
# Windows
.\scripts\uninstall.ps1
```
To remove the CA manually:
**macOS:** Open Keychain Access → System Roots → find `Holesail Browser CA` → delete it.
**Linux:**
```bash
sudo rm /usr/local/share/ca-certificates/holesail-browser-ca.crt
sudo update-ca-certificates
```
**Windows:**
```powershell
certutil -delstore ROOT "Holesail Browser CA"
```
+560
View File
@@ -0,0 +1,560 @@
# Native Host
The native host is a [Bare](https://github.com/nicolo-ribaudo/bare) process that runs alongside the browser. It handles all tunnel management, SSH sessions, remote desktop sessions, backups, proxy control, and CA installation. The browser extension communicates with it via Chrome/Firefox native messaging.
---
## Runtime: Bare
The native host runs on [Bare](https://github.com/nicolo-ribaudo/bare), a lightweight JavaScript runtime designed for native addons. Bare is not Node.js — it does not include the Node.js standard library. Instead, it uses a set of `bare-*` packages that provide equivalent functionality:
| Bare package | Node.js equivalent |
|-------------|-------------------|
| `bare-process` | `process` global |
| `bare-fs` | `fs` |
| `bare-path` | `path` |
| `bare-tcp` | `net` (TCP) |
| `bare-http1` | `http` |
| `bare-https` | `https` |
| `bare-ws` | `ws` (WebSocket) |
| `bare-subprocess` | `child_process` |
| `bare-module` | `module` |
The entry point (`index.mjs`) is an ES module. All other files use CommonJS (`require`).
---
## Native Messaging Protocol
The Chrome/Firefox native messaging protocol frames each message as:
```
┌─────────────────────────────────────────────┐
│ 4 bytes: message length (uint32 LE) │
│ N bytes: UTF-8 JSON payload │
└─────────────────────────────────────────────┘
```
- The length is a 32-bit unsigned integer in **little-endian** byte order
- The payload is a UTF-8 JSON string
- Maximum message size: **1 MB** (1,048,576 bytes)
- Messages are read from `process.stdin` and written to `process.stdout`
- On Windows, stdin/stdout are opened in binary mode to prevent CRLF translation
### Message Structure
**Request (extension → native host):**
```json
{
"id": "req_1700000000000_abc123",
"type": "commandName",
"payload": { "key": "value" }
}
```
**Response (native host → extension):**
```json
{
"id": "req_1700000000000_abc123",
"type": "response",
"payload": { "ok": true, "result": "..." }
}
```
**Event (native host → extension, unsolicited):**
```json
{
"type": "event",
"event": "tunnelReady",
"payload": {
"hostname": "myapp.hole.sail",
"hsUrl": "hs://abc123...",
"localHost": "127.0.0.1",
"localPort": 19000
}
}
```
---
## Command Reference
All commands are sent as `{ id, type, payload }`. All responses have `{ ok: boolean }` plus command-specific fields.
### System
#### `getState`
Get the complete current state of the native host.
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"servers": [...],
"virtualHosts": [...],
"serviceTunnels": [...],
"proxyPort": 8443,
"connectProxyPort": 8442,
"caInstalled": true,
"settings": { "proxyPort": 8443, "backupRetention": 5, "..." : "..." },
"sshConnections": [...],
"rdpConnections": [...]
}
```
---
#### `installRootCA`
Install the local root CA into the OS trust store.
**Payload:** `{}`
**Response:** `{ "ok": true }` or `{ "ok": false, "error": "..." }`
---
#### `getSettings`
Get the current settings object.
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"settings": {
"proxyPort": 8443,
"connectProxyPort": 8442,
"readyTimeoutMs": 0,
"notifyOnDisconnect": true,
"debug": false,
"disableOnFileUrls": false,
"backupRetention": 5
}
}
```
---
#### `updateSettings`
Apply a partial settings patch. Only the provided keys are updated; all others are preserved.
**Payload:**
```json
{ "proxyPort": 9443, "backupRetention": 10 }
```
**Response:** `{ "ok": true }`
---
### SSH Connection Storage
These commands manage the *saved connection list* (metadata only — not active sessions). Active sessions are managed by `startSshSession` / `stopSshSession`.
#### `getSshConnections`
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"sshConnections": [
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://xyz...", "username": "admin" }
]
}
```
---
#### `setSshConnections`
Overwrite the entire saved SSH connection list.
**Payload:**
```json
{
"sshConnections": [
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://xyz...", "username": "admin" }
]
}
```
**Response:** `{ "ok": true }`
---
### RDP/VNC Connection Storage
These commands manage the *saved connection list* (metadata only — not active sessions). Active sessions are managed by `startRdpSession` / `stopRdpSession`.
#### `getRdpConnections`
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"rdpConnections": [
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://xyz...", "type": "vnc", "port": 5900 }
]
}
```
---
#### `setRdpConnections`
Overwrite the entire saved RDP/VNC connection list.
**Payload:**
```json
{
"rdpConnections": [
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://xyz...", "type": "vnc", "port": 5900 }
]
}
```
**Response:** `{ "ok": true }`
---
### Tunnel Management
#### `startServer`
Expose a local port as an `hs://` URL.
**Payload:**
```json
{
"port": 3000,
"host": "127.0.0.1",
"secure": true,
"udp": false,
"serverId": "server_1"
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `port` | number | — | Local port to expose |
| `host` | string | `"127.0.0.1"` | Local host to bind |
| `secure` | boolean | `true` | Whether the local service uses TLS |
| `udp` | boolean | `false` | Use UDP instead of TCP |
| `serverId` | string | — | Unique ID for this server tunnel |
**Response:**
```json
{
"ok": true,
"serverId": "server_1",
"url": "hs://abc123...",
"port": 3000,
"host": "127.0.0.1",
"secure": true,
"udp": false
}
```
---
#### `stopServer`
Stop a server tunnel.
**Payload:** `{ "serverId": "server_1" }`
**Response:** `{ "ok": true }`
---
#### `setVirtualHost`
Map a `*.hole.sail` hostname to an `hs://` URL.
**Payload:**
```json
{
"hostname": "myapp.hole.sail",
"hsUrl": "hs://abc123..."
}
```
**Response:**
```json
{
"ok": true,
"hostname": "myapp.hole.sail",
"localHost": "127.0.0.1",
"localPort": 19000,
"state": "ready"
}
```
If the Holesail connection fails, the virtual host entry is kept with `"state": "error"` so it can be retried.
---
#### `removeVirtualHost`
Remove a virtual host mapping.
**Payload:** `{ "hostname": "myapp.hole.sail" }`
**Response:** `{ "ok": true }`
---
#### `getVirtualHosts`
List all virtual hosts.
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"hosts": [{
"hostname": "myapp.hole.sail",
"hsUrl": "hs://abc123...",
"localHost": "127.0.0.1",
"localPort": 19000,
"state": "ready",
"createdAt": 1700000000000
}]
}
```
---
#### `startServiceTunnel`
Forward an `hs://` URL to a local TCP port.
**Payload:**
```json
{
"label": "Postgres",
"hsUrl": "hs://def456...",
"localPort": 5432,
"tunnelId": "svc-1"
}
```
**Response:**
```json
{
"ok": true,
"tunnelId": "svc-1",
"label": "Postgres",
"hsUrl": "hs://def456...",
"localPort": 5432,
"state": "ready"
}
```
---
#### `updateServiceTunnel`
Stop and restart a service tunnel with new settings (same `tunnelId`).
**Payload:** Same as `startServiceTunnel`.
**Response:** Same as `startServiceTunnel`.
---
#### `stopServiceTunnel`
Stop a service tunnel.
**Payload:** `{ "tunnelId": "svc-1" }`
**Response:** `{ "ok": true }`
---
#### `getServiceTunnels`
List all service tunnels.
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"tunnels": [{
"id": "svc-1",
"label": "Postgres",
"hsUrl": "hs://def456...",
"localPort": 5432,
"state": "ready",
"createdAt": 1700000000000
}]
}
```
---
#### `lookup`
Look up whether an `hs://` URL is reachable on the network.
**Payload:** `{ "url": "hs://abc123..." }`
**Response:** `{ "ok": true, ...lookupResult }`
---
#### `getProxyPort`
Get the current HTTPS proxy port.
**Payload:** `{}`
**Response:** `{ "ok": true, "port": 8443 }`
---
### SSH
See [SSH.md](SSH.md) for full documentation.
| Command | Description |
|---------|-------------|
| `startSshSession` | Start an SSH session (returns `wsPort` for xterm.js) |
| `stopSshSession` | Stop an SSH session |
| `resizeSshSession` | Resize the terminal PTY |
| `getSshSessions` | List active sessions |
---
### Remote Desktop
See [REMOTE-DESKTOP.md](REMOTE-DESKTOP.md) for full documentation.
| Command | Description |
|---------|-------------|
| `startRdpSession` | Start a VNC or RDP session (returns `wsPort` for the viewer) |
| `stopRdpSession` | Stop a remote desktop session |
| `getRdpSessions` | List active sessions |
---
### Backups
See [BACKUP.md](BACKUP.md) for full documentation.
| Command | Description |
|---------|-------------|
| `createBackup` | Create a tar.gz backup of state + certs, then prune old backups |
| `listBackups` | List all available backup files |
| `restoreBackup` | Restore state and certs from a backup file |
| `deleteBackup` | Delete a specific backup file |
---
## Events
Events are sent from the native host to the extension without a corresponding request. The extension routes tunnel events to subscribed tabs (the dashboard).
### `tunnelReady`
A virtual host tunnel connected successfully.
```json
{
"type": "event",
"event": "tunnelReady",
"payload": {
"hostname": "myapp.hole.sail",
"hsUrl": "hs://abc123...",
"localHost": "127.0.0.1",
"localPort": 19000
}
}
```
### `tunnelClosed`
A virtual host tunnel closed.
```json
{
"type": "event",
"event": "tunnelClosed",
"payload": { "hostname": "myapp.hole.sail" }
}
```
### `tunnelError`
A virtual host tunnel encountered an error.
```json
{
"type": "event",
"event": "tunnelError",
"payload": {
"hostname": "myapp.hole.sail",
"error": "connection refused"
}
}
```
---
## Logging
The native host writes logs to:
```
native-host/holesail-browser.log
```
Enable verbose debug logging by setting `debug: true` in Settings, or by running:
```bash
HOLESAIL_DEBUG=1 bare index.mjs
```
Logs are also streamed to the Dashboard → Logs panel in real time.
---
## Startup Sequence
1. `index.mjs` loads `bare-process/global`, `messenger.js`, and `host.js`
2. `host.js` loads `state.json` via `holesailManager.restorePersistedState()`
3. `host.js` starts the HTTPS proxy on the persisted `proxyPort` (default 8443)
4. `host.js` starts the CONNECT proxy on the persisted `connectProxyPort` (default 8442)
5. `host.js` restores all persisted server tunnels, virtual hosts, and service tunnels
6. The messenger begins reading from `process.stdin`
7. The extension sends `getState` to confirm the host is ready
## Shutdown Sequence
On `SIGTERM` or `SIGINT`:
1. All Holesail tunnels are closed
2. All SSH sessions are stopped
3. All RDP/VNC sessions are stopped
4. The HTTPS and CONNECT proxies are stopped
5. The messenger is destroyed
6. The process exits with code 0
+211
View File
@@ -0,0 +1,211 @@
# Remote Desktop
Holesail Browser includes a built-in remote desktop client that connects to VNC and RDP servers over Holesail P2P tunnels. No port forwarding or VPN is required — the connection travels entirely over the Holesail P2P network.
---
## Supported Protocols
| Protocol | Use case | Browser rendering |
|----------|----------|-------------------|
| **VNC** | Linux desktops, macOS Screen Sharing, most remote desktop servers | noVNC (RFB protocol, full in-browser) |
| **RDP** | Windows Remote Desktop, Windows Server | Canvas-based renderer (bitmap streaming) |
---
## How It Works
### VNC Sessions
```
Dashboard (noVNC RFB client)
│ WebSocket ws://127.0.0.1:23000+
rdp-manager.js (WebSocket server)
│ Raw byte pipe (bare-ws ↔ bare-tcp)
bare-tcp socket 127.0.0.1:22000+
Holesail client tunnel
│ P2P over Holesail
Remote VNC server
```
For VNC, the native host acts as a transparent byte pipe between the browser's noVNC client and the remote VNC server. All RFB protocol framing, authentication, and rendering is handled by noVNC in the browser. Up to 512 KB of VNC server data is buffered in memory while waiting for the browser WebSocket to connect.
### RDP Sessions
```
Dashboard (canvas renderer)
│ WebSocket ws://127.0.0.1:23000+ (JSON messages)
rdp-manager.js (WebSocket server + node-rdpjs-2 client)
│ RDP protocol (node-rdpjs-2)
Holesail client tunnel 127.0.0.1:22000+
│ P2P over Holesail
Remote RDP server (Windows Remote Desktop)
```
For RDP, the native host runs a full RDP client (`node-rdpjs-2`) that connects to the remote server via the Holesail tunnel. Bitmap updates from the RDP server are converted to JSON and streamed to the browser. The browser renders them on a `<canvas>` element and sends mouse/keyboard input events back as JSON.
---
## Using Remote Desktop via the Dashboard
1. Click the Holesail Browser extension icon to open the **Dashboard**
2. Go to **Remote Desktop**
3. Click **Add Connection** and fill in:
- **Label** — a name for the connection (e.g. `Work PC`)
- **hs:// URL** — the Holesail key of the remote machine
- **Protocol** — VNC or RDP
- **Port** — remote desktop port (default: VNC=5900, RDP=3389)
- **Width / Height** — initial viewport dimensions (RDP only; default: 1280×720)
- **Username / Password** — credentials (RDP only; password is session-only, not saved)
4. Click **Save**, then click **Connect** on the connection card
5. The viewer opens in a modal. Use the fullscreen button to expand it.
6. Click **Disconnect** in the viewer toolbar to end the session.
---
## Native Host Commands
### `startRdpSession`
Start a new VNC or RDP session.
**Payload:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `"vnc"` or `"rdp"` |
| `hsUrl` | string | yes | `hs://` key of the remote peer |
| `port` | number | yes | Remote desktop port (5900 for VNC, 3389 for RDP) |
| `label` | string | no | Human-readable session name |
| `username` | string | RDP only | RDP username |
| `password` | string | RDP only | RDP password |
| `domain` | string | no | RDP domain |
| `width` | number | no | Initial width in pixels (default: 1280) |
| `height` | number | no | Initial height in pixels (default: 720) |
**Response:**
```json
{
"ok": true,
"sessionId": "rdp-1",
"wsPort": 23000
}
```
---
### `stopRdpSession`
Stop and clean up a session.
**Payload:** `{ "sessionId": "rdp-1" }`
**Response:** `{ "ok": true }`
---
### `getRdpSessions`
List all active sessions.
**Payload:** `{}`
**Response:**
```json
{
"ok": true,
"sessions": [{
"sessionId": "rdp-1",
"type": "vnc",
"label": "Work PC",
"hsUrl": "hs://abc123...",
"port": 5900,
"wsPort": 23000,
"state": "connected",
"createdAt": 1700000000000
}]
}
```
---
## WebSocket Message Protocol
After `startRdpSession` returns a `wsPort`, the browser connects to `ws://127.0.0.1:<wsPort>`.
### VNC
For VNC sessions, the WebSocket carries raw binary RFB protocol data in both directions. The browser uses `window.RFB` (noVNC) to handle all protocol details:
```javascript
const rfb = new RFB(container, `ws://127.0.0.1:${wsPort}`);
rfb.scaleViewport = true;
rfb.resizeSession = true;
```
### RDP
For RDP sessions, the WebSocket carries JSON messages.
**Server → Browser:**
```json
{ "type": "connected", "width": 1280, "height": 720 }
{ "type": "bitmap", "destLeft": 0, "destTop": 0, "destRight": 100, "destBottom": 50,
"width": 100, "height": 50, "bitsPerPixel": 32, "isCompress": false,
"data": "<base64-encoded pixel data>" }
{ "type": "close" }
{ "type": "error", "message": "Connection refused" }
```
**Browser → Server:**
```json
{ "type": "mouseMove", "x": 500, "y": 300 }
{ "type": "mouseButton", "x": 500, "y": 300, "button": 1, "isDown": true }
{ "type": "keyEvent", "code": 65, "isDown": true }
{ "type": "keyUnicode", "code": 65, "isDown": true }
```
---
## Port Allocation
| Range | Component | Purpose |
|-------|-----------|---------|
| 2200022999 | `rdp-manager.js` | Holesail client tunnels for RDP/VNC sessions |
| 2300023999 | `rdp-manager.js` | WebSocket servers for the browser viewer |
Ports are returned to a free list when a session ends and can be reused by subsequent sessions.
---
## Requirements
- The remote machine must be running a VNC server (e.g. TigerVNC, RealVNC, macOS Screen Sharing) or Windows Remote Desktop (RDP)
- The remote machine must have a Holesail server tunnel active, exposing the VNC/RDP port
- `bare-tcp` and `bare-ws` must be installed (they are dependencies of `native-host/package.json`)
- For RDP: `node-rdpjs-2` must be installed (also a dependency)
---
## Troubleshooting
**"Tunnel failed"** — The `hs://` URL is not reachable. Verify the remote machine is running a Holesail server tunnel.
**VNC: "Authentication failed"** — Check your VNC password. noVNC handles VNC authentication in the browser; the native host is a transparent pipe.
**VNC: Black screen** — The VNC server may require a specific security type. Try connecting with a VNC client directly to verify the server is working.
**RDP: "Connection refused"** — Ensure Windows Remote Desktop is enabled on the remote machine (System → Remote Desktop → Enable Remote Desktop).
**RDP: Blank canvas** — The RDP session may have connected but no bitmap updates were received. Check that the remote machine is not locked or showing a login screen that `node-rdpjs-2` cannot render.
**"bare-tcp not available"** — Rebuild the native host: `npm run build:host` in the project root.
+185
View File
@@ -0,0 +1,185 @@
# Security
## Overview
Holesail Browser is a system that runs a native process with your user privileges and installs a root CA certificate into your OS trust store. Understanding the security model is important before installing it.
---
## Certificate Authority
### What It Is
The native host generates a local root CA named `Holesail Browser CA` using `node-forge`. This CA is used to sign a wildcard TLS certificate for `*.hole.sail`, which the HTTPS proxy presents to the browser.
### Why It Is Needed
Browsers enforce TLS for all HTTPS connections. The local HTTPS proxy (`127.0.0.1:8443`) must present a valid certificate for `*.hole.sail` hostnames, or the browser will show a certificate error and refuse to load the page. Installing the local CA into the OS trust store makes the browser trust this certificate.
### What It Can Do
Once installed, the `Holesail Browser CA` can sign certificates for **any domain** — not just `*.hole.sail`. This is an inherent property of root CA trust. However:
- The CA private key is stored locally at `holesail-browser-certs/ca.key.pem`, accessible only to your user account.
- The HTTPS proxy only issues certificates for `*.hole.sail` hostnames.
- The CA is not shared with any third party.
### Certificate Lifetime
| Certificate | Validity |
|-------------|---------|
| Root CA | 10 years |
| `*.hole.sail` wildcard | 1 year |
If the CA expires or is regenerated, you must re-install it via Dashboard → Proxy & CA → Install CA.
### CA Storage
```
holesail-browser-certs/
├── ca.key.pem ← Root CA private key (keep secret)
├── ca.cert.pem ← Root CA certificate
└── wildcard.hole.sail/
├── key.pem ← Wildcard cert private key
└── cert.pem ← Wildcard cert + CA chain
```
---
## P2P Connection Encryption
All Holesail connections are encrypted end-to-end using the [Noise protocol](https://noiseprotocol.org/) (`Noise_XX_25519_XChaChaPoly_BLAKE2b`). This provides:
- **Mutual authentication** — both peers verify each other's public keys
- **Forward secrecy** — session keys are ephemeral; compromising a long-term key does not expose past sessions
- **Integrity** — all data is authenticated with a MAC; tampering is detected
Encryption is handled automatically by Holesail. No configuration is required.
---
## Native Host Privilege Model
### What the Native Host Can Do
The native host runs as your user account. It can:
- Make arbitrary network connections (P2P via Holesail tunnels)
- Read and write files in `holesail-browser-storage/` and `holesail-browser-certs/`
- Spawn child processes (the `ssh` binary, `tar` for backups, `osascript` for CA installation on macOS)
- Listen on local TCP ports (8442, 8443, 19000+, 2000021999 for SSH, 2200023999 for Remote Desktop)
### What the Native Host Cannot Do
- It cannot access files outside its working directory unless explicitly instructed via a command
- It cannot access other users' data
- It does not run as root (except briefly during CA installation on macOS, where `osascript` prompts for your password)
### Extension ↔ Native Host Trust
The browser extension communicates with the native host via Chrome/Firefox native messaging. The native messaging manifest (`com.holesail.browser.json`) specifies which extension IDs are allowed to connect. Only the extension with the matching ID can send commands to the native host.
---
## Threat Model
### Threats Mitigated
| Threat | Mitigation |
|--------|-----------|
| Eavesdropping on P2P connections | Noise protocol end-to-end encryption |
| Peer identity spoofing | Cryptographic key pairs; Noise mutual authentication |
| Man-in-the-middle on P2P | Noise protocol; keys are verified before data exchange |
| Malicious peers flooding connections | `maxPeers` option on `new Holesail({ maxPeers: N })` |
| Unauthorized extension connecting to native host | `allowed_origins` in native messaging manifest |
| Certificate errors for `*.hole.sail` | Local CA installed in OS trust store |
| Stale CA after regeneration | `isRootCAInstalled` checks fingerprint, not just CN |
### Threats Not Mitigated
| Threat | Notes |
|--------|-------|
| Malicious `hs://` URLs | Any `hs://` key you add as a virtual host will receive your HTTP requests. Only add keys from sources you trust. |
| Local network attacks | The HTTPS proxy and CONNECT proxy listen on `127.0.0.1` only, not on network interfaces. |
| Compromised native host binary | If the binary is replaced by a malicious version, it has full user-level access. Only install from trusted sources. |
| CA private key theft | If `holesail-browser-certs/ca.key.pem` is stolen, the attacker can issue certificates trusted by your browser. Protect this file. |
---
## Proxy Security
### HTTPS Proxy (`127.0.0.1:8443`)
- Listens on loopback only — not accessible from the network
- Presents the `*.hole.sail` wildcard certificate for all `*.hole.sail` requests
- Only forwards requests to hostnames registered as virtual hosts via `holesailManager.getLocalBackend()`
- Unknown hostnames receive a 502 error
### CONNECT Proxy (`127.0.0.1:8442`)
- Listens on loopback only
- Accepts `CONNECT` requests from the browser (via the PAC script)
- Pipes raw TCP to the HTTPS proxy on port 8443
- Does not inspect or modify the tunneled traffic
### PAC Script
The PAC script routes only `*.hole.sail` traffic through the local proxy. All other traffic goes directly (`DIRECT`). The extension re-applies the PAC script if it is overridden by another extension or system setting.
---
## SSH Security
SSH sessions use the system `ssh` binary with:
```
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
```
These options disable host key verification because the SSH connection goes to `127.0.0.1` (the Holesail tunnel endpoint), not the actual remote host. The Holesail tunnel itself provides authentication — only the peer with the correct `hs://` key can receive the connection.
If you require SSH host key verification, you can configure it manually by editing the `sshArgs` array in `native-host/ssh-manager.js`.
---
## Tunnel State Storage
Tunnel configuration (server tunnels, virtual hosts, service tunnels) is persisted to:
```
native-host/holesail-browser-storage/state.json
```
This file is owned by your user account and contains only tunnel metadata (ports, hostnames, `hs://` keys). No tunnel traffic is written to disk.
---
## Remote Desktop Security
VNC and RDP sessions are forwarded over Holesail tunnels, so the connection is encrypted end-to-end by the Noise protocol. However:
- **VNC passwords** are handled by the VNC server's own authentication. The native host acts as a transparent byte pipe; it does not inspect VNC credentials.
- **RDP passwords** are passed to `node-rdpjs-2` and transmitted over the Holesail tunnel. They are held in memory only for the duration of the session and are never written to `state.json`.
- The WebSocket servers for Remote Desktop sessions (`127.0.0.1:23000+`) listen on loopback only.
---
## Backup Security
Backup archives (`tar.gz`) contain `state.json` (which includes `hs://` keys and connection metadata) and the CA private key (`ca.key.pem`). Treat backup files with the same care as the originals:
- Backup files are stored in `holesail-browser-storage/backups/` with user-only permissions
- Do not share backup files — they contain your CA private key and all tunnel keys
- After restoring a backup, reinstall the CA if the certificates changed
---
## Recommendations
1. **Only add `hs://` URLs from sources you trust.** A virtual host forwards your browser's HTTP requests to the remote peer.
2. **Protect `holesail-browser-certs/ca.key.pem`.** This file allows issuing certificates trusted by your browser.
3. **Protect backup archives.** They contain the CA private key and all tunnel metadata.
4. **Review the extension permissions.** The extension has `proxy` permission (required to set the PAC script) and `nativeMessaging` (required to communicate with the native host).
5. **Keep the native host up to date.** Security fixes may be released as updates.
6. **Uninstall the CA when you uninstall Holesail Browser.** See [INSTALLATION.md](INSTALLATION.md) for uninstall instructions.
+246
View File
@@ -0,0 +1,246 @@
# SSH
Holesail Browser includes a built-in SSH client that connects to remote machines over Holesail P2P tunnels. No port forwarding or VPN is required — the SSH connection travels entirely over the Holesail P2P network.
---
## How It Works
```
Dashboard (xterm.js)
│ WebSocket ws://127.0.0.1:21000+
ssh-manager.js (WebSocket server)
│ PTY stream (bare-ws ↔ tt-native)
ssh process (spawned with real PTY via forkpty)
│ TCP 127.0.0.1:20000+
Holesail client tunnel
│ P2P over Holesail
Remote sshd
```
Each session:
1. **Holesail tunnel** — A Holesail client tunnel is created, binding to `127.0.0.1` on a port from 20000+. This tunnel connects to the remote peer identified by the `hs://` key.
2. **SSH process** — The system `ssh` binary is spawned with `tt-native`, which calls `forkpty(3)` to allocate a real local PTY. SSH connects to `127.0.0.1:<tunnelPort>`.
3. **WebSocket server** — A `bare-ws` WebSocket server starts on a port from 21000+. The xterm.js terminal in the dashboard connects to this WebSocket.
4. **PTY ↔ WebSocket bridge** — Data from the PTY is forwarded to the WebSocket client, and data from the WebSocket client is written to the PTY stdin.
### Why a Real PTY?
Using `forkpty(3)` (via `tt-native`) gives SSH a proper local PTY, which means:
- The initial terminal size (`cols × rows`) is set in the PTY `winsize` at spawn time and forwarded to the remote via the SSH handshake.
- `pty.resize(cols, rows)` calls `ioctl(TIOCSWINSZ)` on the local PTY master. SSH detects the `SIGWINCH` signal and sends an SSH window-change request to the remote `sshd`, which resizes the remote PTY and delivers `SIGWINCH` to the foreground process.
- Full-screen apps (`htop`, `vim`, `nano`, `tmux`) resize correctly without any shell command injection.
---
## Using SSH via the Dashboard
1. Click the Holesail Browser extension icon to open the **Dashboard**
2. Go to **SSH**
3. Fill in:
- **Label** — a name for the session (e.g. `my-server`)
- **hs:// URL** — the Holesail key of the remote machine running `sshd`
- **Username** — the SSH username
- **Columns / Rows** — initial terminal size (default: 80×24)
4. Click **Connect**
5. An xterm.js terminal opens in the dashboard. Type your SSH password or passphrase when prompted.
---
## Native Host Commands
SSH sessions are managed via `Holesail.request()` calls, which the dashboard uses internally.
### `startSshSession`
Start a new SSH session.
```javascript
const result = await Holesail.request('startSshSession', {
hsUrl: 'hs://abc123...',
username: 'alice',
cols: 120,
rows: 40,
label: 'my-server'
});
// { ok: true, sessionId: 'ssh-1', wsPort: 21000 }
```
**Payload:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `hsUrl` | string | — | `hs://` key of the remote peer |
| `username` | string | — | SSH username |
| `cols` | number | 80 | Initial terminal width |
| `rows` | number | 24 | Initial terminal height |
| `label` | string | `''` | Human-readable session name |
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `ok` | boolean | `true` on success |
| `sessionId` | string | Unique session ID (e.g. `ssh-1`) |
| `wsPort` | number | Local WebSocket port for xterm.js to connect to |
---
### `stopSshSession`
Stop and clean up a session.
```javascript
await Holesail.request('stopSshSession', { sessionId: 'ssh-1' });
// { ok: true }
```
---
### `resizeSshSession`
Resize the terminal. Should be called whenever the xterm.js terminal resizes.
```javascript
await Holesail.request('resizeSshSession', {
sessionId: 'ssh-1',
cols: 160,
rows: 50
});
// { ok: true }
```
This calls `ioctl(TIOCSWINSZ)` on the local PTY, which SSH forwards as a window-change request to the remote `sshd`.
---
### `getSshSessions`
List all active sessions.
```javascript
const result = await Holesail.request('getSshSessions');
// {
// ok: true,
// sessions: [{
// sessionId: 'ssh-1',
// label: 'my-server',
// username: 'alice',
// hsUrl: 'hs://abc123...',
// wsPort: 21000,
// state: 'connected',
// cols: 120,
// rows: 40,
// createdAt: 1700000000000
// }]
// }
```
**Session states:**
| State | Meaning |
|-------|---------|
| `connected` | Session is active |
| `closed` | SSH process exited or connection closed |
| `stopping` | Session is being torn down |
---
## Connecting xterm.js to the WebSocket
After `startSshSession` returns a `wsPort`, connect xterm.js to `ws://127.0.0.1:<wsPort>`:
```javascript
const { Terminal } = window.xterm; // or import from xterm
const { FitAddon } = window.xtermFitAddon;
const term = new Terminal({ cols: 120, rows: 40 });
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));
fitAddon.fit();
const ws = new WebSocket(`ws://127.0.0.1:${wsPort}`);
ws.onopen = () => {
// Send a ready signal (single null byte) to flush buffered PTY output
ws.send(new Uint8Array([0x00]));
};
ws.onmessage = (event) => {
event.data.arrayBuffer().then((buf) => {
term.write(new Uint8Array(buf));
});
};
term.onData((data) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(new TextEncoder().encode(data));
}
});
// Handle terminal resize
const resizeObserver = new ResizeObserver(() => {
fitAddon.fit();
Holesail.request('resizeSshSession', {
sessionId,
cols: term.cols,
rows: term.rows
});
});
resizeObserver.observe(document.getElementById('terminal'));
```
---
## Port Allocation
| Range | Component |
|-------|-----------|
| 20000+ | Holesail client tunnels for SSH sessions |
| 21000+ | WebSocket servers for xterm.js |
Ports are returned to a free list when a session ends and can be reused by subsequent sessions.
---
## SSH Options
The `ssh` process is spawned with these options:
```
ssh -p <tunnelPort> \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
<username>@127.0.0.1
```
`StrictHostKeyChecking=no` and `UserKnownHostsFile=/dev/null` are set because the SSH connection goes to `127.0.0.1` (the Holesail tunnel endpoint), not the actual remote host — so host key verification would always fail or produce spurious warnings.
---
## Requirements
- The `ssh` binary must be available on `PATH` on the machine running the native host
- `tt-native` must be installed (it is a dependency of `native-host/package.json`)
- `bare-ws` must be installed (also a dependency)
- The remote machine must be running `sshd` and have a Holesail server tunnel active
---
## Troubleshooting
**"tt-native not available"** — Rebuild the native host: `npm run build:host` in the project root.
**"Tunnel failed"** — The `hs://` URL is not reachable. Verify the remote machine is running a Holesail server tunnel.
**"Failed to spawn ssh"** — The `ssh` binary is not on `PATH`. On macOS/Linux, `ssh` is usually pre-installed. On Windows, install OpenSSH.
**Terminal does not resize** — Ensure `resizeSshSession` is called with the correct `sessionId` after each resize.
+244
View File
@@ -0,0 +1,244 @@
# Tunneling
Holesail Browser supports three tunnel modes, all managed through the Dashboard or the native host API.
---
## How `hs://` URL Browsing Works
When you navigate to a `*.hole.sail` hostname, the following chain executes:
```
Browser DNS lookup: myapp.hole.sail
│ PAC script: "PROXY 127.0.0.1:8442"
CONNECT proxy (127.0.0.1:8442)
│ Browser sends: CONNECT myapp.hole.sail:443 HTTP/1.1
│ Proxy replies: 200 Connection established
│ Raw TCP piped to 127.0.0.1:8443
HTTPS proxy (127.0.0.1:8443)
│ TLS handshake with wildcard *.hole.sail cert
│ Reads Host header: myapp.hole.sail
│ Resolves: holesailManager.getLocalBackend('myapp.hole.sail')
│ → { host: '127.0.0.1', port: 19000 }
Holesail client tunnel (127.0.0.1:19000)
│ P2P connection over Holesail
Remote peer (HTTP server)
└─ HTTP response travels back through the chain
```
### PAC Script
The extension sets a Proxy Auto-Config (PAC) script that routes only `*.hole.sail` traffic through the local proxy. All other traffic goes directly:
```javascript
function FindProxyForURL(url, host) {
if (dnsDomainIs(host, ".hole.sail"))
return "PROXY 127.0.0.1:8442";
return "DIRECT";
}
```
The PAC script is applied immediately on browser startup using the last-known proxy port from `chrome.storage.local`. It is re-applied if another extension or system setting overrides it.
### Why Two Proxies?
Browsers send `CONNECT hostname:443` for HTTPS targets — they cannot connect directly to an HTTPS server and negotiate TLS themselves in a proxy context. The CONNECT proxy accepts this handshake and pipes the raw TCP stream to the HTTPS proxy, which terminates TLS and forwards the HTTP request.
---
## Virtual Hosts
A **virtual host** maps a `*.hole.sail` hostname to an `hs://` URL. The native host creates a Holesail client tunnel that listens on `127.0.0.1:19000+` and connects to the remote peer identified by the `hs://` key.
### Adding a Virtual Host
**Via Dashboard:**
1. Dashboard → **Virtual Hosts** → enter an `hs://` URL → click **Add**
2. The system assigns a hostname like `myapp.hole.sail`
3. Navigate to `https://myapp.hole.sail` in your browser
**Via explicit hostname:**
You can specify your own subdomain:
```
hostname: myapp.hole.sail
hsUrl: hs://abc123...
```
### Hostname Rules
- Must end in `.hole.sail`
- Subdomains are arbitrary — `myapp.hole.sail`, `db.hole.sail`, `game.hole.sail`
- Hostnames are persisted across native host restarts
### Port Allocation
Each virtual host gets a unique port starting from 19000. Ports are released back to a free list when the virtual host is removed, so they can be reused.
### Tunnel Lifecycle Events
The native host emits events that the extension forwards to subscribed tabs:
| Event | Payload | Meaning |
|-------|---------|---------|
| `tunnelReady` | `{ hostname, hsUrl, localHost, localPort }` | Tunnel connected and ready |
| `tunnelClosed` | `{ hostname }` | Tunnel closed (removed or error) |
| `tunnelError` | `{ hostname, error }` | Tunnel encountered an error |
---
## Server Tunnels
A **server tunnel** exposes a local TCP port as an `hs://` URL that any Holesail client can connect to.
### Starting a Server Tunnel
**Via Dashboard:**
1. Dashboard → **Servers** → enter the local port (e.g. `3000`) → select **TCP** or **UDP** → click **Start**
2. Copy the generated `hs://` URL and share it
**Via native host command:**
```json
{
"type": "startServer",
"payload": {
"port": 3000,
"host": "127.0.0.1",
"secure": true,
"udp": false
}
}
```
Response:
```json
{
"ok": true,
"serverId": "server_1",
"url": "hs://abc123...",
"port": 3000,
"host": "127.0.0.1",
"secure": true,
"udp": false
}
```
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `port` | number | 3000 | Local port to expose |
| `host` | string | `127.0.0.1` | Local host to bind to |
| `secure` | boolean | `true` | Whether to use encrypted Holesail transport |
| `udp` | boolean | `false` | Use UDP instead of TCP |
| `serverId` | string | auto | Optional stable ID for restore |
### Stopping a Server Tunnel
```json
{
"type": "stopServer",
"payload": { "serverId": "server_1" }
}
```
### Persistence
Server tunnels are persisted to `holesail-browser-storage/state.json` and restored automatically when the native host starts.
---
## Service Tunnels
A **service tunnel** is a client tunnel that forwards a remote Holesail peer to a **user-chosen local TCP port**. Unlike virtual hosts (which go through the HTTPS proxy), service tunnels bind directly to `127.0.0.1:<localPort>` so any TCP client can connect — database clients, game clients, custom tools, etc.
### Starting a Service Tunnel
**Via Dashboard:**
1. Dashboard → **Service Tunnels** → enter a label, `hs://` URL, and local port → click **Add**
**Via native host command:**
```json
{
"type": "startServiceTunnel",
"payload": {
"label": "Postgres",
"hsUrl": "hs://def456...",
"localPort": 5432
}
}
```
Response:
```json
{
"ok": true,
"tunnelId": "svc-1",
"label": "Postgres",
"hsUrl": "hs://def456...",
"localPort": 5432,
"state": "ready"
}
```
### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `label` | string | yes | Human-readable name |
| `hsUrl` | string | yes | `hs://` key of the remote peer |
| `localPort` | number | yes | Local port to bind (165535) |
| `tunnelId` | string | no | Optional stable ID for restore |
### Stopping a Service Tunnel
```json
{
"type": "stopServiceTunnel",
"payload": { "tunnelId": "svc-1" }
}
```
### Use Cases
- Connect a local database client to a remote database over P2P
- Access a remote game server without port forwarding
- Forward any TCP service through a Holesail tunnel
---
## Tunnel Lookup
You can look up whether an `hs://` URL is reachable on the DHT without creating a full tunnel:
```json
{
"type": "lookup",
"payload": { "url": "hs://abc123..." }
}
```
---
## Comparison of Tunnel Types
| Feature | Virtual Host | Service Tunnel | Server Tunnel |
|---------|-------------|----------------|---------------|
| Direction | remote → browser | remote → local TCP | local → remote |
| Protocol | HTTP/HTTPS via proxy | raw TCP | raw TCP (or UDP) |
| Port | auto-assigned (19000+) | user-chosen | existing local port |
| Accessible via | `*.hole.sail` URL in browser | any TCP client | `hs://` URL |
| Persisted | yes | yes | yes |
| UDP support | no | no | yes |
| Use case | Browse P2P web apps | Forward TCP services | Share local services |