nixos/nordvpn: init module

This commit is contained in:
Sanfer
2026-07-12 19:58:59 +05:30
parent 1d2b03b1c7
commit fe14acf062
7 changed files with 372 additions and 0 deletions
+3
View File
@@ -1891,6 +1891,9 @@
"sec-kubernetes": [
"index.html#sec-kubernetes"
],
"module-services-nordvpn": [
"index.html#module-services-nordvpn"
],
"ch-running": [
"index.html#ch-running"
],
@@ -54,6 +54,8 @@
- [OO7](https://github.com/linux-credentials/oo7) is a desktop-agnostic Secret Service provider. Available as [services.oo7](#opt-services.oo7.enable)
- [NordVPN](https://github.com/NordSecurity/nordvpn-linux), a NordVPN client for linux. Available as [services.nordvpn](options.html#opt-services.nordvpn.enable).
## Backward Incompatibilities {#sec-release-26.11-incompatibilities}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+1
View File
@@ -1341,6 +1341,7 @@
./services/networking/nncp.nix
./services/networking/nntp-proxy.nix
./services/networking/nomad.nix
./services/networking/nordvpn.nix
./services/networking/nsd.nix
./services/networking/ntopng.nix
./services/networking/ntp/chrony.nix
@@ -0,0 +1,65 @@
# NordVPN {#module-services-nordvpn}
*Source:* {file}`modules/services/networking/nordvpn.nix`
*Upstream documentation:* <https://github.com/NordSecurity/nordvpn-linux>
NordVPN offers a paid virtual private network (VPN) service.
The service operates as closed-source,
but the Linux client uses open-source code licensed under GPLv3.
A minimal configuration in NixOS appears as follows:
```nix
{
services.nordvpn.enable = true;
networking.firewall.enable = true;
networking.firewall.checkReversePath = "loose";
}
```
When using a firewall, set `networking.firewall.checkReversePath` to `"loose"` or `false`.
NordVPN includes a `kill-switch` feature that blocks all packets not associated with the VPN connection.
Additionally, add your user to the `nordvpn` group.
```nix
{
users.users.yourUser = {
#..
extraGroups = [
#..
"nordvpn"
];
};
}
```
If you prefer to use your own user and group, you can do so using
```nix
{
services.nordvpn.user = "SOME-USER";
services.nordvpn.group = "SOME-GROUP";
}
```
NordVPN provides several useful CLI commands, including:
```bash
nordvpn login # Log in using an OAuth URL
nordvpn login --token <token> # Log in with a token obtained from your NordVPN account
nordvpn c # Connect to the VPN
nordvpn c ie # Connect to a NordVPN server in Ireland
nordvpn d # Disconnect from the VPN
nordvpn set technology openvpn # Switch to OpenVPN technology
nordvpn c # Reconnect after changing technology
```
Additionally, if you prefer to use the friendly GUI,
```bash
nordvpn-gui
```
**Disclaimer:** NixOS currently does not support meshnet.
Contributions welcome!
@@ -0,0 +1,171 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.nordvpn;
defaultUser = "nordvpn";
defaultGroup = "nordvpn";
nordvpn =
let
cli = cfg.package.cli.overrideAttrs (old: {
preBuild =
let
extraPreBuild = lib.optionalString (cfg.group != defaultGroup) ''
substituteInPlace internal/permissions.go \
--replace-fail \
'string{"nordvpn"}' \
'string{"${cfg.group}"}'
substituteInPlace internal/constants.go \
--replace-fail \
'NordvpnGroup = "nordvpn"' \
'NordvpnGroup = "${cfg.group}"'
'';
in
extraPreBuild + (old.preBuild or "");
# postFixup wraps nordvpnd so that it can find binaries that it calls.
# here, instead, use systemd to update the path to those binaries.
postFixup = "";
});
in
pkgs.symlinkJoin {
inherit (cfg.package) pname version meta;
paths = [
cli
cfg.package.gui
];
};
in
{
options.services.nordvpn = {
enable = lib.mkEnableOption "Enable NordVPN";
package = lib.mkPackageOption pkgs "nordvpn" { };
user = lib.mkOption {
type = lib.types.str;
default = defaultUser;
description = ''
The User that owns the `nordvpnd` systemd process.
If overriding the default, a user with the same name must exist.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = defaultGroup;
description = ''
The Group that owns the `nordvpnd` systemd process.
If overriding the default, a group with the same name must exist.
'';
};
};
config = lib.mkIf cfg.enable {
# create the default user if that's the one used
users.users = lib.mkIf (cfg.user == defaultUser) {
${defaultUser} = {
description = "User that runs `nordvpnd`.";
group = cfg.group;
isSystemUser = true;
};
};
# create the default group if that's the one used
users.groups = lib.mkIf (cfg.group == defaultGroup) {
${defaultGroup} = { };
};
# nordvpnd uses resolved to configure dns
services.resolved.enable = true;
# policy that allows nordvpnd to configure dns
security.polkit = {
enable = true;
extraConfig = ''
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.resolve1.set-dns-servers"
&& subject.isInGroup("${cfg.group}")) {
return polkit.Result.YES;
}
});
'';
};
environment.systemPackages = [
nordvpn
];
systemd.services.nordvpnd = {
after = [ "network-online.target" ];
description = "NordVPN daemon.";
path = (
with pkgs;
[
e2fsprogs
iproute2
libxslt
nftables
procps
wireguard-tools
]
++ [ nordvpn ]
);
serviceConfig = {
# nordvpnd needs CAP_NET_ADMIN to configure network interfaces
AmbientCapabilities = "CAP_NET_ADMIN";
CapabilityBoundingSet = "CAP_NET_ADMIN";
ExecStart = lib.getExe' nordvpn "nordvpnd";
Group = cfg.group;
KillMode = "process";
NonBlocking = true;
Requires = "nordvpnd.socket";
Restart = "on-failure";
RestartSec = 5;
RuntimeDirectory = "nordvpn";
RuntimeDirectoryMode = "0750";
StateDirectory = "nordvpn";
StateDirectoryMode = "0750";
User = cfg.user;
};
wantedBy = [ "default.target" ];
wants = [ "network-online.target" ];
};
systemd.sockets.nordvpnd = {
description = "NordVPN Daemon Socket";
listenStreams = [ "/run/nordvpn/nordvpnd.sock" ];
partOf = [ "nordvpnd.service" ];
socketConfig = {
DirectoryMode = "0750";
NoDelay = true;
SocketGroup = cfg.group;
SocketMode = "0770";
SocketUser = cfg.user;
};
wantedBy = [ "sockets.target" ];
};
systemd.user.services.norduserd = {
after = [ "network-online.target" ];
description = "NordUserD Service";
serviceConfig = {
ExecStart = lib.getExe' nordvpn "norduserd";
NonBlocking = true;
Restart = "on-failure";
RestartSec = 5;
};
wantedBy = [ "graphical-session.target" ];
wants = [ "network-online.target" ];
};
};
meta = {
doc = ./nordvpn.md;
maintainers = with lib.maintainers; [ different-error ];
};
}
+1
View File
@@ -1205,6 +1205,7 @@ in
nominatim = runTest ./nominatim.nix;
non-default-filesystems = handleTest ./non-default-filesystems.nix { };
non-switchable-system = runTest ./non-switchable-system.nix;
nordvpn = runTest ./nordvpn.nix;
noto-fonts = runTest ./noto-fonts.nix;
noto-fonts-cjk-qt-default-weight = runTest ./noto-fonts-cjk-qt-default-weight.nix;
novacomd = handleTestOn [ "x86_64-linux" ] ./novacomd.nix { };
+129
View File
@@ -0,0 +1,129 @@
{ lib, ... }:
{
name = "nordvpn";
meta.maintainers = with lib.maintainers; [ different-error ];
nodes =
let
commonConfig = user: {
# norduserd reads DBUS_SESSION_BUS_ADDRESS which the
# desktopManager sets on user session creation (i.e. login)
services.xserver.enable = true;
services.desktopManager.plasma6.enable = true;
services.displayManager.gdm.enable = true;
services.displayManager.autoLogin = {
enable = true;
user = user;
};
};
in
{
nada = { ... }: { };
basic =
{ ... }:
lib.recursiveUpdate {
users.users.alice = {
extraGroups = [ "nordvpn" ];
isNormalUser = true;
};
# default: run nordvpnd as nordvpn:nordvpn
services.nordvpn.enable = true;
} (commonConfig "alice");
userOnly =
{ ... }:
lib.recursiveUpdate {
users.users.kanye = {
extraGroups = [ "nordvpn" ];
isNormalUser = true;
};
# run nordvpnd as kanye:nordvpn
services.nordvpn = {
enable = true;
user = "kanye";
};
} (commonConfig "kanye");
groupOnly =
{ ... }:
lib.recursiveUpdate {
users.users.alice = {
extraGroups = [ "kanye" ];
isNormalUser = true;
};
users.groups.kanye = { };
# run nordvpnd as nordvpn:kanye
services.nordvpn = {
enable = true;
group = "kanye";
};
} (commonConfig "alice");
userAndGroup =
{ ... }:
lib.recursiveUpdate {
users.users.kanye = {
group = "kanye";
isNormalUser = true;
};
users.groups.kanye = { };
# run nordvpnd as kanye:kanye
services.nordvpn = {
enable = true;
group = "kanye";
user = "kanye";
};
} (commonConfig "kanye");
};
testScript = ''
class UserGroupTestCase:
def __init__(self, machine, user, has_nordvpn_usr, has_nordvpn_gp):
self.machine = machine
self.user = user
self.has_nordvpn_usr = has_nordvpn_usr
self.has_nordvpn_gp = has_nordvpn_gp
def run(self):
self.machine.start()
self.verify_nordvpn_user()
self.verify_nordvpn_group()
self.verify_services()
self.machine.shutdown()
def verify_nordvpn_user(self):
if self.has_nordvpn_usr:
self.machine.succeed("id nordvpn")
else:
self.machine.fail("id nordvpn")
def verify_nordvpn_group(self):
group_str = self.machine.succeed(f"sudo -u {self.user} groups")
groups = [x.strip() for x in group_str.split(" ")]
if self.has_nordvpn_gp:
assert "nordvpn" in groups, f"nordvpn is not in {groups} but should be"
else:
assert "nordvpn" not in groups, f"nordvpn is in {groups} but should not be"
def verify_services(self):
self.machine.wait_for_unit("nordvpnd", timeout=60)
self.machine.wait_for_unit("norduserd", self.user, timeout=90)
# verify can talk to nordvpnd. give nordvpnd at most 5s to initialize.
self.machine.wait_until_succeeds("nordvpn status", timeout=5)
self.machine.succeed("nordvpn status")
test_cases = [
UserGroupTestCase(basic, "alice", has_nordvpn_usr=True, has_nordvpn_gp=True),
UserGroupTestCase(userOnly, "kanye", has_nordvpn_usr=False, has_nordvpn_gp=True),
UserGroupTestCase(groupOnly, "alice", has_nordvpn_usr=True, has_nordvpn_gp=False),
UserGroupTestCase(userAndGroup, "kanye", has_nordvpn_usr=False, has_nordvpn_gp=False),
]
# NADA
nada.start()
nada.wait_for_unit("multi-user.target", timeout=60)
nada.fail("nordvpnd")
nada.fail("nordvpn")
nada.fail("norduserd")
nada.shutdown()
for test_case in test_cases:
test_case.run()
'';
}