docs: fully rewrite README and all docs to reflect current features and architecture
CI / Build & Test (push) Successful in 2m29s

Made-with: Cursor
This commit is contained in:
Raven Scott
2026-02-27 20:26:50 -05:00
parent 75def4eee6
commit 62ab961714
9 changed files with 1448 additions and 1743 deletions
+130 -153
View File
@@ -1,201 +1,178 @@
# Holesail Browser
A browser extension + native host that brings **Holesail P2P tunneling** into standard desktop browsers (Chrome, Edge, Firefox) without forking the browser. Open `hs://` URLs as if they were normal HTTPS sites, expose local services as P2P tunnels, SSH into remote machines, and connect to remote desktops — all through the browser.
Browse P2P Holesail tunnels directly in your browser. Holesail Browser is a Chrome/Firefox extension paired with a native host that routes `*.hole.sail` domains through Holesail tunnels — with automatic TLS, no port-forwarding, and no central servers.
## What It Does
## How it works
- **Browse `hs://` URLs** — paste any Holesail key and navigate to it like a normal HTTPS site
- **Expose local services** — share a local port as an `hs://` URL that anyone with the key can reach
- **Service tunnels** — forward a remote Holesail peer to a local TCP port (databases, game servers, etc.)
- **SSH in the browser** — full xterm.js terminal over a Holesail tunnel, no port forwarding needed
- **Remote Desktop** — VNC and RDP sessions over Holesail tunnels, rendered in the browser
- **Automatic backups** — scheduled tar.gz backups of all state and certificates with configurable retention
1. You add a virtual host in the dashboard: `myapp.hole.sail``hs://abc123...`
2. The extension sets a PAC script that routes `*.hole.sail` → local CONNECT proxy (port 8442)
3. The CONNECT proxy pipes the raw TLS stream to the HTTPS proxy (port 8443)
4. The HTTPS proxy terminates TLS (using a locally-trusted wildcard cert) and forwards HTTP to the Holesail tunnel
5. The Holesail tunnel connects P2P to the remote peer over the DHT
## How It Works
All traffic is end-to-end encrypted via the Noise protocol. The local CA is only used for the browser↔proxy TLS leg.
```
Browser Extension (MV3)
│ Chrome Native Messaging (stdin/stdout)
Native Host (Bare runtime)
├── Holesail tunnel manager
├── SSH session manager (ports 2000021999)
├── Remote Desktop manager (ports 2200023999)
├── Backup manager (tar.gz, configurable retention)
├── HTTPS proxy (127.0.0.1:8443) ← TLS termination, *.hole.sail cert
└── CONNECT proxy (127.0.0.1:8442) ← PAC script target
```
## Features
When you navigate to `myapp.hole.sail`, the extension's PAC script routes the request through the local CONNECT proxy → HTTPS proxy → Holesail tunnel → remote peer. The browser sees a valid HTTPS response with no certificate errors (once the CA is installed).
All state — tunnels, settings, SSH connections, RDP connections — is stored in a single `state.json` file owned by the native host.
- **Virtual Hosts** — browse any `hs://` URL as `https://name.hole.sail/`
- **Server Tunnels** — expose a local port as an `hs://` key (TCP or UDP)
- **Service Tunnels** — forward a remote `hs://` peer to a local TCP port
- **SSH** — in-browser SSH terminal via xterm.js, over a Holesail tunnel
- **Remote Desktop** — VNC (noVNC) and RDP viewer in the browser, over a Holesail tunnel
- **Backups** — `tar.gz` snapshots of all state and certificates, with configurable retention
- **Auto CA** — generates and installs a local root CA; signs a wildcard `*.hole.sail` cert
- **Persistent state** — all tunnels, connections, and settings survive restarts
## Installation
### macOS / Linux — one-liner
### macOS / Linux (recommended)
```bash
curl -fsSL https://git.ssh.surf/snxraven/holesail-browser/raw/branch/main/scripts/install.sh | bash
curl -fsSL https://git.ssh.surf/snxraven/holesail-browser/raw/branch/main/scripts/web-installer.sh | bash
```
Downloads the pre-built native host binary and extension package from the [latest release](https://git.ssh.surf/snxraven/holesail-browser/releases). No git clone, no Node.js, no build step required.
### Windows
Download the latest release from [Releases](https://git.ssh.surf/snxraven/holesail-browser/releases), extract `holesail-browser-host-win32-x64.zip`, then run in PowerShell:
Download and run the PowerShell installer:
```powershell
irm https://git.ssh.surf/snxraven/holesail-browser/raw/branch/main/scripts/install.ps1 | iex
```
### Load the Extension
Or download `install.ps1` from the [latest release](https://git.ssh.surf/snxraven/holesail-browser/releases/tag/latest-main) and run it manually.
After the installer completes, the extension package is saved to `~/Downloads`.
### What the installer does
**Chrome / Edge:**
1. Go to `chrome://extensions`
2. Enable **Developer mode**
3. Drag and drop `Holesail-Browser-*.zip` onto the page (or click **Load unpacked** after extracting)
1. Downloads the pre-built native host binary for your platform from the latest release
2. Installs it to `~/.holesail-browser/` (macOS/Linux) or `%LOCALAPPDATA%\holesail-browser\` (Windows)
3. Writes the native messaging manifest so Chrome/Firefox can find it
4. Downloads the extension `.zip` (Chrome) and `.xpi` (Firefox) to `~/Downloads`
5. On macOS: removes Gatekeeper quarantine and ad-hoc signs the binary and native addons
**Firefox:**
1. Go to `about:addons`
## Loading the extension
### Chrome / Edge
1. Open `chrome://extensions`
2. Enable **Developer mode** (top right)
3. Drag `~/Downloads/Holesail-Browser-1.0.0.zip` onto the page
*(or click **Load unpacked** after extracting the zip)*
### Firefox
1. Open `about:addons`
2. Click the gear icon → **Install Add-on From File**
3. Select `Holesail-Browser-*.xpi`
3. Select `~/Downloads/Holesail-Browser-1.0.0.xpi`
### Install the CA Certificate
## First run
Open the **Holesail Dashboard** (click the extension icon) → go to **Proxy & CA** → click **Install CA**. This installs the local `Holesail Browser CA` root certificate so `*.hole.sail` sites load without certificate warnings. Fully quit and reopen your browser after installing.
1. Click the Holesail Browser icon in your toolbar to open the dashboard
2. Go to **Proxy & CA** → click **Install Root CA**
3. **Fully quit and reopen Chrome** (Cmd+Q on macOS) for the CA trust to take effect
4. Go to **Virtual Hosts** → add a hostname and `hs://` key
5. Navigate to `https://your-hostname.hole.sail/`
## Quick Start
> **Note:** The CA must be installed and Chrome must be restarted before `*.hole.sail` sites will load without a certificate warning.
### Browse an `hs://` URL
## Dashboard pages
1. Click the extension icon to open the **Dashboard**
2. Go to **Virtual Hosts** → paste an `hs://` key → click **Add**
3. Navigate to the assigned `*.hole.sail` hostname in your browser
| Page | Description |
|------|-------------|
| Overview | Status summary, connection count, proxy ports |
| Virtual Hosts | Map `hs://` keys to `*.hole.sail` hostnames |
| Server Tunnels | Expose local ports as `hs://` keys (TCP/UDP) |
| Service Tunnels | Forward remote `hs://` peers to local TCP ports |
| Proxy & CA | Proxy port settings, CA install/status |
| SSH | In-browser SSH terminal over Holesail |
| Remote Desktop | VNC/RDP viewer over Holesail |
| Backups | Create, restore, and manage backups |
| Logs | Live log stream from the native host |
| Settings | Proxy ports, timeouts, notifications, debug mode |
### Expose a Local Service
## File locations
1. Dashboard → **Servers** → enter your local port → select TCP or UDP → click **Start**
2. Copy the generated `hs://` URL and share it
### SSH into a Remote Machine
1. Dashboard → **SSH** → click **Add Connection**
2. Enter the `hs://` key, username, and an optional label
3. Click **Connect** — an xterm.js terminal opens in the dashboard
### Connect to a Remote Desktop
1. Dashboard → **Remote Desktop** → click **Add Connection**
2. Enter the `hs://` key, select VNC or RDP, configure port and credentials
3. Click **Connect** — the viewer opens in the dashboard
### Take a Backup
1. Dashboard → **Backups** → click **Take Backup**
2. Backups are stored as `tar.gz` archives including all state and certificates
3. Configure retention in **Settings****Backup Retention** (default: 5)
## Architecture
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full system diagram and message flow.
## Documentation
| Document | Description |
|----------|-------------|
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | System architecture, component breakdown, message flow |
| [docs/INSTALLATION.md](docs/INSTALLATION.md) | Detailed install guide for all platforms |
| [docs/TUNNELING.md](docs/TUNNELING.md) | How `hs://` browsing, virtual hosts, and service tunnels work |
| [docs/SSH.md](docs/SSH.md) | SSH tunnel feature |
| [docs/REMOTE-DESKTOP.md](docs/REMOTE-DESKTOP.md) | VNC and RDP remote desktop feature |
| [docs/BACKUP.md](docs/BACKUP.md) | Backup and restore feature |
| [docs/SECURITY.md](docs/SECURITY.md) | CA trust model, encryption, threat mitigations |
| [docs/NATIVE-HOST.md](docs/NATIVE-HOST.md) | Native messaging protocol and host command reference |
## Technologies
| Technology | Role |
|-----------|------|
| [Holesail](https://github.com/holesail/holesail) | P2P TCP tunneling |
| [Bare](https://github.com/holepunchto/bare) | Lightweight JS runtime for the native host |
| [node-forge](https://github.com/digitalbazaar/forge) | TLS/PKI for CA and certificate generation |
| [xterm.js](https://xtermjs.org/) | Terminal emulator for SSH sessions |
| [noVNC](https://github.com/novnc/noVNC) | Browser-side VNC client (RFB protocol) |
| [node-rdpjs-2](https://www.npmjs.com/package/node-rdpjs-2) | Server-side RDP client for Remote Desktop |
## Build Commands
```bash
npm run build:host # Generate bare launcher script (requires bare on PATH)
npm run build # Same as build:host
npm run pack # Package extension as .zip / .xpi
# Standalone distributable binaries (no bare runtime required at runtime)
npm run build:dist # Build for current host only
npm run build:dist:all # Build for all platforms (mac + linux + windows)
npm run build:dist:mac # darwin-arm64 + darwin-x64
npm run build:dist:linux # linux-arm64 + linux-x64
npm run build:dist:win # win32-x64
npm run build:dist:package # All platforms + create .zip archives
```
Distributable binaries are written to `releases/`. They are self-contained — no `bare` runtime, no `node_modules`, no dependencies needed on the target machine.
## File Layout
```
holesail-browser/
├── extension/ # Browser extension (Manifest V3)
│ ├── manifest.json
│ ├── background.js # Service worker, native messaging, PAC proxy
│ ├── content.js # Content script, event relay
│ ├── dashboard.html/js # Management UI (10 pages)
│ └── vendor/ # Bundled xterm.js, noVNC
├── native-host/ # Native messaging host (Bare runtime)
│ ├── index.mjs # Entry point
│ ├── host.js # Command handler (30+ commands)
│ ├── holesail-manager.js # Tunnel lifecycle + state.json persistence
│ ├── https-proxy.js # HTTPS reverse proxy (port 8443)
│ ├── connect-proxy.js # HTTP CONNECT proxy (port 8442)
│ ├── certificate-authority.js
│ ├── ssh-manager.js # SSH sessions (ports 2000021999)
│ ├── rdp-manager.js # Remote Desktop sessions (ports 2200023999)
│ ├── backup-manager.js # tar.gz backup/restore
│ └── holesail-browser-storage/
│ ├── state.json # All persistent state
│ └── backups/ # Backup archives
├── holesail-browser-certs/ # Generated CA and domain certs (also backed up)
├── scripts/ # Install and build scripts
└── com.holesail.browser.json # Native messaging manifest
```
| Path | Description |
|------|-------------|
| `~/.holesail-browser/holesail-browser-host` | Native host binary |
| `~/.holesail-browser/holesail-browser-storage/state.json` | All persistent state |
| `~/.holesail-browser/holesail-browser-certs/` | CA and domain certificates |
| `~/.holesail-browser/holesail-browser.log` | Native host log file |
| `~/.holesail-browser/holesail-browser-storage/backups/` | Backup archives |
## Troubleshooting
**"Native host has exited"** — Re-run the installer: `curl -fsSL https://git.ssh.surf/snxraven/holesail-browser/raw/branch/main/scripts/install.sh | bash`
**`*.hole.sail` sites show a certificate error**
**"Access to the specified native messaging host is forbidden"** — Extension ID mismatch. Reinstall the extension from `~/Downloads/Holesail-Browser-*.zip` and re-run the installer.
The root CA is not trusted, or Chrome was not restarted after installing it. Open the dashboard → Proxy & CA and check the CA status. If it shows "Not Installed", click **Install Root CA**, then fully quit and reopen Chrome (Cmd+Q).
**`*.hole.sail` sites show certificate errors** — Install the CA via Dashboard → Proxy & CA → Install CA, then fully quit and reopen your browser.
If the CA shows "Installed" but you still see errors, the keychain may have a stale entry from a previous installation. Click **Install Root CA** again — it will detect the mismatch, remove the old entry, and install the correct one.
**macOS "cannot be opened because Apple cannot check it"** — The installer handles this automatically by ad-hoc signing the binary and all native addons. If it recurs, re-run the installer.
**`No tunnel for this hostname`**
**PAC proxy not active** — Another extension may be controlling proxy settings. Check Dashboard → Proxy & CA for proxy status.
The native host has no active tunnel for that hostname. Possible causes:
- The tunnel is still connecting — wait a few seconds and refresh
- The native host was restarted and is reconnecting — open the dashboard to check
- The hostname in the dashboard doesn't exactly match what you're browsing
**Backup "tar failed"** — Ensure `tar` is available on `PATH` (standard on macOS/Linux; install via WSL or Git Bash on Windows).
**Native host not connecting / `hostConnected: false`**
## Security
- Make sure you haven't started `holesail-browser-host` manually from a terminal — only Chrome should spawn it via native messaging
- Check `~/.holesail-browser/holesail-browser.log` for errors
- Try reloading the extension at `chrome://extensions`
The native host runs with your user privileges. Only install from sources you trust.
**macOS: "Apple cannot verify..." / Gatekeeper warning**
All Holesail connections use the [Noise protocol](https://noiseprotocol.org/) for end-to-end encryption. See [docs/SECURITY.md](docs/SECURITY.md) for the full security model.
Run the installer again — it handles quarantine removal and ad-hoc signing automatically. If you moved the binary manually, run:
## Compatibility
```bash
xattr -rd com.apple.quarantine ~/.holesail-browser/holesail-browser-host
codesign --force --sign - ~/.holesail-browser/holesail-browser-host
```
- Chrome 88+, Edge 88+, Firefox 79+
- macOS, Linux, Windows
- Desktop only (native messaging is not available on mobile browsers)
**`Access to the specified native messaging host is forbidden`**
The extension ID in the native messaging manifest doesn't match the installed extension. Re-run the installer, or manually update the `allowed_origins` field in the manifest JSON to match the extension ID shown in `chrome://extensions`.
## Building from source
```bash
git clone https://git.ssh.surf/snxraven/holesail-browser
cd holesail-browser
npm install
cd native-host && npm install && cd ..
# Build extension
npm run pack
# Build native host binary for current platform
npm run build:dist
# Build for all platforms
npm run build:dist:all
```
The built extension will be in `releases/Holesail-Browser-*.zip` and `.xpi`. Platform binaries will be in `releases/<platform>-<arch>/`.
## Platform support
| Platform | Architecture | Binary |
|----------|-------------|--------|
| macOS | Apple Silicon (arm64) | `darwin-arm64` |
| macOS | Intel (x64) | `darwin-x64` |
| Linux | x64 | `linux-x64` |
| Linux | ARM64 | `linux-arm64` |
| Windows | x64 | `win32-x64` (.exe) |
## Documentation
- [Architecture](docs/ARCHITECTURE.md) — system design, component overview, message flow
- [Installation](docs/INSTALLATION.md) — detailed install guide for all platforms
- [Tunneling](docs/TUNNELING.md) — virtual hosts, server tunnels, service tunnels
- [SSH](docs/SSH.md) — in-browser SSH terminal
- [Remote Desktop](docs/REMOTE-DESKTOP.md) — VNC and RDP over Holesail
- [Backups](docs/BACKUP.md) — backup and restore
- [Security](docs/SECURITY.md) — CA model, encryption, threat model
- [Native Host Protocol](docs/NATIVE-HOST.md) — message types and API reference
## License
+153 -220
View File
@@ -1,245 +1,168 @@
# 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.
Holesail Browser is composed of three parts: a browser extension, a native host process, and the Holesail P2P network. The extension and native host communicate via Chrome's native messaging protocol; the native host manages all tunnel connections and runs a local HTTPS proxy that the browser routes `*.hole.sail` traffic through.
## Layer Overview
## Component overview
```
┌─────────────────────────────────────────────────────────────────
│ Browser Extension (Manifest V3)
┌─────────────────────────────────────────────────────────────────┐
│ Browser (Chrome / Firefox)
│ │
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)
┌──────────────┐ ┌──────────────────────────────────────┐
│ background.js│ │ dashboard.html / dashboard.js
│ (service │◄──►│ (management UI — virtual hosts,
│ │ worker) │ │ SSH, RDP, backups, settings, logs) │ │
│ └──────┬───────┘ └──────────────────────────────────────┘ │
│ │ PAC script: *.hole.sail → PROXY 127.0.0.1:8442 │
│ Native messaging: chrome.runtime.connectNative(...)
└─────────┼───────────────────────────────────────────────────────┘
│ stdin/stdout (4-byte length-prefixed JSON)
┌─────────────────────────────────────────────────────────────────┐
│ Native Host (~/.holesail-browser/holesail-browser-host) │
│ │
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) │
┌──────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ host.js │ holesail- │ certificate-
│ (command │ │ manager.js │ │ authority.js
│ │ dispatch)│ │ (tunnel lifecycle│ │ (root CA + domain │ │
└────┬─────┘ │ + state.json) │ │ cert generation, │ │
│ └────────┬─────────┘ │ keychain install) │
│ │ └──────────────────────┘
│ ┌────▼─────────────────▼──────────────────────────────────┐ │
│ │ https-proxy.js connect-proxy.js │ │
│ │ 127.0.0.1:8443 127.0.0.1:8442 │ │
│ │ (TLS termination, (HTTP CONNECT handler, │ │
│ │ wildcard *.hole.sail pipes to 127.0.0.1:8443) │ │
│ │ cert, HTTP forwarding) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
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) │
└──────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ ssh-manager │ │ rdp-manager │ │ backup-manager │
│ (PTY + WS │ │ (VNC/RDP + │ │ (tar.gz snapshots) │
│ bridge) │ │ WS bridge)
└──────────────┘ └──────────────┘ └──────────────────────┘
└─────────────────────────────────────────────────────────────────┘
│ Holesail P2P (Noise protocol, DHT)
┌──────────────────────────────────────────────────────────────┐
│ Remote peers (HTTP servers, SSH daemons, VNC/RDP servers) │
└──────────────────────────────────────────────────────────────┘
```
---
## Source files
## Extension Components
### Native host (`native-host/`)
### `manifest.json`
| File | Purpose |
|------|---------|
| `index.mjs` | Entry point. Bootstraps Bare globals, creates the native messaging `messenger`, wires `handleMessage` from `host.js`. Handles `SIGTERM`/`SIGINT` for graceful shutdown. |
| `messenger.js` | Chrome/Firefox native messaging framing: 4-byte little-endian length prefix + UTF-8 JSON body. Max 1 MB per message. |
| `host.js` | Central command dispatcher. On startup: loads persisted state, starts both proxies, then restores all persisted tunnels asynchronously. Handles all message types from the extension. |
| `holesail-manager.js` | Manages all tunnel types (server, virtual host, service tunnel). Owns in-memory maps, port allocator (starting at 19000), and all persistence to `state.json`. |
| `https-proxy.js` | HTTPS reverse proxy on `127.0.0.1:8443`. Presents a wildcard `*.hole.sail` TLS cert. Reads the `Host` header, looks up the local tunnel port, and proxies HTTP. Returns a 502 HTML page for unknown hostnames. |
| `connect-proxy.js` | HTTP CONNECT proxy on `127.0.0.1:8442`. Accepts `CONNECT hostname:443`, replies `200 Connection established`, then pipes the raw TCP stream to `127.0.0.1:8443`. |
| `certificate-authority.js` | Generates a 2048-bit RSA root CA (10-year validity) using `node-forge`. Signs wildcard `*.hole.sail` domain certs (1-year). Installs the CA into the OS trust store. Fingerprint-verifies to detect stale entries. |
| `ssh-manager.js` | Per SSH session: starts a Holesail client tunnel, spawns `ssh` with a real PTY via `tt-native` (`forkpty`), starts a `bare-ws` WebSocket server, and bridges PTY ↔ WebSocket. |
| `rdp-manager.js` | Per RDP/VNC session: starts a Holesail client tunnel, starts a WebSocket server. VNC: transparent byte pipe. RDP: `node-rdpjs-2` client, converts bitmap updates to JSON. |
| `backup-manager.js` | Creates/restores `tar.gz` backups of `state.json` + all certificates. Supports create, list, restore, delete, and auto-prune by retention count. |
Manifest V3. Key permissions:
### Extension (`extension/`)
| 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 |
| File | Purpose |
|------|---------|
| `manifest.json` | Manifest V3. Permissions: `nativeMessaging`, `proxy`, `declarativeNetRequest`, `scripting`, `storage`, `tabs`, `notifications`. |
| `background.js` | Service worker. Connects to native host via `connectNative`. Sets PAC script. Reconnects with exponential backoff (100ms → 30s). Routes dashboard requests to native host. Broadcasts tunnel events to subscribed tabs. |
| `content.js` | Minimal content script. Relays `holesail-host-disconnect` to the page as a `CustomEvent`. |
| `dashboard.html/js` | Full management UI (~2200 lines). 10 pages: Overview, Virtual Hosts, Server Tunnels, Service Tunnels, Proxy & CA, SSH, Remote Desktop, Backups, Logs, Settings. |
| `wrong-domain.html/js` | Error page for `*.host.test` (common typo), redirected via `declarativeNetRequest`. |
Host permissions cover `*://*.hole.sail/*` and `*://*.host.local/*`.
## Proxy architecture
### `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)
The browser cannot connect directly to a custom HTTPS server via a proxy — it sends a `CONNECT` request instead. This requires two proxy layers:
```
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
Browser navigates to https://myapp.hole.sail/
│ PAC script (applied by background.js):
│ *.hole.sail → PROXY 127.0.0.1:8442
│ everything else → DIRECT
CONNECT proxy (connect-proxy.js) 127.0.0.1:8442
│ Browser sends: CONNECT myapp.hole.sail:443 HTTP/1.1
│ Proxy replies: HTTP/1.1 200 Connection established
│ Pipes raw TCP stream to 127.0.0.1:8443
HTTPS proxy (https-proxy.js) 127.0.0.1:8443
│ TLS handshake — wildcard *.hole.sail cert (signed by local CA)
│ Reads Host header: myapp.hole.sail
│ Calls holesailManager.getLocalBackend('myapp.hole.sail')
│ → { host: '127.0.0.1', port: 19042 }
│ Proxies HTTP request to 127.0.0.1:19042
Holesail client tunnel (holesail-manager.js) 127.0.0.1:19042
│ P2P connection via Noise protocol over Holesail DHT
Remote peer (HTTP server)
```
### Native Host → Extension (tunnel events)
The PAC script is applied immediately on browser startup using the default port (8442), then updated once the native host reports its actual configured port. It is re-applied if overridden by another extension.
```
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
## Native messaging protocol
All messages are JSON objects framed with a 4-byte little-endian length prefix (Chrome native messaging format). The extension sends requests; the native host sends responses and events.
**Request:**
```json
{ "id": 42, "type": "setVirtualHost", "payload": { "hostname": "myapp.hole.sail", "hsUrl": "hs://abc..." } }
```
### 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
**Response:**
```json
{ "id": 42, "type": "response", "payload": { "ok": true, "hostname": "myapp.hole.sail", "localPort": 19000 } }
```
---
**Event (unsolicited):**
```json
{ "type": "event", "event": "tunnelReady", "payload": { "hostname": "myapp.hole.sail", "localPort": 19000 } }
```
## Port Assignments
See [NATIVE-HOST.md](NATIVE-HOST.md) for the full message type reference.
| 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 |
## Port allocation
---
| Port | Component | Description |
|------|-----------|-------------|
| 8443 | `https-proxy.js` | HTTPS proxy (TLS termination) |
| 8442 | `connect-proxy.js` | CONNECT proxy (PAC target) |
| 19000+ | `holesail-manager.js` | Virtual host and service tunnel client ports |
| 20000+ | `ssh-manager.js` | SSH Holesail client tunnel ports |
| 21000+ | `ssh-manager.js` | SSH WebSocket server ports |
| 22000+ | `rdp-manager.js` | RDP/VNC Holesail client tunnel ports |
| 23000+ | `rdp-manager.js` | RDP/VNC WebSocket server ports |
## Data Persistence
All ports are bound to `127.0.0.1` only.
`native-host/holesail-browser-storage/state.json` stores all application state:
## State persistence
All state is owned by the native host and persisted to `state.json` next to the binary:
```
~/.holesail-browser/
├── holesail-browser-host # binary
├── holesail-browser.log # log file
├── holesail-browser-certs/
│ ├── ca.key.pem # root CA private key
│ ├── ca.cert.pem # root CA certificate
│ └── wildcard.hole.sail/
│ ├── key.pem # wildcard domain key
│ └── cert.pem # wildcard domain cert + CA chain
└── holesail-browser-storage/
├── state.json # all persistent state
└── backups/ # tar.gz backup archives
```
`state.json` schema (version 2):
```json
{
@@ -253,11 +176,9 @@ Browser: DNS lookup for myapp.hole.sail
"disableOnFileUrls": false,
"backupRetention": 5
},
"nextServerId": 3,
"nextServiceTunnelId": 2,
"servers": [
{ "id": "server_1", "port": 3000, "host": "127.0.0.1", "secure": true, "udp": false }
],
"nextServerId": 0,
"nextServiceTunnelId": 0,
"servers": [],
"virtualHosts": [
{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." }
],
@@ -265,14 +186,26 @@ Browser: DNS lookup for myapp.hole.sail
{ "id": "svc-1", "label": "Postgres", "hsUrl": "hs://def456...", "localPort": 5432 }
],
"sshConnections": [
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://xyz...", "username": "admin" }
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://xyz...", "username": "root" }
],
"rdpConnections": [
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://xyz...", "type": "vnc", "port": 5900 }
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://xyz...", "type": "vnc", "port": 5901, "width": 1280, "height": 720, "username": "" }
]
}
```
All state is owned by the native host. The extension reads state via `getState` / `getSettings` / `getSshConnections` / `getRdpConnections` and writes it via `updateSettings` / `setSshConnections` / `setRdpConnections`.
## Native host lifecycle
On first run after upgrading from a previous version, `holesail-persist.json` is automatically migrated to `state.json` and the old file is removed.
Chrome spawns the native host process when the background service worker first calls `connectNative`. The process exits when Chrome disconnects (e.g. browser closed, service worker killed). On next connection, Chrome spawns a fresh process which restores all tunnels from `state.json`.
If port 8443 or 8442 is already in use when the native host starts, it exits immediately — this prevents ghost instances with no tunnel state from accumulating.
## Build system
The native host is built with the [Bare](https://github.com/holepunchto/bare) runtime:
1. `bare-pack` bundles the JS module graph (resolving `node:*` imports via `bare-node-*` shims)
2. `bare-build` embeds the bundle into a pre-built Bare runtime binary, producing a self-contained executable
3. Native addons (`.bare` files) are embedded and extracted to a content-addressed temp directory at runtime
Targets: `darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `win32-x64`.
+98 -125
View File
@@ -1,196 +1,169 @@
# Backup
# Backups
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.
Holesail Browser can create `tar.gz` snapshots of all persistent state and certificates. Backups can be created manually from the dashboard or restored at any time.
---
## What is backed up
## What Is Backed Up
Each backup archive contains:
Each backup archive contains two directories:
| Path in archive | Source |
|-----------------|--------|
| `storage/state.json` | All virtual hosts, server tunnels, service tunnels, SSH connections, RDP connections, and settings |
| `certs/ca.key.pem` | Root CA private key |
| `certs/ca.cert.pem` | Root CA certificate |
| `certs/wildcard.hole.sail/key.pem` | Wildcard domain private key |
| `certs/wildcard.hole.sail/cert.pem` | Wildcard domain certificate + CA chain |
| 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 |
Backups do **not** include the binary itself, the log file, or other backup archives.
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.
## Storage location
---
## Backup Storage
Backups are stored in:
Backups are stored at:
```
native-host/holesail-browser-storage/backups/
~/.holesail-browser/holesail-browser-storage/backups/
```
Each backup file is named with a timestamp:
Each backup is a `tar.gz` file named with a timestamp:
```
holesail-backup-2026-02-27T14-30-00-000Z.tar.gz
holesail-browser-backup-2026-02-28T12-00-00.tar.gz
```
## Usage
### Creating a backup
1. Open the dashboard → **Backups**
2. Click **Create Backup**
3. The backup is created immediately and appears in the list
### Restoring a backup
1. Open the dashboard → **Backups**
2. Find the backup you want to restore
3. Click **Restore**
4. The native host extracts the archive, overwrites the current state and certs, and reloads state from disk
> After restoring a backup that contains different certificates, you may need to reinstall the root CA (dashboard → Proxy & CA → Install Root CA) and restart Chrome.
### Deleting a backup
1. Open the dashboard → **Backups**
2. Click **Delete** next to the backup you want to remove
### Retention
Backups are automatically pruned after each new backup is created. The default retention is **5 backups**. To change it:
1. Open the dashboard → **Settings**
2. Set **Backup Retention** to the desired number
3. Click **Save**
---
## 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
## Native host commands
### `createBackup`
Create a new backup and prune old backups based on the current `backupRetention` setting.
Create a new backup.
**Payload:** `{}`
**Request payload:** `{}`
**Response:**
**Response payload:**
```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
"filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz",
"path": "/Users/you/.holesail-browser/holesail-browser-storage/backups/holesail-browser-backup-2026-02-28T12-00-00.tar.gz"
}
```
---
### `listBackups`
List all available backup files, sorted newest first.
List all available backups.
**Payload:** `{}`
**Response:**
**Response payload:**
```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",
"filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz",
"size": 12345,
"createdAt": 1700000000000
"createdAt": "2026-02-28T12:00:00.000Z"
}
]
}
```
---
### `restoreBackup`
Restore state and certificates from a backup file. The native host reloads `state.json` after a successful restore.
Restore a backup by filename.
**Payload:** `{ "filename": "holesail-backup-2026-02-27T14-30-00-000Z.tar.gz" }`
**Response:**
**Request payload:**
```json
{
"ok": true,
"restoredStorage": true,
"restoredCerts": true
}
{ "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz" }
```
`restoredCerts` is `false` if the backup did not contain a `certs/` directory (legacy backups) or if the certs directory could not be written.
---
**Response payload:**
```json
{ "ok": true }
```
### `deleteBackup`
Delete a specific backup file.
Delete a backup by filename.
**Payload:** `{ "filename": "holesail-backup-2026-02-27T14-30-00-000Z.tar.gz" }`
**Request payload:**
```json
{ "filename": "holesail-browser-backup-2026-02-28T12-00-00.tar.gz" }
```
**Response:** `{ "ok": true }`
**Response payload:**
```json
{ "ok": true }
```
---
## Implementation Details
## 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.
### Creating a backup
### 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
1. A staging directory is created in the backups folder
2. `state.json` is copied to `staging/storage/state.json`
3. All cert files are copied to `staging/certs/`
4. `tar czf <output.tar.gz> -C staging .` creates the archive
5. The staging directory is removed
6. Old backups exceeding the retention count are deleted (oldest first)
### Restore Process
### Restoring a backup
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
1. The archive is extracted to a staging directory
2. If `storage/state.json` exists in the archive, it overwrites the current `state.json`
3. If `certs/` exists in the archive, cert files are copied to the certs directory
4. The staging directory is removed
5. The native host reloads state from disk
### Archive Layout
### Legacy archive support
```
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
Backups created by older versions of Holesail Browser used a flat layout (files at the root of the archive rather than under `storage/` and `certs/` prefixes). These are still supported for restore.
---
## Troubleshooting
**"tar failed"** — Ensure `tar` is available on `PATH`. On Windows, use WSL or install Git Bash.
**Backup creation fails**
**"Backup directory not found"** — The `holesail-browser-storage/` directory must exist. Start the native host at least once before taking a backup.
- Check that `~/.holesail-browser/holesail-browser-storage/backups/` is writable
- Check `~/.holesail-browser/holesail-browser.log` for error details
- Ensure `tar` is available on the system
**"Restore failed: file not found"** — The backup file may have been deleted manually. Use `listBackups` to see available backups.
**Restore doesn't take effect**
**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.
After restoring, the native host reloads state from disk. If tunnels don't reconnect, open the dashboard — the Overview page will show the current state. You may need to manually reconnect tunnels if the restored state references different `hs://` keys.
**CA mismatch after restore**
If the restored backup contains different CA certificates than what is currently installed in the OS keychain, `*.hole.sail` sites will show certificate errors. Go to dashboard → Proxy & CA → click **Install Root CA** to reinstall the correct CA, then restart Chrome.
+160 -199
View File
@@ -1,248 +1,209 @@
# 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
## Quick install
### macOS / Linux
```bash
git clone https://github.com/holesail/holesail-browser.git
cd holesail-browser
# Install dependencies and build
./scripts/install.sh
curl -fsSL https://git.ssh.surf/snxraven/holesail-browser/raw/branch/main/scripts/web-installer.sh | bash
```
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)
### Windows (PowerShell)
### 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
```powershell
irm https://git.ssh.surf/snxraven/holesail-browser/raw/branch/main/scripts/install.ps1 | iex
```
Binaries are written to `releases/`. The install script automatically uses the distributable binary if one is present.
Or download `install.ps1` from the [latest release](https://git.ssh.surf/snxraven/holesail-browser/releases/tag/latest-main) and run it from PowerShell.
---
## What the installer does
### macOS / Linux (`install.sh`)
1. Detects your OS and architecture (`darwin`/`linux`, `x64`/`arm64`)
2. Kills any running `holesail-browser-host` process
3. Removes any previous installation (`~/.holesail-browser/`) and old native messaging manifests
4. Downloads `holesail-browser-host-<platform>-<arch>.zip` from the latest release
5. Extracts the binary to `~/.holesail-browser/holesail-browser-host`
6. **macOS only:** removes Gatekeeper quarantine, ad-hoc signs the binary, pre-runs it to extract `.bare` native addons, then signs all extracted addons
7. Downloads `Holesail-Browser-1.0.0.zip` and `Holesail-Browser-1.0.0.xpi` to `~/Downloads`
8. Writes the native messaging manifest to all browser locations
### Windows (`install.ps1`)
1. Kills any running `holesail-browser-host.exe` process
2. Removes any previous installation (`%LOCALAPPDATA%\holesail-browser\`) and old registry keys
3. Downloads `holesail-browser-host-win32-x64.zip` and extracts the `.exe`
4. Downloads `Holesail-Browser-1.0.0.zip` to `~/Downloads`
5. Writes the native messaging manifest JSON and creates registry keys for Chrome, Chromium, and Firefox
---
## Native messaging manifest locations
The installer writes the manifest to all of these locations automatically:
### macOS
| Browser | Path |
|---------|------|
| Chrome | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.holesail.browser.json` |
| Chromium | `~/Library/Application Support/Chromium/NativeMessagingHosts/com.holesail.browser.json` |
| Firefox | `~/Library/Application Support/Mozilla/NativeMessagingHosts/com.holesail.browser.json` |
### Linux
| Browser | Path |
|---------|------|
| Chrome | `~/.config/google-chrome/NativeMessagingHosts/com.holesail.browser.json` |
| Chromium | `~/.config/chromium/NativeMessagingHosts/com.holesail.browser.json` |
| Firefox | `~/.mozilla/native-messaging-hosts/com.holesail.browser.json` |
### Windows
```powershell
git clone https://github.com/holesail/holesail-browser.git
cd holesail-browser
.\scripts\install.ps1
```
Registry keys under `HKCU\Software\`:
- `Google\Chrome\NativeMessagingHosts\com.holesail.browser`
- `Chromium\NativeMessagingHosts\com.holesail.browser`
- `Mozilla\NativeMessagingHosts\com.holesail.browser`
Each key's default value points to the manifest JSON file at `%LOCALAPPDATA%\holesail-browser\com.holesail.browser.json`.
---
## 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
## 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
1. Open `chrome://extensions`
2. Enable **Developer mode** (toggle in the top right)
3. Drag `~/Downloads/Holesail-Browser-1.0.0.zip` onto the extensions page
*(or click **Load unpacked** and select the extracted folder)*
### 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.
1. Open `about:addons`
2. Click the gear icon → **Install Add-on From File**
3. Select `~/Downloads/Holesail-Browser-1.0.0.xpi`
---
## Installing the CA Certificate
## First-time setup
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.
After loading the extension:
### Via the Dashboard (recommended)
1. Click the Holesail Browser icon in the toolbar to open the dashboard
2. Go to **Proxy & CA** → click **Install Root CA**
3. **Fully quit and reopen Chrome** (Cmd+Q on macOS, Alt+F4 on Windows) for the CA trust to take effect
4. Go to **Virtual Hosts** → add a hostname (e.g. `myapp.hole.sail`) and its `hs://` key
5. Navigate to `https://myapp.hole.sail/`
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.
> The root CA must be installed and Chrome must be restarted before `*.hole.sail` sites will load without a certificate warning. The CA is installed silently to your user keychain — no admin password is required on macOS.
---
## Updating the Extension ID
## 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.
The native messaging manifest contains an `allowed_origins` field with the extension's Chrome ID. If you load the extension from a different source (e.g. a locally built zip), the ID may differ.
If the ID changes (e.g. after re-generating the key), update the native messaging manifest:
To update it, run:
```bash
./scripts/update-native-manifest-extension-id.sh NEW_EXTENSION_ID
# macOS / Linux
bash scripts/update-native-manifest-extension-id.sh <new-extension-id>
# Windows
.\scripts\update-native-manifest-extension-id.ps1 <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
Or edit the manifest JSON files directly and replace the `chrome-extension://...` value in `allowed_origins`.
---
## Uninstalling
```bash
# macOS / Linux
./scripts/uninstall.sh
### macOS / Linux
# Windows
.\scripts\uninstall.ps1
```bash
pkill -f holesail-browser-host 2>/dev/null || true
rm -rf ~/.holesail-browser
# Remove native messaging manifests
rm -f ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts/com.holesail.browser.json
rm -f ~/Library/Application\ Support/Chromium/NativeMessagingHosts/com.holesail.browser.json
rm -f ~/Library/Application\ Support/Mozilla/NativeMessagingHosts/com.holesail.browser.json
# Remove CA from keychain (macOS)
security delete-certificate -c "Holesail Browser CA" ~/Library/Keychains/login.keychain-db 2>/dev/null || true
```
To remove the CA manually:
### Windows
**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"
Stop-Process -Name holesail-browser-host -Force -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\holesail-browser"
Remove-Item -Path "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.holesail.browser" -ErrorAction SilentlyContinue
Remove-Item -Path "HKCU:\Software\Chromium\NativeMessagingHosts\com.holesail.browser" -ErrorAction SilentlyContinue
Remove-Item -Path "HKCU:\Software\Mozilla\NativeMessagingHosts\com.holesail.browser" -ErrorAction SilentlyContinue
```
Then remove the extension from `chrome://extensions`.
---
## Building from source
### Prerequisites
- [Node.js](https://nodejs.org/) 18+
- [Bare](https://github.com/holepunchto/bare) runtime (for running the native host in development)
- npm
### Steps
```bash
git clone https://git.ssh.surf/snxraven/holesail-browser
cd holesail-browser
# Install root dependencies
npm install
# Install native host dependencies
cd native-host && npm install && cd ..
# Build extension (.zip + .xpi)
npm run pack
# Build native host binary for current platform
npm run build:dist
# Build for all platforms
npm run build:dist:all
```
Built artifacts:
- `releases/Holesail-Browser-*.zip` — Chrome extension
- `releases/Holesail-Browser-*.xpi` — Firefox extension
- `releases/<platform>-<arch>/holesail-browser-host[.exe]` — native host binary
### Running in development
```bash
# Run the native host directly with Bare
cd native-host
bare index.mjs
```
Load the `extension/` directory as an unpacked extension in Chrome.
---
## Environment variables
| Variable | Description |
|----------|-------------|
| `HOLESAIL_DEBUG=1` | Enable verbose debug logging in the native host |
| `HOLESAIL_BROWSER_STORAGE=<path>` | Override the storage directory (default: next to binary) |
| `HOLESAIL_BROWSER_CERTS=<path>` | Override the certs directory (default: next to binary) |
| `BRIDGE_SWARM_LOG=<path>` | Override the log file path |
+391 -407
View File
@@ -1,267 +1,96 @@
# Native Host
# Native Host Protocol
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.
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**.
## 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):**
## Message format
### Request (extension → native host)
```json
{
"id": "req_1700000000000_abc123",
"type": "commandName",
"payload": { "key": "value" }
}
{ "id": 42, "type": "<command>", "payload": { ... } }
```
**Response (native host → extension):**
### Response (native host → extension)
```json
{
"id": "req_1700000000000_abc123",
"type": "response",
"payload": { "ok": true, "result": "..." }
}
{ "id": 42, "type": "response", "payload": { ... } }
```
**Event (native host → extension, unsolicited):**
Responses always echo the `id` from the request. If an error occurred, the payload contains an `error` field:
```json
{
"type": "event",
"event": "tunnelReady",
"payload": {
"hostname": "myapp.hole.sail",
"hsUrl": "hs://abc123...",
"localHost": "127.0.0.1",
"localPort": 19000
}
}
{ "id": 42, "type": "response", "payload": { "error": "Tunnel failed to connect" } }
```
### Event (native host → extension, unsolicited)
```json
{ "type": "event", "event": "<eventName>", "payload": { ... } }
```
---
## Command Reference
## Commands
All commands are sent as `{ id, type, payload }`. All responses have `{ ok: boolean }` plus command-specific fields.
### `getState`
### System
Returns the full current state of the native host. The extension calls this on startup and after reconnecting.
#### `getState`
Get the complete current state of the native host.
**Payload:** `{}`
**Response:**
**Response payload:**
```json
{
"ok": true,
"servers": [...],
"virtualHosts": [...],
"serviceTunnels": [...],
"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,
"settings": { "proxyPort": 8443, "backupRetention": 5, "..." : "..." },
"sshConnections": [...],
"rdpConnections": [...]
"caInstalled": true
}
```
---
#### `installRootCA`
### `setVirtualHost`
Install the local root CA into the OS trust store.
Add or update a virtual host. If the hostname already exists, the old tunnel is closed and a new one is started.
**Payload:** `{}`
**Request payload:**
```json
{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." }
```
**Response:** `{ "ok": true }` or `{ "ok": false, "error": "..." }`
**Response payload:**
```json
{ "ok": true, "hostname": "myapp.hole.sail", "localHost": "127.0.0.1", "localPort": 19000, "state": "ready" }
```
---
#### `getSettings`
### `removeVirtualHost`
Get the current settings object.
Remove a virtual host and close its tunnel.
**Payload:** `{}`
**Request payload:**
```json
{ "hostname": "myapp.hole.sail" }
```
**Response:**
**Response payload:**
```json
{ "ok": true }
```
---
### `startServer`
Start a server tunnel (expose a local port as an `hs://` key).
**Request payload:**
```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,
@@ -269,231 +98,383 @@ Expose a local port as an `hs://` URL.
}
```
**Response payload:**
```json
{ "ok": true, "serverId": "server_1", "hsUrl": "hs://abc123...", "state": "ready" }
```
---
#### `stopServer`
### `stopServer`
Stop a server tunnel.
**Payload:** `{ "serverId": "server_1" }`
**Response:** `{ "ok": true }`
---
#### `setVirtualHost`
Map a `*.hole.sail` hostname to an `hs://` URL.
**Payload:**
**Request payload:**
```json
{
"hostname": "myapp.hole.sail",
"hsUrl": "hs://abc123..."
}
{ "serverId": "server_1" }
```
**Response:**
**Response payload:**
```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
}]
}
{ "ok": true }
```
---
#### `startServiceTunnel`
### `startServiceTunnel`
Forward an `hs://` URL to a local TCP port.
Start a service tunnel (forward a remote `hs://` peer to a local TCP port).
**Payload:**
**Request 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"
"hsUrl": "hs://abc123...",
"localPort": 5432
}
```
**Response payload:**
```json
{ "ok": true, "tunnelId": "svc-1", "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`
### `stopServiceTunnel`
Stop a service tunnel.
**Payload:** `{ "tunnelId": "svc-1" }`
**Request payload:**
```json
{ "tunnelId": "svc-1" }
```
**Response:** `{ "ok": true }`
**Response payload:**
```json
{ "ok": true }
```
---
#### `getServiceTunnels`
### `lookupTunnel`
List all service tunnels.
Check whether an `hs://` key is reachable on the DHT.
**Payload:** `{}`
**Request payload:**
```json
{ "hsUrl": "hs://abc123..." }
```
**Response:**
**Response payload:**
```json
{ "ok": true, "reachable": true }
```
---
### `startSshSession`
Start an SSH session.
**Request payload:**
```json
{
"connectionId": "ssh-abc123",
"hsUrl": "hs://abc123...",
"username": "root",
"port": 22,
"cols": 80,
"rows": 24
}
```
**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,
"tunnels": [{
"id": "svc-1",
"label": "Postgres",
"hsUrl": "hs://def456...",
"localPort": 5432,
"state": "ready",
"createdAt": 1700000000000
}]
"sessions": [ { "sessionId": "ssh-session-1", "connectionId": "ssh-abc123", "wsPort": 21000 } ]
}
```
---
#### `lookup`
### `setSshConnections`
Look up whether an `hs://` URL is reachable on the network.
Save SSH connection definitions.
**Payload:** `{ "url": "hs://abc123..." }`
**Request payload:**
```json
{
"connections": [
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://abc...", "username": "root" }
]
}
```
**Response:** `{ "ok": true, ...lookupResult }`
**Response payload:**
```json
{ "ok": true }
```
---
#### `getProxyPort`
### `startRdpSession`
Get the current HTTPS proxy port.
Start a VNC or RDP session.
**Payload:** `{}`
**Request payload:**
```json
{
"connectionId": "rdp-abc123",
"hsUrl": "hs://abc123...",
"type": "vnc",
"port": 5900,
"width": 1280,
"height": 720,
"username": ""
}
```
**Response:** `{ "ok": true, "port": 8443 }`
**Response payload:**
```json
{ "ok": true, "sessionId": "rdp-session-1", "wsPort": 23000, "tunnelPort": 22000 }
```
---
### SSH
### `stopRdpSession`
See [SSH.md](SSH.md) for full documentation.
Stop a VNC or RDP session.
| 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 |
**Request payload:**
```json
{ "sessionId": "rdp-session-1" }
```
**Response payload:**
```json
{ "ok": true }
```
---
### Remote Desktop
### `getRdpSessions`
See [REMOTE-DESKTOP.md](REMOTE-DESKTOP.md) for full documentation.
List active VNC/RDP sessions.
| Command | Description |
|---------|-------------|
| `startRdpSession` | Start a VNC or RDP session (returns `wsPort` for the viewer) |
| `stopRdpSession` | Stop a remote desktop session |
| `getRdpSessions` | List active sessions |
**Response payload:**
```json
{
"ok": true,
"sessions": [ { "sessionId": "rdp-session-1", "connectionId": "rdp-abc123", "type": "vnc", "wsPort": 23000 } ]
}
```
---
### Backups
### `setRdpConnections`
See [BACKUP.md](BACKUP.md) for full documentation.
Save RDP/VNC connection definitions.
| 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 |
**Request payload:**
```json
{
"connections": [
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://abc...", "type": "vnc", "port": 5900, "width": 1280, "height": 720, "username": "" }
]
}
```
**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": { ... } }
```
---
### `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 from the native host to the extension without a corresponding request. The extension routes tunnel events to subscribed tabs (the dashboard).
Events are sent by the native host without a corresponding request. The extension broadcasts them to subscribed tabs.
### `tunnelReady`
A virtual host tunnel connected successfully.
A tunnel connected successfully.
```json
{
"type": "event",
"event": "tunnelReady",
"payload": {
"hostname": "myapp.hole.sail",
"hsUrl": "hs://abc123...",
"localHost": "127.0.0.1",
"localPort": 19000
}
"payload": { "hostname": "myapp.hole.sail", "hsUrl": "hs://...", "localHost": "127.0.0.1", "localPort": 19000 }
}
```
### `tunnelClosed`
A virtual host tunnel closed.
A tunnel disconnected.
```json
{
@@ -505,56 +486,59 @@ A virtual host tunnel closed.
### `tunnelError`
A virtual host tunnel encountered an error.
A tunnel failed to connect.
```json
{
"type": "event",
"event": "tunnelError",
"payload": {
"hostname": "myapp.hole.sail",
"error": "connection refused"
"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" }
}
```
---
## Logging
## Tunnel states
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
| 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) |
+170 -126
View File
@@ -1,211 +1,255 @@
# 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.
Holesail Browser supports VNC and RDP connections in the browser, tunneled over Holesail. VNC uses the noVNC RFB client; RDP uses `node-rdpjs-2`. Both are accessed from the dashboard's **Remote Desktop** tab.
---
## VNC vs RDP
## Supported Protocols
| Feature | VNC | RDP |
|---------|-----|-----|
| Protocol | RFB (VNC) | RDP (Microsoft) |
| Browser client | noVNC (WebSocket → RFB) | node-rdpjs-2 (JSON bridge) |
| Performance | Good for most use cases | Moderate (JSON serialization overhead) |
| Compatibility | Any VNC server | Windows Remote Desktop, FreeRDP |
| Default port | 5900 | 3389 |
| 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) |
## Architecture
---
## How It Works
### VNC Sessions
### VNC
```
Dashboard (noVNC RFB client)
│ WebSocket ws://127.0.0.1:23000+
Dashboard (noVNC / canvas)
│ WebSocket ws://127.0.0.1:23000+ (binary frames)
rdp-manager.js (WebSocket server)
│ Raw byte pipe (bare-ws ↔ bare-tcp)
rdp-manager.js (transparent byte pipe)
│ TCP
bare-tcp socket 127.0.0.1:22000+
127.0.0.1:22000+ (Holesail client tunnel)
│ P2P / Noise protocol
Holesail client tunnel
│ P2P over Holesail
Remote VNC server
Remote VNC server (port 5900)
```
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.
For VNC, the native host is a transparent pipe — it does not interpret the RFB protocol. noVNC handles all VNC protocol logic in the browser.
### RDP Sessions
### RDP
```
Dashboard (canvas renderer)
│ WebSocket ws://127.0.0.1:23000+ (JSON messages)
│ WebSocket ws://127.0.0.1:23000+ (JSON frames)
rdp-manager.js (WebSocket server + node-rdpjs-2 client)
│ RDP protocol (node-rdpjs-2)
rdp-manager.js (node-rdpjs-2 RDP client)
│ TCP
Holesail client tunnel 127.0.0.1:22000+
│ P2P over Holesail
127.0.0.1:22000+ (Holesail client tunnel)
│ P2P / Noise protocol
Remote RDP server (Windows Remote Desktop)
Remote RDP server (port 3389)
```
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.
For RDP, `node-rdpjs-2` handles the RDP protocol in the native host. Bitmap updates are serialized to JSON and sent to the browser, which renders them on a canvas. Mouse and keyboard input is sent as JSON from the browser to the native host.
## Usage
### Saving a connection
1. Open the dashboard → **Remote Desktop**
2. Click **Add Connection**
3. Enter:
- **Label** — a name for this connection
- **Type** — VNC or RDP
- **hs:// key** — the Holesail key for the remote peer
- **Port** — remote port (default: 5900 for VNC, 3389 for RDP)
- **Width / Height** — initial display resolution
- **Username** — (RDP only) login username
4. Click **Save**
### Connecting
1. Click **Connect** next to a saved connection
2. A viewer window opens in the dashboard
3. The native host starts a Holesail tunnel to the remote peer and begins the session
### Disconnecting
Click **Disconnect** or close the viewer. The tunnel and WebSocket server are cleaned up.
---
## 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
## 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:**
**Request payload:**
```json
{
"ok": true,
"sessionId": "rdp-1",
"wsPort": 23000
"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 and clean up a session.
Stop a running session.
**Payload:** `{ "sessionId": "rdp-1" }`
**Request payload:**
```json
{ "sessionId": "rdp-session-1" }
```
**Response:** `{ "ok": true }`
---
**Response payload:**
```json
{ "ok": true }
```
### `getRdpSessions`
List all active sessions.
List active sessions.
**Payload:** `{}`
**Response:**
**Response payload:**
```json
{
"ok": true,
"sessions": [{
"sessionId": "rdp-1",
"type": "vnc",
"sessions": [
{ "sessionId": "rdp-session-1", "connectionId": "rdp-abc123", "type": "vnc", "wsPort": 23000 }
]
}
```
### `setRdpConnections`
Save RDP/VNC connection definitions to `state.json`.
**Request payload:**
```json
{
"connections": [
{
"id": "rdp-abc123",
"label": "Work PC",
"hsUrl": "hs://abc123...",
"hsUrl": "hs://abc...",
"type": "vnc",
"port": 5900,
"wsPort": 23000,
"state": "connected",
"createdAt": 1700000000000
}]
"width": 1280,
"height": 720,
"username": ""
}
]
}
```
---
## WebSocket Message Protocol
After `startRdpSession` returns a `wsPort`, the browser connects to `ws://127.0.0.1:<wsPort>`.
## WebSocket protocol
### 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;
```
The dashboard connects to `ws://127.0.0.1:<wsPort>`. All frames are binary — raw RFB bytes passed transparently between noVNC and the VNC server. The native host does not interpret the protocol.
### RDP
For RDP sessions, the WebSocket carries JSON messages.
**Server → Browser:**
**Native host → browser (bitmap updates):**
```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" }
{
"type": "bitmap",
"x": 100, "y": 50,
"width": 200, "height": 100,
"data": "<base64-encoded bitmap>"
}
```
**Browser → Server:**
**Browser → native host (input):**
```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 }
{ "type": "mousemove", "x": 150, "y": 75 }
{ "type": "mousedown", "button": 1 }
{ "type": "mouseup", "button": 1 }
{ "type": "keydown", "key": 65 }
{ "type": "keyup", "key": 65 }
```
---
## Port Allocation
## 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 |
| Range | Usage |
|-------|-------|
| 22000+ | Holesail client tunnel (one per session) |
| 23000+ | WebSocket server (one per session) |
Ports are returned to a free list when a session ends and can be reused by subsequent sessions.
---
## Saved connections
RDP/VNC connections are stored in `state.json` under `rdpConnections`:
```json
{
"rdpConnections": [
{
"id": "rdp-abc123",
"label": "Work PC",
"hsUrl": "hs://abc123...",
"type": "vnc",
"port": 5900,
"width": 1280,
"height": 720,
"username": ""
}
]
}
```
Connections are restored on startup. Active sessions are not persisted — they must be reconnected after a native host restart.
---
## 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)
### VNC
- A VNC server on the remote machine (e.g. TigerVNC, RealVNC, macOS Screen Sharing)
- The VNC server must be accessible via a Holesail `hs://` key
### RDP
- Windows Remote Desktop enabled on the remote machine, or a compatible RDP server (e.g. xrdp on Linux)
- The RDP server must be accessible via a Holesail `hs://` key
---
## Troubleshooting
**"Tunnel failed"** — The `hs://` URL is not reachable. Verify the remote machine is running a Holesail server tunnel.
**Connection times out**
**VNC: "Authentication failed"** — Check your VNC password. noVNC handles VNC authentication in the browser; the native host is a transparent pipe.
- The remote peer may be offline or the `hs://` key may be wrong
- Check `~/.holesail-browser/holesail-browser.log` for tunnel errors
**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.
**VNC: black screen or no updates**
**RDP: "Connection refused"** — Ensure Windows Remote Desktop is enabled on the remote machine (System → Remote Desktop → Enable Remote Desktop).
- Verify the VNC server is running and listening on the expected port
- Try disconnecting and reconnecting
**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.
**RDP: authentication fails**
**"bare-tcp not available"** — Rebuild the native host: `npm run build:host` in the project root.
- Verify the username and password on the remote machine
- Ensure the remote machine has Remote Desktop enabled and the user has permission to connect
**RDP: slow or choppy display**
RDP performance depends on the latency of the Holesail tunnel and the speed of JSON serialization. For better performance, use VNC if the remote machine supports it.
+75 -130
View File
@@ -1,185 +1,130 @@
# 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
### 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.
Holesail Browser generates a local root CA called **Holesail Browser CA** and installs it into your OS trust store. This CA is used to sign a wildcard `*.hole.sail` TLS certificate that the HTTPS proxy presents to your browser.
### Why It Is Needed
### Why it's 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.
Browsers require HTTPS for modern web features and refuse to load mixed content. The HTTPS proxy must present a valid TLS certificate for `*.hole.sail` — but no public CA will sign a certificate for a non-public domain. A locally-trusted CA is the standard approach used by tools like mkcert, Burp Suite, and corporate proxies.
### What It Can Do
### What the CA 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 is a standard X.509 root CA. In principle, it could be used to sign certificates for any domain — not just `*.hole.sail`. 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.
- The CA private key (`ca.key.pem`) is stored at `~/.holesail-browser/holesail-browser-certs/ca.key.pem`, readable only by your user account
- The native host only ever uses the CA to sign `*.hole.sail` wildcard certificates
- The CA is installed with the SSL trust policy only (macOS: `-p ssl`), limiting its scope
### Certificate Lifetime
### Lifetime
| Certificate | Validity |
|-------------|---------|
| Root CA | 10 years |
| `*.hole.sail` wildcard | 1 year |
- Root CA: 10 years
- Wildcard `*.hole.sail` cert: 1 year (auto-renewed on expiry)
If the CA expires or is regenerated, you must re-install it via Dashboard → Proxy & CA → Install CA.
### Fingerprint verification
### CA Storage
`isRootCAInstalled` and `installRootCA` both verify the SHA-1 fingerprint of the CA cert in the keychain against the CA cert on disk. This detects stale entries — for example, after reinstalling Holesail Browser, the new CA will be installed and the old one removed automatically.
```
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
```
### Installation
| Platform | Method | Requires admin? |
|----------|--------|----------------|
| macOS | `security add-trusted-cert -r trustRoot -p ssl -k ~/Library/Keychains/login.keychain-db` | No |
| Linux | `cp` to `/usr/local/share/ca-certificates/` + `update-ca-certificates` | Yes (sudo) |
| Windows | `certutil -addstore -f ROOT` | Yes (admin) |
On macOS, the CA is installed to the user's login keychain with the SSL trust policy. Chrome evaluates TLS trust using the SSL policy against all user keychains, so this is sufficient without admin privileges.
---
## P2P Connection Encryption
## P2P encryption
All Holesail connections are encrypted end-to-end using the [Noise protocol](https://noiseprotocol.org/) (`Noise_XX_25519_XChaChaPoly_BLAKE2b`). This provides:
All traffic between the native host and remote peers is encrypted using the **Noise protocol** (specifically the Noise_XX handshake pattern used by Holesail). 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
- **Mutual authentication** — both peers verify each other's identity using their public keys
- **Forward secrecy** — session keys are ephemeral; compromise of long-term keys does not expose past sessions
- **Integrity** — all messages are authenticated with a MAC
Encryption is handled automatically by Holesail. No configuration is required.
The `hs://` key is the remote peer's public key. Connecting to a peer requires knowing their key — there is no way to enumerate or discover peers without a key.
---
## Native Host Privilege Model
## Native host privilege model
### What the Native Host Can Do
The native host runs as your user account (not root). It:
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.
- Binds only to `127.0.0.1` (loopback) — no network interfaces are exposed
- Reads and writes only to `~/.holesail-browser/` (state, certs, logs, backups)
- Spawns child processes: `ssh` (for SSH sessions), `tar` (for backups), `security`/`certutil`/`update-ca-certificates` (for CA installation)
- Does not require elevated privileges for normal operation
---
## Threat Model
## Proxy security
### Threats Mitigated
### Loopback-only binding
Both the HTTPS proxy (8443) and CONNECT proxy (8442) bind to `127.0.0.1` only. They are not accessible from other machines on the network.
### Unknown hostname handling
If the HTTPS proxy receives a request for a hostname with no registered tunnel, it returns a `502 Bad Gateway` response with a plain HTML error page. It does not attempt to forward the request to any external host.
### PAC script scope
The PAC script routes only `*.hole.sail` traffic through the proxy. All other traffic goes `DIRECT`. The extension monitors `proxy.settings.onChange` and re-applies the PAC script if it is overridden by another extension.
---
## 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 |
| Network eavesdropping on tunnel traffic | Noise protocol encryption (end-to-end) |
| Man-in-the-middle on browser↔proxy TLS | Locally-trusted wildcard cert; CA key stored locally |
| Unauthorized access to remote peers | `hs://` key required to connect; Noise mutual auth |
| Proxy accessible from other machines | Loopback-only binding |
| Stale CA after reinstall causing cert errors | Fingerprint verification on every CA install check |
| Multiple native host instances with no tunnels | Process exits on EADDRINUSE; only one instance owns the ports |
### Threats Not Mitigated
## 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. |
| Malicious code with local user access | If an attacker has local user access, they can read the CA private key and the `hs://` keys in `state.json` |
| Compromised remote peer | Holesail authenticates the peer's public key but cannot verify what the peer does with your connection |
| Browser extension permissions | The extension has `proxy` and `nativeMessaging` permissions; a compromised extension could redirect all browser traffic |
---
## Proxy Security
## SSH security
### HTTPS Proxy (`127.0.0.1:8443`)
SSH sessions use the system `ssh` binary with its default security settings:
- 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.
- `StrictHostKeyChecking` is enabled by default — the first connection to a new host will prompt for host key verification
- SSH keys and `~/.ssh/known_hosts` are used normally
- The Holesail tunnel provides transport; `ssh`'s own encryption is layered on top
---
## SSH Security
## Tunnel state storage
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`.
`state.json` contains `hs://` keys for all configured tunnels. These keys are the public keys of remote peers — they are not secret. However, `state.json` also contains the server tunnel keypairs (which allow others to connect to your exposed services). Protect `~/.holesail-browser/` accordingly.
---
## Tunnel State Storage
## Backup security
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
Backup archives contain `state.json` and all certificate files including the CA private key. Store backups securely. The archives are not encrypted.
---
## 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.
1. **Keep `~/.holesail-browser/` private** — it contains the CA private key and server tunnel keypairs
2. **Restart Chrome after CA installation** — Chrome caches trust store state; a restart is required for the new CA to take effect
3. **Use `readyTimeoutMs`** — setting a timeout (e.g. 30000ms) surfaces tunnel connection failures instead of hanging indefinitely
4. **Review `hs://` keys before adding them** — only add keys from sources you trust
5. **Encrypt backups** if storing them in a shared location
+145 -176
View File
@@ -1,246 +1,215 @@
# 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.
Holesail Browser includes a full in-browser SSH terminal. It uses a Holesail tunnel to reach the remote host, spawns a real PTY via `tt-native`, and bridges it to an xterm.js terminal in the dashboard over WebSocket.
---
## How It Works
## Architecture
```
Dashboard (xterm.js)
│ WebSocket ws://127.0.0.1:21000+
ssh-manager.js (WebSocket server)
│ PTY stream (bare-ws ↔ tt-native)
│ PTY I/O (tt-native / forkpty)
ssh process (spawned with real PTY via forkpty)
│ TCP 127.0.0.1:20000+
ssh binary (system ssh)
│ TCP
Holesail client tunnel
│ P2P over Holesail
127.0.0.1:20000+ (Holesail client tunnel)
│ P2P / Noise protocol
Remote sshd
Remote SSH daemon
```
Each session:
The `ssh` binary is the system's own SSH client — all its features (key auth, agent forwarding, known hosts, etc.) work as normal. The Holesail tunnel provides the transport; `ssh` sees it as a plain TCP connection to `127.0.0.1`.
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.
## Usage
### Why a Real PTY?
### Saving a connection
Using `forkpty(3)` (via `tt-native`) gives SSH a proper local PTY, which means:
1. Open the dashboard → **SSH**
2. Click **Add Connection**
3. Enter a label, the `hs://` key for the remote peer, and the username
4. Click **Save** — the connection is stored in `state.json`
- 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.
### Connecting
1. Click **Connect** next to a saved connection
2. An xterm.js terminal opens in the dashboard
3. The native host starts a Holesail tunnel to the remote peer, then spawns `ssh`
### Terminal resize
The terminal resizes automatically when you resize the dashboard window. Resize events are sent to the native host, which calls `ioctl(TIOCSWINSZ)` on the PTY and sends an SSH window-change request.
### Disconnecting
Click **Disconnect** or close the terminal tab. The PTY and Holesail tunnel are cleaned up.
---
## 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.
## Native host commands
### `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 }
**Request payload:**
```json
{
"connectionId": "ssh-abc123",
"hostname": "myserver.hole.sail",
"hsUrl": "hs://abc123...",
"username": "root",
"port": 22,
"cols": 80,
"rows": 24
}
```
**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 |
---
**Response payload:**
```json
{
"ok": true,
"sessionId": "ssh-session-1",
"wsPort": 21000,
"tunnelPort": 20000
}
```
### `stopSshSession`
Stop and clean up a session.
Stop a running SSH session.
```javascript
await Holesail.request('stopSshSession', { sessionId: 'ssh-1' });
// { ok: true }
**Request payload:**
```json
{ "sessionId": "ssh-session-1" }
```
---
**Response payload:**
```json
{ "ok": true }
```
### `resizeSshSession`
Resize the terminal. Should be called whenever the xterm.js terminal resizes.
Resize the terminal.
```javascript
await Holesail.request('resizeSshSession', {
sessionId: 'ssh-1',
cols: 160,
rows: 50
});
// { ok: true }
**Request payload:**
```json
{ "sessionId": "ssh-session-1", "cols": 120, "rows": 40 }
```
This calls `ioctl(TIOCSWINSZ)` on the local PTY, which SSH forwards as a window-change request to the remote `sshd`.
---
**Response payload:**
```json
{ "ok": true }
```
### `getSshSessions`
List all active sessions.
List active SSH 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));
**Response payload:**
```json
{
"ok": true,
"sessions": [
{ "sessionId": "ssh-session-1", "connectionId": "ssh-abc123", "wsPort": 21000 }
]
}
});
```
// Handle terminal resize
const resizeObserver = new ResizeObserver(() => {
fitAddon.fit();
Holesail.request('resizeSshSession', {
sessionId,
cols: term.cols,
rows: term.rows
});
});
resizeObserver.observe(document.getElementById('terminal'));
### `setSshConnections`
Save SSH connection definitions to `state.json`.
**Request payload:**
```json
{
"connections": [
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://abc...", "username": "root" }
]
}
```
---
## Port Allocation
## WebSocket protocol
| Range | Component |
|-------|-----------|
| 20000+ | Holesail client tunnels for SSH sessions |
| 21000+ | WebSocket servers for xterm.js |
The dashboard connects to `ws://127.0.0.1:<wsPort>` after receiving the `startSshSession` response.
Ports are returned to a free list when a session ends and can be reused by subsequent sessions.
**Browser → native host:**
- Binary frames: raw terminal input (keystrokes, paste)
- Text frames: JSON control messages
```json
{ "type": "resize", "cols": 120, "rows": 40 }
```
**Native host → browser:**
- Binary frames: raw PTY output (terminal data)
- Text frames: JSON control messages
```json
{ "type": "exit", "code": 0 }
```
---
## SSH Options
## Port allocation
The `ssh` process is spawned with these options:
| Range | Usage |
|-------|-------|
| 20000+ | Holesail client tunnel (one per session) |
| 21000+ | WebSocket server (one per session) |
```
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.
Ports are allocated sequentially and released when the session ends.
---
## 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
- The `ssh` binary must be installed on the machine running the native host
- macOS: included with the OS
- Linux: install `openssh-client`
- Windows: install OpenSSH via Windows Optional Features or Git for Windows
- The remote host must be running an SSH daemon accessible via a Holesail `hs://` key
---
## Saved connections
SSH connections are stored in `state.json` under `sshConnections`:
```json
{
"sshConnections": [
{
"id": "ssh-abc123",
"label": "My Server",
"hsUrl": "hs://abc123...",
"username": "root"
}
]
}
```
Connections are restored on startup. Active sessions (the running PTY processes) are not persisted — they must be reconnected after a native host restart.
---
## Troubleshooting
**"tt-native not available"** — Rebuild the native host: `npm run build:host` in the project root.
**Connection times out**
**"Tunnel failed"** — The `hs://` URL is not reachable. Verify the remote machine is running a Holesail server tunnel.
- The remote peer may be offline or the `hs://` key may be wrong
- Check `~/.holesail-browser/holesail-browser.log` for tunnel errors
**"Failed to spawn ssh"** — The `ssh` binary is not on `PATH`. On macOS/Linux, `ssh` is usually pre-installed. On Windows, install OpenSSH.
**`ssh: command not found`**
**Terminal does not resize** — Ensure `resizeSshSession` is called with the correct `sessionId` after each resize.
The `ssh` binary is not in the PATH seen by the native host. Install OpenSSH or ensure `ssh` is in `/usr/bin/ssh` (macOS/Linux) or available in `PATH` (Windows).
**Host key verification fails**
SSH's `StrictHostKeyChecking` applies normally. If you see a host key warning, accept it in the terminal or add the host to `~/.ssh/known_hosts`.
**Terminal display is garbled**
Try resizing the dashboard window to trigger a resize event, or disconnect and reconnect.
+119 -200
View File
@@ -1,244 +1,163 @@
# Tunneling
Holesail Browser supports three tunnel modes, all managed through the Dashboard or the native host API.
Holesail Browser supports three tunnel types: virtual hosts (for browsing), server tunnels (for sharing), and service tunnels (for raw TCP forwarding). All tunnels use the Holesail P2P network and are persisted across restarts.
---
## How `*.hole.sail` browsing works
## How `hs://` URL Browsing Works
When you navigate to `https://myapp.hole.sail/`:
When you navigate to a `*.hole.sail` hostname, the following chain executes:
1. The PAC script (set by the extension) matches `*.hole.sail` and routes the request to `PROXY 127.0.0.1:8442`
2. Chrome sends `CONNECT myapp.hole.sail:443 HTTP/1.1` to the CONNECT proxy
3. The CONNECT proxy replies `200 Connection established` and pipes the stream to `127.0.0.1:8443`
4. The HTTPS proxy terminates TLS using the wildcard `*.hole.sail` cert (signed by the local CA)
5. The HTTPS proxy reads the `Host` header, looks up the Holesail tunnel for `myapp.hole.sail`, and proxies the HTTP request to the tunnel's local port
6. The Holesail client tunnel forwards the request to the remote peer over the DHT
```
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
```
All traffic between the browser and proxy is TLS-encrypted using a locally-trusted certificate. Traffic between the native host and the remote peer is encrypted via the Noise protocol.
### PAC Script
## Virtual hosts
The extension sets a Proxy Auto-Config (PAC) script that routes only `*.hole.sail` traffic through the local proxy. All other traffic goes directly:
Virtual hosts map an `hs://` key to a `*.hole.sail` hostname. The browser navigates to the hostname; the proxy looks up the tunnel and forwards the request.
```javascript
function FindProxyForURL(url, host) {
if (dnsDomainIs(host, ".hole.sail"))
return "PROXY 127.0.0.1:8442";
return "DIRECT";
}
```
### Adding a virtual host
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.
1. Open the dashboard → **Virtual Hosts**
2. Enter a hostname (e.g. `myapp.hole.sail`) and the `hs://` key for the remote peer
3. Click **Add** — the tunnel connects immediately and the entry is saved to `state.json`
### 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
### 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
- Must be a valid DNS label (lowercase letters, numbers, hyphens)
- Examples: `myapp.hole.sail`, `dev-server.hole.sail`, `api.hole.sail`
- The bare domain `hole.sail` is also routed through the proxy
### Port Allocation
### 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.
Each virtual host gets a local port allocated from 19000 upward. The HTTPS proxy forwards requests for that hostname to `127.0.0.1:<allocated-port>`. Ports are released when the virtual host is removed.
### Tunnel Lifecycle Events
### Lifecycle events
The native host emits events that the extension forwards to subscribed tabs:
The native host emits events that the extension relays 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" }
}
```
| Event | Description |
|-------|-------------|
| `tunnelReady` | Tunnel connected and ready to serve requests |
| `tunnelClosed` | Tunnel disconnected (peer went offline or was removed) |
| `tunnelError` | Tunnel failed to connect |
### Persistence
Server tunnels are persisted to `holesail-browser-storage/state.json` and restored automatically when the native host starts.
Virtual hosts are saved to `state.json` and restored on every native host startup. The tunnel reconnects automatically — if the remote peer is temporarily offline, the tunnel will reconnect when it comes back.
### Removing a virtual host
Open the dashboard → **Virtual Hosts** → click the remove button. The tunnel is closed and the entry is deleted from `state.json`.
---
## Service Tunnels
## Server 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.
Server tunnels expose a local TCP (or UDP) port as an `hs://` key. Other peers can connect to your service using that key.
### Starting a Service Tunnel
### Creating a server tunnel
**Via Dashboard:**
1. Dashboard → **Service Tunnels** → enter a label, `hs://` URL, and local port → click **Add**
1. Open the dashboard → **Server Tunnels**
2. Enter the local port to expose (e.g. `3000`)
3. Select **TCP** or **UDP**
4. Optionally enable **Secure** mode (requires the connecting peer to have the key)
5. Click **Start** — an `hs://` key is generated and displayed
**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"
}
```
Share the `hs://` key with anyone who should be able to connect to your service.
### 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 |
| Parameter | Description |
|-----------|-------------|
| `port` | Local port to expose |
| `host` | Local host to bind (default: `127.0.0.1`) |
| `udp` | Use UDP instead of TCP (default: `false`) |
| `secure` | Require key authentication (default: `true`) |
### Stopping a Service Tunnel
### Persistence
```json
{
"type": "stopServiceTunnel",
"payload": { "tunnelId": "svc-1" }
}
```
Server tunnels are saved to `state.json` and restarted on every native host startup. The `hs://` key is stable — it is derived from the server's keypair, which is preserved in state.
### Use Cases
### Stopping a server tunnel
- 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
Open the dashboard → **Server Tunnels** → click **Stop**. The tunnel is closed and the entry is deleted from `state.json`.
---
## Tunnel Lookup
## Service tunnels
You can look up whether an `hs://` URL is reachable on the DHT without creating a full tunnel:
Service tunnels forward a remote `hs://` peer to a local TCP port. Any local application can connect to `127.0.0.1:<port>` and communicate with the remote peer directly — without going through the browser proxy.
```json
{
"type": "lookup",
"payload": { "url": "hs://abc123..." }
}
```
Use cases:
- Connect a local database client to a remote database over Holesail
- Forward a remote TCP service to a local port for use by any application
- Expose a Minecraft server, game server, or other TCP service locally
### Creating a service tunnel
1. Open the dashboard → **Service Tunnels**
2. Enter a label (e.g. `Postgres`), the `hs://` key, and the local port to bind
3. Click **Start** — the tunnel connects and begins listening on the specified port
### Parameters
| Parameter | Description |
|-----------|-------------|
| `label` | Human-readable name |
| `hsUrl` | `hs://` key for the remote peer |
| `localPort` | Local TCP port to bind |
### Persistence
Service tunnels are saved to `state.json` and restored on startup.
### Stopping a service tunnel
Open the dashboard → **Service Tunnels** → click **Stop**.
---
## Comparison of Tunnel Types
## Tunnel lookup
| 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 |
You can check whether an `hs://` key is reachable on the DHT without creating a full tunnel. This is useful for verifying a key before adding it as a virtual host or service tunnel.
From the dashboard → **Virtual Hosts** or **Service Tunnels**, use the lookup/test button next to the `hs://` field.
---
## Tunnel type comparison
| Feature | Virtual Host | Server Tunnel | Service Tunnel |
|---------|-------------|---------------|----------------|
| Direction | Inbound (you browse to remote) | Outbound (remote connects to you) | Inbound (you connect to remote) |
| Access method | Browser via `*.hole.sail` URL | `hs://` key shared with peers | Local TCP port |
| Protocol | HTTPS (TLS terminated locally) | TCP or UDP | TCP |
| Use case | Browse remote web apps | Share local services | Connect local apps to remote services |
| Persisted | Yes | Yes | Yes |
---
## Troubleshooting
**`No tunnel for this hostname`**
The HTTPS proxy received a request for a hostname it has no tunnel for. Causes:
- The tunnel is still connecting — wait a few seconds and refresh
- The native host restarted and is still restoring tunnels — open the dashboard to check
- The hostname doesn't exactly match what was configured (check for typos)
**Tunnel shows `connecting` but never becomes `ready`**
- The remote peer may be offline
- The `hs://` key may be incorrect
- Check `~/.holesail-browser/holesail-browser.log` for error details
- Try setting `readyTimeoutMs` to `30000` in Settings to surface timeout errors
**Service tunnel port is already in use**
Another process is using the requested local port. Choose a different port, or stop the conflicting process.