1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.networking.networkmanager;
12 ini = pkgs.formats.ini { };
13
14 delegateWireless = config.networking.wireless.enable == true && cfg.unmanaged != [ ];
15
16 enableIwd = cfg.wifi.backend == "iwd";
17
18 configAttrs = lib.recursiveUpdate {
19 main = {
20 plugins = "keyfile";
21 inherit (cfg) dhcp dns;
22 # If resolvconf is disabled that means that resolv.conf is managed by some other module.
23 rc-manager = if config.networking.resolvconf.enable then "resolvconf" else "unmanaged";
24 };
25 keyfile = {
26 unmanaged-devices = if cfg.unmanaged == [ ] then null else lib.concatStringsSep ";" cfg.unmanaged;
27 };
28 logging = {
29 audit = config.security.audit.enable;
30 level = cfg.logLevel;
31 };
32 connection = cfg.connectionConfig;
33 device = {
34 "wifi.scan-rand-mac-address" = cfg.wifi.scanRandMacAddress;
35 "wifi.backend" = cfg.wifi.backend;
36 };
37 } cfg.settings;
38 configFile = ini.generate "NetworkManager.conf" configAttrs;
39
40 /*
41 [network-manager]
42 Identity=unix-group:networkmanager
43 Action=org.freedesktop.NetworkManager.*
44 ResultAny=yes
45 ResultInactive=no
46 ResultActive=yes
47 */
48 polkitConf = ''
49 polkit.addRule(function(action, subject) {
50 if (
51 subject.isInGroup("networkmanager")
52 && action.id.indexOf("org.freedesktop.NetworkManager.") == 0
53 )
54 { return polkit.Result.YES; }
55 });
56 '';
57
58 ns = xs: pkgs.writeText "nameservers" (concatStrings (map (s: "nameserver ${s}\n") xs));
59
60 overrideNameserversScript = pkgs.writeScript "02overridedns" ''
61 #!/bin/sh
62 PATH=${
63 with pkgs;
64 makeBinPath [
65 gnused
66 gnugrep
67 coreutils
68 ]
69 }
70 tmp=$(mktemp)
71 sed '/nameserver /d' /etc/resolv.conf > $tmp
72 grep 'nameserver ' /etc/resolv.conf | \
73 grep -vf ${ns (cfg.appendNameservers ++ cfg.insertNameservers)} > $tmp.ns
74 cat $tmp ${ns cfg.insertNameservers} $tmp.ns ${ns cfg.appendNameservers} > /etc/resolv.conf
75 rm -f $tmp $tmp.ns
76 '';
77
78 dispatcherTypesSubdirMap = {
79 basic = "";
80 pre-up = "pre-up.d/";
81 pre-down = "pre-down.d/";
82 };
83
84 macAddressOptWifi = mkOption {
85 type = types.either types.str (
86 types.enum [
87 "permanent"
88 "preserve"
89 "random"
90 "stable"
91 "stable-ssid"
92 ]
93 );
94 default = "preserve";
95 example = "00:11:22:33:44:55";
96 description = ''
97 Set the MAC address of the interface.
98
99 - `"XX:XX:XX:XX:XX:XX"`: MAC address of the interface
100 - `"permanent"`: Use the permanent MAC address of the device
101 - `"preserve"`: Don’t change the MAC address of the device upon activation
102 - `"random"`: Generate a randomized value upon each connect
103 - `"stable"`: Generate a stable, hashed MAC address
104 - `"stable-ssid"`: Generate a stable MAC addressed based on Wi-Fi network
105 '';
106 };
107
108 macAddressOptEth = mkOption {
109 type = types.either types.str (
110 types.enum [
111 "permanent"
112 "preserve"
113 "random"
114 "stable"
115 ]
116 );
117 default = "preserve";
118 example = "00:11:22:33:44:55";
119 description = ''
120 Set the MAC address of the interface.
121
122 - `"XX:XX:XX:XX:XX:XX"`: MAC address of the interface
123 - `"permanent"`: Use the permanent MAC address of the device
124 - `"preserve"`: Don’t change the MAC address of the device upon activation
125 - `"random"`: Generate a randomized value upon each connect
126 - `"stable"`: Generate a stable, hashed MAC address
127 '';
128 };
129
130 concatPluginAttrs = attr: lib.concatMap (plugin: plugin.${attr} or [ ]) cfg.plugins;
131 pluginRuntimeDeps = concatPluginAttrs "networkManagerRuntimeDeps";
132 pluginDbusDeps = concatPluginAttrs "networkManagerDbusDeps";
133 pluginTmpfilesRules = concatPluginAttrs "networkManagerTmpfilesRules";
134
135 packages = [
136 cfg.package
137 ]
138 ++ cfg.plugins
139 ++ pluginRuntimeDeps
140 ++ lib.optionals (!delegateWireless && !enableIwd) [
141 pkgs.wpa_supplicant
142 ];
143in
144{
145
146 meta = {
147 maintainers = teams.freedesktop.members ++ [
148 lib.maintainers.frontear
149 ];
150 };
151
152 ###### interface
153
154 options = {
155
156 networking.networkmanager = {
157
158 enable = mkOption {
159 type = types.bool;
160 default = false;
161 description = ''
162 Whether to use NetworkManager to obtain an IP address and other
163 configuration for all network interfaces that are not manually
164 configured. If enabled, a group `networkmanager`
165 will be created. Add all users that should have permission
166 to change network settings to this group.
167 '';
168 };
169
170 package = mkPackageOption pkgs "networkmanager" { };
171
172 connectionConfig = mkOption {
173 type =
174 with types;
175 attrsOf (
176 nullOr (oneOf [
177 bool
178 int
179 str
180 ])
181 );
182 default = { };
183 description = ''
184 Configuration for the [connection] section of NetworkManager.conf.
185 Refer to
186 [
187 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#id-1.2.3.11
188 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
189 or
190 {manpage}`NetworkManager.conf(5)`
191 for more information.
192 '';
193 };
194
195 settings = mkOption {
196 type = ini.type;
197 default = { };
198 description = ''
199 Configuration added to the generated NetworkManager.conf, note that you can overwrite settings with this.
200 Refer to
201 [
202 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html
203 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
204 or
205 {manpage}`NetworkManager.conf(5)`
206 for more information.
207 '';
208 };
209
210 unmanaged = mkOption {
211 type = types.listOf types.str;
212 default = [ ];
213 description = ''
214 List of interfaces that will not be managed by NetworkManager.
215 Interface name can be specified here, but if you need more fidelity,
216 refer to
217 [
218 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#device-spec
219 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#device-spec)
220 or the "Device List Format" Appendix of
221 {manpage}`NetworkManager.conf(5)`.
222 '';
223 };
224
225 plugins = mkOption {
226 type =
227 let
228 networkManagerPluginPackage = types.package // {
229 description = "NetworkManager plugin package";
230 check =
231 p:
232 lib.assertMsg
233 (types.package.check p && p ? networkManagerPlugin && lib.isString p.networkManagerPlugin)
234 ''
235 Package ‘${p.name}’, is not a NetworkManager plugin.
236 Those need to have a ‘networkManagerPlugin’ attribute.
237 '';
238 };
239 in
240 types.listOf networkManagerPluginPackage;
241 default = [ ];
242 example = literalExpression ''
243 [
244 networkmanager-fortisslvpn
245 networkmanager-iodine
246 networkmanager-l2tp
247 networkmanager-openconnect
248 networkmanager-openvpn
249 networkmanager-sstp
250 networkmanager-strongswan
251 networkmanager-vpnc
252 ]
253 '';
254 description = ''
255 List of plugin packages to install.
256
257 See <https://search.nixos.org/packages?query=networkmanager-> for available plugin packages.
258 and <https://networkmanager.dev/docs/vpn/> for an overview over builtin and external plugins
259 and their support status.
260 '';
261 };
262
263 dhcp = mkOption {
264 type = types.enum [
265 "dhcpcd"
266 "internal"
267 ];
268 default = "internal";
269 description = ''
270 Which program (or internal library) should be used for DHCP.
271 '';
272 };
273
274 logLevel = mkOption {
275 type = types.enum [
276 "OFF"
277 "ERR"
278 "WARN"
279 "INFO"
280 "DEBUG"
281 "TRACE"
282 ];
283 default = "WARN";
284 description = ''
285 Set the default logging verbosity level.
286 '';
287 };
288
289 appendNameservers = mkOption {
290 type = types.listOf types.str;
291 default = [ ];
292 description = ''
293 A list of name servers that should be appended
294 to the ones configured in NetworkManager or received by DHCP.
295 '';
296 };
297
298 insertNameservers = mkOption {
299 type = types.listOf types.str;
300 default = [ ];
301 description = ''
302 A list of name servers that should be inserted before
303 the ones configured in NetworkManager or received by DHCP.
304 '';
305 };
306
307 ethernet.macAddress = macAddressOptEth;
308
309 wifi = {
310 macAddress = macAddressOptWifi;
311
312 backend = mkOption {
313 type = types.enum [
314 "wpa_supplicant"
315 "iwd"
316 ];
317 default = "wpa_supplicant";
318 description = ''
319 Specify the Wi-Fi backend used for the device.
320 Currently supported are {option}`wpa_supplicant` or {option}`iwd` (experimental).
321 '';
322 };
323
324 powersave = mkOption {
325 type = types.nullOr types.bool;
326 default = null;
327 description = ''
328 Whether to enable Wi-Fi power saving.
329 '';
330 };
331
332 scanRandMacAddress = mkOption {
333 type = types.bool;
334 default = true;
335 description = ''
336 Whether to enable MAC address randomization of a Wi-Fi device
337 during scanning.
338 '';
339 };
340 };
341
342 dns = mkOption {
343 type = types.enum [
344 "default"
345 "dnsmasq"
346 "systemd-resolved"
347 "none"
348 ];
349 default = "default";
350 description = ''
351 Set the DNS (`resolv.conf`) processing mode.
352
353 A description of these modes can be found in the main section of
354 [
355 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html
356 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
357 or in
358 {manpage}`NetworkManager.conf(5)`.
359 '';
360 };
361
362 dispatcherScripts = mkOption {
363 type = types.listOf (
364 types.submodule {
365 options = {
366 source = mkOption {
367 type = types.path;
368 description = ''
369 Path to the hook script.
370 '';
371 };
372
373 type = mkOption {
374 type = types.enum (attrNames dispatcherTypesSubdirMap);
375 default = "basic";
376 description = ''
377 Dispatcher hook type. Look up the hooks described at
378 [https://developer.gnome.org/NetworkManager/stable/NetworkManager.html](https://developer.gnome.org/NetworkManager/stable/NetworkManager.html)
379 and choose the type depending on the output folder.
380 You should then filter the event type (e.g., "up"/"down") from within your script.
381 '';
382 };
383 };
384 }
385 );
386 default = [ ];
387 example = literalExpression ''
388 [ {
389 source = pkgs.writeText "upHook" '''
390 if [ "$2" != "up" ]; then
391 logger "exit: event $2 != up"
392 exit
393 fi
394
395 # coreutils and iproute are in PATH too
396 logger "Device $DEVICE_IFACE coming up"
397 ''';
398 type = "basic";
399 } ]
400 '';
401 description = ''
402 A list of scripts which will be executed in response to network events.
403 '';
404 };
405
406 ensureProfiles = {
407 profiles =
408 with lib.types;
409 mkOption {
410 type = attrsOf (submodule {
411 freeformType = ini.type;
412
413 options = {
414 connection = {
415 id = lib.mkOption {
416 type = str;
417 description = "This is the name that will be displayed by NetworkManager and GUIs.";
418 };
419 type = lib.mkOption {
420 type = str;
421 description = "The connection type defines the connection kind, like vpn, wireguard, gsm, wifi and more.";
422 example = "vpn";
423 };
424 };
425 };
426 });
427 apply = (lib.filterAttrsRecursive (n: v: v != { }));
428 default = { };
429 example = {
430 home-wifi = {
431 connection = {
432 id = "home-wifi";
433 type = "wifi";
434 permissions = "";
435 };
436 wifi = {
437 mac-address-blacklist = "";
438 mode = "infrastructure";
439 ssid = "Home Wi-Fi";
440 };
441 wifi-security = {
442 auth-alg = "open";
443 key-mgmt = "wpa-psk";
444 psk = "$HOME_WIFI_PASSWORD";
445 };
446 ipv4 = {
447 dns-search = "";
448 method = "auto";
449 };
450 ipv6 = {
451 addr-gen-mode = "stable-privacy";
452 dns-search = "";
453 method = "auto";
454 };
455 };
456 };
457 description = ''
458 Declaratively define NetworkManager profiles. You can find information about the generated file format [here](https://networkmanager.dev/docs/api/latest/nm-settings-keyfile.html) and [here](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/assembly_networkmanager-connection-profiles-in-keyfile-format_configuring-and-managing-networking).
459 You current profiles which are most likely stored in `/etc/NetworkManager/system-connections` and there is [a tool](https://github.com/janik-haag/nm2nix) to convert them to the needed nix code.
460 If you add a new ad-hoc connection via a GUI or nmtui or anything similar it should just work together with the declarative ones.
461 And if you edit a declarative profile NetworkManager will move it to the persistent storage and treat it like a ad-hoc one,
462 but there will be two profiles as soon as the systemd unit from this option runs again which can be confusing since NetworkManager tools will start displaying two profiles with the same name and probably a bit different settings depending on what you edited.
463 A profile won't be deleted even if it's removed from the config until the system reboots because that's when NetworkManager clears it's temp directory.
464 If `networking.resolvconf.enable` is true, attributes affecting the name resolution (such as `ignore-auto-dns`) may not end up changing `/etc/resolv.conf` as expected when other name services (for example `networking.dhcpcd`) are enabled. Run `resolvconf -l` in the terminal to see what each service produces.
465 '';
466 };
467 environmentFiles = mkOption {
468 default = [ ];
469 type = types.listOf types.path;
470 example = [ "/run/secrets/network-manager.env" ];
471 description = ''
472 Files to load as environment file. Environment variables from this file
473 will be substituted into the static configuration file using [envsubst](https://github.com/a8m/envsubst).
474 '';
475 };
476 };
477 };
478 };
479
480 imports = [
481 (mkRenamedOptionModule
482 [ "networking" "networkmanager" "packages" ]
483 [ "networking" "networkmanager" "plugins" ]
484 )
485 (mkRenamedOptionModule
486 [ "networking" "networkmanager" "useDnsmasq" ]
487 [ "networking" "networkmanager" "dns" ]
488 )
489 (mkRemovedOptionModule [ "networking" "networkmanager" "extraConfig" ] ''
490 This option was removed in favour of `networking.networkmanager.settings`,
491 which accepts structured nix-code equivalent to the ini
492 and allows for overriding settings.
493 Example patch:
494 ```patch
495 networking.networkmanager = {
496 - extraConfig = '''
497 - [main]
498 - no-auto-default=*
499 - '''
500 + settings.main.no-auto-default = "*";
501 };
502 ```
503 '')
504 (mkRemovedOptionModule [ "networking" "networkmanager" "enableFccUnlock" ] ''
505 This option was removed, because using bundled FCC unlock scripts is risky,
506 might conflict with vendor-provided unlock scripts, and should
507 be a conscious decision on a per-device basis.
508 Instead it's recommended to use the
509 `networking.modemmanager.fccUnlockScripts` option.
510 '')
511 (mkRemovedOptionModule [ "networking" "networkmanager" "dynamicHosts" ] ''
512 This option was removed because allowing (multiple) regular users to
513 override host entries affecting the whole system opens up a huge attack
514 vector. There seem to be very rare cases where this might be useful.
515 Consider setting system-wide host entries using networking.hosts, provide
516 them via the DNS server in your network, or use environment.etc
517 to add a file into /etc/NetworkManager/dnsmasq.d reconfiguring hostsdir.
518 '')
519 (mkRemovedOptionModule [ "networking" "networkmanager" "firewallBackend" ] ''
520 This option was removed as NixOS is now using iptables-nftables-compat even when using iptables, therefore Networkmanager now uses the nftables backend unconditionally.
521 '')
522 (mkRenamedOptionModule
523 [ "networking" "networkmanager" "fccUnlockScripts" ]
524 [ "networking" "modemmanager" "fccUnlockScripts" ]
525 )
526 (mkRemovedOptionModule [
527 "networking"
528 "networkmanager"
529 "enableStrongSwan"
530 ] "Pass `pkgs.networkmanager-strongswan` into `networking.networkmanager.plugins` instead.")
531 (mkRemovedOptionModule [
532 "networking"
533 "networkmanager"
534 "enableDefaultPlugins"
535 ] "Configure the required plugins explicitly in `networking.networkmanager.plugins`.")
536 ];
537
538 ###### implementation
539
540 config = mkIf cfg.enable {
541
542 assertions = [
543 {
544 assertion = config.networking.wireless.enable == true -> cfg.unmanaged != [ ];
545 message = ''
546 You can not use networking.networkmanager with networking.wireless.
547 Except if you mark some interfaces as <literal>unmanaged</literal> by NetworkManager.
548 '';
549 }
550 ];
551
552 hardware.wirelessRegulatoryDatabase = true;
553
554 environment.etc = {
555 "NetworkManager/NetworkManager.conf".source = configFile;
556
557 # The networkmanager-l2tp plugin expects /etc/ipsec.secrets to include /etc/ipsec.d/ipsec.nm-l2tp.secrets;
558 # see https://github.com/NixOS/nixpkgs/issues/64965
559 "ipsec.secrets".text = ''
560 include ipsec.d/ipsec.nm-l2tp.secrets
561 '';
562 }
563 // builtins.listToAttrs (
564 map (
565 pkg:
566 nameValuePair "NetworkManager/${pkg.networkManagerPlugin}" {
567 source = "${pkg}/lib/NetworkManager/${pkg.networkManagerPlugin}";
568 }
569 ) cfg.plugins
570 )
571 // optionalAttrs (cfg.appendNameservers != [ ] || cfg.insertNameservers != [ ]) {
572 "NetworkManager/dispatcher.d/02overridedns".source = overrideNameserversScript;
573 }
574 // listToAttrs (
575 lib.imap1 (i: s: {
576 name = "NetworkManager/dispatcher.d/${
577 dispatcherTypesSubdirMap.${s.type}
578 }03userscript${lib.fixedWidthNumber 4 i}";
579 value = {
580 mode = "0544";
581 inherit (s) source;
582 };
583 }) cfg.dispatcherScripts
584 );
585
586 environment.systemPackages = packages;
587
588 users.groups = {
589 networkmanager.gid = config.ids.gids.networkmanager;
590 nm-openvpn.gid = config.ids.gids.nm-openvpn;
591 };
592
593 users.users = {
594 nm-openvpn = {
595 uid = config.ids.uids.nm-openvpn;
596 group = "nm-openvpn";
597 extraGroups = [ "networkmanager" ];
598 };
599 nm-iodine = {
600 isSystemUser = true;
601 group = "networkmanager";
602 };
603 };
604
605 systemd.packages = packages;
606
607 systemd.tmpfiles.rules = [
608 "d /etc/NetworkManager/system-connections 0700 root root -"
609 "d /var/lib/misc 0755 root root -" # for dnsmasq.leases
610 # ppp isn't able to mkdir that directory at runtime
611 "d /run/pppd/lock 0700 root root -"
612 ]
613 ++ pluginTmpfilesRules;
614
615 systemd.services.NetworkManager = {
616 wantedBy = [ "multi-user.target" ];
617 restartTriggers = [ configFile ];
618
619 aliases = [ "dbus-org.freedesktop.NetworkManager.service" ];
620
621 serviceConfig = {
622 StateDirectory = "NetworkManager";
623 StateDirectoryMode = 755; # not sure if this really needs to be 755
624 };
625 };
626
627 systemd.services.NetworkManager-wait-online = {
628 wantedBy = [ "network-online.target" ];
629 };
630
631 systemd.services.NetworkManager-dispatcher = {
632 wantedBy = [ "multi-user.target" ];
633 restartTriggers = [
634 configFile
635 overrideNameserversScript
636 ];
637
638 # useful binaries for user-specified hooks
639 path = [
640 pkgs.iproute2
641 pkgs.util-linux
642 pkgs.coreutils
643 ];
644 aliases = [ "dbus-org.freedesktop.nm-dispatcher.service" ];
645 };
646
647 systemd.services.NetworkManager-ensure-profiles = mkIf (cfg.ensureProfiles.profiles != { }) {
648 description = "Ensure that NetworkManager declarative profiles are created";
649 wantedBy = [ "multi-user.target" ];
650 before = [ "network-online.target" ];
651 after = [ "NetworkManager.service" ];
652 path = pluginRuntimeDeps;
653 script =
654 let
655 path = id: "/run/NetworkManager/system-connections/${id}.nmconnection";
656 in
657 ''
658 mkdir -p /run/NetworkManager/system-connections
659 ''
660 + lib.concatMapStringsSep "\n" (profile: ''
661 ${pkgs.envsubst}/bin/envsubst -i ${ini.generate (lib.escapeShellArg profile.n) profile.v} > ${path (lib.escapeShellArg profile.n)}
662 '') (lib.mapAttrsToList (n: v: { inherit n v; }) cfg.ensureProfiles.profiles)
663 + ''
664 ${cfg.package}/bin/nmcli connection reload
665 '';
666 serviceConfig = {
667 EnvironmentFile = cfg.ensureProfiles.environmentFiles;
668 UMask = "0177";
669 Type = "oneshot";
670 };
671 };
672
673 # Turn off NixOS' network management when networking is managed entirely by NetworkManager
674 networking = mkMerge [
675 (mkIf (!delegateWireless) {
676 useDHCP = false;
677 })
678
679 (mkIf enableIwd {
680 wireless.iwd.enable = true;
681 })
682
683 {
684 modemmanager.enable = lib.mkDefault true;
685
686 networkmanager.connectionConfig = {
687 "ethernet.cloned-mac-address" = cfg.ethernet.macAddress;
688 "wifi.cloned-mac-address" = cfg.wifi.macAddress;
689 "wifi.powersave" = lib.mkIf (cfg.wifi.powersave != null) (if cfg.wifi.powersave then 3 else 2);
690 };
691 }
692 ];
693
694 boot.kernelModules = [ "ctr" ];
695
696 security.polkit.enable = true;
697 security.polkit.extraConfig = polkitConf;
698
699 services.dbus.packages = packages ++ pluginDbusDeps ++ optional (cfg.dns == "dnsmasq") pkgs.dnsmasq;
700
701 services.udev.packages = packages;
702
703 systemd.services.NetworkManager.path = pluginRuntimeDeps;
704 };
705}