Bastion: a browser where every feature is a module you can turn off

Bastion is a personal Electron browser built around a manifest-driven module system, with Mullvad routing, a fail-closed kill switch, and per-tab proxy circuits.

Bastion is a personal desktop browser. Electron, TypeScript in strict mode, tabs as WebContentsViews inside a BaseWindow. Zero telemetry. The only network calls it makes on its own are the ones you configure: filter-list downloads, DoH lookups, the Mullvad connection check, and optional DuckDuckGo omnibox suggestions.

Every feature is a module. Ad-blocking, fingerprint protection, the Mullvad integration, history. Each one is a folder, a line in modules/manifest.json, and a default export with enable() and disable(). The rule is that disable() undoes everything enable() did. The settings toggle calls both live, no restart. The settings page renders itself from each module’s settingsSchema, so a new module needs no UI code.

Core stays small: window shell, tabs, settings store, event bus, module loader, network router, proxy controller, and the bastion:// protocol.

One listener, many modules

Electron allows one webRequest listener per event. Core owns them and fans out to modules by priority. https-only runs at 10, adblock at 20. Modules never touch session.webRequest directly, so they can’t clobber each other.

The proxy controller works the same way, as layers. The kill-switch black-hole is priority 100. Mullvad SOCKS is 50. Direct is the floor. The highest active layer wins, and changing layers closes live connections. A dropped tunnel can’t leave an old socket carrying traffic.

Fail closed

The SOCKS endpoint exists only inside the Mullvad tunnel. Leave the tunnel and it stops answering. The kill switch goes further: on a Mullvad drop it points every session at a black-hole proxy, severs sockets, and covers the window with a warning. It ships off, so a first launch without Mullvad doesn’t land on a red screen. Arm it in Settings.

Throwaway and circuit tabs

Ctrl+Shift+N opens a tab in a disposable in-memory session with its own cookie jar. It is wiped on close. File → Reopen Tab Through pins a single tab to Direct, Mullvad SOCKS5, or Tor at 127.0.0.1:9050 while everything else keeps the default route.

The session manager gives these sessions the same treatment as the main one: network router, bastion:// pages, proxy, and each module’s per-session setup via ctx.onBrowserSession(). Circuit tabs opt out of the global proxy and pin their own.

Limits

UA client hints still advertise Chromium. The standardized UA covers navigator.userAgent and the classic header string, nothing more. Bastion trails Chrome on engine patches and the custom code is unaudited. It is a privacy tool, not a high-assurance boundary.

bastion://leak-test runs IP, WebRTC, and DoH checks through the current tab’s connection and returns green or red.

Full paper →

↑