nixos/netbird: harden and extend options

authored by Krzysztof Nazarewski and committed by Valentin Gagarin 49a26eda 04c07c70

+637 -87
+48 -24
nixos/modules/services/networking/netbird.md
··· 2 3 ## Quickstart {#module-services-netbird-quickstart} 4 5 - The absolute minimal configuration for the netbird daemon looks like this: 6 7 ```nix 8 { ··· 13 This will set up a netbird service listening on the port `51820` associated to the 14 `wt0` interface. 15 16 - It is strictly equivalent to setting: 17 18 ```nix 19 { 20 - services.netbird.tunnels.wt0.stateDir = "netbird"; 21 } 22 ``` 23 24 - The `enable` option is mainly kept for backward compatibility, as defining netbird 25 - tunnels through the `tunnels` option is more expressive. 26 27 ## Multiple connections setup {#module-services-netbird-multiple-connections} 28 29 - Using the `services.netbird.tunnels` option, it is also possible to define more than 30 one netbird service running at the same time. 31 32 - The following configuration will start a netbird daemon using the interface `wt1` and 33 - the port 51830. Its configuration file will then be located at `/var/lib/netbird-wt1/config.json`. 34 35 ```nix 36 { 37 - services.netbird.tunnels = { 38 - wt1 = { 39 - port = 51830; 40 - }; 41 - }; 42 } 43 ``` 44 45 - To interact with it, you will need to specify the correct daemon address: 46 47 - ```bash 48 - netbird --daemon-addr unix:///var/run/netbird-wt1/sock ... 49 - ``` 50 - 51 - The address will by default be `unix:///var/run/netbird-<name>`. 52 53 - It is also possible to overwrite default options passed to the service, for 54 - example: 55 56 ```nix 57 { 58 - services.netbird.tunnels.wt1.environment = { 59 - NB_DAEMON_ADDR = "unix:///var/run/toto.sock"; 60 }; 61 } 62 ``` 63 64 - This will set the socket to interact with the netbird service to `/var/run/toto.sock`.
··· 2 3 ## Quickstart {#module-services-netbird-quickstart} 4 5 + The absolute minimal configuration for the Netbird client daemon looks like this: 6 7 ```nix 8 { ··· 13 This will set up a netbird service listening on the port `51820` associated to the 14 `wt0` interface. 15 16 + Which is equivalent to: 17 18 ```nix 19 { 20 + services.netbird.clients.default = { 21 + port = 51820; 22 + interface = "wt0"; 23 + name = "netbird"; 24 + hardened = false; 25 + }; 26 } 27 ``` 28 29 + This will set up a `netbird.service` listening on the port `51820` associated to the 30 + `wt0` interface. There will also be `netbird-wt0` binary installed in addition to `netbird`. 31 + 32 + see [clients](#opt-services.netbird.clients) option documentation for more details. 33 34 ## Multiple connections setup {#module-services-netbird-multiple-connections} 35 36 + Using the `services.netbird.clients` option, it is possible to define more than 37 one netbird service running at the same time. 38 39 + You must at least define a `port` for the service to listen on, the rest is optional: 40 41 ```nix 42 { 43 + services.netbird.clients.wt1.port = 51830; 44 + services.netbird.clients.wt2.port = 51831; 45 } 46 ``` 47 48 + see [clients](#opt-services.netbird.clients) option documentation for more details. 49 50 + ## Exposing services internally on the Netbird network {#module-services-netbird-firewall} 51 52 + You can easily expose services exclusively to Netbird network by combining 53 + [`networking.firewall.interfaces`](#opt-networking.firewall.interfaces) rules 54 + with [`interface`](#opt-services.netbird.clients._name_.interface) names: 55 56 ```nix 57 { 58 + services.netbird.clients.priv.port = 51819; 59 + services.netbird.clients.work.port = 51818; 60 + networking.firewall.interfaces = { 61 + "${config.services.netbird.clients.priv.interface}" = { 62 + allowedUDPPorts = [ 1234 ]; 63 + }; 64 + "${config.services.netbird.clients.work.interface}" = { 65 + allowedTCPPorts = [ 8080 ]; 66 + }; 67 }; 68 } 69 ``` 70 71 + ### Additional customizations {#module-services-netbird-customization} 72 + 73 + Each Netbird client service by default: 74 + 75 + - runs in a [hardened](#opt-services.netbird.clients._name_.hardened) mode, 76 + - starts with the system, 77 + - [opens up a firewall](#opt-services.netbird.clients._name_.openFirewall) for direct (without TURN servers) 78 + peer-to-peer communication, 79 + - can be additionally configured with environment variables, 80 + - automatically determines whether `netbird-ui-<name>` should be available, 81 + 82 + [autoStart](#opt-services.netbird.clients._name_.autoStart) allows you to start the client (an actual systemd service) 83 + on demand, for example to connect to work-related or otherwise conflicting network only when required. 84 + See the option description for more information. 85 + 86 + [environment](#opt-services.netbird.clients._name_.environment) allows you to pass additional configurations 87 + through environment variables, but special care needs to be taken for overriding config location and 88 + daemon address due [hardened](#opt-services.netbird.clients._name_.hardened) option.
+534 -48
nixos/modules/services/networking/netbird.nix
··· 4 pkgs, 5 ... 6 }: 7 - 8 let 9 inherit (lib) 10 - attrNames 11 getExe 12 literalExpression 13 maintainers 14 mapAttrs' 15 mkDefault 16 - mkEnableOption 17 mkIf 18 mkMerge 19 mkOption 20 mkPackageOption 21 nameValuePair 22 optional 23 versionOlder 24 ; 25 26 inherit (lib.types) 27 attrsOf 28 port 29 str 30 submodule 31 ; 32 33 - kernel = config.boot.kernelPackages; 34 35 cfg = config.services.netbird; 36 in 37 { 38 - meta.maintainers = with maintainers; [ ]; 39 meta.doc = ./netbird.md; 40 41 options.services.netbird = { 42 - enable = mkEnableOption "Netbird daemon"; 43 package = mkPackageOption pkgs "netbird" { }; 44 45 - tunnels = mkOption { 46 type = attrsOf ( 47 submodule ( 48 { name, config, ... }: 49 { 50 options = { 51 port = mkOption { 52 type = port; 53 - default = 51820; 54 description = '' 55 - Port for the ${name} netbird interface. 56 ''; 57 }; 58 59 environment = mkOption { 60 type = attrsOf str; 61 defaultText = literalExpression '' 62 { 63 - NB_CONFIG = "/var/lib/''${stateDir}/config.json"; 64 - NB_LOG_FILE = "console"; 65 - NB_WIREGUARD_PORT = builtins.toString port; 66 - NB_INTERFACE_NAME = name; 67 - NB_DAMEON_ADDR = "/var/run/''${stateDir}" 68 } 69 ''; 70 description = '' ··· 72 ''; 73 }; 74 75 - stateDir = mkOption { 76 type = str; 77 - default = "netbird-${name}"; 78 description = '' 79 - Directory storing the netbird configuration. 80 ''; 81 }; 82 }; 83 84 - config.environment = builtins.mapAttrs (_: mkDefault) { 85 - NB_CONFIG = "/var/lib/${config.stateDir}/config.json"; 86 - NB_LOG_FILE = "console"; 87 - NB_WIREGUARD_PORT = builtins.toString config.port; 88 - NB_INTERFACE_NAME = name; 89 - NB_DAEMON_ADDR = "unix:///var/run/${config.stateDir}/sock"; 90 - }; 91 } 92 ) 93 ); 94 default = { }; 95 description = '' 96 - Attribute set of Netbird tunnels, each one will spawn a daemon listening on ... 97 ''; 98 }; 99 }; 100 101 config = mkMerge [ 102 (mkIf cfg.enable { 103 - # For backwards compatibility 104 - services.netbird.tunnels.wt0.stateDir = "netbird"; 105 }) 106 107 - (mkIf (cfg.tunnels != { }) { 108 - boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard; 109 110 - environment.systemPackages = [ cfg.package ]; 111 112 - networking.dhcpcd.denyInterfaces = attrNames cfg.tunnels; 113 114 systemd.network.networks = mkIf config.networking.useNetworkd ( 115 - mapAttrs' ( 116 - name: _: 117 - nameValuePair "50-netbird-${name}" { 118 matchConfig = { 119 - Name = name; 120 }; 121 linkConfig = { 122 Unmanaged = true; 123 ActivationPolicy = "manual"; 124 }; 125 } 126 - ) cfg.tunnels 127 ); 128 129 - systemd.services = mapAttrs' ( 130 - name: 131 - { environment, stateDir, ... }: 132 - nameValuePair "netbird-${name}" { 133 description = "A WireGuard-based mesh network that connects your devices into a single private network"; 134 135 documentation = [ "https://netbird.io/docs/" ]; ··· 137 after = [ "network.target" ]; 138 wantedBy = [ "multi-user.target" ]; 139 140 - path = with pkgs; [ openresolv ]; 141 - 142 - inherit environment; 143 144 serviceConfig = { 145 - ExecStart = "${getExe cfg.package} service run"; 146 Restart = "always"; 147 - RuntimeDirectory = stateDir; 148 - StateDirectory = stateDir; 149 StateDirectoryMode = "0700"; 150 - WorkingDirectory = "/var/lib/${stateDir}"; 151 }; 152 153 unitConfig = { ··· 157 158 stopIfChanged = false; 159 } 160 - ) cfg.tunnels; 161 }) 162 ]; 163 }
··· 4 pkgs, 5 ... 6 }: 7 let 8 inherit (lib) 9 + attrValues 10 + concatLists 11 + concatStringsSep 12 + escapeShellArgs 13 + filterAttrs 14 getExe 15 literalExpression 16 maintainers 17 + makeBinPath 18 mapAttrs' 19 + mapAttrsToList 20 + mkAliasOptionModule 21 mkDefault 22 mkIf 23 mkMerge 24 mkOption 25 + mkOptionDefault 26 mkPackageOption 27 nameValuePair 28 optional 29 + optionalAttrs 30 + optionalString 31 + toShellVars 32 + versionAtLeast 33 versionOlder 34 ; 35 36 inherit (lib.types) 37 attrsOf 38 + bool 39 + enum 40 + nullOr 41 + package 42 + path 43 port 44 str 45 submodule 46 ; 47 48 + inherit (config.boot) kernelPackages; 49 + inherit (config.boot.kernelPackages) kernel; 50 51 cfg = config.services.netbird; 52 + 53 + toClientList = fn: map fn (attrValues cfg.clients); 54 + toClientAttrs = fn: mapAttrs' (_: fn) cfg.clients; 55 + 56 + hardenedClients = filterAttrs (_: client: client.hardened) cfg.clients; 57 + toHardenedClientList = fn: map fn (attrValues hardenedClients); 58 + toHardenedClientAttrs = fn: mapAttrs' (_: fn) hardenedClients; 59 + 60 + mkBinName = 61 + client: name: 62 + if client.bin.suffix == "" || client.bin.suffix == "netbird" then 63 + name 64 + else 65 + "${name}-${client.bin.suffix}"; 66 + 67 + nixosConfig = config; 68 in 69 { 70 + meta.maintainers = with maintainers; [ 71 + nazarewk 72 + ]; 73 meta.doc = ./netbird.md; 74 75 + imports = [ 76 + (mkAliasOptionModule [ "services" "netbird" "tunnels" ] [ "services" "netbird" "clients" ]) 77 + ]; 78 + 79 options.services.netbird = { 80 + enable = mkOption { 81 + type = bool; 82 + default = false; 83 + description = '' 84 + Enables backwards compatible Netbird client service. 85 + 86 + This is strictly equivalent to: 87 + 88 + ```nix 89 + services.netbird.clients.default = { 90 + port = 51820; 91 + name = "netbird"; 92 + systemd.name = "netbird"; 93 + interface = "wt0"; 94 + hardened = false; 95 + }; 96 + ``` 97 + ''; 98 + }; 99 package = mkPackageOption pkgs "netbird" { }; 100 101 + ui.enable = mkOption { 102 + type = bool; 103 + default = config.services.displayManager.sessionPackages != [ ] || config.services.xserver.enable; 104 + defaultText = literalExpression '' 105 + config.services.displayManager.sessionPackages != [ ] || config.services.xserver.enable 106 + ''; 107 + description = '' 108 + Controls presence `netbird-ui` wrappers, defaults to presence of graphical sessions. 109 + ''; 110 + }; 111 + ui.package = mkPackageOption pkgs "netbird-ui" { }; 112 + 113 + clients = mkOption { 114 type = attrsOf ( 115 submodule ( 116 { name, config, ... }: 117 + let 118 + client = config; 119 + in 120 { 121 options = { 122 port = mkOption { 123 type = port; 124 + example = literalExpression "51820"; 125 + description = '' 126 + Port the Netbird client listens on. 127 + ''; 128 + }; 129 + 130 + name = mkOption { 131 + type = str; 132 + default = name; 133 + description = '' 134 + Primary name for use (as a suffix) in: 135 + - systemd service name, 136 + - hardened user name and group, 137 + - [systemd `*Directory=`](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RuntimeDirectory=) names, 138 + - desktop application identification, 139 + ''; 140 + }; 141 + 142 + dns-resolver.address = mkOption { 143 + type = nullOr str; 144 + default = null; 145 + example = "127.0.0.123"; 146 + description = '' 147 + An explicit address that Netbird will serve `*.netbird.cloud.` (usually) entries on. 148 + 149 + Netbird serves DNS on it's own (dynamic) client address by default. 150 + ''; 151 + }; 152 + 153 + dns-resolver.port = mkOption { 154 + type = port; 155 + default = 53; 156 description = '' 157 + A port to serve DNS entries on when `dns-resolver.address` is enabled. 158 ''; 159 }; 160 161 + interface = mkOption { 162 + type = str; 163 + default = "nb-${client.name}"; 164 + description = '' 165 + Name of the network interface managed by this client. 166 + ''; 167 + apply = 168 + iface: 169 + lib.throwIfNot ( 170 + builtins.stringLength iface <= 15 171 + ) "Network interface name must be 15 characters or less" iface; 172 + }; 173 + 174 environment = mkOption { 175 type = attrsOf str; 176 defaultText = literalExpression '' 177 { 178 + NB_STATE_DIR = client.dir.state; 179 + NB_CONFIG = "''${client.dir.state}/config.json"; 180 + NB_DAEMON_ADDR = "unix://''${client.dir.runtime}/sock"; 181 + NB_INTERFACE_NAME = client.interface; 182 + NB_LOG_FILE = mkOptionDefault "console"; 183 + NB_LOG_LEVEL = client.logLevel; 184 + NB_SERVICE = client.service.name; 185 + NB_WIREGUARD_PORT = toString client.port; 186 + } // optionalAttrs (client.dns-resolver.address != null) { 187 + NB_DNS_RESOLVER_ADDRESS = "''${client.dns-resolver.address}:''${builtins.toString client.dns-resolver.port}"; 188 } 189 ''; 190 description = '' ··· 192 ''; 193 }; 194 195 + autoStart = mkOption { 196 + type = bool; 197 + default = true; 198 + description = '' 199 + Start the service with the system. 200 + 201 + As of 2024-02-13 it is not possible to start a Netbird client daemon without immediately 202 + connecting to the network, but it is [planned for a near future](https://github.com/netbirdio/netbird/projects/2#card-91718018). 203 + ''; 204 + }; 205 + 206 + openFirewall = mkOption { 207 + type = bool; 208 + default = true; 209 + description = '' 210 + Opens up firewall `port` for communication between Netbird peers directly over LAN or public IP, 211 + without using (internet-hosted) TURN servers as intermediaries. 212 + ''; 213 + }; 214 + 215 + hardened = mkOption { 216 + type = bool; 217 + default = true; 218 + description = '' 219 + Hardened service: 220 + - runs as a dedicated user with minimal set of permissions (see caveats), 221 + - restricts daemon configuration socket access to dedicated user group 222 + (you can grant access to it with `users.users."<user>".extraGroups = [ ${client.user.group} ]`), 223 + 224 + Even though the local system resources access is restricted: 225 + - `CAP_NET_RAW`, `CAP_NET_ADMIN` and `CAP_BPF` still give unlimited network manipulation possibilites, 226 + - older kernels don't have `CAP_BPF` and use `CAP_SYS_ADMIN` instead, 227 + 228 + Known security features that are not (yet) integrated into the module: 229 + - 2024-02-14: `rosenpass` is an experimental feature configurable solely 230 + through `--enable-rosenpass` flag on the `netbird up` command, 231 + see [the docs](https://docs.netbird.io/how-to/enable-post-quantum-cryptography) 232 + ''; 233 + }; 234 + 235 + logLevel = mkOption { 236 + type = enum [ 237 + # logrus loglevels 238 + "panic" 239 + "fatal" 240 + "error" 241 + "warn" 242 + "warning" 243 + "info" 244 + "debug" 245 + "trace" 246 + ]; 247 + default = "info"; 248 + description = "Log level of the Netbird daemon."; 249 + }; 250 + 251 + ui.enable = mkOption { 252 + type = bool; 253 + default = nixosConfig.services.netbird.ui.enable; 254 + defaultText = literalExpression ''client.ui.enable''; 255 + description = '' 256 + Controls presence of `netbird-ui` wrapper for this Netbird client. 257 + ''; 258 + }; 259 + 260 + wrapper = mkOption { 261 + type = package; 262 + internal = true; 263 + default = 264 + let 265 + makeWrapperArgs = concatLists ( 266 + mapAttrsToList (key: value: [ 267 + "--set-default" 268 + key 269 + value 270 + ]) client.environment 271 + ); 272 + mkBin = mkBinName client; 273 + in 274 + pkgs.stdenv.mkDerivation { 275 + name = "${cfg.package.name}-wrapper-${client.name}"; 276 + meta.mainProgram = mkBin "netbird"; 277 + nativeBuildInputs = with pkgs; [ makeWrapper ]; 278 + phases = [ "installPhase" ]; 279 + installPhase = concatStringsSep "\n" [ 280 + '' 281 + mkdir -p "$out/bin" 282 + makeWrapper ${lib.getExe cfg.package} "$out/bin/${mkBin "netbird"}" \ 283 + ${escapeShellArgs makeWrapperArgs} 284 + '' 285 + (optionalString cfg.ui.enable '' 286 + # netbird-ui doesn't support envvars 287 + makeWrapper ${lib.getExe cfg.ui.package} "$out/bin/${mkBin "netbird-ui"}" \ 288 + --add-flags '--daemon-addr=${client.environment.NB_DAEMON_ADDR}' 289 + 290 + mkdir -p "$out/share/applications" 291 + substitute ${cfg.ui.package}/share/applications/netbird.desktop \ 292 + "$out/share/applications/${mkBin "netbird"}.desktop" \ 293 + --replace-fail 'Name=Netbird' "Name=Netbird @ ${client.service.name}" \ 294 + --replace-fail '${lib.getExe cfg.ui.package}' "$out/bin/${mkBin "netbird-ui"}" 295 + '') 296 + ]; 297 + }; 298 + }; 299 + 300 + # see https://github.com/netbirdio/netbird/blob/88747e3e0191abc64f1e8c7ecc65e5e50a1527fd/client/internal/config.go#L49-L82 301 + config = mkOption { 302 + type = (pkgs.formats.json { }).type; 303 + defaultText = literalExpression '' 304 + { 305 + DisableAutoConnect = !client.autoStart; 306 + WgIface = client.interface; 307 + WgPort = client.port; 308 + } // optionalAttrs (client.dns-resolver.address != null) { 309 + CustomDNSAddress = "''${client.dns-resolver.address}:''${builtins.toString client.dns-resolver.port}"; 310 + } 311 + ''; 312 + description = '' 313 + Additional configuration that exists before the first start and 314 + later overrides the existing values in `config.json`. 315 + 316 + It is mostly helpful to manage configuration ignored/not yet implemented 317 + outside of `netbird up` invocation. 318 + 319 + WARNING: this is not an upstream feature, it could break in the future 320 + (by having lower priority) after upstream implements an equivalent. 321 + 322 + It is implemented as a `preStart` script which overrides `config.json` 323 + with content of `/etc/${client.dir.baseName}/config.d/*.json` files. 324 + This option manages specifically `50-nixos.json` file. 325 + 326 + Consult [the source code](https://github.com/netbirdio/netbird/blob/88747e3e0191abc64f1e8c7ecc65e5e50a1527fd/client/internal/config.go#L49-L82) 327 + or inspect existing file for a complete list of available configurations. 328 + ''; 329 + }; 330 + 331 + suffixedName = mkOption { 332 type = str; 333 + default = if client.name != "netbird" then "netbird-${client.name}" else client.name; 334 description = '' 335 + A systemd service name to use (without `.service` suffix). 336 + ''; 337 + }; 338 + dir.baseName = mkOption { 339 + type = str; 340 + default = client.suffixedName; 341 + description = '' 342 + A systemd service name to use (without `.service` suffix). 343 + ''; 344 + }; 345 + dir.state = mkOption { 346 + type = path; 347 + default = "/var/lib/${client.dir.baseName}"; 348 + description = '' 349 + A state directory used by Netbird client to store `config.json`, `state.json` & `resolv.conf`. 350 + ''; 351 + }; 352 + dir.runtime = mkOption { 353 + type = path; 354 + default = "/var/run/${client.dir.baseName}"; 355 + description = '' 356 + A runtime directory used by Netbird client. 357 + ''; 358 + }; 359 + service.name = mkOption { 360 + type = str; 361 + default = client.suffixedName; 362 + description = '' 363 + A systemd service name to use (without `.service` suffix). 364 + ''; 365 + }; 366 + user.name = mkOption { 367 + type = str; 368 + default = client.suffixedName; 369 + description = '' 370 + A system user name for this client instance. 371 + ''; 372 + }; 373 + user.group = mkOption { 374 + type = str; 375 + default = client.suffixedName; 376 + description = '' 377 + A system group name for this client instance. 378 + ''; 379 + }; 380 + bin.suffix = mkOption { 381 + type = str; 382 + default = if client.name != "netbird" then client.name else ""; 383 + description = '' 384 + A system group name for this client instance. 385 ''; 386 }; 387 }; 388 389 + config.environment = 390 + { 391 + NB_STATE_DIR = client.dir.state; 392 + NB_CONFIG = "${client.dir.state}/config.json"; 393 + NB_DAEMON_ADDR = "unix://${client.dir.runtime}/sock"; 394 + NB_INTERFACE_NAME = client.interface; 395 + NB_LOG_FILE = mkOptionDefault "console"; 396 + NB_LOG_LEVEL = client.logLevel; 397 + NB_SERVICE = client.service.name; 398 + NB_WIREGUARD_PORT = toString client.port; 399 + } 400 + // optionalAttrs (client.dns-resolver.address != null) { 401 + NB_DNS_RESOLVER_ADDRESS = "${client.dns-resolver.address}:${builtins.toString client.dns-resolver.port}"; 402 + }; 403 + 404 + config.config = 405 + { 406 + DisableAutoConnect = !client.autoStart; 407 + WgIface = client.interface; 408 + WgPort = client.port; 409 + } 410 + // optionalAttrs (client.dns-resolver.address != null) { 411 + CustomDNSAddress = "${client.dns-resolver.address}:${builtins.toString client.dns-resolver.port}"; 412 + }; 413 } 414 ) 415 ); 416 default = { }; 417 description = '' 418 + Attribute set of Netbird client daemons, by default each one will: 419 + 420 + 1. be manageable using dedicated tooling: 421 + - `netbird-<name>` script, 422 + - `Netbird - netbird-<name>` graphical interface when appropriate (see `ui.enable`), 423 + 2. run as a `netbird-<name>.service`, 424 + 3. listen for incoming remote connections on the port `51820` (`openFirewall` by default), 425 + 4. manage the `netbird-<name>` wireguard interface, 426 + 5. use the `/var/lib/netbird-<name>/config.json` configuration file, 427 + 6. override `/var/lib/netbird-<name>/config.json` with values from `/etc/netbird-<name>/config.d/*.json`, 428 + 7. (`hardened`) be locally manageable by `netbird-<name>` system group, 429 + 430 + With following caveats: 431 + 432 + - multiple daemons will interfere with each other's DNS resolution of `netbird.cloud`, but 433 + should remain fully operational otherwise. 434 + Setting up custom (non-conflicting) DNS zone is currently possible only when self-hosting. 435 + ''; 436 + example = lib.literalExpression '' 437 + { 438 + services.netbird.clients.wt0.port = 51820; 439 + services.netbird.clients.personal.port = 51821; 440 + services.netbird.clients.work1.port = 51822; 441 + } 442 ''; 443 }; 444 }; 445 446 config = mkMerge [ 447 (mkIf cfg.enable { 448 + services.netbird.clients.default = { 449 + port = mkDefault 51820; 450 + interface = mkDefault "wt0"; 451 + name = mkDefault "netbird"; 452 + hardened = mkDefault false; 453 + }; 454 }) 455 + { 456 + boot.extraModulePackages = optional ( 457 + cfg.clients != { } && (versionOlder kernel.version "5.6") 458 + ) kernelPackages.wireguard; 459 460 + environment.systemPackages = toClientList (client: client.wrapper) 461 + # omitted due to https://github.com/netbirdio/netbird/issues/1562 462 + #++ optional (cfg.clients != { }) cfg.package 463 + # omitted due to https://github.com/netbirdio/netbird/issues/1581 464 + #++ optional (cfg.clients != { } && cfg.ui.enable) cfg.ui.package 465 + ; 466 467 + networking.dhcpcd.denyInterfaces = toClientList (client: client.interface); 468 + networking.networkmanager.unmanaged = toClientList (client: "interface-name:${client.interface}"); 469 470 + networking.firewall.allowedUDPPorts = concatLists ( 471 + toClientList (client: optional client.openFirewall client.port) 472 + ); 473 474 systemd.network.networks = mkIf config.networking.useNetworkd ( 475 + toClientAttrs ( 476 + client: 477 + nameValuePair "50-netbird-${client.interface}" { 478 matchConfig = { 479 + Name = client.interface; 480 }; 481 linkConfig = { 482 Unmanaged = true; 483 ActivationPolicy = "manual"; 484 }; 485 } 486 + ) 487 + ); 488 + 489 + environment.etc = toClientAttrs ( 490 + client: 491 + nameValuePair "${client.dir.baseName}/config.d/50-nixos.json" { 492 + text = builtins.toJSON client.config; 493 + mode = "0444"; 494 + } 495 ); 496 497 + systemd.services = toClientAttrs ( 498 + client: 499 + nameValuePair client.service.name { 500 description = "A WireGuard-based mesh network that connects your devices into a single private network"; 501 502 documentation = [ "https://netbird.io/docs/" ]; ··· 504 after = [ "network.target" ]; 505 wantedBy = [ "multi-user.target" ]; 506 507 + path = optional (!config.services.resolved.enable) pkgs.openresolv; 508 509 serviceConfig = { 510 + ExecStart = "${getExe client.wrapper} service run"; 511 Restart = "always"; 512 + 513 + RuntimeDirectory = client.dir.baseName; 514 + RuntimeDirectoryMode = mkDefault "0755"; 515 + ConfigurationDirectory = client.dir.baseName; 516 + StateDirectory = client.dir.baseName; 517 StateDirectoryMode = "0700"; 518 + 519 + WorkingDirectory = client.dir.state; 520 }; 521 522 unitConfig = { ··· 526 527 stopIfChanged = false; 528 } 529 + ); 530 + } 531 + # Hardening section 532 + (mkIf (hardenedClients != { }) { 533 + users.groups = toHardenedClientAttrs (client: nameValuePair client.user.group { }); 534 + users.users = toHardenedClientAttrs ( 535 + client: 536 + nameValuePair client.user.name { 537 + isSystemUser = true; 538 + home = client.dir.state; 539 + group = client.user.group; 540 + } 541 + ); 542 + 543 + systemd.services = toHardenedClientAttrs ( 544 + client: 545 + nameValuePair client.service.name ( 546 + mkIf client.hardened { 547 + serviceConfig = { 548 + RuntimeDirectoryMode = "0750"; 549 + 550 + User = client.user.name; 551 + Group = client.user.group; 552 + 553 + # settings implied by DynamicUser=true, without actully using it, 554 + # see https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#DynamicUser= 555 + RemoveIPC = true; 556 + PrivateTmp = true; 557 + ProtectSystem = "strict"; 558 + ProtectHome = "yes"; 559 + 560 + AmbientCapabilities = 561 + [ 562 + # see https://man7.org/linux/man-pages/man7/capabilities.7.html 563 + # see https://docs.netbird.io/how-to/installation#running-net-bird-in-docker 564 + # 565 + # seems to work fine without CAP_SYS_ADMIN and CAP_SYS_RESOURCE 566 + # CAP_NET_BIND_SERVICE could be added to allow binding on low ports, but is not required, 567 + # see https://github.com/netbirdio/netbird/pull/1513 568 + 569 + # failed creating tunnel interface wt-priv: [operation not permitted 570 + "CAP_NET_ADMIN" 571 + # failed to pull up wgInterface [wt-priv]: failed to create ipv4 raw socket: socket: operation not permitted 572 + "CAP_NET_RAW" 573 + ] 574 + # required for eBPF filter, used to be subset of CAP_SYS_ADMIN 575 + ++ optional (versionAtLeast kernel.version "5.8") "CAP_BPF" 576 + ++ optional (versionOlder kernel.version "5.8") "CAP_SYS_ADMIN" 577 + ++ optional ( 578 + client.dns-resolver.address != null && client.dns-resolver.port < 1024 579 + ) "CAP_NET_BIND_SERVICE"; 580 + }; 581 + } 582 + ) 583 + ); 584 + 585 + # see https://github.com/systemd/systemd/blob/17f3e91e8107b2b29fe25755651b230bbc81a514/src/resolve/org.freedesktop.resolve1.policy#L43-L43 586 + # see all actions used at https://github.com/netbirdio/netbird/blob/13e7198046a0d73a9cd91bf8e063fafb3d41885c/client/internal/dns/systemd_linux.go#L29-L32 587 + security.polkit.extraConfig = mkIf config.services.resolved.enable '' 588 + // systemd-resolved access for Netbird clients 589 + polkit.addRule(function(action, subject) { 590 + var actions = [ 591 + "org.freedesktop.resolve1.revert", 592 + "org.freedesktop.resolve1.set-default-route", 593 + "org.freedesktop.resolve1.set-dns-servers", 594 + "org.freedesktop.resolve1.set-domains", 595 + ]; 596 + var users = ${builtins.toJSON (toHardenedClientList (client: client.user.name))}; 597 + 598 + if (actions.indexOf(action.id) >= 0 && users.indexOf(subject.user) >= 0 ) { 599 + return polkit.Result.YES; 600 + } 601 + }); 602 + ''; 603 }) 604 + # migration & temporary fixups section 605 + { 606 + systemd.services = toClientAttrs ( 607 + client: 608 + nameValuePair client.service.name { 609 + preStart = '' 610 + set -eEuo pipefail 611 + ${optionalString (client.logLevel == "trace" || client.logLevel == "debug") "set -x"} 612 + 613 + PATH="${ 614 + makeBinPath ( 615 + with pkgs; 616 + [ 617 + coreutils 618 + jq 619 + diffutils 620 + ] 621 + ) 622 + }:$PATH" 623 + export ${toShellVars client.environment} 624 + 625 + # merge /etc/${client.dir.baseName}/config.d' into "$NB_CONFIG" 626 + { 627 + test -e "$NB_CONFIG" || echo -n '{}' > "$NB_CONFIG" 628 + 629 + # merge config.d with "$NB_CONFIG" into "$NB_CONFIG.new" 630 + jq -sS 'reduce .[] as $i ({}; . * $i)' \ 631 + "$NB_CONFIG" \ 632 + /etc/${client.dir.baseName}/config.d/*.json \ 633 + > "$NB_CONFIG.new" 634 + 635 + echo "Comparing $NB_CONFIG with $NB_CONFIG.new ..." 636 + if ! diff <(jq -S <"$NB_CONFIG") "$NB_CONFIG.new" ; then 637 + echo "Updating $NB_CONFIG ..." 638 + mv "$NB_CONFIG.new" "$NB_CONFIG" 639 + else 640 + echo "Files are the same, not doing anything." 641 + rm "$NB_CONFIG.new" 642 + fi 643 + } 644 + ''; 645 + } 646 + ); 647 + } 648 ]; 649 }
+55 -15
nixos/tests/netbird.nix
··· 1 - import ./make-test-python.nix ({ pkgs, lib, ... }: 2 - { 3 - name = "netbird"; 4 5 - meta.maintainers = with pkgs.lib.maintainers; [ ]; 6 7 - nodes = { 8 - node = { ... }: { 9 - services.netbird.enable = true; 10 }; 11 - }; 12 13 - testScript = '' 14 - start_all() 15 - node.wait_for_unit("netbird-wt0.service") 16 - node.wait_for_file("/var/run/netbird/sock") 17 - node.succeed("netbird status | grep -q 'Daemon status: NeedsLogin'") 18 - ''; 19 - })
··· 1 + import ./make-test-python.nix ( 2 + { pkgs, lib, ... }: 3 + { 4 + name = "netbird"; 5 6 + meta.maintainers = with pkgs.lib.maintainers; [ 7 + nazarewk 8 + ]; 9 10 + nodes = { 11 + clients = 12 + { ... }: 13 + { 14 + services.netbird.enable = true; 15 + services.netbird.clients.custom.port = 51819; 16 + }; 17 }; 18 19 + # TODO: confirm the whole solution is working end-to-end when netbird server is implemented 20 + testScript = '' 21 + start_all() 22 + def did_start(node, name): 23 + node.wait_for_unit(f"{name}.service") 24 + node.wait_for_file(f"/var/run/{name}/sock") 25 + output = node.succeed(f"{name} status") 26 + 27 + # not sure why, but it can print either of: 28 + # - Daemon status: NeedsLogin 29 + # - Management: Disconnected 30 + expected = [ 31 + "Disconnected", 32 + "NeedsLogin", 33 + ] 34 + assert any(msg in output for msg in expected) 35 + 36 + did_start(clients, "netbird") 37 + did_start(clients, "netbird-custom") 38 + ''; 39 + 40 + /* 41 + `netbird status` used to print `Daemon status: NeedsLogin` 42 + https://github.com/netbirdio/netbird/blob/23a14737974e3849fa86408d136cc46db8a885d0/client/cmd/status.go#L154-L164 43 + as the first line, but now it is just: 44 + 45 + Daemon version: 0.26.3 46 + CLI version: 0.26.3 47 + Management: Disconnected 48 + Signal: Disconnected 49 + Relays: 0/0 Available 50 + Nameservers: 0/0 Available 51 + FQDN: 52 + NetBird IP: N/A 53 + Interface type: N/A 54 + Quantum resistance: false 55 + Routes: - 56 + Peers count: 0/0 Connected 57 + */ 58 + } 59 + )