1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 dhcpcd = if !config.boot.isContainer then pkgs.dhcpcd else pkgs.dhcpcd.override { udev = null; };
10
11 cfg = config.networking.dhcpcd;
12
13 interfaces = lib.attrValues config.networking.interfaces;
14
15 enableDHCP =
16 config.networking.dhcpcd.enable
17 && (config.networking.useDHCP || lib.any (i: i.useDHCP == true) interfaces);
18
19 useResolvConf = config.networking.resolvconf.enable;
20
21 # Don't start dhcpcd on explicitly configured interfaces or on
22 # interfaces that are part of a bridge, bond or sit device.
23 ignoredInterfaces =
24 map (i: i.name) (
25 lib.filter (i: if i.useDHCP != null then !i.useDHCP else i.ipv4.addresses != [ ]) interfaces
26 )
27 ++ lib.mapAttrsToList (i: _: i) config.networking.sits
28 ++ lib.concatLists (lib.attrValues (lib.mapAttrs (n: v: v.interfaces) config.networking.bridges))
29 ++ lib.flatten (
30 lib.concatMap (
31 i: lib.attrNames (lib.filterAttrs (_: config: config.type != "internal") i.interfaces)
32 ) (lib.attrValues config.networking.vswitches)
33 )
34 ++ lib.concatLists (lib.attrValues (lib.mapAttrs (n: v: v.interfaces) config.networking.bonds))
35 ++ config.networking.dhcpcd.denyInterfaces;
36
37 arrayAppendOrNull =
38 a1: a2:
39 if a1 == null && a2 == null then
40 null
41 else if a1 == null then
42 a2
43 else if a2 == null then
44 a1
45 else
46 a1 ++ a2;
47
48 # If dhcp is disabled but explicit interfaces are enabled,
49 # we need to provide dhcp just for those interfaces.
50 allowInterfaces = arrayAppendOrNull cfg.allowInterfaces (
51 if !config.networking.useDHCP && enableDHCP then
52 map (i: i.name) (lib.filter (i: i.useDHCP == true) interfaces)
53 else
54 null
55 );
56
57 staticIPv6Addresses = map (i: i.name) (lib.filter (i: i.ipv6.addresses != [ ]) interfaces);
58
59 noIPv6rs = lib.concatStringsSep "\n" (
60 map (name: ''
61 interface ${name}
62 noipv6rs
63 '') staticIPv6Addresses
64 );
65
66 # Config file adapted from the one that ships with dhcpcd.
67 dhcpcdConf = pkgs.writeText "dhcpcd.conf" ''
68 # Inform the DHCP server of our hostname for DDNS.
69 hostname
70
71 # A list of options to request from the DHCP server.
72 option domain_name_servers, domain_name, domain_search
73 option classless_static_routes, ntp_servers, interface_mtu
74
75 # A ServerID is required by RFC2131.
76 # Commented out because of many non-compliant DHCP servers in the wild :(
77 #require dhcp_server_identifier
78
79 # A hook script is provided to lookup the hostname if not set by
80 # the DHCP server, but it should not be run by default.
81 nohook lookup-hostname
82
83 # Ignore peth* devices; on Xen, they're renamed physical
84 # Ethernet cards used for bridging. Likewise for vif* and tap*
85 # (Xen) and virbr* and vnet* (libvirt).
86 denyinterfaces ${toString ignoredInterfaces} lo peth* vif* tap* tun* virbr* vnet* vboxnet* sit*
87
88 # Use the list of allowed interfaces if specified
89 ${lib.optionalString (allowInterfaces != null) "allowinterfaces ${toString allowInterfaces}"}
90
91 # Immediately fork to background if specified, otherwise wait for IP address to be assigned
92 ${
93 {
94 background = "background";
95 any = "waitip";
96 ipv4 = "waitip 4";
97 ipv6 = "waitip 6";
98 both = "waitip 4\nwaitip 6";
99 if-carrier-up = "";
100 }
101 .${cfg.wait}
102 }
103
104 ${lib.optionalString (config.networking.enableIPv6 == false) ''
105 # Don't solicit or accept IPv6 Router Advertisements and DHCPv6 if disabled IPv6
106 noipv6
107 ''}
108
109 ${lib.optionalString (
110 config.networking.enableIPv6 && cfg.IPv6rs == null && staticIPv6Addresses != [ ]
111 ) noIPv6rs}
112 ${lib.optionalString (config.networking.enableIPv6 && cfg.IPv6rs == false) ''
113 noipv6rs
114 ''}
115 ${lib.optionalString cfg.setHostname "option host_name"}
116
117 ${cfg.extraConfig}
118 '';
119
120in
121
122{
123
124 ###### interface
125
126 options = {
127
128 networking.dhcpcd.enable = lib.mkOption {
129 type = lib.types.bool;
130 default = true;
131 description = ''
132 Whether to enable dhcpcd for device configuration. This is mainly to
133 explicitly disable dhcpcd (for example when using networkd).
134 '';
135 };
136
137 networking.dhcpcd.persistent = lib.mkOption {
138 type = lib.types.bool;
139 default = false;
140 description = ''
141 Whether to leave interfaces configured on dhcpcd daemon
142 shutdown. Set to true if you have your root or store mounted
143 over the network or this machine accepts SSH connections
144 through DHCP interfaces and clients should be notified when
145 it shuts down.
146 '';
147 };
148
149 networking.dhcpcd.setHostname = lib.mkOption {
150 type = lib.types.bool;
151 default = true;
152 description = ''
153 Whether to set the machine hostname based on the information
154 received from the DHCP server.
155
156 ::: {.note}
157 The hostname will be changed only if the current one is
158 the empty string, `localhost` or `nixos`.
159
160 Polkit ([](#opt-security.polkit.enable)) is also required.
161 :::
162 '';
163 };
164
165 networking.dhcpcd.denyInterfaces = lib.mkOption {
166 type = lib.types.listOf lib.types.str;
167 default = [ ];
168 description = ''
169 Disable the DHCP client for any interface whose name matches
170 any of the shell glob patterns in this list. The purpose of
171 this option is to blacklist virtual interfaces such as those
172 created by Xen, libvirt, LXC, etc.
173 '';
174 };
175
176 networking.dhcpcd.allowInterfaces = lib.mkOption {
177 type = lib.types.nullOr (lib.types.listOf lib.types.str);
178 default = null;
179 description = ''
180 Enable the DHCP client for any interface whose name matches
181 any of the shell glob patterns in this list. Any interface not
182 explicitly matched by this pattern will be denied. This pattern only
183 applies when non-null.
184 '';
185 };
186
187 networking.dhcpcd.extraConfig = lib.mkOption {
188 type = lib.types.lines;
189 default = "";
190 description = ''
191 Literal string to append to the config file generated for dhcpcd.
192 '';
193 };
194
195 networking.dhcpcd.IPv6rs = lib.mkOption {
196 type = lib.types.nullOr lib.types.bool;
197 default = null;
198 description = ''
199 Force enable or disable solicitation and receipt of IPv6 Router Advertisements.
200 This is required, for example, when using a static unique local IPv6 address (ULA)
201 and global IPv6 address auto-configuration with SLAAC.
202 '';
203 };
204
205 networking.dhcpcd.allowSetuid = lib.mkOption {
206 type = lib.types.bool;
207 default = false;
208 description = ''
209 Whether to relax the security sandbox to allow running setuid
210 binaries (e.g. `sudo`) in the dhcpcd hooks.
211 '';
212 };
213
214 networking.dhcpcd.runHook = lib.mkOption {
215 type = lib.types.lines;
216 default = "";
217 example = "if [[ $reason =~ BOUND ]]; then echo $interface: Routers are $new_routers - were $old_routers; fi";
218 description = ''
219 Shell code that will be run after all other hooks. See
220 `man dhcpcd-run-hooks` for details on what is possible.
221
222 ::: {.note}
223 To use sudo or similar tools in your script you may have to set:
224
225 networking.dhcpcd.allowSetuid = true;
226
227 In addition, as most of the filesystem is inaccessible to dhcpcd
228 by default, you may want to define some exceptions, e.g.
229
230 systemd.services.dhcpcd.serviceConfig.ReadOnlyPaths = [
231 "/run/user/1000/bus" # to send desktop notifications
232 ];
233 :::
234 '';
235 };
236
237 networking.dhcpcd.wait = lib.mkOption {
238 type = lib.types.enum [
239 "background"
240 "any"
241 "ipv4"
242 "ipv6"
243 "both"
244 "if-carrier-up"
245 ];
246 default = "any";
247 description = ''
248 This option specifies when the dhcpcd service will fork to background.
249 If set to "background", dhcpcd will fork to background immediately.
250 If set to "ipv4" or "ipv6", dhcpcd will wait for the corresponding IP
251 address to be assigned. If set to "any", dhcpcd will wait for any type
252 (IPv4 or IPv6) to be assigned. If set to "both", dhcpcd will wait for
253 both an IPv4 and an IPv6 address before forking.
254 The option "if-carrier-up" is equivalent to "any" if either ethernet
255 is plugged or WiFi is powered, and to "background" otherwise.
256 '';
257 };
258
259 };
260
261 ###### implementation
262
263 config = lib.mkIf enableDHCP {
264
265 systemd.services.dhcpcd =
266 let
267 cfgN = config.networking;
268 hasDefaultGatewaySet =
269 (cfgN.defaultGateway != null && cfgN.defaultGateway.address != "")
270 && (!cfgN.enableIPv6 || (cfgN.defaultGateway6 != null && cfgN.defaultGateway6.address != ""));
271 in
272 {
273 description = "DHCP Client";
274
275 documentation = [ "man:dhcpcd(8)" ];
276
277 wantedBy = [ "multi-user.target" ] ++ lib.optional (!hasDefaultGatewaySet) "network-online.target";
278 wants = [
279 "network.target"
280 "resolvconf.service"
281 ];
282 after = [ "resolvconf.service" ];
283 before = [ "network-online.target" ];
284
285 restartTriggers = [ cfg.runHook ];
286
287 # Stopping dhcpcd during a reconfiguration is undesirable
288 # because it brings down the network interfaces configured by
289 # dhcpcd. So do a "systemctl restart" instead.
290 stopIfChanged = false;
291
292 path =
293 [
294 dhcpcd
295 config.networking.resolvconf.package
296 ]
297 ++ lib.optional cfg.setHostname (
298 pkgs.writeShellScriptBin "hostname" ''
299 ${lib.getExe' pkgs.systemd "hostnamectl"} set-hostname --transient $1
300 ''
301 );
302
303 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
304
305 serviceConfig = {
306 Type = "forking";
307 PIDFile = "/run/dhcpcd/pid";
308 SupplementaryGroups = lib.optional useResolvConf "resolvconf";
309 User = "dhcpcd";
310 Group = "dhcpcd";
311 StateDirectory = "dhcpcd";
312 RuntimeDirectory = "dhcpcd";
313
314 ExecStartPre = "+${pkgs.writeShellScript "migrate-dhcpcd" ''
315 # migrate from old database directory
316 if test -f /var/db/dhcpcd/duid; then
317 echo 'migrating DHCP leases from /var/db/dhcpcd to /var/lib/dhcpcd ...'
318 mv /var/db/dhcpcd/* -t /var/lib/dhcpcd
319 chown dhcpcd:dhcpcd /var/lib/dhcpcd/*
320 rmdir /var/db/dhcpcd || true
321 echo done
322 fi
323 ''}";
324
325 ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd --quiet ${lib.optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
326 ExecReload = "${dhcpcd}/sbin/dhcpcd --rebind";
327 Restart = "always";
328 AmbientCapabilities = [
329 "CAP_NET_ADMIN"
330 "CAP_NET_RAW"
331 "CAP_NET_BIND_SERVICE"
332 ];
333 CapabilityBoundingSet = lib.optionals (!cfg.allowSetuid) [
334 "CAP_NET_ADMIN"
335 "CAP_NET_RAW"
336 "CAP_NET_BIND_SERVICE"
337 ];
338 ReadWritePaths =
339 [ "/proc/sys/net/ipv4" ]
340 ++ lib.optional cfgN.enableIPv6 "/proc/sys/net/ipv6"
341 ++ lib.optionals useResolvConf (
342 [ "/run/resolvconf" ] ++ config.networking.resolvconf.subscriberFiles
343 );
344 DeviceAllow = "";
345 LockPersonality = true;
346 MemoryDenyWriteExecute = true;
347 NoNewPrivileges = lib.mkDefault (!cfg.allowSetuid); # may be disabled for sudo in runHook
348 PrivateDevices = true;
349 PrivateMounts = true;
350 PrivateTmp = true;
351 PrivateUsers = false;
352 ProtectClock = true;
353 ProtectControlGroups = true;
354 ProtectHome = "tmpfs"; # allow exceptions to be added to ReadOnlyPaths, etc.
355 ProtectHostname = true;
356 ProtectKernelLogs = true;
357 ProtectKernelModules = true;
358 ProtectKernelTunables = true;
359 ProtectProc = "invisible";
360 ProtectSystem = "strict";
361 RemoveIPC = true;
362 RestrictAddressFamilies = [
363 "AF_UNIX"
364 "AF_INET"
365 "AF_INET6"
366 "AF_NETLINK"
367 "AF_PACKET"
368 ];
369 RestrictNamespaces = true;
370 RestrictRealtime = true;
371 RestrictSUIDSGID = true;
372 SystemCallFilter =
373 [
374 "@system-service"
375 "~@aio"
376 "~@keyring"
377 "~@memlock"
378 "~@mount"
379 ]
380 ++ lib.optionals (!cfg.allowSetuid) [
381 "~@privileged"
382 "~@resources"
383 ];
384 SystemCallArchitectures = "native";
385 UMask = "0027";
386 };
387 };
388
389 # Note: the service could run with `DynamicUser`, however that makes
390 # impossible (for no good reason, see systemd issue #20495) to disable
391 # `NoNewPrivileges` or `ProtectHome`, which users may want to in order
392 # to run certain scripts in `networking.dhcpcd.runHook`.
393 users.users.dhcpcd = {
394 isSystemUser = true;
395 group = "dhcpcd";
396 };
397 users.groups.dhcpcd = { };
398
399 environment.systemPackages = [ dhcpcd ];
400
401 environment.etc."dhcpcd.exit-hook".text = cfg.runHook;
402
403 powerManagement.resumeCommands = lib.mkIf config.systemd.services.dhcpcd.enable ''
404 # Tell dhcpcd to rebind its interfaces if it's running.
405 /run/current-system/systemd/bin/systemctl reload dhcpcd.service
406 '';
407
408 security.polkit.extraConfig = lib.mkMerge [
409 (lib.mkIf config.services.resolved.enable ''
410 polkit.addRule(function(action, subject) {
411 if (action.id == 'org.freedesktop.resolve1.revert' ||
412 action.id == 'org.freedesktop.resolve1.set-dns-servers' ||
413 action.id == 'org.freedesktop.resolve1.set-domains') {
414 if (subject.user == 'dhcpcd') {
415 return polkit.Result.YES;
416 }
417 }
418 });
419 '')
420 (lib.mkIf cfg.setHostname ''
421 polkit.addRule(function(action, subject) {
422 if (action.id == 'org.freedesktop.hostname1.set-hostname' &&
423 subject.user == 'dhcpcd') {
424 return polkit.Result.YES;
425 }
426 });
427 '')
428 ];
429
430 };
431
432}