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 # 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 ## How it works
1. You add a virtual host in the dashboard: `myapp.hole.sail``hs://abc123...` 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) 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 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. All traffic is end-to-end encrypted via the Noise protocol. The local CA is only used for the browser↔proxy TLS leg.
## Features ## Features
- **Virtual Hosts** — browse any `hs://` URL as `https://name.hole.sail/` - **Virtual Hosts** — browse any `hs://` URL as `https://name.hole.sail/` or any custom private TLD (e.g. `https://api.my.internal/`)
- **Server Tunnels** — expose a local port as an `hs://` key (TCP or UDP) - **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 - **Service Tunnels** — forward a remote `hs://` peer to a local TCP port
- **SSH** — in-browser SSH terminal via xterm.js, 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 - **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 - **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 - **Persistent state** — all tunnels, connections, and settings survive restarts
## Installation ## Installation
@@ -83,21 +84,23 @@ For a permanent install in regular Firefox, the extension must be signed by Mozi
## First run ## 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** 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 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 4. Go to **Virtual Hosts** → add a hostname (e.g. `myapp.hole.sail`) and its `hs://` key
5. Navigate to `https://your-hostname.hole.sail/` 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 ## 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 | | Page | Description |
|------|-------------| |------|-------------|
| Overview | Status summary, connection count, proxy ports | | Overview | Status summary, stat cards, scrollable lists for all data types, Quick Actions |
| Virtual Hosts | Map `hs://` keys to `*.hole.sail` hostnames | | Virtual Hosts | Map `hs://` keys to hostnames on any private TLD |
| Server Tunnels | Expose local ports as `hs://` keys (TCP/UDP) | | Server Tunnels | Expose local ports as `hs://` keys (TCP/UDP), with optional label |
| Service Tunnels | Forward remote `hs://` peers to local TCP ports | | Service Tunnels | Forward remote `hs://` peers to local TCP ports |
| Proxy & CA | Proxy port settings, CA install/status | | Proxy & CA | Proxy port settings, CA install/status |
| SSH | In-browser SSH terminal over Holesail | | 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 ## 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). 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 - [Architecture](docs/ARCHITECTURE.md) — system design, component overview, message flow
- [Installation](docs/INSTALLATION.md) — detailed install guide for all platforms - [Installation](docs/INSTALLATION.md) — detailed install guide for all platforms
- [Tunneling](docs/TUNNELING.md) — virtual hosts, server tunnels, service tunnels - [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 - [SSH](docs/SSH.md) — in-browser SSH terminal
- [Remote Desktop](docs/REMOTE-DESKTOP.md) — VNC and RDP over Holesail - [Remote Desktop](docs/REMOTE-DESKTOP.md) — VNC and RDP over Holesail
- [Backups](docs/BACKUP.md) — backup and restore - [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. | | `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. | | `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`. | | `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. | | `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`. | | `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. | | `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. | | `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`. | | `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`. | | `wrong-domain.html` | Error page for `*.host.test` (common typo), redirected via `declarativeNetRequest`. |
## Proxy architecture ## Proxy architecture
@@ -237,7 +237,9 @@ All state is owned by the native host and persisted to `state.json` next to the
}, },
"nextServerId": 0, "nextServerId": 0,
"nextServiceTunnelId": 0, "nextServiceTunnelId": 0,
"servers": [], "servers": [
{ "id": "server_1", "port": 3000, "host": "127.0.0.1", "secure": true, "udp": false, "label": "My Web App" }
],
"virtualHosts": [ "virtualHosts": [
{ "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." }, { "hostname": "myapp.hole.sail", "hsUrl": "hs://abc123..." },
{ "hostname": "i.love.hole.sail", "hsUrl": "hs://def456..." }, { "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 } { "id": "svc-1", "label": "Postgres", "hsUrl": "hs://def456...", "localPort": 5432 }
], ],
"sshConnections": [ "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": [ "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 | | 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.key.pem` | Root CA private key |
| `certs/ca.cert.pem` | Root CA certificate | | `certs/ca.cert.pem` | Root CA certificate |
| `certs/wildcard.hole.sail/key.pem` | Wildcard domain private key | | `certs/wildcard.hole.sail/` | Wildcard cert for the default `.hole.sail` TLD |
| `certs/wildcard.hole.sail/cert.pem` | Wildcard domain certificate + CA chain | | `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 ## 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` 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 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 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` 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. Writes the native messaging manifest to all browser locations 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`) ### 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 3. Removes the previous installation directory and old registry keys
4. Downloads `holesail-browser-host-win32-x64.zip` and extracts the `.exe` 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 5. **Restores user data** — the preserved directories are copied back into the new installation
6. Downloads `Holesail-Browser-1.0.0.zip` to `~/Downloads` 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. Writes the native messaging manifest JSON and creates registry keys for Chrome, Chromium, and Firefox 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 ### What is preserved across upgrades
| Directory | Contents | | Directory | Contents |
|-----------|----------| |-----------|----------|
| `holesail-browser-storage/` | `state.json` (all virtual hosts, tunnels, SSH/RDP connections, settings), backup archives | | `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. 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, "port": 3000,
"host": "127.0.0.1", "host": "127.0.0.1",
"secure": true, "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:** **Response payload:**
```json ```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` ### `lookupTunnel`
Check whether an `hs://` key is reachable on the DHT. Check whether an `hs://` key is reachable on the DHT.
@@ -194,14 +219,14 @@ Start an SSH session.
"connectionId": "ssh-abc123", "connectionId": "ssh-abc123",
"hsUrl": "hs://abc123...", "hsUrl": "hs://abc123...",
"username": "root", "username": "root",
"password": "", "passwordB64": "cGFzc3dvcmQ=",
"cols": 80, "cols": 80,
"rows": 24, "rows": 24,
"label": "My Server" "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:** **Response payload:**
```json ```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) - **Port** — remote port (default: 5900 for VNC, 3389 for RDP)
- **Width / Height** — initial display resolution - **Width / Height** — initial display resolution
- **Username** — (RDP only) login username - **Username** — (RDP only) login username
- **Password** — optional; saved as base64 in `state.json` and used automatically on reconnect
4. Click **Save** 4. Click **Save**
### Connecting ### Connecting
@@ -90,10 +91,13 @@ Start a new VNC or RDP session.
"port": 5900, "port": 5900,
"width": 1280, "width": 1280,
"height": 720, "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:** **Response payload:**
```json ```json
{ {
@@ -148,12 +152,15 @@ Save RDP/VNC connection definitions to `state.json`.
"port": 5900, "port": 5900,
"width": 1280, "width": 1280,
"height": 720, "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 ## WebSocket protocol
@@ -233,13 +240,14 @@ RDP/VNC connections are stored in `state.json` under `rdpConnections`:
"port": 5900, "port": 5900,
"width": 1280, "width": 1280,
"height": 720, "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. 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 - **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 - **`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 - 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 ## 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 ### 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 ### 3. Password — typed interactively
@@ -65,7 +65,7 @@ If no public key is available and no password is saved, the terminal displays a
### Connecting ### Connecting
1. Click **Connect** next to a saved connection 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` 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 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", "connectionId": "ssh-abc123",
"hsUrl": "hs://abc123...", "hsUrl": "hs://abc123...",
"username": "root", "username": "root",
"password": "", "passwordB64": "cGFzc3dvcmQ=",
"cols": 80, "cols": 80,
"rows": 24, "rows": 24,
"label": "My Server" "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:** **Response payload:**
```json ```json
@@ -154,17 +154,25 @@ List active SSH sessions.
### `setSshConnections` ### `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:** **Request payload:**
```json ```json
{ {
"connections": [ "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 ## WebSocket protocol
@@ -199,7 +207,7 @@ Ports are allocated sequentially and released when the session ends.
## Saved connections ## 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 ```json
{ {
@@ -208,12 +216,15 @@ SSH connections are stored in `state.json` under `sshConnections`. Passwords are
"id": "ssh-abc123", "id": "ssh-abc123",
"label": "My Server", "label": "My Server",
"hsUrl": "hs://abc123...", "hsUrl": "hs://abc123...",
"username": "root" "username": "root",
"passwordB64": "cGFzc3dvcmQ="
} }
] ]
} }
``` ```
`passwordB64` is omitted when no password was saved for the connection.
--- ---
## Requirements ## 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` 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 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` 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 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 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 ### Hostname rules
- Must end in `.hole.sail` - Must have **at least 3 labels** (e.g. `myapp.hole.sail`, not `hole.sail`)
- Must be a valid DNS label (lowercase letters, numbers, hyphens) - The TLD portion must be **two-tier** (e.g. `.hole.sail`, `.my.internal`) — single-label TLDs like `.holesail` are not allowed
- Examples: `myapp.hole.sail`, `dev-server.hole.sail`, `api.hole.sail` - Must **not** use a real public TLD (`.com`, `.net`, `.co.uk`, etc.)
- The bare domain `hole.sail` is also routed through the proxy - 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 ### 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 ### Creating a server tunnel
1. Open the dashboard → **Server Tunnels** 1. Open the dashboard → **Server Tunnels**
2. Enter the local port to expose (e.g. `3000`) 2. Optionally enter a **Label** (e.g. `My Web App`) to identify the tunnel
3. Select **TCP** or **UDP** 3. Enter the local port to expose (e.g. `3000`)
4. Optionally enable **Secure** mode (requires the connecting peer to have the key) 4. Select **TCP** or **UDP**
5. Click **Start** — an `hs://` key is generated and displayed 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. 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 | | Parameter | Description |
|-----------|-------------| |-----------|-------------|
| `label` | Optional human-readable name shown in the dashboard |
| `port` | Local port to expose | | `port` | Local port to expose |
| `host` | Local host to bind (default: `127.0.0.1`) | | `host` | Local host to bind (default: `127.0.0.1`) |
| `udp` | Use UDP instead of TCP (default: `false`) | | `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. 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 ### Stopping a service tunnel
Open the dashboard → **Service Tunnels** → click **Stop**. 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 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 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. No proxy restart is needed when adding new TLDs — the cert is generated on the first connection.