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