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