update docs
CI / Build & Test (push) Has been cancelled

This commit is contained in:
Raven Scott
2026-02-28 21:18:23 -05:00
parent 8303c72140
commit c51717699d
4 changed files with 283 additions and 33 deletions
+80 -19
View File
@@ -1,6 +1,6 @@
# Architecture
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.
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 virtual host traffic through.
## Component overview
@@ -14,6 +14,7 @@ Holesail Browser is composed of three parts: a browser extension, a native host
│ │ worker) │ │ SSH, RDP, backups, settings, logs) │ │
│ └──────┬───────┘ └──────────────────────────────────────┘ │
│ │ PAC script: *.hole.sail → PROXY 127.0.0.1:8442 │
│ │ *.custom.tld → PROXY 127.0.0.1:8442 │
│ │ Native messaging: chrome.runtime.connectNative(...) │
└─────────┼───────────────────────────────────────────────────────┘
│ stdin/stdout (4-byte length-prefixed JSON)
@@ -24,16 +25,16 @@ Holesail Browser is composed of three parts: a browser extension, a native host
│ ┌──────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ host.js │ │ holesail- │ │ certificate- │ │
│ │ (command │ │ manager.js │ │ authority.js │ │
│ │ dispatch)│ │ (tunnel lifecycle│ │ (root CA + domain │ │
│ └────┬─────┘ │ + state.json) │ │ cert generation, │ │
│ │ dispatch)│ │ (tunnel lifecycle│ │ (root CA + per-TLD │ │
│ └────┬─────┘ │ + state.json) │ │ wildcard certs, │ │
│ │ └────────┬─────────┘ │ 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) │ │
│ │ (SNI-aware TLS, (HTTP CONNECT handler, │ │
│ │ per-TLD wildcard pipes to 127.0.0.1:8443) │ │
│ │ certs, HTTP forwarding) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
@@ -60,9 +61,9 @@ Holesail Browser is composed of three parts: a browser extension, a native host
| `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. |
| `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`. |
| `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. |
| `certificate-authority.js` | Generates a 2048-bit RSA root CA (10-year validity) using `node-forge`. Signs per-TLD wildcard domain certs on demand (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. SSH is spawned only after the browser WebSocket client connects and sends a ready-signal. Password delivery uses `SSH_ASKPASS` + a named FIFO — public key auth is tried first; a password prompt appears in xterm.js only if key auth fails and no password is saved. |
| `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. |
@@ -71,11 +72,11 @@ Holesail Browser is composed of three parts: a browser extension, a native host
| 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. |
| `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 (~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`. |
| `dashboard.html/js` | Full management UI. 10 pages: Overview, Virtual Hosts, Server Tunnels, Service Tunnels, Proxy & CA, SSH, Remote Desktop, Backups, Logs, Settings. |
| `wrong-domain.html` | Error page for `*.host.test` (common typo), redirected via `declarativeNetRequest`. |
## Proxy architecture
@@ -85,7 +86,8 @@ The browser cannot connect directly to a custom HTTPS server via a proxy — it
Browser navigates to https://myapp.hole.sail/
│ PAC script (applied by background.js):
│ *.hole.sail → PROXY 127.0.0.1:8442
│ *.hole.sail → PROXY 127.0.0.1:8442
│ *.custom.tld → PROXY 127.0.0.1:8442 (custom TLDs)
│ everything else → DIRECT
CONNECT proxy (connect-proxy.js) 127.0.0.1:8442
@@ -94,7 +96,10 @@ CONNECT proxy (connect-proxy.js) 127.0.0.1:8442
│ 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)
Peeks TLS ClientHello → extracts SNI: "myapp.hole.sail"
│ Derives wildcard parent: "hole.sail"
│ Selects cert: wildcard.hole.sail/ (*.hole.sail)
│ TLS handshake with SNI-matched cert
│ Reads Host header: myapp.hole.sail
│ Calls holesailManager.getLocalBackend('myapp.hole.sail')
│ → { host: '127.0.0.1', port: 19042 }
@@ -106,8 +111,56 @@ Holesail client tunnel (holesail-manager.js) 127.0.0.1:19042
Remote peer (HTTP server)
```
### Deep hostname example
```
Browser navigates to https://i.love.hole.sail/
│ PAC: dnsDomainIs(host, ".hole.sail") → PROXY 127.0.0.1:8442
CONNECT proxy 127.0.0.1:8442
│ CONNECT i.love.hole.sail:443
HTTPS proxy 127.0.0.1:8443
│ SNI: "i.love.hole.sail"
│ Wildcard parent: "love.hole.sail"
│ Cert: wildcard.love.hole.sail/ (*.love.hole.sail)
│ Host header → backend lookup → proxy
Holesail tunnel → remote peer
```
### SNI implementation
Because `bare-tls` does not expose `SSL_CTX_set_tlsext_servername_callback`, SNI is implemented entirely in JavaScript:
1. Raw TCP connections are accepted via `bare-tcp`
2. The first data chunk (TLS ClientHello) is read and parsed with a pure-JS TLS record parser (RFC 5246 extension type `0x0000`)
3. The SNI hostname is extracted; its wildcard parent is derived by stripping the leftmost label
4. `certificate-authority.getOrCreateWildcardCert(parent)` returns (or generates) a wildcard cert for that parent
5. A `bare-tls.Socket` is created with that cert; the already-read ClientHello bytes are replayed into it so the handshake proceeds normally
6. A `bare-http1.ServerConnection` wraps the TLS socket for HTTP parsing
Each unique wildcard parent gets its own cert directory under `holesail-browser-certs/`, generated on first connection and cached for subsequent ones.
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.
## Custom TLDs
Virtual hosts can use any private TLD — not just `.hole.sail`. The dashboard validates that the hostname:
- Has at least 3 labels (e.g. `app.hole.sail`, not `hole.sail`)
- Uses only letters, digits, and hyphens per label
- Does not use a real public TLD (`.com`, `.net`, `.co.uk`, etc.)
When a virtual host with a new TLD is added:
1. The PAC script is updated to include `dnsDomainIs(host, ".new.tld")` — traffic is routed through the CONNECT proxy
2. The browser requests `optional_host_permissions` for `*://*.new.tld/*` at runtime
3. On the first HTTPS connection to any hostname under that TLD, the HTTPS proxy generates a wildcard cert for the exact wildcard parent on demand
No proxy restart is needed when adding new TLDs.
## 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.
@@ -133,7 +186,7 @@ See [NATIVE-HOST.md](NATIVE-HOST.md) for the full message type reference.
| Port | Component | Description |
|------|-----------|-------------|
| 8443 | `https-proxy.js` | HTTPS proxy (TLS termination) |
| 8443 | `https-proxy.js` | HTTPS proxy (SNI-aware 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 |
@@ -154,9 +207,15 @@ All state is owned by the native host and persisted to `state.json` next to the
├── 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
── wildcard.hole.sail/ # cert for *.hole.sail (default TLD)
├── key.pem
└── cert.pem # leaf cert + CA chain
│ ├── wildcard.haha.wooo/ # cert for *.haha.wooo (custom TLD)
│ │ ├── key.pem
│ │ └── cert.pem
│ └── wildcard.love.hole.sail/ # cert for *.love.hole.sail (deep hostname)
│ ├── key.pem
│ └── cert.pem
└── holesail-browser-storage/
├── state.json # all persistent state
└── backups/ # tar.gz backup archives
@@ -180,7 +239,9 @@ All state is owned by the native host and persisted to `state.json` next to the
"nextServiceTunnelId": 0,
"servers": [],
"virtualHosts": [
{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." }
{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." },
{ "hostname": "i.love.hole.sail", "hsUrl": "hs://def456..." },
{ "hostname": "api.haha.wooo", "hsUrl": "hs://ghi789..." }
],
"serviceTunnels": [
{ "id": "svc-1", "label": "Postgres", "hsUrl": "hs://def456...", "localPort": 5432 }
+35 -3
View File
@@ -56,11 +56,21 @@ Returns the full current state of the native host. The extension calls this on s
Add or update a virtual host. If the hostname already exists, the old tunnel is closed and a new one is started.
Hostnames can use any private TLD — not just `.hole.sail`. The hostname must have at least 3 labels and must not use a real public TLD. Any depth is supported (e.g. `i.love.hole.sail`, `api.v2.my.internal`).
After a successful response, the background service worker automatically updates the PAC script to include the new TLD and requests host permissions for it.
**Request payload:**
```json
{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." }
```
Custom TLD examples:
```json
{ "hostname": "api.haha.wooo", "hsUrl": "hs://abc123..." }
{ "hostname": "i.love.hole.sail", "hsUrl": "hs://abc123..." }
```
**Response payload:**
```json
{ "ok": true, "hostname": "myapp.hole.sail", "localHost": "127.0.0.1", "localPort": 19000, "state": "ready" }
@@ -254,11 +264,19 @@ Save SSH connection definitions.
```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 stored in `state.json` and delivered to SSH via `SSH_ASKPASS` + a named FIFO — never via the command line or environment.
**Response payload:**
```json
{ "ok": true }
@@ -328,11 +346,23 @@ Save RDP/VNC connection definitions.
```json
{
"connections": [
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://abc...", "type": "vnc", "port": 5900, "width": 1280, "height": 720, "username": "" }
{
"id": "rdp-abc123",
"label": "Work PC",
"hsUrl": "hs://abc...",
"type": "vnc",
"port": 5900,
"width": 1280,
"height": 720,
"username": "",
"passwordB64": "cGFzc3dvcmQ="
}
]
}
```
`passwordB64` is optional. When present it is a base64-encoded UTF-8 password string. It is stored in `state.json` and used automatically when starting a session.
**Response payload:**
```json
{ "ok": true }
@@ -379,9 +409,11 @@ All fields are optional — only the provided fields are updated.
**Response payload:**
```json
{ "ok": true, "settings": { ... } }
{ "ok": true, "settings": { ... }, "requiresRestart": false }
```
`requiresRestart` is `true` if `proxyPort` or `connectProxyPort` were changed — the dashboard displays a warning in this case.
---
### `createBackup`
+41 -11
View File
@@ -4,24 +4,37 @@
### What it is
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.
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 per-TLD wildcard TLS certificates that the HTTPS proxy presents to your browser — one certificate per wildcard parent domain, generated on demand.
### Why it's needed
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.
Browsers require HTTPS for modern web features and refuse to load mixed content. The HTTPS proxy must present a valid TLS certificate for each virtual host's domain — but no public CA will sign certificates for non-public domains. A locally-trusted CA is the standard approach used by tools like mkcert, Burp Suite, and corporate proxies.
### What the CA can do
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 is a standard X.509 root CA. In principle, it could be used to sign certificates for any domain — not just Holesail virtual host domains. However:
- 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 native host only ever uses the CA to sign wildcard certificates for configured virtual host TLDs
- The CA is installed with the SSL trust policy only (macOS: `-p ssl`), limiting its scope
### Certificate issuance
A separate wildcard certificate is generated for each unique wildcard parent domain:
| Virtual host hostname | Wildcard parent | Certificate covers |
|-----------------------|-----------------|-------------------|
| `myapp.hole.sail` | `hole.sail` | `*.hole.sail` |
| `api.haha.wooo` | `haha.wooo` | `*.haha.wooo` |
| `i.love.hole.sail` | `love.hole.sail` | `*.love.hole.sail` |
| `a.b.c.my.internal` | `b.c.my.internal` | `*.b.c.my.internal` |
Certificates are generated on the first HTTPS connection to a new wildcard parent and cached on disk. No proxy restart is required.
### Lifetime
- Root CA: 10 years
- Wildcard `*.hole.sail` cert: 1 year (auto-renewed on expiry)
- Wildcard domain certs: 1 year (auto-renewed on expiry)
### Fingerprint verification
@@ -68,13 +81,27 @@ The native host runs as your user account (not root). It:
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.
### SNI-based certificate selection
The HTTPS proxy reads the TLS ClientHello from each raw TCP connection to extract the SNI hostname before the TLS handshake begins. This allows it to present the correct wildcard certificate for each TLD without requiring a proxy restart. The SNI parsing is done entirely in JavaScript — no native TLS SNI callback is used.
### 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.
The PAC script routes only configured TLD traffic through the proxy. For each active TLD (e.g. `.hole.sail`, `.haha.wooo`), a `dnsDomainIs` clause is added. All other traffic goes `DIRECT`. The extension monitors `proxy.settings.onChange` and re-applies the PAC script if it is overridden by another extension.
### Custom TLD validation
The dashboard enforces that virtual host hostnames:
- Have at least 3 labels (host + two-label TLD minimum)
- Use only letters, digits, and hyphens per label
- Do not use any real public TLD (`.com`, `.net`, `.co.uk`, and hundreds of others)
This prevents accidentally routing real internet traffic through the local proxy.
---
@@ -83,11 +110,13 @@ The PAC script routes only `*.hole.sail` traffic through the proxy. All other tr
| Threat | Mitigation |
|--------|-----------|
| 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 |
| Man-in-the-middle on browser↔proxy TLS | Locally-trusted per-TLD wildcard certs; CA key stored locally |
| Wrong cert served for custom TLD | JS-layer SNI: cert selected per-connection based on ClientHello hostname |
| 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 |
| Custom TLD colliding with real internet domain | TLD blocklist validation in dashboard before saving |
## Threats not mitigated
@@ -104,7 +133,7 @@ The PAC script routes only `*.hole.sail` traffic through the proxy. All other tr
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 never written to disk; a saved password is held in memory only and delivered to SSH via `SSH_ASKPASS` and a named FIFO pipe (not via the process environment or command line); interactive passwords are collected in xterm.js and delivered the same way
- **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
- **`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
@@ -112,7 +141,7 @@ 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). 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 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.
---
@@ -124,8 +153,9 @@ Backup archives contain `state.json` and all certificate files including the CA
## Recommendations
1. **Keep `~/.holesail-browser/` private** — it contains the CA private key and server tunnel keypairs
1. **Keep `~/.holesail-browser/` private** — it contains the CA private key, server tunnel keypairs, and any saved passwords
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
5. **Use private TLDs only** — the dashboard enforces this, but be aware that any TLD you add will have a locally-trusted wildcard certificate issued for it
6. **Encrypt backups** if storing them in a shared location
+127
View File
@@ -0,0 +1,127 @@
# Virtual Hosts
Virtual hosts let you access remote services over Holesail tunnels using human-readable HTTPS URLs in your browser — for example `https://myapp.hole.sail/` instead of `http://127.0.0.1:19042/`.
## How it works
1. You add a virtual host in the dashboard: a hostname (e.g. `myapp.hole.sail`) and an `hs://` key
2. The native host opens a Holesail client tunnel to the remote peer and assigns it a local port (e.g. `127.0.0.1:19042`)
3. The extension updates the PAC script so the browser routes that TLD through the local CONNECT proxy
4. When you navigate to `https://myapp.hole.sail/`, the browser sends `CONNECT myapp.hole.sail:443` to the CONNECT proxy, which pipes it to the HTTPS proxy
5. The HTTPS proxy performs a TLS handshake with a wildcard cert for that TLD, then proxies your HTTP request to the local tunnel port
## Supported hostname formats
Any hostname with at least 3 labels and a private (non-public) TLD is accepted:
| Hostname | Valid? | Notes |
|----------|--------|-------|
| `myapp.hole.sail` | ✓ | Default TLD |
| `api.haha.wooo` | ✓ | Custom TLD |
| `i.love.hole.sail` | ✓ | Deep hostname — 4 labels |
| `a.b.c.my.internal` | ✓ | Any depth supported |
| `myapp.com` | ✗ | Real public TLD |
| `api.co.uk` | ✗ | Real public SLD |
| `myapp` | ✗ | Single label — no TLD |
| `hole.sail` | ✗ | Two labels — no host prefix |
### TLD requirements
- Must be **two-tier**: the TLD portion must be two labels (e.g. `.hole.sail`, `.my.internal`, `.haha.wooo`) — single-label TLDs like `.holesail` are not allowed
- Must **not** be a real public TLD or second-level domain (`.com`, `.net`, `.co.uk`, `.github.io`, etc.)
- The full hostname must have **at least 3 labels** total
## Custom TLDs
You are not limited to `.hole.sail`. Any private two-tier TLD works:
```
https://api.haha.wooo/
https://dashboard.my.internal/
https://service.dev.local/
```
When you add a virtual host with a new TLD for the first time:
1. **PAC script updated** — the extension adds a `dnsDomainIs(host, ".haha.wooo")` clause so the browser routes all `*.haha.wooo` traffic through the proxy
2. **Host permissions requested** — the extension requests `*://*.haha.wooo/*` at runtime via `chrome.permissions.request()`
3. **Certificate generated on first connection** — the HTTPS proxy generates a wildcard cert for the exact wildcard parent on demand (see below)
## Certificate generation
The HTTPS proxy uses **JS-layer SNI** to select the right certificate per connection:
1. The raw TLS ClientHello bytes are read from each incoming TCP connection
2. The SNI hostname is extracted (RFC 5246 extension type `0x0000`)
3. The **wildcard parent** is derived by stripping the leftmost label:
| SNI hostname | Wildcard parent | Certificate |
|---|---|---|
| `myapp.hole.sail` | `hole.sail` | `*.hole.sail` |
| `api.haha.wooo` | `haha.wooo` | `*.haha.wooo` |
| `i.love.hole.sail` | `love.hole.sail` | `*.love.hole.sail` |
| `a.b.c.my.internal` | `b.c.my.internal` | `*.b.c.my.internal` |
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.
No proxy restart is needed when adding new TLDs — the cert is generated on the first connection.
## Deep hostnames
Hostnames with more than 3 labels work at any depth. Each unique wildcard parent gets its own certificate:
```
https://i.love.hole.sail/ → cert: *.love.hole.sail
https://also.love.hole.sail/ → cert: *.love.hole.sail (same cert, reused)
https://deep.a.b.c.my.internal/ → cert: *.a.b.c.my.internal
```
The PAC script matches all depths automatically — `dnsDomainIs(host, ".hole.sail")` returns `true` for `i.love.hole.sail` as well as `myapp.hole.sail`.
## PAC script
The PAC script is generated dynamically from the set of active virtual host TLDs:
```javascript
function FindProxyForURL(url, host) {
if (dnsDomainIs(host, ".hole.sail")) return "PROXY 127.0.0.1:8442";
if (dnsDomainIs(host, ".haha.wooo")) return "PROXY 127.0.0.1:8442";
return "DIRECT";
}
```
It is updated whenever:
- A virtual host is added or removed
- The native host connects or reconnects
- The proxy port changes
## WebSocket support
WebSocket connections (`ws://` / `wss://`) through virtual hosts are supported. The HTTPS proxy handles `Upgrade: websocket` requests by opening a raw TCP connection to the backend tunnel and piping the socket bidirectionally.
## Troubleshooting
### `DNS_PROBE_FINISHED_NXDOMAIN`
The PAC script has not yet been updated for this TLD. This can happen if:
- The virtual host was just added and the background script hasn't refreshed yet — wait a moment and reload
- The extension is not active — check the extension is enabled in `chrome://extensions`
### `ERR_CERT_AUTHORITY_INVALID`
The local root CA is not trusted by your browser. Go to the dashboard → **Proxy & CA** and click **Install Root CA**, then restart Chrome.
### `ERR_SSL_SERVER_CERT_BAD_FORMAT` / cert mismatch
The HTTPS proxy presented a certificate that doesn't cover the requested hostname. This should not happen with the SNI-aware proxy. If it does:
1. Check the native host is running (dashboard should show connected)
2. Delete the stale cert directory: `rm -rf ~/.holesail-browser/holesail-browser-certs/wildcard.<parent>/`
3. Reload the page — the proxy will regenerate the cert on the next connection
### `502 Bad Gateway`
The virtual host tunnel is not connected. Check the Virtual Hosts page in the dashboard — the tunnel may still be connecting or may have failed. Click **Reconnect** if it shows an error state.