at 25.11-pre 24 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 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; 68in 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 = '' 191 Environment for the netbird service, used to pass configuration options. 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/" ]; 503 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 = { 523 StartLimitInterval = 5; 524 StartLimitBurst = 10; 525 }; 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}