nixos/nordvpn: init module
This commit is contained in:
@@ -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. -->
|
||||
|
||||
@@ -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 ];
|
||||
};
|
||||
}
|
||||
@@ -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 { };
|
||||
|
||||
@@ -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()
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user