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