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 packages =
131 [
132 cfg.package
133 ]
134 ++ cfg.plugins
135 ++ lib.optionals (!delegateWireless && !enableIwd) [
136 pkgs.wpa_supplicant
137 ];
138
139in
140{
141
142 meta = {
143 maintainers = teams.freedesktop.members;
144 };
145
146 ###### interface
147
148 options = {
149
150 networking.networkmanager = {
151
152 enable = mkOption {
153 type = types.bool;
154 default = false;
155 description = ''
156 Whether to use NetworkManager to obtain an IP address and other
157 configuration for all network interfaces that are not manually
158 configured. If enabled, a group `networkmanager`
159 will be created. Add all users that should have permission
160 to change network settings to this group.
161 '';
162 };
163
164 package = mkPackageOption pkgs "networkmanager" { };
165
166 connectionConfig = mkOption {
167 type =
168 with types;
169 attrsOf (
170 nullOr (oneOf [
171 bool
172 int
173 str
174 ])
175 );
176 default = { };
177 description = ''
178 Configuration for the [connection] section of NetworkManager.conf.
179 Refer to
180 [
181 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#id-1.2.3.11
182 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
183 or
184 {manpage}`NetworkManager.conf(5)`
185 for more information.
186 '';
187 };
188
189 settings = mkOption {
190 type = ini.type;
191 default = { };
192 description = ''
193 Configuration added to the generated NetworkManager.conf, note that you can overwrite settings with this.
194 Refer to
195 [
196 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html
197 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
198 or
199 {manpage}`NetworkManager.conf(5)`
200 for more information.
201 '';
202 };
203
204 unmanaged = mkOption {
205 type = types.listOf types.str;
206 default = [ ];
207 description = ''
208 List of interfaces that will not be managed by NetworkManager.
209 Interface name can be specified here, but if you need more fidelity,
210 refer to
211 [
212 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#device-spec
213 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#device-spec)
214 or the "Device List Format" Appendix of
215 {manpage}`NetworkManager.conf(5)`.
216 '';
217 };
218
219 plugins = mkOption {
220 type =
221 let
222 networkManagerPluginPackage = types.package // {
223 description = "NetworkManager plug-in";
224 check =
225 p:
226 lib.assertMsg
227 (types.package.check p && p ? networkManagerPlugin && lib.isString p.networkManagerPlugin)
228 ''
229 Package ‘${p.name}’, is not a NetworkManager plug-in.
230 Those need to have a ‘networkManagerPlugin’ attribute.
231 '';
232 };
233 in
234 types.listOf networkManagerPluginPackage;
235 default = [ ];
236 description = ''
237 List of NetworkManager plug-ins to enable.
238 Some plug-ins are enabled by the NetworkManager module by default.
239 '';
240 };
241
242 dhcp = mkOption {
243 type = types.enum [
244 "dhcpcd"
245 "internal"
246 ];
247 default = "internal";
248 description = ''
249 Which program (or internal library) should be used for DHCP.
250 '';
251 };
252
253 logLevel = mkOption {
254 type = types.enum [
255 "OFF"
256 "ERR"
257 "WARN"
258 "INFO"
259 "DEBUG"
260 "TRACE"
261 ];
262 default = "WARN";
263 description = ''
264 Set the default logging verbosity level.
265 '';
266 };
267
268 appendNameservers = mkOption {
269 type = types.listOf types.str;
270 default = [ ];
271 description = ''
272 A list of name servers that should be appended
273 to the ones configured in NetworkManager or received by DHCP.
274 '';
275 };
276
277 insertNameservers = mkOption {
278 type = types.listOf types.str;
279 default = [ ];
280 description = ''
281 A list of name servers that should be inserted before
282 the ones configured in NetworkManager or received by DHCP.
283 '';
284 };
285
286 ethernet.macAddress = macAddressOptEth;
287
288 wifi = {
289 macAddress = macAddressOptWifi;
290
291 backend = mkOption {
292 type = types.enum [
293 "wpa_supplicant"
294 "iwd"
295 ];
296 default = "wpa_supplicant";
297 description = ''
298 Specify the Wi-Fi backend used for the device.
299 Currently supported are {option}`wpa_supplicant` or {option}`iwd` (experimental).
300 '';
301 };
302
303 powersave = mkOption {
304 type = types.nullOr types.bool;
305 default = null;
306 description = ''
307 Whether to enable Wi-Fi power saving.
308 '';
309 };
310
311 scanRandMacAddress = mkOption {
312 type = types.bool;
313 default = true;
314 description = ''
315 Whether to enable MAC address randomization of a Wi-Fi device
316 during scanning.
317 '';
318 };
319 };
320
321 dns = mkOption {
322 type = types.enum [
323 "default"
324 "dnsmasq"
325 "systemd-resolved"
326 "none"
327 ];
328 default = "default";
329 description = ''
330 Set the DNS (`resolv.conf`) processing mode.
331
332 A description of these modes can be found in the main section of
333 [
334 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html
335 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
336 or in
337 {manpage}`NetworkManager.conf(5)`.
338 '';
339 };
340
341 dispatcherScripts = mkOption {
342 type = types.listOf (
343 types.submodule {
344 options = {
345 source = mkOption {
346 type = types.path;
347 description = ''
348 Path to the hook script.
349 '';
350 };
351
352 type = mkOption {
353 type = types.enum (attrNames dispatcherTypesSubdirMap);
354 default = "basic";
355 description = ''
356 Dispatcher hook type. Look up the hooks described at
357 [https://developer.gnome.org/NetworkManager/stable/NetworkManager.html](https://developer.gnome.org/NetworkManager/stable/NetworkManager.html)
358 and choose the type depending on the output folder.
359 You should then filter the event type (e.g., "up"/"down") from within your script.
360 '';
361 };
362 };
363 }
364 );
365 default = [ ];
366 example = literalExpression ''
367 [ {
368 source = pkgs.writeText "upHook" '''
369 if [ "$2" != "up" ]; then
370 logger "exit: event $2 != up"
371 exit
372 fi
373
374 # coreutils and iproute are in PATH too
375 logger "Device $DEVICE_IFACE coming up"
376 ''';
377 type = "basic";
378 } ]
379 '';
380 description = ''
381 A list of scripts which will be executed in response to network events.
382 '';
383 };
384
385 enableStrongSwan = mkOption {
386 type = types.bool;
387 default = false;
388 description = ''
389 Enable the StrongSwan plugin.
390
391 If you enable this option the
392 `networkmanager_strongswan` plugin will be added to
393 the {option}`networking.networkmanager.plugins` option
394 so you don't need to do that yourself.
395 '';
396 };
397
398 ensureProfiles = {
399 profiles =
400 with lib.types;
401 mkOption {
402 type = attrsOf (submodule {
403 freeformType = ini.type;
404
405 options = {
406 connection = {
407 id = lib.mkOption {
408 type = str;
409 description = "This is the name that will be displayed by NetworkManager and GUIs.";
410 };
411 type = lib.mkOption {
412 type = str;
413 description = "The connection type defines the connection kind, like vpn, wireguard, gsm, wifi and more.";
414 example = "vpn";
415 };
416 };
417 };
418 });
419 apply = (lib.filterAttrsRecursive (n: v: v != { }));
420 default = { };
421 example = {
422 home-wifi = {
423 connection = {
424 id = "home-wifi";
425 type = "wifi";
426 permissions = "";
427 };
428 wifi = {
429 mac-address-blacklist = "";
430 mode = "infrastructure";
431 ssid = "Home Wi-Fi";
432 };
433 wifi-security = {
434 auth-alg = "open";
435 key-mgmt = "wpa-psk";
436 psk = "$HOME_WIFI_PASSWORD";
437 };
438 ipv4 = {
439 dns-search = "";
440 method = "auto";
441 };
442 ipv6 = {
443 addr-gen-mode = "stable-privacy";
444 dns-search = "";
445 method = "auto";
446 };
447 };
448 };
449 description = ''
450 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).
451 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.
452 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.
453 And if you edit a declarative profile NetworkManager will move it to the persistent storage and treat it like a ad-hoc one,
454 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.
455 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.
456 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.
457 '';
458 };
459 environmentFiles = mkOption {
460 default = [ ];
461 type = types.listOf types.path;
462 example = [ "/run/secrets/network-manager.env" ];
463 description = ''
464 Files to load as environment file. Environment variables from this file
465 will be substituted into the static configuration file using [envsubst](https://github.com/a8m/envsubst).
466 '';
467 };
468 };
469 };
470 };
471
472 imports = [
473 (mkRenamedOptionModule
474 [ "networking" "networkmanager" "packages" ]
475 [ "networking" "networkmanager" "plugins" ]
476 )
477 (mkRenamedOptionModule
478 [ "networking" "networkmanager" "useDnsmasq" ]
479 [ "networking" "networkmanager" "dns" ]
480 )
481 (mkRemovedOptionModule [ "networking" "networkmanager" "extraConfig" ] ''
482 This option was removed in favour of `networking.networkmanager.settings`,
483 which accepts structured nix-code equivalent to the ini
484 and allows for overriding settings.
485 Example patch:
486 ```patch
487 networking.networkmanager = {
488 - extraConfig = '''
489 - [main]
490 - no-auto-default=*
491 - '''
492 + settings.main.no-auto-default = "*";
493 };
494 ```
495 '')
496 (mkRemovedOptionModule [ "networking" "networkmanager" "enableFccUnlock" ] ''
497 This option was removed, because using bundled FCC unlock scripts is risky,
498 might conflict with vendor-provided unlock scripts, and should
499 be a conscious decision on a per-device basis.
500 Instead it's recommended to use the
501 `networking.modemmanager.fccUnlockScripts` option.
502 '')
503 (mkRemovedOptionModule [ "networking" "networkmanager" "dynamicHosts" ] ''
504 This option was removed because allowing (multiple) regular users to
505 override host entries affecting the whole system opens up a huge attack
506 vector. There seem to be very rare cases where this might be useful.
507 Consider setting system-wide host entries using networking.hosts, provide
508 them via the DNS server in your network, or use environment.etc
509 to add a file into /etc/NetworkManager/dnsmasq.d reconfiguring hostsdir.
510 '')
511 (mkRemovedOptionModule [ "networking" "networkmanager" "firewallBackend" ] ''
512 This option was removed as NixOS is now using iptables-nftables-compat even when using iptables, therefore Networkmanager now uses the nftables backend unconditionally.
513 '')
514 (mkRenamedOptionModule
515 [ "networking" "networkmanager" "fccUnlockScripts" ]
516 [ "networking" "modemmanager" "fccUnlockScripts" ]
517 )
518 ];
519
520 ###### implementation
521
522 config = mkIf cfg.enable {
523
524 assertions = [
525 {
526 assertion = config.networking.wireless.enable == true -> cfg.unmanaged != [ ];
527 message = ''
528 You can not use networking.networkmanager with networking.wireless.
529 Except if you mark some interfaces as <literal>unmanaged</literal> by NetworkManager.
530 '';
531 }
532 ];
533
534 hardware.wirelessRegulatoryDatabase = true;
535
536 environment.etc =
537 {
538 "NetworkManager/NetworkManager.conf".source = configFile;
539
540 # The networkmanager-l2tp plugin expects /etc/ipsec.secrets to include /etc/ipsec.d/ipsec.nm-l2tp.secrets;
541 # see https://github.com/NixOS/nixpkgs/issues/64965
542 "ipsec.secrets".text = ''
543 include ipsec.d/ipsec.nm-l2tp.secrets
544 '';
545 }
546 // builtins.listToAttrs (
547 map (
548 pkg:
549 nameValuePair "NetworkManager/${pkg.networkManagerPlugin}" {
550 source = "${pkg}/lib/NetworkManager/${pkg.networkManagerPlugin}";
551 }
552 ) cfg.plugins
553 )
554 // optionalAttrs (cfg.appendNameservers != [ ] || cfg.insertNameservers != [ ]) {
555 "NetworkManager/dispatcher.d/02overridedns".source = overrideNameserversScript;
556 }
557 // listToAttrs (
558 lib.imap1 (i: s: {
559 name = "NetworkManager/dispatcher.d/${
560 dispatcherTypesSubdirMap.${s.type}
561 }03userscript${lib.fixedWidthNumber 4 i}";
562 value = {
563 mode = "0544";
564 inherit (s) source;
565 };
566 }) cfg.dispatcherScripts
567 );
568
569 environment.systemPackages = packages;
570
571 users.groups = {
572 networkmanager.gid = config.ids.gids.networkmanager;
573 nm-openvpn.gid = config.ids.gids.nm-openvpn;
574 };
575
576 users.users = {
577 nm-openvpn = {
578 uid = config.ids.uids.nm-openvpn;
579 group = "nm-openvpn";
580 extraGroups = [ "networkmanager" ];
581 };
582 nm-iodine = {
583 isSystemUser = true;
584 group = "networkmanager";
585 };
586 };
587
588 systemd.packages = packages;
589
590 systemd.tmpfiles.rules = [
591 "d /etc/NetworkManager/system-connections 0700 root root -"
592 "d /etc/ipsec.d 0700 root root -"
593 "d /var/lib/NetworkManager-fortisslvpn 0700 root root -"
594
595 "d /var/lib/misc 0755 root root -" # for dnsmasq.leases
596 # ppp isn't able to mkdir that directory at runtime
597 "d /run/pppd/lock 0700 root root -"
598 ];
599
600 systemd.services.NetworkManager = {
601 wantedBy = [ "multi-user.target" ];
602 restartTriggers = [ configFile ];
603
604 aliases = [ "dbus-org.freedesktop.NetworkManager.service" ];
605
606 serviceConfig = {
607 StateDirectory = "NetworkManager";
608 StateDirectoryMode = 755; # not sure if this really needs to be 755
609 };
610 };
611
612 systemd.services.NetworkManager-wait-online = {
613 wantedBy = [ "network-online.target" ];
614 };
615
616 systemd.services.NetworkManager-dispatcher = {
617 wantedBy = [ "multi-user.target" ];
618 restartTriggers = [
619 configFile
620 overrideNameserversScript
621 ];
622
623 # useful binaries for user-specified hooks
624 path = [
625 pkgs.iproute2
626 pkgs.util-linux
627 pkgs.coreutils
628 ];
629 aliases = [ "dbus-org.freedesktop.nm-dispatcher.service" ];
630 };
631
632 systemd.services.NetworkManager-ensure-profiles = mkIf (cfg.ensureProfiles.profiles != { }) {
633 description = "Ensure that NetworkManager declarative profiles are created";
634 wantedBy = [ "multi-user.target" ];
635 before = [ "network-online.target" ];
636 after = [ "NetworkManager.service" ];
637 script =
638 let
639 path = id: "/run/NetworkManager/system-connections/${id}.nmconnection";
640 in
641 ''
642 mkdir -p /run/NetworkManager/system-connections
643 ''
644 + lib.concatMapStringsSep "\n" (profile: ''
645 ${pkgs.envsubst}/bin/envsubst -i ${ini.generate (lib.escapeShellArg profile.n) profile.v} > ${path (lib.escapeShellArg profile.n)}
646 '') (lib.mapAttrsToList (n: v: { inherit n v; }) cfg.ensureProfiles.profiles)
647 + ''
648 ${cfg.package}/bin/nmcli connection reload
649 '';
650 serviceConfig = {
651 EnvironmentFile = cfg.ensureProfiles.environmentFiles;
652 UMask = "0177";
653 Type = "oneshot";
654 };
655 };
656
657 # Turn off NixOS' network management when networking is managed entirely by NetworkManager
658 networking = mkMerge [
659 (mkIf (!delegateWireless) {
660 useDHCP = false;
661 })
662
663 {
664 networkmanager.plugins = with pkgs; [
665 networkmanager-fortisslvpn
666 networkmanager-iodine
667 networkmanager-l2tp
668 networkmanager-openconnect
669 networkmanager-openvpn
670 networkmanager-vpnc
671 networkmanager-sstp
672 ];
673 }
674
675 (mkIf cfg.enableStrongSwan {
676 networkmanager.plugins = [ pkgs.networkmanager_strongswan ];
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" =
690 if cfg.wifi.powersave == null then
691 null
692 else if cfg.wifi.powersave then
693 3
694 else
695 2;
696 };
697 }
698 ];
699
700 boot.kernelModules = [ "ctr" ];
701
702 security.polkit.enable = true;
703 security.polkit.extraConfig = polkitConf;
704
705 services.dbus.packages =
706 packages
707 ++ optional cfg.enableStrongSwan pkgs.strongswanNM
708 ++ optional (cfg.dns == "dnsmasq") pkgs.dnsmasq;
709
710 services.udev.packages = packages;
711 };
712}