docs: document Firefox support across changelog and docs
CI / Build & Test (push) Successful in 3m8s

- CHANGELOG: add Firefox support subsection (manifest_firefox, background
  scripts, proxy/native-messaging/CSP, installer, pack, extension-id)
- CONTRIBUTING: repo layout with background-boot/main, Firefox load steps
  and private-windows note, install.sh Chrome vs Firefox manifests,
  DEBUG_VERBOSE in background-boot.js
- ARCHITECTURE: manifest.json vs manifest_firefox.json, background.js
  importScripts, background-boot/main and proxy/native-messaging notes,
  native host lifecycle and separate manifests
- INSTALLATION: allowed_origins vs allowed_extensions, Allow in Private
  Windows for Firefox, extension ID updates both manifests
- SECURITY: PAC scope Chrome vs Firefox (autoConfig/data URL), proxy
  security and private-windows requirement
This commit is contained in:
Raven Scott
2026-03-03 03:46:16 -05:00
parent 90646d6cb1
commit 516d387aca
5 changed files with 41 additions and 12 deletions
+10 -5
View File
@@ -98,8 +98,9 @@ Holesail Browser is composed of three parts: a browser extension, a native host
| File | Purpose |
|------|---------|
| `manifest.json` | Manifest V3. Permissions: `nativeMessaging`, `proxy`, `declarativeNetRequest`, `tabs`, `notifications`. Optional host permissions requested at runtime for custom TLDs. |
| `background.js` | Service worker entry point. Declares constants (`DEBUG_VERBOSE`, `browser` shim), then loads all background modules via `importScripts()` in dependency order, applies the initial PAC script, and connects to the native host. |
| `manifest.json` | Manifest V3 (Chrome). Permissions: `nativeMessaging`, `proxy`, `declarativeNetRequest`, `tabs`, `notifications`. Optional host permissions requested at runtime for custom TLDs. |
| `manifest_firefox.json` | Manifest V3 for Firefox. Same permissions; uses `background.scripts` instead of `service_worker`, `extension_ids` for `web_accessible_resources`, and `content_security_policy` for extension pages. Packed as `manifest.json` in the `.xpi`. |
| `background.js` | Chrome service worker entry point. Loads all background modules via `importScripts()` (boot, then logs/state/proxy/native-messaging/tab-lifecycle/message-router, then main); no inline logic. Firefox uses `background.scripts` in the manifest to load the same files in order. |
| `content.js` | Minimal content script. Relays `holesail-host-disconnect` to the page as a `CustomEvent`. |
| `wrong-domain.html` | Error page for `*.host.test` (common typo), redirected via `declarativeNetRequest`. |
@@ -107,10 +108,12 @@ Holesail Browser is composed of three parts: a browser extension, a native host
| File | Purpose |
|------|---------|
| `background-boot.js` | Shared globals: `SW_VERSION`, `DEBUG_VERBOSE`, `browser` (Chrome vs Firefox shim). Loaded first so other modules can use them. |
| `background-main.js` | Startup logic: applies initial PAC, calls `connect()`, wires `declarativeNetRequest` and action click. Loaded last after all modules. Used by both Chrome (via `importScripts` from `background.js`) and Firefox (via `background.scripts`). |
| `logs.js` | In-memory log ring buffer (500 entries), `log()`/`debugLog()` helpers, `broadcastLogs()` to open dashboard tabs, `dashboardTabs` set. |
| `state.js` | All shared mutable state: `extensionState`, `activeConnections`, `tabSwarms`, `swarmRefCount`, `pacConfirmedActive`, `notifyOnDisconnect`, port defaults. |
| `proxy.js` | `applyPAC()` — builds and installs the PAC script; `clearProxy()`; `getActiveTlds()` helper; `proxy.settings.onChange` listener to re-apply if overridden. |
| `native-messaging.js` | `connect()`, `send()`, `scheduleReconnect()`, `retryGetStateForConnectProxy()`. Owns the `port` reference, `pending` map, `subscribedTabs` set, and all `port.onMessage`/`port.onDisconnect` logic. |
| `proxy.js` | `applyPAC()` — builds and installs the PAC script; `clearProxy()`; `getActiveTlds()` helper; `proxy.settings.onChange` listener to re-apply if overridden. Chrome uses `mode: 'pac_script'` and inline PAC data; Firefox uses `proxyType: 'autoConfig'` with a `data:` URL for the PAC script. Handles Firefox private-browsing permission error. |
| `native-messaging.js` | `connect()`, `send()`, `scheduleReconnect()`, `retryGetStateForConnectProxy()`. Owns the `port` reference, `pending` map, `subscribedTabs` set, and all `port.onMessage`/`port.onDisconnect` logic. Reads disconnect reason from `port.error` (Firefox) or `runtime.lastError` (Chrome). |
| `tab-lifecycle.js` | `tabs.onRemoved` listener — decrements swarm ref counts, destroys swarms when their last tab closes, cleans up `subscribedTabs` and `dashboardTabs`. |
| `message-router.js` | `runtime.onMessage` dispatcher — handles `registerSwarm`, `send`, `subscribe`/`unsubscribe`, `registerDashboard`/`unregisterDashboard`, and `getState` actions. |
@@ -344,7 +347,9 @@ All state is owned by the native host and persisted to `state.json` next to the
## Native host lifecycle
Chrome spawns the native host process when the background service worker first calls `connectNative`. The process exits when Chrome disconnects (e.g. browser closed, service worker killed). On next connection, Chrome spawns a fresh process which restores all tunnels from `state.json`.
Chrome spawns the native host process when the background service worker first calls `connectNative`. Firefox does the same when the extension's background scripts call `connectNative`. The process exits when the browser disconnects (e.g. browser closed, background unloaded). On next connection, the browser spawns a fresh process which restores all tunnels from `state.json`.
The native messaging manifest must match the browser: Chrome/Chromium use `allowed_origins` with the extension's Chrome ID; Firefox uses `allowed_extensions` only (Firefox rejects manifests that contain `allowed_origins`). The installer writes separate manifest content to Chrome vs Firefox paths.
If port 8443 or 8442 is already in use when the native host starts, it exits immediately — this prevents ghost instances with no tunnel state from accumulating.
+8 -4
View File
@@ -25,10 +25,13 @@ cd native-host && npm install
```
Holesail-Browser/
├── extension/ # Browser extension (MV3, Chrome + Firefox)
│ ├── background.js # Service worker entry point
│ ├── background.js # Chrome service worker entry; loads modules via importScripts()
│ ├── manifest.json # Extension manifest (Chrome)
│ ├── manifest_firefox.json # Firefox-only manifest (packed as manifest.json in .xpi)
│ ├── background/ # Background module files
│ │ ├── background-boot.js # Shared globals (SW_VERSION, DEBUG_VERBOSE, browser)
│ │ ├── background-main.js # Startup logic (applyPAC, connect, listeners)
│ │ └── ... # logs, state, proxy, native-messaging, etc.
│ └── dashboard/ # Dashboard UI (HTML + JS modules)
├── native-host/ # Native host process (runs under Bare runtime)
│ ├── index.mjs # Entry point
@@ -73,7 +76,8 @@ This runs `scripts/run-install.js`, which delegates to `scripts/install.sh` (mac
1. Open `about:debugging#/runtime/this-firefox`
2. Click **Load Temporary Add-on**
3. Select `extension/manifest.json` (for development). For a permanent install, use the packed `.xpi`, which contains the Firefox manifest.
3. Select `extension/manifest.json` for development. For a permanent install, use the packed `.xpi` (which contains the Firefox manifest as `manifest.json`).
4. For the proxy to work, enable **Allow in Private Windows** for this extension in `about:addons` (Firefox requires this permission for `proxy.settings`).
## npm scripts reference
@@ -122,7 +126,7 @@ Cross-platform launcher: spawns `install.sh` on Unix or `install.ps1` on Windows
Full install scripts that:
1. Detect platform and architecture
2. Download or copy the native host binary to `~/.holesail-browser/`
3. Write the native messaging manifest to the correct OS path
3. Write the native messaging manifest to the correct OS path**Chrome/Chromium** manifests use `allowed_origins` (Chrome extension ID); **Firefox** manifests use `allowed_extensions` only (Firefox rejects manifests that contain `allowed_origins`)
4. Optionally download the extension package
## Adding a new native host message type
@@ -198,7 +202,7 @@ For distributable builds, run `npm run build:dist` and replace the binary in `~/
### Native host logs
The native host writes logs to `~/.holesail-browser/holesail-browser.log` (macOS/Linux) or `%APPDATA%\holesail-browser\holesail-browser.log` (Windows). Set `DEBUG_VERBOSE = true` in `extension/background.js` to enable verbose logging from the extension side.
The native host writes logs to `~/.holesail-browser/holesail-browser.log` (macOS/Linux) or `%APPDATA%\holesail-browser\holesail-browser.log` (Windows). Set `DEBUG_VERBOSE = true` in `extension/background/background-boot.js` to enable verbose logging from the extension side.
### Extension background logs
+9 -3
View File
@@ -85,6 +85,8 @@ Registry keys under `HKCU\Software\`:
Each key's default value points to the manifest JSON file at `%LOCALAPPDATA%\holesail-browser\com.holesail.browser.json`.
**Chrome vs Firefox manifests:** Chrome and Chromium expect the manifest to include `allowed_origins` with the extension's Chrome ID (`chrome-extension://...`). Firefox expects `allowed_extensions` only (the extension's add-on ID, e.g. `[email protected]`) and will reject a manifest that contains `allowed_origins`. The installer writes separate manifest content to Chrome/Chromium paths and to Firefox paths so each browser gets the correct format.
---
## Loading the extension
@@ -107,6 +109,8 @@ Firefox requires extensions to be signed by Mozilla for permanent installation v
> Temporary add-ons are removed when Firefox restarts — you will need to reload it each session.
> **Proxy in Firefox:** For the PAC proxy to work, enable **Allow in Private Windows** for this extension in `about:addons` (click the extension → gear or details → check "Allow in Private Windows"). Firefox requires this permission for the extension to use `proxy.settings`.
### Firefox Developer Edition / Nightly (permanent, unsigned)
Developer Edition and Nightly allow disabling Mozilla's signature requirement:
@@ -115,6 +119,8 @@ Developer Edition and Nightly allow disabling Mozilla's signature requirement:
2. Open `about:addons` → gear icon → **Install Add-on From File**
3. Select `~/Downloads/Holesail-Browser-1.0.0.xpi`
For the proxy to work, enable **Allow in Private Windows** for the extension in `about:addons`.
### Firefox (permanent, signed via AMO)
For a permanent install in regular Firefox without any workarounds, the extension must be submitted to [addons.mozilla.org](https://addons.mozilla.org) (AMO) and signed by Mozilla. Once signed, the `.xpi` can be installed permanently in any Firefox version via `about:addons`.
@@ -167,9 +173,9 @@ After loading the extension:
## Updating the extension ID
The native messaging manifest contains an `allowed_origins` field with the extension's Chrome ID. If you load the extension from a different source (e.g. a locally built zip), the ID may differ.
The native messaging manifest contains browser-specific fields: Chrome/Chromium use `allowed_origins` with the extension's Chrome ID; Firefox uses `allowed_extensions` with the extension's add-on ID. If you load the extension from a different source (e.g. a locally built zip or XPI), the IDs may differ.
To update it, run:
To update them, run:
```bash
# macOS / Linux
@@ -179,7 +185,7 @@ bash scripts/update-native-manifest-extension-id.sh <new-extension-id>
.\scripts\update-native-manifest-extension-id.ps1 <new-extension-id>
```
Or edit the manifest JSON files directly and replace the `chrome-extension://...` value in `allowed_origins`.
Or run the installer again (`npm run setup`), which calls `scripts/generate-extension-id.js` to generate new IDs and update both `extension/manifest.json` (Chrome key and gecko.id) and `extension/manifest_firefox.json` (gecko.id only), then writes the native messaging manifests. You can also edit the manifest JSON files directly: set `allowed_origins` for Chrome and `allowed_extensions` for Firefox.
---
+4
View File
@@ -81,6 +81,8 @@ 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.
**Firefox:** The extension can only call `proxy.settings.set` if the user has enabled **Allow in Private Windows** for the extension in `about:addons`. If not enabled, the extension shows a notification and skips further proxy updates until the user enables it.
### 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.
@@ -93,6 +95,8 @@ If the HTTPS proxy receives a request for a hostname with no registered tunnel,
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.
Chrome accepts a PAC script as inline data (`mode: 'pac_script'`, `pacScript: { data: string }`). Firefox requires a URL: the extension uses `proxyType: 'autoConfig'` with `autoConfigUrl` set to a `data:application/javascript;base64,...` URL containing the same PAC script, so behaviour is equivalent.
### Custom TLD validation
The dashboard enforces that virtual host hostnames: