1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 dhcpcd = if !config.boot.isContainer then pkgs.dhcpcd else pkgs.dhcpcd.override { udev = null; };
8
9 cfg = config.networking.dhcpcd;
10
11 interfaces = attrValues config.networking.interfaces;
12
13 enableDHCP = config.networking.dhcpcd.enable &&
14 (config.networking.useDHCP || any (i: i.useDHCP == true) interfaces);
15
16 enableNTPService = (config.services.ntp.enable || config.services.ntpd-rs.enable || config.services.openntpd.enable || config.services.chrony.enable);
17
18 # Don't start dhcpcd on explicitly configured interfaces or on
19 # interfaces that are part of a bridge, bond or sit device.
20 ignoredInterfaces =
21 map (i: i.name) (filter (i: if i.useDHCP != null then !i.useDHCP else i.ipv4.addresses != [ ]) interfaces)
22 ++ mapAttrsToList (i: _: i) config.networking.sits
23 ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bridges))
24 ++ flatten (concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues config.networking.vswitches))
25 ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bonds))
26 ++ config.networking.dhcpcd.denyInterfaces;
27
28 arrayAppendOrNull = a1: a2: if a1 == null && a2 == null then null
29 else if a1 == null then a2 else if a2 == null then a1
30 else a1 ++ a2;
31
32 # If dhcp is disabled but explicit interfaces are enabled,
33 # we need to provide dhcp just for those interfaces.
34 allowInterfaces = arrayAppendOrNull cfg.allowInterfaces
35 (if !config.networking.useDHCP && enableDHCP then
36 map (i: i.name) (filter (i: i.useDHCP == true) interfaces) else null);
37
38 staticIPv6Addresses = map (i: i.name) (filter (i: i.ipv6.addresses != [ ]) interfaces);
39
40 noIPv6rs = concatStringsSep "\n" (map (name: ''
41 interface ${name}
42 noipv6rs
43 '') staticIPv6Addresses);
44
45 # Config file adapted from the one that ships with dhcpcd.
46 dhcpcdConf = pkgs.writeText "dhcpcd.conf"
47 ''
48 # Inform the DHCP server of our hostname for DDNS.
49 hostname
50
51 # A list of options to request from the DHCP server.
52 option domain_name_servers, domain_name, domain_search, host_name
53 option classless_static_routes, ntp_servers, interface_mtu
54
55 # A ServerID is required by RFC2131.
56 # Commented out because of many non-compliant DHCP servers in the wild :(
57 #require dhcp_server_identifier
58
59 # A hook script is provided to lookup the hostname if not set by
60 # the DHCP server, but it should not be run by default.
61 nohook lookup-hostname
62
63 # Ignore peth* devices; on Xen, they're renamed physical
64 # Ethernet cards used for bridging. Likewise for vif* and tap*
65 # (Xen) and virbr* and vnet* (libvirt).
66 denyinterfaces ${toString ignoredInterfaces} lo peth* vif* tap* tun* virbr* vnet* vboxnet* sit*
67
68 # Use the list of allowed interfaces if specified
69 ${optionalString (allowInterfaces != null) "allowinterfaces ${toString allowInterfaces}"}
70
71 # Immediately fork to background if specified, otherwise wait for IP address to be assigned
72 ${{
73 background = "background";
74 any = "waitip";
75 ipv4 = "waitip 4";
76 ipv6 = "waitip 6";
77 both = "waitip 4\nwaitip 6";
78 if-carrier-up = "";
79 }.${cfg.wait}}
80
81 ${optionalString (config.networking.enableIPv6 == false) ''
82 # Don't solicit or accept IPv6 Router Advertisements and DHCPv6 if disabled IPv6
83 noipv6
84 ''}
85
86 ${optionalString (config.networking.enableIPv6 && cfg.IPv6rs == null && staticIPv6Addresses != [ ]) noIPv6rs}
87 ${optionalString (config.networking.enableIPv6 && cfg.IPv6rs == false) ''
88 noipv6rs
89 ''}
90
91 ${cfg.extraConfig}
92 '';
93
94 exitHook = pkgs.writeText "dhcpcd.exit-hook" ''
95 ${optionalString enableNTPService ''
96 if [ "$reason" = BOUND -o "$reason" = REBOOT ]; then
97 # Restart ntpd. We need to restart it to make sure that it will actually do something:
98 # if ntpd cannot resolve the server hostnames in its config file, then it will never do
99 # anything ever again ("couldn't resolve ..., giving up on it"), so we silently lose
100 # time synchronisation. This also applies to openntpd.
101 ${optionalString config.services.ntp.enable "/run/current-system/systemd/bin/systemctl try-reload-or-restart ntpd.service || true"}
102 ${optionalString config.services.ntpd-rs.enable "/run/current-system/systemd/bin/systemctl try-reload-or-restart ntpd-rs.service || true"}
103 ${optionalString config.services.openntpd.enable "/run/current-system/systemd/bin/systemctl try-reload-or-restart openntpd.service || true"}
104 ${optionalString config.services.chrony.enable "/run/current-system/systemd/bin/systemctl try-reload-or-restart chronyd.service || true"}
105 fi
106 ''}
107
108 ${cfg.runHook}
109 '';
110
111in
112
113{
114
115 ###### interface
116
117 options = {
118
119 networking.dhcpcd.enable = mkOption {
120 type = types.bool;
121 default = true;
122 description = ''
123 Whether to enable dhcpcd for device configuration. This is mainly to
124 explicitly disable dhcpcd (for example when using networkd).
125 '';
126 };
127
128 networking.dhcpcd.persistent = mkOption {
129 type = types.bool;
130 default = false;
131 description = ''
132 Whenever to leave interfaces configured on dhcpcd daemon
133 shutdown. Set to true if you have your root or store mounted
134 over the network or this machine accepts SSH connections
135 through DHCP interfaces and clients should be notified when
136 it shuts down.
137 '';
138 };
139
140 networking.dhcpcd.denyInterfaces = mkOption {
141 type = types.listOf types.str;
142 default = [];
143 description = ''
144 Disable the DHCP client for any interface whose name matches
145 any of the shell glob patterns in this list. The purpose of
146 this option is to blacklist virtual interfaces such as those
147 created by Xen, libvirt, LXC, etc.
148 '';
149 };
150
151 networking.dhcpcd.allowInterfaces = mkOption {
152 type = types.nullOr (types.listOf types.str);
153 default = null;
154 description = ''
155 Enable the DHCP client for any interface whose name matches
156 any of the shell glob patterns in this list. Any interface not
157 explicitly matched by this pattern will be denied. This pattern only
158 applies when non-null.
159 '';
160 };
161
162 networking.dhcpcd.extraConfig = mkOption {
163 type = types.lines;
164 default = "";
165 description = ''
166 Literal string to append to the config file generated for dhcpcd.
167 '';
168 };
169
170 networking.dhcpcd.IPv6rs = mkOption {
171 type = types.nullOr types.bool;
172 default = null;
173 description = ''
174 Force enable or disable solicitation and receipt of IPv6 Router Advertisements.
175 This is required, for example, when using a static unique local IPv6 address (ULA)
176 and global IPv6 address auto-configuration with SLAAC.
177 '';
178 };
179
180 networking.dhcpcd.runHook = mkOption {
181 type = types.lines;
182 default = "";
183 example = "if [[ $reason =~ BOUND ]]; then echo $interface: Routers are $new_routers - were $old_routers; fi";
184 description = ''
185 Shell code that will be run after all other hooks. See
186 `man dhcpcd-run-hooks` for details on what is possible.
187 '';
188 };
189
190 networking.dhcpcd.wait = mkOption {
191 type = types.enum [ "background" "any" "ipv4" "ipv6" "both" "if-carrier-up" ];
192 default = "any";
193 description = ''
194 This option specifies when the dhcpcd service will fork to background.
195 If set to "background", dhcpcd will fork to background immediately.
196 If set to "ipv4" or "ipv6", dhcpcd will wait for the corresponding IP
197 address to be assigned. If set to "any", dhcpcd will wait for any type
198 (IPv4 or IPv6) to be assigned. If set to "both", dhcpcd will wait for
199 both an IPv4 and an IPv6 address before forking.
200 The option "if-carrier-up" is equivalent to "any" if either ethernet
201 is plugged nor WiFi is powered, and to "background" otherwise.
202 '';
203 };
204
205 };
206
207
208 ###### implementation
209
210 config = mkIf enableDHCP {
211
212 assertions = [ {
213 # dhcpcd doesn't start properly with malloc ∉ [ libc scudo ]
214 # see https://github.com/NixOS/nixpkgs/issues/151696
215 assertion =
216 dhcpcd.enablePrivSep
217 -> elem config.environment.memoryAllocator.provider [ "libc" "scudo" ];
218 message = ''
219 dhcpcd with privilege separation is incompatible with chosen system malloc.
220 Currently only the `libc` and `scudo` allocators are known to work.
221 To disable dhcpcd's privilege separation, overlay Nixpkgs and override dhcpcd
222 to set `enablePrivSep = false`.
223 '';
224 } ];
225
226 environment.etc."dhcpcd.conf".source = dhcpcdConf;
227
228 systemd.services.dhcpcd = let
229 cfgN = config.networking;
230 hasDefaultGatewaySet = (cfgN.defaultGateway != null && cfgN.defaultGateway.address != "")
231 && (!cfgN.enableIPv6 || (cfgN.defaultGateway6 != null && cfgN.defaultGateway6.address != ""));
232 in
233 { description = "DHCP Client";
234
235 wantedBy = [ "multi-user.target" ] ++ optional (!hasDefaultGatewaySet) "network-online.target";
236 wants = [ "network.target" ];
237 before = [ "network-online.target" ];
238
239 restartTriggers = optional (enableNTPService || cfg.runHook != "") [ exitHook ];
240
241 # Stopping dhcpcd during a reconfiguration is undesirable
242 # because it brings down the network interfaces configured by
243 # dhcpcd. So do a "systemctl restart" instead.
244 stopIfChanged = false;
245
246 path = [ dhcpcd pkgs.nettools config.networking.resolvconf.package ];
247
248 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
249
250 serviceConfig =
251 { Type = "forking";
252 PIDFile = "/run/dhcpcd/pid";
253 RuntimeDirectory = "dhcpcd";
254 ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd --quiet ${optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
255 ExecReload = "${dhcpcd}/sbin/dhcpcd --rebind";
256 Restart = "always";
257 };
258 };
259
260 users.users.dhcpcd = {
261 isSystemUser = true;
262 group = "dhcpcd";
263 };
264 users.groups.dhcpcd = {};
265
266 environment.systemPackages = [ dhcpcd ];
267
268 environment.etc."dhcpcd.exit-hook" = mkIf (enableNTPService || cfg.runHook != "") {
269 source = exitHook;
270 };
271
272 powerManagement.resumeCommands = mkIf config.systemd.services.dhcpcd.enable
273 ''
274 # Tell dhcpcd to rebind its interfaces if it's running.
275 /run/current-system/systemd/bin/systemctl reload dhcpcd.service
276 '';
277
278 };
279
280}