1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.networking.networkmanager;
7
8 delegateWireless = config.networking.wireless.enable == true && cfg.unmanaged != [];
9
10 enableIwd = cfg.wifi.backend == "iwd";
11
12 mkValue = v:
13 if v == true then "yes"
14 else if v == false then "no"
15 else if lib.isInt v then toString v
16 else v;
17
18 mkSection = name: attrs: ''
19 [${name}]
20 ${
21 lib.concatStringsSep "\n"
22 (lib.mapAttrsToList
23 (k: v: "${k}=${mkValue v}")
24 (lib.filterAttrs
25 (k: v: v != null)
26 attrs))
27 }
28 '';
29
30 configFile = pkgs.writeText "NetworkManager.conf" (lib.concatStringsSep "\n" [
31 (mkSection "main" {
32 plugins = "keyfile";
33 dhcp = cfg.dhcp;
34 dns = cfg.dns;
35 # If resolvconf is disabled that means that resolv.conf is managed by some other module.
36 rc-manager =
37 if config.networking.resolvconf.enable then "resolvconf"
38 else "unmanaged";
39 firewall-backend = cfg.firewallBackend;
40 })
41 (mkSection "keyfile" {
42 unmanaged-devices =
43 if cfg.unmanaged == [] then null
44 else lib.concatStringsSep ";" cfg.unmanaged;
45 })
46 (mkSection "logging" {
47 audit = config.security.audit.enable;
48 level = cfg.logLevel;
49 })
50 (mkSection "connection" cfg.connectionConfig)
51 (mkSection "device" {
52 "wifi.scan-rand-mac-address" = cfg.wifi.scanRandMacAddress;
53 "wifi.backend" = cfg.wifi.backend;
54 })
55 cfg.extraConfig
56 ]);
57
58 /*
59 [network-manager]
60 Identity=unix-group:networkmanager
61 Action=org.freedesktop.NetworkManager.*
62 ResultAny=yes
63 ResultInactive=no
64 ResultActive=yes
65
66 [modem-manager]
67 Identity=unix-group:networkmanager
68 Action=org.freedesktop.ModemManager*
69 ResultAny=yes
70 ResultInactive=no
71 ResultActive=yes
72 */
73 polkitConf = ''
74 polkit.addRule(function(action, subject) {
75 if (
76 subject.isInGroup("networkmanager")
77 && (action.id.indexOf("org.freedesktop.NetworkManager.") == 0
78 || action.id.indexOf("org.freedesktop.ModemManager") == 0
79 ))
80 { return polkit.Result.YES; }
81 });
82 '';
83
84 ns = xs: pkgs.writeText "nameservers" (
85 concatStrings (map (s: "nameserver ${s}\n") xs)
86 );
87
88 overrideNameserversScript = pkgs.writeScript "02overridedns" ''
89 #!/bin/sh
90 PATH=${with pkgs; makeBinPath [ gnused gnugrep coreutils ]}
91 tmp=$(mktemp)
92 sed '/nameserver /d' /etc/resolv.conf > $tmp
93 grep 'nameserver ' /etc/resolv.conf | \
94 grep -vf ${ns (cfg.appendNameservers ++ cfg.insertNameservers)} > $tmp.ns
95 cat $tmp ${ns cfg.insertNameservers} $tmp.ns ${ns cfg.appendNameservers} > /etc/resolv.conf
96 rm -f $tmp $tmp.ns
97 '';
98
99 dispatcherTypesSubdirMap = {
100 basic = "";
101 pre-up = "pre-up.d/";
102 pre-down = "pre-down.d/";
103 };
104
105 macAddressOpt = mkOption {
106 type = types.either types.str (types.enum ["permanent" "preserve" "random" "stable"]);
107 default = "preserve";
108 example = "00:11:22:33:44:55";
109 description = lib.mdDoc ''
110 Set the MAC address of the interface.
111
112 - `"XX:XX:XX:XX:XX:XX"`: MAC address of the interface
113 - `"permanent"`: Use the permanent MAC address of the device
114 - `"preserve"`: Don’t change the MAC address of the device upon activation
115 - `"random"`: Generate a randomized value upon each connect
116 - `"stable"`: Generate a stable, hashed MAC address
117 '';
118 };
119
120 packages = [
121 pkgs.modemmanager
122 pkgs.networkmanager
123 ]
124 ++ cfg.plugins
125 ++ lib.optionals (!delegateWireless && !enableIwd) [
126 pkgs.wpa_supplicant
127 ];
128
129in {
130
131 meta = {
132 maintainers = teams.freedesktop.members;
133 };
134
135 ###### interface
136
137 options = {
138
139 networking.networkmanager = {
140
141 enable = mkOption {
142 type = types.bool;
143 default = false;
144 description = lib.mdDoc ''
145 Whether to use NetworkManager to obtain an IP address and other
146 configuration for all network interfaces that are not manually
147 configured. If enabled, a group `networkmanager`
148 will be created. Add all users that should have permission
149 to change network settings to this group.
150 '';
151 };
152
153 connectionConfig = mkOption {
154 type = with types; attrsOf (nullOr (oneOf [
155 bool
156 int
157 str
158 ]));
159 default = {};
160 description = lib.mdDoc ''
161 Configuration for the [connection] section of NetworkManager.conf.
162 Refer to
163 [
164 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#id-1.2.3.11
165 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
166 or
167 {manpage}`NetworkManager.conf(5)`
168 for more information.
169 '';
170 };
171
172 extraConfig = mkOption {
173 type = types.lines;
174 default = "";
175 description = lib.mdDoc ''
176 Configuration appended to the generated NetworkManager.conf.
177 Refer to
178 [
179 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html
180 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
181 or
182 {manpage}`NetworkManager.conf(5)`
183 for more information.
184 '';
185 };
186
187 unmanaged = mkOption {
188 type = types.listOf types.str;
189 default = [];
190 description = lib.mdDoc ''
191 List of interfaces that will not be managed by NetworkManager.
192 Interface name can be specified here, but if you need more fidelity,
193 refer to
194 [
195 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#device-spec
196 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#device-spec)
197 or the "Device List Format" Appendix of
198 {manpage}`NetworkManager.conf(5)`.
199 '';
200 };
201
202 plugins = mkOption {
203 type =
204 let
205 networkManagerPluginPackage = types.package // {
206 description = "NetworkManager plug-in";
207 check =
208 p:
209 lib.assertMsg
210 (types.package.check p
211 && p ? networkManagerPlugin
212 && lib.isString p.networkManagerPlugin)
213 ''
214 Package ‘${p.name}’, is not a NetworkManager plug-in.
215 Those need to have a ‘networkManagerPlugin’ attribute.
216 '';
217 };
218 in
219 types.listOf networkManagerPluginPackage;
220 default = [ ];
221 description = lib.mdDoc ''
222 List of NetworkManager plug-ins to enable.
223 Some plug-ins are enabled by the NetworkManager module by default.
224 '';
225 };
226
227 dhcp = mkOption {
228 type = types.enum [ "dhcpcd" "internal" ];
229 default = "internal";
230 description = lib.mdDoc ''
231 Which program (or internal library) should be used for DHCP.
232 '';
233 };
234
235 firewallBackend = mkOption {
236 type = types.enum [ "iptables" "nftables" "none" ];
237 default = "iptables";
238 description = lib.mdDoc ''
239 Which firewall backend should be used for configuring masquerading with shared mode.
240 If set to none, NetworkManager doesn't manage the configuration at all.
241 '';
242 };
243
244 logLevel = mkOption {
245 type = types.enum [ "OFF" "ERR" "WARN" "INFO" "DEBUG" "TRACE" ];
246 default = "WARN";
247 description = lib.mdDoc ''
248 Set the default logging verbosity level.
249 '';
250 };
251
252 appendNameservers = mkOption {
253 type = types.listOf types.str;
254 default = [];
255 description = lib.mdDoc ''
256 A list of name servers that should be appended
257 to the ones configured in NetworkManager or received by DHCP.
258 '';
259 };
260
261 insertNameservers = mkOption {
262 type = types.listOf types.str;
263 default = [];
264 description = lib.mdDoc ''
265 A list of name servers that should be inserted before
266 the ones configured in NetworkManager or received by DHCP.
267 '';
268 };
269
270 ethernet.macAddress = macAddressOpt;
271
272 wifi = {
273 macAddress = macAddressOpt;
274
275 backend = mkOption {
276 type = types.enum [ "wpa_supplicant" "iwd" ];
277 default = "wpa_supplicant";
278 description = lib.mdDoc ''
279 Specify the Wi-Fi backend used for the device.
280 Currently supported are {option}`wpa_supplicant` or {option}`iwd` (experimental).
281 '';
282 };
283
284 powersave = mkOption {
285 type = types.nullOr types.bool;
286 default = null;
287 description = lib.mdDoc ''
288 Whether to enable Wi-Fi power saving.
289 '';
290 };
291
292 scanRandMacAddress = mkOption {
293 type = types.bool;
294 default = true;
295 description = lib.mdDoc ''
296 Whether to enable MAC address randomization of a Wi-Fi device
297 during scanning.
298 '';
299 };
300 };
301
302 dns = mkOption {
303 type = types.enum [ "default" "dnsmasq" "unbound" "systemd-resolved" "none" ];
304 default = "default";
305 description = lib.mdDoc ''
306 Set the DNS (`resolv.conf`) processing mode.
307
308 A description of these modes can be found in the main section of
309 [
310 https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html
311 ](https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html)
312 or in
313 {manpage}`NetworkManager.conf(5)`.
314 '';
315 };
316
317 dispatcherScripts = mkOption {
318 type = types.listOf (types.submodule {
319 options = {
320 source = mkOption {
321 type = types.path;
322 description = lib.mdDoc ''
323 Path to the hook script.
324 '';
325 };
326
327 type = mkOption {
328 type = types.enum (attrNames dispatcherTypesSubdirMap);
329 default = "basic";
330 description = lib.mdDoc ''
331 Dispatcher hook type. Look up the hooks described at
332 [https://developer.gnome.org/NetworkManager/stable/NetworkManager.html](https://developer.gnome.org/NetworkManager/stable/NetworkManager.html)
333 and choose the type depending on the output folder.
334 You should then filter the event type (e.g., "up"/"down") from within your script.
335 '';
336 };
337 };
338 });
339 default = [];
340 example = literalExpression ''
341 [ {
342 source = pkgs.writeText "upHook" '''
343
344 if [ "$2" != "up" ]; then
345 logger "exit: event $2 != up"
346 exit
347 fi
348
349 # coreutils and iproute are in PATH too
350 logger "Device $DEVICE_IFACE coming up"
351 ''';
352 type = "basic";
353 } ]'';
354 description = lib.mdDoc ''
355 A list of scripts which will be executed in response to network events.
356 '';
357 };
358
359 enableStrongSwan = mkOption {
360 type = types.bool;
361 default = false;
362 description = lib.mdDoc ''
363 Enable the StrongSwan plugin.
364
365 If you enable this option the
366 `networkmanager_strongswan` plugin will be added to
367 the {option}`networking.networkmanager.plugins` option
368 so you don't need to do that yourself.
369 '';
370 };
371
372 enableFccUnlock = mkOption {
373 type = types.bool;
374 default = false;
375 description = lib.mdDoc ''
376 Enable FCC unlock procedures. Since release 1.18.4, the ModemManager daemon no longer
377 automatically performs the FCC unlock procedure by default. See
378 [the docs](https://modemmanager.org/docs/modemmanager/fcc-unlock/)
379 for more details.
380 '';
381 };
382 };
383 };
384
385 imports = [
386 (mkRenamedOptionModule
387 [ "networking" "networkmanager" "packages" ]
388 [ "networking" "networkmanager" "plugins" ])
389 (mkRenamedOptionModule [ "networking" "networkmanager" "useDnsmasq" ] [ "networking" "networkmanager" "dns" ])
390 (mkRemovedOptionModule ["networking" "networkmanager" "dynamicHosts"] ''
391 This option was removed because allowing (multiple) regular users to
392 override host entries affecting the whole system opens up a huge attack
393 vector. There seem to be very rare cases where this might be useful.
394 Consider setting system-wide host entries using networking.hosts, provide
395 them via the DNS server in your network, or use environment.etc
396 to add a file into /etc/NetworkManager/dnsmasq.d reconfiguring hostsdir.
397 '')
398 ];
399
400
401 ###### implementation
402
403 config = mkIf cfg.enable {
404
405 assertions = [
406 { assertion = config.networking.wireless.enable == true -> cfg.unmanaged != [];
407 message = ''
408 You can not use networking.networkmanager with networking.wireless.
409 Except if you mark some interfaces as <literal>unmanaged</literal> by NetworkManager.
410 '';
411 }
412 ];
413
414 hardware.wirelessRegulatoryDatabase = true;
415
416 environment.etc = {
417 "NetworkManager/NetworkManager.conf".source = configFile;
418 }
419 // builtins.listToAttrs (map (pkg: nameValuePair "NetworkManager/${pkg.networkManagerPlugin}" {
420 source = "${pkg}/lib/NetworkManager/${pkg.networkManagerPlugin}";
421 }) cfg.plugins)
422 // optionalAttrs cfg.enableFccUnlock
423 {
424 "ModemManager/fcc-unlock.d".source =
425 "${pkgs.modemmanager}/share/ModemManager/fcc-unlock.available.d/*";
426 }
427 // optionalAttrs (cfg.appendNameservers != [] || cfg.insertNameservers != [])
428 {
429 "NetworkManager/dispatcher.d/02overridedns".source = overrideNameserversScript;
430 }
431 // listToAttrs (lib.imap1 (i: s:
432 {
433 name = "NetworkManager/dispatcher.d/${dispatcherTypesSubdirMap.${s.type}}03userscript${lib.fixedWidthNumber 4 i}";
434 value = { mode = "0544"; inherit (s) source; };
435 }) cfg.dispatcherScripts);
436
437 environment.systemPackages = packages;
438
439 users.groups = {
440 networkmanager.gid = config.ids.gids.networkmanager;
441 nm-openvpn.gid = config.ids.gids.nm-openvpn;
442 };
443
444 users.users = {
445 nm-openvpn = {
446 uid = config.ids.uids.nm-openvpn;
447 group = "nm-openvpn";
448 extraGroups = [ "networkmanager" ];
449 };
450 nm-iodine = {
451 isSystemUser = true;
452 group = "networkmanager";
453 };
454 };
455
456 systemd.packages = packages;
457
458 systemd.tmpfiles.rules = [
459 "d /etc/NetworkManager/system-connections 0700 root root -"
460 "d /etc/ipsec.d 0700 root root -"
461 "d /var/lib/NetworkManager-fortisslvpn 0700 root root -"
462
463 "d /var/lib/misc 0755 root root -" # for dnsmasq.leases
464 ];
465
466 systemd.services.NetworkManager = {
467 wantedBy = [ "network.target" ];
468 restartTriggers = [ configFile ];
469
470 aliases = [ "dbus-org.freedesktop.NetworkManager.service" ];
471
472 serviceConfig = {
473 StateDirectory = "NetworkManager";
474 StateDirectoryMode = 755; # not sure if this really needs to be 755
475 };
476 };
477
478 systemd.services.NetworkManager-wait-online = {
479 wantedBy = [ "network-online.target" ];
480 };
481
482 systemd.services.ModemManager.aliases = [ "dbus-org.freedesktop.ModemManager1.service" ];
483
484 systemd.services.NetworkManager-dispatcher = {
485 wantedBy = [ "network.target" ];
486 restartTriggers = [ configFile overrideNameserversScript ];
487
488 # useful binaries for user-specified hooks
489 path = [ pkgs.iproute2 pkgs.util-linux pkgs.coreutils ];
490 aliases = [ "dbus-org.freedesktop.nm-dispatcher.service" ];
491 };
492
493 # Turn off NixOS' network management when networking is managed entirely by NetworkManager
494 networking = mkMerge [
495 (mkIf (!delegateWireless) {
496 useDHCP = false;
497 })
498
499 {
500 networkmanager.plugins = with pkgs; [
501 networkmanager-fortisslvpn
502 networkmanager-iodine
503 networkmanager-l2tp
504 networkmanager-openconnect
505 networkmanager-openvpn
506 networkmanager-vpnc
507 networkmanager-sstp
508 ];
509 }
510
511 (mkIf cfg.enableStrongSwan {
512 networkmanager.plugins = [ pkgs.networkmanager_strongswan ];
513 })
514
515 (mkIf enableIwd {
516 wireless.iwd.enable = true;
517 })
518
519 {
520 networkmanager.connectionConfig = {
521 "ethernet.cloned-mac-address" = cfg.ethernet.macAddress;
522 "wifi.cloned-mac-address" = cfg.wifi.macAddress;
523 "wifi.powersave" =
524 if cfg.wifi.powersave == null then null
525 else if cfg.wifi.powersave then 3
526 else 2;
527 };
528 }
529 ];
530
531 boot.kernelModules = [ "ctr" ];
532
533 security.polkit.enable = true;
534 security.polkit.extraConfig = polkitConf;
535
536 services.dbus.packages = packages
537 ++ optional cfg.enableStrongSwan pkgs.strongswanNM
538 ++ optional (cfg.dns == "dnsmasq") pkgs.dnsmasq;
539
540 services.udev.packages = packages;
541 };
542}