mullvad: install the client on the Desktop Role only
The Mullvad VPN app and its daemon now ship with Desktop Only, as the stock
NixOS service with nothing configured on top of it: no local-network boot
service, no DNS or firewall edits of our own, no Hub tile, no Feature Manager
card, no toggles. The account, the relay, the kill switch and the app's other
switches belong to the app.
modules/mullvad.nix is the whole change to the system: a role gate around
services.mullvad-vpn = {
enable = true; # daemon, pkgs.mullvad
gui.enable = true; # app, pkgs.mullvad-vpn
};
Server + Desktop and Bitcoin Node are machines that serve; a VPN client with its
own routing and DNS policy is not something their services asked for, so only
roles.desktop builds it. Every other option stays at the distribution's default
(early boot blocking off, the mullvad-exclude wrapper on), and the only
consequences are the ones the NixOS module already defines — the tun module, and
systemd-resolved with NetworkManager pointed at it while the client is enabled.
The Hub and the desktop shell are untouched: no tile, no feature card, no
service description, no icon, no GNOME folder edit. An app that is simply
installed has no decision to put in front of the user, and no state to report.
README, SECURITY.md (what the app's own kill switch and DNS behaviour mean, so
the change in network behaviour is not a surprise), CHANGELOG, the custom.nix
template (the mkForce line that removes it) and the installer's Desktop card
document it as an app rather than a service.
tests/test_mullvad_desktop_only.py guards the gate and the restraint: the client
builds on Desktop and nowhere else, the module adds no services, no DNS or
firewall edits and no packages, the Hub and desktop shell never mention it, and
it ships no tile icon.
Checked by evaluating the real module against the revision flake.lock pins
(3d23ea05a8a3) with eval-config.nix, four cases: Desktop -> daemon and GUI on,
one unit (mullvad-daemon), mullvad + mullvad-vpn in systemPackages, every other
option at the distribution default; Bitcoin Node and Server + Desktop -> nothing
at all, not even the tun module or resolved; Desktop with
`services.mullvad-vpn.enable = lib.mkForce false;` -> no unit, no package.
This commit is contained in:
1 parent
36bd2b4e78
commit
8f5e5394e6
8 files changed
+193
-5
No files matched your search
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Mullvad: install the VPN app and daemon, as NixOS ships them, on the Desktop Role only
|
||||
|
||||
---
|
||||
|
||||
## [1.2.0] - 2026-10-02
|
||||
|
||||
### Added
|
||||
|
||||
@@ -172,6 +172,7 @@ hardware you control.
|
||||
- [GNOME](https://www.gnome.org) desktop
|
||||
- [Brave Origin](https://brave.com/origin/) and
|
||||
[Firefox](https://www.mozilla.org/firefox) browsers
|
||||
- [Mullvad VPN](https://mullvad.net) desktop app and daemon, on Desktop Only
|
||||
- File management, email, calendar, and office applications
|
||||
- System monitoring and administration tools
|
||||
|
||||
@@ -185,15 +186,25 @@ Bitcoin and self-hosting infrastructure runs on the machine.
|
||||
|
||||
| Mode | Best for | What you get |
|
||||
|---|---|---|
|
||||
| **Desktop** | Everyday users and computers with modest hardware | Sparrow, Bisq, and Bisq 2 for self-custody and peer-to-peer Bitcoin use |
|
||||
| **Desktop** | Everyday users and computers with modest hardware | Sparrow, Bisq, and Bisq 2 for self-custody and peer-to-peer Bitcoin use, plus the Mullvad VPN app |
|
||||
| **Node** | People ready to verify and operate their own Bitcoin infrastructure | Everything in Desktop, plus the full Bitcoin stack: Bitcoin Core, Electrs, LND, Ride The Lightning, BTCPay Server, and wallet-to-node connections. **Publishes nothing unless you enable *Put BTCPay Server Online* or *Lightning Wallet Connections*:** [read this first](#server--desktop-and-your-home-ip-address) |
|
||||
| **Server + Desktop** | Bitcoiners who want the same sovereignty over their communications, cloud, passwords, and web services | The complete Node stack, plus the private self-hosted services. **Makes your home IP address public:** [read this first](#server--desktop-and-your-home-ip-address) |
|
||||
|
||||
**Desktop: start with your keys.** Desktop is not a reduced or Bitcoin-free
|
||||
edition: it is a complete, private everyday computer with a clean GNOME
|
||||
desktop, Tor, and the Sovran Hub, and a lower-hardware path to self-custody and
|
||||
non-KYC Bitcoin tools from the first boot. Move to Node mode when your
|
||||
hardware, storage, and needs are ready.
|
||||
desktop, Tor, the Sovran Hub, and the Mullvad VPN app and daemon — which no
|
||||
other role installs — and a lower-hardware path to self-custody and non-KYC
|
||||
Bitcoin tools from the first boot. Move to Node mode when your hardware,
|
||||
storage, and needs are ready.
|
||||
|
||||
Mullvad is installed the way NixOS ships it and nothing in it is configured for
|
||||
you: create an account at [mullvad.net](https://mullvad.net), sign in from the
|
||||
app, and set the relay and the app's own switches to taste. It is the one thing
|
||||
that reaches the network from this machine as an application rather than as a
|
||||
service, so Server + Desktop and Bitcoin Node — built to serve — do not carry
|
||||
it. On Desktop it is simply there; remove it with
|
||||
`services.mullvad-vpn.enable = lib.mkForce false;` in `custom.nix` if you would
|
||||
rather not have it.
|
||||
|
||||
**Node: verify your own money.** You operate the infrastructure that performs
|
||||
the verification instead of asking someone else's server: your node verifies,
|
||||
@@ -588,6 +599,8 @@ and `/etc/nix-bitcoin-secrets` remain for compatibility.
|
||||
- [GNOME](https://www.gnome.org) desktop environment
|
||||
- [Caddy](https://caddyserver.com) for local and public service routing
|
||||
- [Tor](https://www.torproject.org) for Bitcoin network privacy
|
||||
- [Mullvad VPN](https://mullvad.net), installed as the stock NixOS service on
|
||||
the Desktop Role only (`pkgs.mullvad` daemon and `pkgs.mullvad-vpn` app)
|
||||
- [AGPL-3.0](LICENSE) licensing
|
||||
|
||||
### Build from source
|
||||
@@ -737,6 +750,7 @@ rebuilds the machine into the selected declarative state.
|
||||
| Optional Haven Nostr relay | `modules/haven.nix` |
|
||||
| Nextcloud, Vaultwarden, WordPress | `modules/nextcloud.nix`, `modules/vaultwarden.nix`, `modules/wordpress.nix`, `modules/php.nix` |
|
||||
| Optional remote desktop and public SSH | `modules/rdp.nix`, `modules/sshd.nix` |
|
||||
| Mullvad VPN app (Desktop Role only) | `modules/mullvad.nix` |
|
||||
|
||||
Feature availability and defaults may change as Sovran_SystemsOS develops:
|
||||
review the relevant Nix module before relying on a default in production.
|
||||
|
||||
+19
@@ -92,6 +92,25 @@ the public internet. Key authentication only. Turn it off when you are done.
|
||||
Tor reduces network exposure for configured Bitcoin services. It is not a
|
||||
guarantee against every IP leak, application bug, or traffic-analysis attack.
|
||||
|
||||
### Mullvad VPN (Desktop Only)
|
||||
|
||||
The Mullvad VPN client — daemon and app — is installed on the Desktop Role and
|
||||
on no other role: Server + Desktop and Bitcoin Node keep the one network
|
||||
configuration their services need. `modules/mullvad.nix` is a role gate around
|
||||
the stock NixOS service, with nothing configured by this system on top of it.
|
||||
|
||||
Two behaviours are the app's own, and worth knowing before you connect:
|
||||
|
||||
- While the tunnel is up, Mullvad's kill switch blocks everything outside it,
|
||||
the local network included, so Remote Desktop and other local devices stop
|
||||
answering until you switch local network sharing on in the app.
|
||||
- Name resolution follows the tunnel for as long as the client is enabled: the
|
||||
NixOS service turns on systemd-resolved and points NetworkManager at it.
|
||||
|
||||
Neither is set or restricted here; both are visible and changeable in the app.
|
||||
The firewall rules, the Hub's exposure checks, the Bitcoin stack and Tor are
|
||||
untouched by this module.
|
||||
|
||||
### Restricted support access
|
||||
|
||||
Tech Support runs over SSH, and the Hub refuses to start a session until SSH
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
|
||||
# ─── Add your custom NixOS configuration below ───────────
|
||||
|
||||
# ─── Mullvad VPN (Desktop Only) ───────────────────────────
|
||||
# Installed on the Desktop Role as the stock NixOS service; account, relay
|
||||
# and settings belong to the app. To take it off a Desktop machine:
|
||||
# services.mullvad-vpn.enable = lib.mkForce false;
|
||||
|
||||
# ─── Custom Caddy virtual hosts ──────────────────────────
|
||||
# Uncomment and edit below to add your own Caddy sites:
|
||||
#
|
||||
|
||||
+1
-1
@@ -479,7 +479,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
"Full sovereignty: host your own websites, cloud, chat, passwords, and Bitcoin services instead of relying on Big Tech. Sovran_SystemsOS walks you through getting your domain from Njal.la and connecting everything. One router task is required: forward ports 80 and 443 to this computer. Heads-up: this makes your home IP address public.",
|
||||
"Server+Desktop"),
|
||||
("Desktop Only",
|
||||
"A beautiful, easy-to-use desktop without the background server applications.",
|
||||
"A beautiful, easy-to-use desktop without the background server applications. Includes the Mullvad VPN app.",
|
||||
"Desktop Only"),
|
||||
("Node Only",
|
||||
"Full Bitcoin node with Lightning and non-KYC buying and selling. No desktop.",
|
||||
|
||||
@@ -35,5 +35,8 @@
|
||||
./element-calling.nix
|
||||
./rdp.nix
|
||||
./sshd.nix
|
||||
|
||||
# ── Desktop Role only (gated inside the module) ───────────
|
||||
./mullvad.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# ── modules/mullvad.nix ───────────────────────────────────────────────────────
|
||||
# Mullvad VPN client — Desktop Role only.
|
||||
#
|
||||
# A plain install: the daemon (pkgs.mullvad) and the graphical app
|
||||
# (pkgs.mullvad-vpn) exactly as NixOS ships them, enabled and nothing more. The
|
||||
# account, the relay, the kill switch and every other setting are the app's
|
||||
# business; nothing here configures them, and nothing here is wired into the
|
||||
# Hub or into any other part of the system.
|
||||
#
|
||||
# The role gate is the whole point: Server + Desktop and Bitcoin Node are
|
||||
# machines that serve, and a VPN client with its own DNS and routing policy is
|
||||
# not something their services asked for. Only roles.desktop builds it.
|
||||
#
|
||||
# Turning it off on a Desktop machine: uncomment
|
||||
# services.mullvad-vpn.enable = lib.mkForce false;
|
||||
# in custom.nix (the mkForce is needed because this module sets it too).
|
||||
|
||||
{ config, lib, ... }:
|
||||
|
||||
lib.mkIf config.sovran_systemsOS.roles.desktop {
|
||||
|
||||
services.mullvad-vpn = {
|
||||
# The daemon and its systemd service.
|
||||
enable = true;
|
||||
# The desktop app.
|
||||
gui.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Guards for the Mullvad VPN client being a plain Desktop Role install.
|
||||
|
||||
The desktop app and its daemon ship with Desktop Only and with no other role:
|
||||
Server + Desktop and Bitcoin Node are machines that serve, and a VPN client with
|
||||
its own DNS and routing policy is not something their services asked for.
|
||||
|
||||
Beyond that the install is deliberately boring. It is the NixOS service exactly
|
||||
as NixOS ships it — no local-network boot service, no DNS or firewall edits of
|
||||
our own, no Hub tile, no Feature Manager card, no toggles. The account, the
|
||||
relay and the kill switch belong to the app.
|
||||
|
||||
Like the other nix-file checks these read the shipped sources as text: nothing is
|
||||
run and nothing touches the network.
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
|
||||
def _read(*parts):
|
||||
with open(os.path.join(_ROOT, *parts), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _without_comments(src):
|
||||
"""The source with whole-line `#` comments removed."""
|
||||
return "\n".join(l for l in src.splitlines() if not l.lstrip().startswith("#"))
|
||||
|
||||
|
||||
class TheClientIsBuiltOnDesktopAndNowhereElse(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.module = _read("modules", "mullvad.nix")
|
||||
cls.code = _without_comments(cls.module)
|
||||
|
||||
def test_the_module_is_imported(self):
|
||||
self.assertIn("./mullvad.nix", _read("modules", "modules.nix"))
|
||||
|
||||
def test_the_gate_is_the_desktop_role(self):
|
||||
self.assertRegex(
|
||||
self.code,
|
||||
r"lib\.mkIf\s+config\.sovran_systemsOS\.roles\.desktop\s*\{",
|
||||
)
|
||||
|
||||
def test_no_other_role_is_mentioned(self):
|
||||
self.assertNotIn("server_plus_desktop", self.code)
|
||||
self.assertNotIn("roles.node", self.code)
|
||||
|
||||
def test_the_install_is_the_daemon_and_the_app(self):
|
||||
self.assertRegex(self.code, r"services\.mullvad-vpn\s*=\s*\{")
|
||||
self.assertRegex(self.code, r"(?m)^ enable = true;$")
|
||||
self.assertRegex(self.code, r"gui\.enable = true;")
|
||||
|
||||
|
||||
class TheInstallStaysVanilla(unittest.TestCase):
|
||||
"""Nothing is configured for the user, in the module or out of it."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.module = _without_comments(_read("modules", "mullvad.nix"))
|
||||
|
||||
def test_no_extra_services_are_added(self):
|
||||
self.assertNotIn("systemd.services", self.module)
|
||||
|
||||
def test_no_dns_or_network_edits_of_our_own(self):
|
||||
# The NixOS module already turns on systemd-resolved and points
|
||||
# NetworkManager at it; re-declaring either here would be our policy,
|
||||
# not the distribution's.
|
||||
for key in ("services.resolved", "networkmanager", "networking."):
|
||||
self.assertNotIn(key, self.module)
|
||||
|
||||
def test_no_firewall_or_kernel_edits(self):
|
||||
self.assertNotIn("firewall", self.module)
|
||||
self.assertNotIn("boot.", self.module)
|
||||
|
||||
def test_no_packages_are_forced_in(self):
|
||||
self.assertNotIn("environment.systemPackages", self.module)
|
||||
|
||||
def test_the_feature_flag_does_not_come_back(self):
|
||||
# A Hub-managed feature flag on an app that is simply installed would
|
||||
# put a toggle in front of something that has no decision to make.
|
||||
for path in (("modules", "core", "roles.nix"),
|
||||
("modules", "core", "role-logic.nix")):
|
||||
self.assertNotIn("mullvad", _read(*path).lower(), f"{path[-1]} mentions mullvad")
|
||||
|
||||
|
||||
class TheHubDoesNotMentionIt(unittest.TestCase):
|
||||
"""The app is not a service the Hub manages or announces."""
|
||||
|
||||
def test_the_hub_tiles_and_roles_are_untouched(self):
|
||||
self.assertNotIn("mullvad", _read("modules", "core", "sovran-hub.nix").lower())
|
||||
|
||||
def test_the_hub_backend_is_untouched(self):
|
||||
self.assertNotIn("mullvad", _read("app", "sovran_systemsos_web", "server.py").lower())
|
||||
|
||||
def test_the_desktop_shell_is_untouched(self):
|
||||
# The app appears in the GNOME app grid by itself; no folder edit, no
|
||||
# dock entry.
|
||||
self.assertNotIn(
|
||||
"mullvad", _read("modules", "core", "sovran_systemsos-desktop.nix").lower())
|
||||
|
||||
def test_there_is_no_icon_for_it(self):
|
||||
# app/icons holds Hub tile icons only.
|
||||
self.assertFalse(
|
||||
os.path.exists(os.path.join(_ROOT, "app", "icons", "mullvad.svg")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user