docs
CI / Build & Test (push) Successful in 2m56s

This commit is contained in:
Raven Scott
2026-02-28 22:29:56 -05:00
parent 8cc76e580a
commit f5c349a4a3
10 changed files with 125 additions and 58 deletions
+20 -16
View File
@@ -1,26 +1,27 @@
# Holesail 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.
Browse P2P Holesail tunnels directly in your browser. Holesail Browser is a Chrome/Firefox extension paired with a native host that routes virtual host domains (e.g. `*.hole.sail` or any private TLD you choose) through Holesail tunnels — with automatic TLS, no port-forwarding, and no central servers.
## How it works
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)
2. The extension sets a PAC script that routes `*.hole.sail` (and any other custom TLDs) → 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
4. The HTTPS proxy reads the TLS ClientHello, extracts the SNI hostname, and presents a per-TLD wildcard cert (signed by the local CA)
5. The Holesail tunnel connects P2P to the remote peer over the DHT
All traffic is end-to-end encrypted via the Noise protocol. The local CA is only used for the browser↔proxy TLS leg.
## Features
- **Virtual Hosts** — browse any `hs://` URL as `https://name.hole.sail/`
- **Server Tunnels** — expose a local port as an `hs://` key (TCP or UDP)
- **Virtual Hosts** — browse any `hs://` URL as `https://name.hole.sail/` or any custom private TLD (e.g. `https://api.my.internal/`)
- **Custom TLDs** — use any private two-tier TLD, not just `.hole.sail`; PAC script and certificates are updated automatically
- **Server Tunnels** — expose a local port as an `hs://` key (TCP or UDP); supports an optional label for easy identification
- **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
- **SSH** — in-browser SSH terminal via xterm.js, over a Holesail tunnel; supports public key, saved password, and interactive auth
- **Remote Desktop** — VNC (noVNC) and RDP viewer in the browser, over a Holesail tunnel; passwords saved securely
- **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
- **Auto CA** — generates and installs a local root CA; issues per-TLD wildcard certs on demand via JS-layer SNI
- **Persistent state** — all tunnels, connections, and settings survive restarts
## Installation
@@ -83,21 +84,23 @@ For a permanent install in regular Firefox, the extension must be signed by Mozi
## First run
1. Click the Holesail Browser icon in your toolbar to open the dashboard
1. Click the Holesail Browser icon in your toolbar to open the **Overview** 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/`
4. Go to **Virtual Hosts** → add a hostname (e.g. `myapp.hole.sail`) and its `hs://` key
5. Navigate to `https://myapp.hole.sail/`
> **Note:** The CA must be installed and Chrome must be restarted before `*.hole.sail` sites will load without a certificate warning.
> **Note:** The CA must be installed and Chrome must be restarted before virtual host sites will load without a certificate warning. You can use any private two-tier TLD — not just `.hole.sail`.
## Dashboard pages
The dashboard uses a single topbar title per page. The **Overview** page is the landing page and features a sticky Quick Actions bar at the bottom with equal-height scrollable list cards for all data types.
| 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) |
| Overview | Status summary, stat cards, scrollable lists for all data types, Quick Actions |
| Virtual Hosts | Map `hs://` keys to hostnames on any private TLD |
| Server Tunnels | Expose local ports as `hs://` keys (TCP/UDP), with optional label |
| 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 |
@@ -118,7 +121,7 @@ For a permanent install in regular Firefox, the extension must be signed by Mozi
## Troubleshooting
**`*.hole.sail` sites show a certificate error**
**Virtual host sites show a certificate error**
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).
@@ -185,6 +188,7 @@ The built extension will be in `releases/Holesail-Browser-*.zip` and `.xpi`. Pla
- [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
- [Virtual Hosts](docs/VIRTUAL-HOSTS.md) — custom TLDs, certificate generation, deep hostnames
- [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
+7 -5
View File
@@ -59,7 +59,7 @@ Holesail Browser is composed of three parts: a browser extension, a native host
|------|---------|
| `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. |
| `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, including `updateServiceTunnel` (stop + restart a service tunnel in one call). |
| `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` | SNI-aware HTTPS reverse proxy on `127.0.0.1:8443`. Uses a pure-JS TLS ClientHello parser to extract the SNI hostname from each incoming connection, derives the wildcard parent domain, and presents a per-TLD wildcard cert. Supports any hostname depth (e.g. `i.love.hole.sail`). 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`. |
@@ -75,7 +75,7 @@ Holesail Browser is composed of three parts: a browser extension, a native host
| `manifest.json` | Manifest V3. Permissions: `nativeMessaging`, `proxy`, `declarativeNetRequest`, `tabs`, `notifications`. Optional host permissions requested at runtime for custom TLDs. |
| `background.js` | Service worker. Connects to native host via `connectNative`. Sets PAC script dynamically — includes `*.hole.sail` plus any custom TLDs from virtual hosts. 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. 10 pages: Overview, Virtual Hosts, Server Tunnels, Service Tunnels, Proxy & CA, SSH, Remote Desktop, Backups, Logs, Settings. |
| `dashboard.html/js` | Full management UI. 10 pages: Overview, Virtual Hosts, Server Tunnels, Service Tunnels, Proxy & CA, SSH, Remote Desktop, Backups, Logs, Settings. Each page title is shown only in the sticky topbar. The Overview page uses a flex layout: status bar → stat cards → equal-height scrollable list rows (Virtual Hosts + Server Tunnels on row 1; Service Tunnels + SSH + Remote Desktop on row 2) → sticky Quick Actions pinned at the bottom. |
| `wrong-domain.html` | Error page for `*.host.test` (common typo), redirected via `declarativeNetRequest`. |
## Proxy architecture
@@ -237,7 +237,9 @@ All state is owned by the native host and persisted to `state.json` next to the
},
"nextServerId": 0,
"nextServiceTunnelId": 0,
"servers": [],
"servers": [
{ "id": "server_1", "port": 3000, "host": "127.0.0.1", "secure": true, "udp": false, "label": "My Web App" }
],
"virtualHosts": [
{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." },
{ "hostname": "i.love.hole.sail", "hsUrl": "hs://def456..." },
@@ -247,10 +249,10 @@ All state is owned by the native host and persisted to `state.json` next to the
{ "id": "svc-1", "label": "Postgres", "hsUrl": "hs://def456...", "localPort": 5432 }
],
"sshConnections": [
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://xyz...", "username": "root" }
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://xyz...", "username": "root", "passwordB64": "cGFzc3dvcmQ=" }
],
"rdpConnections": [
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://xyz...", "type": "vnc", "port": 5901, "width": 1280, "height": 720, "username": "" }
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://xyz...", "type": "vnc", "port": 5901, "width": 1280, "height": 720, "username": "", "passwordB64": "cGFzc3dvcmQ=" }
]
}
```
+6 -4
View File
@@ -8,13 +8,15 @@ Each backup archive contains:
| Path in archive | Source |
|-----------------|--------|
| `storage/state.json` | All virtual hosts, server tunnels, service tunnels, SSH connections, RDP connections, and settings |
| `storage/state.json` | All virtual hosts, server tunnels (with labels), service tunnels, SSH connections (with `passwordB64`), RDP connections (with `passwordB64`), 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 |
| `certs/wildcard.hole.sail/` | Wildcard cert for the default `.hole.sail` TLD |
| `certs/wildcard.<parent>/` | One directory per custom TLD parent (e.g. `wildcard.my.internal/`, `wildcard.haha.wooo/`) |
Backups do **not** include the binary itself, the log file, or other backup archives.
The entire `holesail-browser-certs/` directory is archived — all wildcard cert directories are included, not just the default one. Backups do **not** include the binary itself, the log file, or other backup archives.
The default backup retention is **5 backups** (configurable in Settings via `backupRetention`).
## Storage location
+7 -5
View File
@@ -30,8 +30,9 @@ Or download `install.ps1` from the [latest release](https://git.ssh.surf/snxrave
6. Extracts the binary to `~/.holesail-browser/holesail-browser-host`
7. **Restores user data** — the preserved storage and certs directories are copied back, so all tunnels, connections, settings, and certificates survive the upgrade
8. **macOS only:** removes Gatekeeper quarantine, ad-hoc signs the binary, pre-runs it to extract `.bare` native addons, then signs all extracted addons
9. Downloads `Holesail-Browser-1.0.0.zip` and `Holesail-Browser-1.0.0.xpi` to `~/Downloads`
10. Writes the native messaging manifest to all browser locations
9. **Removes old extension files** — any existing `Holesail-Browser-*.zip` and `Holesail-Browser-*.xpi` files in `~/Downloads` are deleted before downloading the new version
10. Downloads `Holesail-Browser-1.0.0.zip` and `Holesail-Browser-1.0.0.xpi` to `~/Downloads`
11. Writes the native messaging manifest to all browser locations
### Windows (`install.ps1`)
@@ -40,15 +41,16 @@ Or download `install.ps1` from the [latest release](https://git.ssh.surf/snxrave
3. Removes the previous installation directory and old registry keys
4. Downloads `holesail-browser-host-win32-x64.zip` and extracts the `.exe`
5. **Restores user data** — the preserved directories are copied back into the new installation
6. Downloads `Holesail-Browser-1.0.0.zip` to `~/Downloads`
7. Writes the native messaging manifest JSON and creates registry keys for Chrome, Chromium, and Firefox
6. **Removes old extension files** — any existing `Holesail-Browser-*.zip` and `Holesail-Browser-*.xpi` files in `%USERPROFILE%\Downloads` are deleted before downloading the new version
7. Downloads `Holesail-Browser-1.0.0.zip` and `Holesail-Browser-1.0.0.xpi` to `%USERPROFILE%\Downloads`
8. Writes the native messaging manifest JSON and creates registry keys for Chrome, Chromium, and Firefox
### What is preserved across upgrades
| Directory | Contents |
|-----------|----------|
| `holesail-browser-storage/` | `state.json` (all virtual hosts, tunnels, SSH/RDP connections, settings), backup archives |
| `holesail-browser-certs/` | Root CA key and certificate, wildcard `*.hole.sail` domain cert |
| `holesail-browser-certs/` | Root CA key and certificate, all wildcard domain certs (one per custom TLD parent, e.g. `wildcard.hole.sail/`, `wildcard.my.internal/`) |
The binary itself is always replaced with the new version. If this is a fresh install (no previous `~/.holesail-browser/` directory), the preserve/restore steps are skipped.
+29 -4
View File
@@ -104,13 +104,16 @@ Start a server tunnel (expose a local port as an `hs://` key).
"port": 3000,
"host": "127.0.0.1",
"secure": true,
"udp": false
"udp": false,
"label": "My Web App"
}
```
`label` is optional. When provided it is shown in the dashboard and persisted in `state.json`.
**Response payload:**
```json
{ "ok": true, "serverId": "server_1", "hsUrl": "hs://abc123...", "state": "ready" }
{ "ok": true, "serverId": "server_1", "url": "hs://abc123...", "port": 3000, "host": "127.0.0.1", "secure": true, "udp": false, "label": "My Web App" }
```
---
@@ -168,6 +171,28 @@ Stop a service tunnel.
---
### `updateServiceTunnel`
Stop and restart a service tunnel with the same (or updated) parameters. Used by the dashboard **Reconnect** button for tunnels in error or closed state.
**Request payload:**
```json
{
"tunnelId": "svc-1",
"label": "Postgres",
"hsUrl": "hs://abc123...",
"localPort": 5432
}
```
**Response payload:** Same as `startServiceTunnel`.
```json
{ "ok": true, "tunnelId": "svc-1", "localPort": 5432, "state": "ready" }
```
---
### `lookupTunnel`
Check whether an `hs://` key is reachable on the DHT.
@@ -194,14 +219,14 @@ Start an SSH session.
"connectionId": "ssh-abc123",
"hsUrl": "hs://abc123...",
"username": "root",
"password": "",
"passwordB64": "cGFzc3dvcmQ=",
"cols": 80,
"rows": 24,
"label": "My Server"
}
```
`password` is optional. If provided, it is delivered to SSH automatically via `SSH_ASKPASS` and a named FIFO — it is never written to disk or passed on the command line. If omitted, SSH tries public key auth first; if that fails, a password prompt appears in the xterm.js terminal.
`passwordB64` is optional. When present it is a base64-encoded UTF-8 password (`btoa(unescape(encodeURIComponent(password)))`). It is delivered to SSH via `SSH_ASKPASS` and a named FIFO — never written to disk separately or passed on the command line. If omitted, SSH tries public key auth first; if that fails, a password prompt appears in the xterm.js terminal.
**Response payload:**
```json
+12 -4
View File
@@ -61,6 +61,7 @@ For RDP, `node-rdpjs-2` handles the RDP protocol in the native host. Bitmap upda
- **Port** — remote port (default: 5900 for VNC, 3389 for RDP)
- **Width / Height** — initial display resolution
- **Username** — (RDP only) login username
- **Password** — optional; saved as base64 in `state.json` and used automatically on reconnect
4. Click **Save**
### Connecting
@@ -90,10 +91,13 @@ Start a new VNC or RDP session.
"port": 5900,
"width": 1280,
"height": 720,
"username": ""
"username": "",
"passwordB64": "cGFzc3dvcmQ="
}
```
`passwordB64` is optional. When present it is a base64-encoded UTF-8 password string used for RDP authentication.
**Response payload:**
```json
{
@@ -148,12 +152,15 @@ Save RDP/VNC connection definitions to `state.json`.
"port": 5900,
"width": 1280,
"height": 720,
"username": ""
"username": "",
"passwordB64": "cGFzc3dvcmQ="
}
]
}
```
`passwordB64` is optional. When present it is a base64-encoded UTF-8 password string (`btoa(unescape(encodeURIComponent(password)))`). It is persisted in `state.json` and used automatically when starting a session.
---
## WebSocket protocol
@@ -233,13 +240,14 @@ RDP/VNC connections are stored in `state.json` under `rdpConnections`:
"port": 5900,
"width": 1280,
"height": 720,
"username": ""
"username": "",
"passwordB64": "cGFzc3dvcmQ="
}
]
}
```
Connections are restored on startup. Active sessions are not persisted — they must be reconnected after a native host restart.
`passwordB64` is omitted when no password was saved. Connections are restored on startup. Active sessions are not persisted — they must be reconnected after a native host restart.
---
+7 -2
View File
@@ -133,7 +133,7 @@ This prevents accidentally routing real internet traffic through the local proxy
SSH sessions use the system `ssh` binary. Authentication is tried in order: public key, then password.
- **Public key auth** — keys from `~/.ssh/` and `ssh-agent` are used automatically; no password prompt appears if a key is accepted
- **Password auth** — passwords are optionally saved as base64 in `state.json` (in memory only during the session); a saved password is delivered to SSH via `SSH_ASKPASS` and a named FIFO pipe (not via the process environment or command line); if no password is saved and key auth fails, an interactive password prompt appears in xterm.js and is delivered the same way
- **Password auth** — passwords are optionally saved as `passwordB64` (base64-encoded) in `state.json` and survive native host restarts; a saved password is delivered to SSH via `SSH_ASKPASS` and a named FIFO pipe (not via the process environment or command line); if no password is saved and key auth fails, an interactive password prompt appears in xterm.js and is delivered the same way
- **`StrictHostKeyChecking=no` / `UserKnownHostsFile=/dev/null`** — host key verification is disabled for Holesail tunnels because the remote host's identity is already established by the `hs://` public key (Noise mutual authentication at the tunnel layer); adding known-hosts entries for `127.0.0.1` would be misleading since the port changes per session
- The Holesail tunnel provides transport; `ssh`'s own encryption is layered on top, giving double encryption
@@ -141,7 +141,12 @@ SSH sessions use the system `ssh` binary. Authentication is tried in order: publ
## Tunnel state storage
`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) and optionally saved SSH/RDP passwords (base64-encoded). Protect `~/.holesail-browser/` accordingly.
`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:
- **Server tunnel keypairs** — the `hs://` URL for each server tunnel is derived from a keypair that is preserved in state; anyone with this URL can connect to your exposed service
- **`passwordB64`** — SSH and RDP connection passwords are stored as base64-encoded strings under `sshConnections[].passwordB64` and `rdpConnections[].passwordB64`; server tunnel `label` fields are also stored but are not sensitive
Protect `~/.holesail-browser/` accordingly.
---
+19 -8
View File
@@ -34,7 +34,7 @@ If you have a key in `~/.ssh/` (e.g. `id_rsa`, `id_ed25519`) or loaded in `ssh-a
### 2. Password — saved on the connection
If you enter a password when saving the connection, it is stored in memory for the session (never written to disk). When connecting, the password is delivered to SSH automatically via a secure helper mechanism — no prompt appears.
If you enter a password when saving the connection, it is stored as base64 (`passwordB64`) in `state.json` and survives native host restarts. When connecting, the password is delivered to SSH automatically via `SSH_ASKPASS` and a named FIFO — no prompt appears and the password is never passed on the command line or in the environment.
### 3. Password — typed interactively
@@ -65,7 +65,7 @@ If no public key is available and no password is saved, the terminal displays a
### Connecting
1. Click **Connect** next to a saved connection
2. An xterm.js terminal opens in the dashboard
2. An xterm.js terminal opens in the dashboard and is automatically focused — you can start typing immediately
3. The native host starts a Holesail tunnel to the remote peer, then spawns `ssh`
4. If authentication requires a password and none is saved, a prompt appears in the terminal
@@ -91,14 +91,14 @@ Start a new SSH session.
"connectionId": "ssh-abc123",
"hsUrl": "hs://abc123...",
"username": "root",
"password": "",
"passwordB64": "cGFzc3dvcmQ=",
"cols": 80,
"rows": 24,
"label": "My Server"
}
```
The `password` field is optional. If omitted or empty, SSH will use public key auth or prompt interactively.
`passwordB64` is optional. When present it is a base64-encoded UTF-8 password (`btoa(unescape(encodeURIComponent(password)))`). If omitted or empty, SSH will use public key auth or prompt interactively in the terminal.
**Response payload:**
```json
@@ -154,17 +154,25 @@ List active SSH sessions.
### `setSshConnections`
Save SSH connection definitions to `state.json`. Passwords are stripped before saving — they are session-only.
Save SSH connection definitions to `state.json`.
**Request payload:**
```json
{
"connections": [
{ "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://abc...", "username": "root" }
{
"id": "ssh-abc123",
"label": "My Server",
"hsUrl": "hs://abc...",
"username": "root",
"passwordB64": "cGFzc3dvcmQ="
}
]
}
```
`passwordB64` is optional. When present it is a base64-encoded UTF-8 password string (`btoa(unescape(encodeURIComponent(password)))`). It is persisted in `state.json` and delivered to SSH via `SSH_ASKPASS` + a named FIFO on each connection — never via the command line or environment.
---
## WebSocket protocol
@@ -199,7 +207,7 @@ Ports are allocated sequentially and released when the session ends.
## Saved connections
SSH connections are stored in `state.json` under `sshConnections`. Passwords are **never** written to disk — they are held in memory for the current session only and must be re-entered after a native host restart.
SSH connections are stored in `state.json` under `sshConnections`. If a password was saved, it is stored as `passwordB64` (base64-encoded) and survives native host restarts.
```json
{
@@ -208,12 +216,15 @@ SSH connections are stored in `state.json` under `sshConnections`. Passwords are
"id": "ssh-abc123",
"label": "My Server",
"hsUrl": "hs://abc123...",
"username": "root"
"username": "root",
"passwordB64": "cGFzc3dvcmQ="
}
]
}
```
`passwordB64` is omitted when no password was saved for the connection.
---
## Requirements
+17 -9
View File
@@ -9,7 +9,7 @@ When you navigate to `https://myapp.hole.sail/`:
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)
4. The HTTPS proxy reads the TLS ClientHello, extracts the SNI hostname (`myapp.hole.sail`), derives the wildcard parent (`hole.sail`), and presents the `*.hole.sail` wildcard 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
@@ -27,10 +27,12 @@ Virtual hosts map an `hs://` key to a `*.hole.sail` hostname. The browser naviga
### Hostname rules
- Must end in `.hole.sail`
- 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
- Must have **at least 3 labels** (e.g. `myapp.hole.sail`, not `hole.sail`)
- The TLD portion must be **two-tier** (e.g. `.hole.sail`, `.my.internal`) — single-label TLDs like `.holesail` are not allowed
- Must **not** use a real public TLD (`.com`, `.net`, `.co.uk`, etc.)
- Each label may contain only lowercase letters, digits, and hyphens; no leading or trailing hyphens
- Any depth is supported: `myapp.hole.sail`, `i.love.hole.sail`, `api.v2.my.internal`
- You are not limited to `.hole.sail` — any valid private two-tier TLD works (see [VIRTUAL-HOSTS.md](VIRTUAL-HOSTS.md))
### Port allocation
@@ -63,10 +65,11 @@ Server tunnels expose a local TCP (or UDP) port as an `hs://` key. Other peers c
### Creating a server tunnel
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
2. Optionally enter a **Label** (e.g. `My Web App`) to identify the tunnel
3. Enter the local port to expose (e.g. `3000`)
4. Select **TCP** or **UDP**
5. Optionally enable **Secure** mode (requires the connecting peer to have the key)
6. Click **Start** — an `hs://` key is generated and displayed
Share the `hs://` key with anyone who should be able to connect to your service.
@@ -74,6 +77,7 @@ Share the `hs://` key with anyone who should be able to connect to your service.
| Parameter | Description |
|-----------|-------------|
| `label` | Optional human-readable name shown in the dashboard |
| `port` | Local port to expose |
| `host` | Local host to bind (default: `127.0.0.1`) |
| `udp` | Use UDP instead of TCP (default: `false`) |
@@ -116,6 +120,10 @@ Use cases:
Service tunnels are saved to `state.json` and restored on startup.
### Reconnecting a service tunnel
If a service tunnel enters an error or closed state, click **Reconnect** in the dashboard. This sends an `updateServiceTunnel` command to the native host, which stops the old tunnel (if any) and starts a fresh one with the same parameters.
### Stopping a service tunnel
Open the dashboard → **Service Tunnels** → click **Stop**.
+1 -1
View File
@@ -65,7 +65,7 @@ The HTTPS proxy uses **JS-layer SNI** to select the right certificate per connec
4. `certificate-authority.getOrCreateWildcardCert(parent)` returns an existing cert or generates a new one signed by the local root CA
5. A `bare-tls.Socket` is created with that cert and the TLS handshake proceeds
Certs are stored in `~/.holesail-browser/holesail-browser-certs/wildcard.<parent>/` and reused on subsequent connections. They are valid for 1 year and auto-renewed on expiry.
Certs are stored in `~/.holesail-browser/holesail-browser-certs/wildcard.<parent>/` (the `*` in the wildcard is stored literally as `wildcard` in the directory name, e.g. `wildcard.hole.sail/`) and reused on subsequent connections. They are valid for 1 year and auto-renewed on expiry.
No proxy restart is needed when adding new TLDs — the cert is generated on the first connection.