Skip to content

07 desktop linux

github-actions[bot] edited this page Sep 25, 2026 · 1 revision

7. Desktop on Linux Mint / Debian

Short answer: yes. You cannot ship a single "browser binary" — the client is a browser app — but you can ship a native Debian package that bundles the app and runs it in a Chromium engine. Several routes exist; the right one depends on one constraint.

7.1 The constraint that decides everything

The mixnet client (@nymproject/mix-tunnel / mix-fetch) is a Chromium-oriented WASM bundle: WebAssembly, Web Workers started from a blob URL, and possibly SharedArrayBuffer (which requires cross-origin isolation). Whatever runtime you pick must be Chromium-based, or you must verify the WASM client works on it.

Engine Used by Risk
Chromium / Blink Chrome, Chromium, Electron, Brave, Edge, Android WebView ✅ the compatibility target
Gecko Firefox (supports WASM/Workers; no SharedArrayBuffer without special headers) ⚠️ test
WebKit / WebKitGTK Tauri, Wails, Safari, GNOME Web ⚠️ distribution WebKitGTK version varies; APIs may be missing

Because Linux Mint 22.3 ships apt and the user very likely already has Chrome or Chromium, the system-Chromium .deb is both the smallest and the safest.

7.2 Options

Option Artifact Size Chromium guaranteed Effort Status
PWA install none (desktop entry) 0 user's browser trivial works today
System-Chromium .deb nysiris_*.deb ~1 MB + web/dist uses system Chromium low implemented
Electron .deb + AppImage ~150 MB installed ✅ bundled medium scaffold provided
Tauri .deb + AppImage ~5–15 MB ❌ WebKitGTK; needs verification medium documented
Flatpak .flatpak depends on runtime ✅ if Electron/Chromium runtime medium documented
AppImage single executable depends depends on choice medium documented

7.3 Recommended: system-Chromium .deb (implemented)

Architecture:

/usr/bin/nysiris                wrapper
/usr/lib/nysiris/
    nysiris-launcher            std-only Rust: loopback static server + browser launcher
    web/                             built PWA (web/dist)
/usr/share/applications/nysiris.desktop
/usr/share/icons/hicolor/scalable/apps/nysiris.svg

The launcher:

  • binds 127.0.0.1 only (never 0.0.0.0),
  • sets Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp so SharedArrayBuffer is available (loopback is a secure context),
  • refuses path traversal, including percent-encoded %2e%2e,
  • serves an SPA fallback to index.html for extension-less routes,
  • opens the app in a Chromium --app window (no tabs, no URL bar), falling back through chromium → chromium-browser → google-chrome → google-chrome-stable → brave-browser → microsoft-edge → xdg-open.

Build and install

./build.sh web                          # build web/dist (npm, network)
./build.sh desktop                      # -> dist/nysiris_0.1.0_amd64.deb
./build.sh desktop --app-version 1.2.3   # custom version
sudo apt install ./dist/nysiris_0.1.0_amd64.deb
nysiris                             # or launch from the menu
sudo apt remove nysiris             # uninstall

Run without packaging

cargo run -p nysiris-launcher -- --root web/dist
cargo run -p nysiris-launcher -- --root web/dist --no-open --port 8737

Verify the package

dpkg-deb --info dist/nysiris_0.1.0_amd64.deb
dpkg-deb --contents dist/nysiris_0.1.0_amd64.deb

The launcher's parser, MIME mapping and traversal rejection are covered by unit tests that run in ./build.sh check.

7.4 Electron (self-contained)

Use when you cannot assume the user has Chromium, or you want one artifact that guarantees Blink.

./build.sh desktop --electron                 # deb + AppImage
./build.sh desktop --electron --app-version 1.2.3

or by hand:

cd desktop/electron
npm install
mkdir -p web && cp -r ../../web/dist/* web/
npm run dist          # -> desktop/electron/dist/{*.deb,*.AppImage}

Output: nysiris-desktop_<version>_amd64.deb (installs to /opt/nysiris) and nysiris-<version>.AppImage (~125 MB; self-contained, no install needed).

electron-builder 26 prints a desktopName warning for window association, but its own schema rejects the desktopName key. It is cosmetic (taskbar grouping) and safely ignored.

Debian metadata is mandatory. electron-builder's Debian packager refuses to build without a maintainer, so package.json sets homepage, author.email, linux.maintainer and an icon (build/icon.png, 512×512, generated by scripts/gen-icons.py). Without these you get:

⨯ Please specify project homepage
  Please specify author 'email' ...
  It is required to set Linux .deb package maintainer.

Trade-off: ~150 MB installed (bundled Chromium) and its own update cadence. desktop/electron/main.js starts a loopback server with the same COOP/COEP headers and opens a hardened BrowserWindow (contextIsolation: true, nodeIntegration: false, sandbox: true).

7.5 Tauri (small, but verify)

Tauri v2 produces tiny binaries and a .deb, but on Linux it renders with WebKitGTK, not Blink. Before committing to it, confirm the Nym WASM client works there:

  1. Serve web/dist with the COOP/COEP headers above.
  2. Point Tauri's window at it (or register a custom protocol that sets the headers; Tauri lets you control response headers).
  3. Test: WASM instantiates, the worker starts from its blob URL, mixFetch completes, and messaging/SURB replies work.

If any of those fail on the target distro's WebKitGTK, use Electron or the system-Chromium .deb instead. WebKitGTK versions differ across Mint/Ubuntu releases, so testing on each supported release is essential.

7.6 Flatpak and AppImage

  • Flatpak is available on Mint (flatpak is installed). Ship the Electron build on a Chromium-based runtime, or a Tauri build on a GNOME runtime (same WebKitGTK caveat). You must publish a manifest and either host your own repo or submit to Flathub. This is the most portable "one artifact for every distro" route.
  • AppImage is a single self-contained executable produced by electron-builder (see npm run dist) or Tauri. No root required; the user chmod +x and runs it. Good for handing someone a file; less good for managed installs.

7.7 Distributing via apt (optional)

For an internal repo, serve the .deb with a tiny Packages index:

mkdir -p repo
cp dist/nysiris_*.deb repo/
cd repo
dpkg-scanpackages . /dev/null | gzip -9 > Packages.gz

Then clients add deb [trusted=yes] file:/path/repo ./ (or an HTTP URL) to /etc/apt/sources.list.d/. For anything public, sign the repository with a GPG key rather than using trusted=yes.

7.8 What this is not

  • It is not a system-wide proxy. Like the browser client, it routes only traffic that goes through the app. For whole-device routing on Linux use nym-socks5-client (SOCKS5 on 127.0.0.1:1080) or NymVPN.
  • It does not reimplement mixnet crypto. The packaged app uses Nym's audited WASM client, exactly like the web build.
  • A Tauri/WebKitGTK build is not validated until you run the checklist in §7.5 on each target release.

7.9 Files

Path Purpose
desktop/launcher/ std-only Rust launcher (static server + browser opener), unit-tested
desktop/debian/ control, postinst, prerm, .desktop, icon, build-deb.sh
desktop/electron/ Electron alternative (package.json, main.js)
scripts/cmd-desktop.sh ./build.sh desktop implementation
scripts/gen-icons.py regenerates web/public/icons/*.png (stdlib only)

7.10 Troubleshooting

A blank window showing placeholder text. The package bundled a non-production web/dist. Build the real app and rebuild:

./build.sh web
./build.sh desktop --app-version 0.1.1
sudo apt install ./dist/nysiris_0.1.1_amd64.deb

Stuck at "bringing up mixnet tunnel" (no visible error). Almost always a Content-Security-Policy problem: the Nym client compiles WebAssembly, so script-src must include 'wasm-unsafe-eval'. With a bare script-src 'self', Chrome/Firefox refuse WebAssembly.compile(), the mix-tunnel worker throws CompileError, and setupMixTunnel never settles.

Diagnose it concretely with the bundled probe (header comment has the exact commands):

node scripts/probe-tunnel.mjs http://127.0.0.1:9222 127.0.0.1
# look for: [log.error] CompileError ... Content Security Policy
# success ends with: [smolmix] tunnel ready

Fix: add 'wasm-unsafe-eval' to script-src in web/index.html (and to any server-sent CSP header). Do not use 'unsafe-eval'; WASM is the only thing this needs to allow.

GET .../assets/index-*.js net::ERR_BLOCKED_BY_CLIENT (blank app). A browser extension blocked the app's own JavaScript. This is the most common desktop failure and it is silent: the page loads, the bundle does not, and the window stays empty. The launcher prevents it by running the app in a dedicated Chrome profile and passing --disable-extensions:

--user-data-dir=$XDG_DATA_HOME/nysiris/chrome-profile
--no-first-run --no-default-browser-check --disable-extensions

Extractors and content blockers are the usual culprits, and they also inject window.ethereum/window.web3 into every page (the WELLDONE Wallet is not initialized / window.web3 is deprecated console spam). A dedicated profile removes both problems. To use your own profile instead:

nysiris --profile ~/.config/google-chrome   # not recommended
nysiris --allow-extensions                  # keep extensions in the app profile

If you launch Chrome manually for testing, do the same:

google-chrome --user-data-dir=/tmp/fly-profile --disable-extensions \
  --app=http://127.0.0.1:<port>/

frame-ancestors is ignored when delivered via <meta>. Expected: that directive only works as an HTTP header. It was removed from the meta CSP; the launcher sends X-Frame-Options: DENY instead. A web deployment should set frame-ancestors (or X-Frame-Options) at the server.

Download error or resource isn't a valid image for a manifest icon. The icons were missing from web/public/icons/. Generate them and rebuild:

python3 scripts/gen-icons.py
./build.sh web

"Google Chrome opens" when I expected a standalone app. By design: this package uses the system Chromium/Chrome engine in --app mode (no tabs or URL bar) and does not bundle a browser. For a self-contained app, use the Electron path (§7.4).

Clone this wiki locally