1{ config, lib, pkgs, ... }:
2
3with pkgs;
4with lib;
5
6let
7 cfg = config.networking.networkmanager;
8
9 dynamicHostsEnabled =
10 cfg.dynamicHosts.enable && cfg.dynamicHosts.hostsDirs != {};
11
12 # /var/lib/misc is for dnsmasq.leases.
13 stateDirs = "/var/lib/NetworkManager /var/lib/dhclient /var/lib/misc";
14
15 configFile = writeText "NetworkManager.conf" ''
16 [main]
17 plugins=keyfile
18 dhcp=${cfg.dhcp}
19 dns=${cfg.dns}
20
21 [keyfile]
22 ${optionalString (cfg.unmanaged != [])
23 ''unmanaged-devices=${lib.concatStringsSep ";" cfg.unmanaged}''}
24
25 [logging]
26 level=${cfg.logLevel}
27
28 [connection]
29 ipv6.ip6-privacy=2
30 ethernet.cloned-mac-address=${cfg.ethernet.macAddress}
31 wifi.cloned-mac-address=${cfg.wifi.macAddress}
32 ${optionalString (cfg.wifi.powersave != null)
33 ''wifi.powersave=${if cfg.wifi.powersave then "3" else "2"}''}
34
35 [device]
36 wifi.scan-rand-mac-address=${if cfg.wifi.scanRandMacAddress then "yes" else "no"}
37
38 ${cfg.extraConfig}
39 '';
40
41 /*
42 [network-manager]
43 Identity=unix-group:networkmanager
44 Action=org.freedesktop.NetworkManager.*
45 ResultAny=yes
46 ResultInactive=no
47 ResultActive=yes
48
49 [modem-manager]
50 Identity=unix-group:networkmanager
51 Action=org.freedesktop.ModemManager*
52 ResultAny=yes
53 ResultInactive=no
54 ResultActive=yes
55 */
56 polkitConf = ''
57 polkit.addRule(function(action, subject) {
58 if (
59 subject.isInGroup("networkmanager")
60 && (action.id.indexOf("org.freedesktop.NetworkManager.") == 0
61 || action.id.indexOf("org.freedesktop.ModemManager") == 0
62 ))
63 { return polkit.Result.YES; }
64 });
65 '';
66
67 ns = xs: writeText "nameservers" (
68 concatStrings (map (s: "nameserver ${s}\n") xs)
69 );
70
71 overrideNameserversScript = writeScript "02overridedns" ''
72 #!/bin/sh
73 tmp=`${coreutils}/bin/mktemp`
74 ${gnused}/bin/sed '/nameserver /d' /etc/resolv.conf > $tmp
75 ${gnugrep}/bin/grep 'nameserver ' /etc/resolv.conf | \
76 ${gnugrep}/bin/grep -vf ${ns (cfg.appendNameservers ++ cfg.insertNameservers)} > $tmp.ns
77 ${optionalString (cfg.appendNameservers != []) "${coreutils}/bin/cat $tmp $tmp.ns ${ns cfg.appendNameservers} > /etc/resolv.conf"}
78 ${optionalString (cfg.insertNameservers != []) "${coreutils}/bin/cat $tmp ${ns cfg.insertNameservers} $tmp.ns > /etc/resolv.conf"}
79 ${coreutils}/bin/rm -f $tmp $tmp.ns
80 '';
81
82 dispatcherTypesSubdirMap = {
83 "basic" = "";
84 "pre-up" = "pre-up.d/";
85 "pre-down" = "pre-down.d/";
86 };
87
88 macAddressOpt = mkOption {
89 type = types.either types.str (types.enum ["permanent" "preserve" "random" "stable"]);
90 default = "preserve";
91 example = "00:11:22:33:44:55";
92 description = ''
93 "XX:XX:XX:XX:XX:XX": MAC address of the interface.
94 <literal>permanent</literal>: use the permanent MAC address of the device.
95 <literal>preserve</literal>: don’t change the MAC address of the device upon activation.
96 <literal>random</literal>: generate a randomized value upon each connect.
97 <literal>stable</literal>: generate a stable, hashed MAC address.
98 '';
99 };
100
101in {
102
103 ###### interface
104
105 options = {
106
107 networking.networkmanager = {
108
109 enable = mkOption {
110 type = types.bool;
111 default = false;
112 description = ''
113 Whether to use NetworkManager to obtain an IP address and other
114 configuration for all network interfaces that are not manually
115 configured. If enabled, a group <literal>networkmanager</literal>
116 will be created. Add all users that should have permission
117 to change network settings to this group.
118 '';
119 };
120
121 extraConfig = mkOption {
122 type = types.lines;
123 default = "";
124 description = ''
125 Configuration appended to the generated NetworkManager.conf.
126 '';
127 };
128
129 unmanaged = mkOption {
130 type = types.listOf types.string;
131 default = [];
132 description = ''
133 List of interfaces that will not be managed by NetworkManager.
134 Interface name can be specified here, but if you need more fidelity
135 see "Device List Format" in NetworkManager.conf man page.
136 '';
137 };
138
139 # Ugly hack for using the correct gnome3 packageSet
140 basePackages = mkOption {
141 type = types.attrsOf types.package;
142 default = { inherit networkmanager modemmanager wpa_supplicant
143 networkmanager-openvpn networkmanager-vpnc
144 networkmanager-openconnect networkmanager-fortisslvpn
145 networkmanager-l2tp networkmanager-iodine; };
146 internal = true;
147 };
148
149 packages = mkOption {
150 type = types.listOf types.path;
151 default = [ ];
152 description = ''
153 Extra packages that provide NetworkManager plugins.
154 '';
155 apply = list: (attrValues cfg.basePackages) ++ list;
156 };
157
158 dhcp = mkOption {
159 type = types.enum [ "dhclient" "dhcpcd" "internal" ];
160 default = "dhclient";
161 description = ''
162 Which program (or internal library) should be used for DHCP.
163 '';
164 };
165
166 logLevel = mkOption {
167 type = types.enum [ "OFF" "ERR" "WARN" "INFO" "DEBUG" "TRACE" ];
168 default = "WARN";
169 description = ''
170 Set the default logging verbosity level.
171 '';
172 };
173
174 appendNameservers = mkOption {
175 type = types.listOf types.str;
176 default = [];
177 description = ''
178 A list of name servers that should be appended
179 to the ones configured in NetworkManager or received by DHCP.
180 '';
181 };
182
183 insertNameservers = mkOption {
184 type = types.listOf types.str;
185 default = [];
186 description = ''
187 A list of name servers that should be inserted before
188 the ones configured in NetworkManager or received by DHCP.
189 '';
190 };
191
192 ethernet.macAddress = macAddressOpt;
193
194 wifi = {
195 macAddress = macAddressOpt;
196
197 powersave = mkOption {
198 type = types.nullOr types.bool;
199 default = null;
200 description = ''
201 Whether to enable Wi-Fi power saving.
202 '';
203 };
204
205 scanRandMacAddress = mkOption {
206 type = types.bool;
207 default = true;
208 description = ''
209 Whether to enable MAC address randomization of a Wi-Fi device
210 during scanning.
211 '';
212 };
213 };
214
215 dns = mkOption {
216 type = types.enum [ "default" "dnsmasq" "unbound" "systemd-resolved" "none" ];
217 default = "default";
218 description = ''
219 Set the DNS (<literal>resolv.conf</literal>) processing mode.
220 </para>
221 <para>
222 Options:
223 <variablelist>
224 <varlistentry>
225 <term><literal>"default"</literal></term>
226 <listitem><para>
227 NetworkManager will update <literal>/etc/resolv.conf</literal> to
228 reflect the nameservers provided by currently active connections.
229 </para></listitem>
230 </varlistentry>
231 <varlistentry>
232 <term><literal>"dnsmasq"</literal></term>
233 <listitem>
234 <para>
235 Enable NetworkManager's dnsmasq integration. NetworkManager will
236 run dnsmasq as a local caching nameserver, using a "split DNS"
237 configuration if you are connected to a VPN, and then update
238 <literal>resolv.conf</literal> to point to the local nameserver.
239 </para>
240 <para>
241 It is possible to pass custom options to the dnsmasq instance by
242 adding them to files in the
243 <literal>/etc/NetworkManager/dnsmasq.d/</literal> directory.
244 </para>
245 <para>
246 When multiple upstream servers are available, dnsmasq will
247 initially contact them in parallel and then use the fastest to
248 respond, probing again other servers after some time. This
249 behavior can be modified passing the
250 <literal>all-servers</literal> or <literal>strict-order</literal>
251 options to dnsmasq (see the manual page for more details).
252 </para>
253 <para>
254 Note that this option causes NetworkManager to launch and manage
255 its own instance of the dnsmasq daemon, which is
256 <emphasis>not</emphasis> the same as setting
257 <literal>services.dnsmasq.enable = true;</literal>.
258 </para>
259 </listitem>
260 </varlistentry>
261 <varlistentry>
262 <term><literal>"unbound"</literal></term>
263 <listitem><para>
264 NetworkManager will talk to unbound and dnssec-triggerd,
265 providing a "split DNS" configuration with DNSSEC support.
266 <literal>/etc/resolv.conf</literal> will be managed by
267 dnssec-trigger daemon.
268 </para></listitem>
269 </varlistentry>
270 <varlistentry>
271 <term><literal>"systemd-resolved"</literal></term>
272 <listitem><para>
273 NetworkManager will push the DNS configuration to systemd-resolved.
274 </para></listitem>
275 </varlistentry>
276 <varlistentry>
277 <term><literal>"none"</literal></term>
278 <listitem><para>
279 NetworkManager will not modify resolv.conf.
280 </para></listitem>
281 </varlistentry>
282 </variablelist>
283 '';
284 };
285
286 dispatcherScripts = mkOption {
287 type = types.listOf (types.submodule {
288 options = {
289 source = mkOption {
290 type = types.path;
291 description = ''
292 Path to the hook script.
293 '';
294 };
295
296 type = mkOption {
297 type = types.enum (attrNames dispatcherTypesSubdirMap);
298 default = "basic";
299 description = ''
300 Dispatcher hook type. Look up the hooks described at
301 <link xlink:href="https://developer.gnome.org/NetworkManager/stable/NetworkManager.html">https://developer.gnome.org/NetworkManager/stable/NetworkManager.html</link>
302 and choose the type depending on the output folder.
303 You should then filter the event type (e.g., "up"/"down") from within your script.
304 '';
305 };
306 };
307 });
308 default = [];
309 example = literalExample ''
310 [ {
311 source = pkgs.writeText "upHook" '''
312
313 if [ "$2" != "up" ]; then
314 logger "exit: event $2 != up"
315 fi
316
317 # coreutils and iproute are in PATH too
318 logger "Device $DEVICE_IFACE coming up"
319 ''';
320 type = "basic";
321 } ]'';
322 description = ''
323 A list of scripts which will be executed in response to network events.
324 '';
325 };
326
327 enableStrongSwan = mkOption {
328 type = types.bool;
329 default = false;
330 description = ''
331 Enable the StrongSwan plugin.
332 </para><para>
333 If you enable this option the
334 <literal>networkmanager_strongswan</literal> plugin will be added to
335 the <option>networking.networkmanager.packages</option> option
336 so you don't need to to that yourself.
337 '';
338 };
339
340 dynamicHosts = {
341 enable = mkOption {
342 type = types.bool;
343 default = false;
344 description = ''
345 Enabling this option requires the
346 <option>networking.networkmanager.dns</option> option to be
347 set to <literal>dnsmasq</literal>. If enabled, the directories
348 defined by the
349 <option>networking.networkmanager.dynamicHosts.hostsDirs</option>
350 option will be set up when the service starts. The dnsmasq instance
351 managed by NetworkManager will then watch those directories for
352 hosts files (see the <literal>--hostsdir</literal> option of
353 dnsmasq). This way a non-privileged user can add or override DNS
354 entries on the local system (depending on what hosts directories
355 that are configured)..
356 '';
357 };
358 hostsDirs = mkOption {
359 type = with types; attrsOf (submodule {
360 options = {
361 user = mkOption {
362 type = types.str;
363 default = "root";
364 description = ''
365 The user that will own the hosts directory.
366 '';
367 };
368 group = mkOption {
369 type = types.str;
370 default = "root";
371 description = ''
372 The group that will own the hosts directory.
373 '';
374 };
375 };
376 });
377 default = {};
378 description = ''
379 Defines a set of directories (relative to
380 <literal>/run/NetworkManager/hostdirs</literal>) that dnsmasq will
381 watch for hosts files.
382 '';
383 };
384 };
385 };
386 };
387
388
389 ###### implementation
390
391 config = mkIf cfg.enable {
392
393 assertions = [
394 { assertion = config.networking.wireless.enable == false;
395 message = "You can not use networking.networkmanager with networking.wireless";
396 }
397 { assertion = !dynamicHostsEnabled || (dynamicHostsEnabled && cfg.dns == "dnsmasq");
398 message = ''
399 To use networking.networkmanager.dynamicHosts you also need to set
400 networking.networkmanager.dns = "dnsmasq"
401 '';
402 }
403 ];
404
405 environment.etc = with cfg.basePackages; [
406 { source = configFile;
407 target = "NetworkManager/NetworkManager.conf";
408 }
409 { source = "${networkmanager-openvpn}/etc/NetworkManager/VPN/nm-openvpn-service.name";
410 target = "NetworkManager/VPN/nm-openvpn-service.name";
411 }
412 { source = "${networkmanager-vpnc}/etc/NetworkManager/VPN/nm-vpnc-service.name";
413 target = "NetworkManager/VPN/nm-vpnc-service.name";
414 }
415 { source = "${networkmanager-openconnect}/etc/NetworkManager/VPN/nm-openconnect-service.name";
416 target = "NetworkManager/VPN/nm-openconnect-service.name";
417 }
418 { source = "${networkmanager-fortisslvpn}/etc/NetworkManager/VPN/nm-fortisslvpn-service.name";
419 target = "NetworkManager/VPN/nm-fortisslvpn-service.name";
420 }
421 { source = "${networkmanager-l2tp}/etc/NetworkManager/VPN/nm-l2tp-service.name";
422 target = "NetworkManager/VPN/nm-l2tp-service.name";
423 }
424 { source = "${networkmanager_strongswan}/etc/NetworkManager/VPN/nm-strongswan-service.name";
425 target = "NetworkManager/VPN/nm-strongswan-service.name";
426 }
427 { source = "${networkmanager-iodine}/etc/NetworkManager/VPN/nm-iodine-service.name";
428 target = "NetworkManager/VPN/nm-iodine-service.name";
429 }
430 ] ++ optional (cfg.appendNameservers == [] || cfg.insertNameservers == [])
431 { source = overrideNameserversScript;
432 target = "NetworkManager/dispatcher.d/02overridedns";
433 }
434 ++ lib.imap1 (i: s: {
435 inherit (s) source;
436 target = "NetworkManager/dispatcher.d/${dispatcherTypesSubdirMap.${s.type}}03userscript${lib.fixedWidthNumber 4 i}";
437 mode = "0544";
438 }) cfg.dispatcherScripts
439 ++ optional (dynamicHostsEnabled)
440 { target = "NetworkManager/dnsmasq.d/dyndns.conf";
441 text = concatMapStrings (n: ''
442 hostsdir=/run/NetworkManager/hostsdirs/${n}
443 '') (attrNames cfg.dynamicHosts.hostsDirs);
444 };
445
446 environment.systemPackages = cfg.packages;
447
448 users.groups = [{
449 name = "networkmanager";
450 gid = config.ids.gids.networkmanager;
451 }
452 {
453 name = "nm-openvpn";
454 gid = config.ids.gids.nm-openvpn;
455 }];
456 users.users = [{
457 name = "nm-openvpn";
458 uid = config.ids.uids.nm-openvpn;
459 extraGroups = [ "networkmanager" ];
460 }
461 {
462 name = "nm-iodine";
463 isSystemUser = true;
464 group = "networkmanager";
465 }];
466
467 systemd.packages = cfg.packages;
468
469 systemd.services."network-manager" = {
470 wantedBy = [ "network.target" ];
471 restartTriggers = [ configFile ];
472
473 preStart = ''
474 mkdir -m 700 -p /etc/NetworkManager/system-connections
475 mkdir -m 700 -p /etc/ipsec.d
476 mkdir -m 755 -p ${stateDirs}
477 '';
478 };
479
480 systemd.services.nm-setup-hostsdirs = mkIf dynamicHostsEnabled {
481 wantedBy = [ "network-manager.service" ];
482 before = [ "network-manager.service" ];
483 partOf = [ "network-manager.service" ];
484 script = concatStrings (mapAttrsToList (n: d: ''
485 mkdir -p "/run/NetworkManager/hostsdirs/${n}"
486 chown "${d.user}:${d.group}" "/run/NetworkManager/hostsdirs/${n}"
487 chmod 0775 "/run/NetworkManager/hostsdirs/${n}"
488 '') cfg.dynamicHosts.hostsDirs);
489 serviceConfig = {
490 Type = "oneshot";
491 RemainAfterExist = true;
492 };
493 };
494
495 systemd.services."NetworkManager-dispatcher" = {
496 wantedBy = [ "network.target" ];
497 restartTriggers = [ configFile ];
498
499 # useful binaries for user-specified hooks
500 path = [ pkgs.iproute pkgs.utillinux pkgs.coreutils ];
501 };
502
503 # Turn off NixOS' network management
504 networking = {
505 useDHCP = false;
506 # use mkDefault to trigger the assertion about the conflict above
507 wireless.enable = lib.mkDefault false;
508 };
509
510 security.polkit.extraConfig = polkitConf;
511
512 networking.networkmanager.packages =
513 mkIf cfg.enableStrongSwan [ pkgs.networkmanager_strongswan ];
514
515 services.dbus.packages =
516 optional cfg.enableStrongSwan pkgs.strongswanNM ++ cfg.packages;
517
518 services.udev.packages = cfg.packages;
519 };
520}