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