# 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./` (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. ## 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./` 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.