diff --git a/CHANGELOG.md b/CHANGELOG.md index f697757..10ead10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index efbae2c..fdac6eb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/SECURITY.md b/SECURITY.md index 34f1e9a..5124f1d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 diff --git a/custom.template.nix b/custom.template.nix index 70ee7f1..178f8b0 100644 --- a/custom.template.nix +++ b/custom.template.nix @@ -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: # diff --git a/iso/installer.py b/iso/installer.py index 0e87363..bccf7cd 100644 --- a/iso/installer.py +++ b/iso/installer.py @@ -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.", diff --git a/modules/modules.nix b/modules/modules.nix index d5f0d15..721e11c 100755 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -35,5 +35,8 @@ ./element-calling.nix ./rdp.nix ./sshd.nix + + # ── Desktop Role only (gated inside the module) ─────────── + ./mullvad.nix ]; } diff --git a/modules/mullvad.nix b/modules/mullvad.nix new file mode 100644 index 0000000..fc24fc5 --- /dev/null +++ b/modules/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; + }; +} diff --git a/tests/test_mullvad_desktop_only.py b/tests/test_mullvad_desktop_only.py new file mode 100644 index 0000000..60d4823 --- /dev/null +++ b/tests/test_mullvad_desktop_only.py @@ -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()