1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg4 = config.services.dhcpd4;
8 cfg6 = config.services.dhcpd6;
9
10 writeConfig = postfix: cfg: pkgs.writeText "dhcpd.conf"
11 ''
12 default-lease-time 600;
13 max-lease-time 7200;
14 ${optionalString (!cfg.authoritative) "not "}authoritative;
15 ddns-update-style interim;
16 log-facility local1; # see dhcpd.nix
17
18 ${cfg.extraConfig}
19
20 ${lib.concatMapStrings
21 (machine: ''
22 host ${machine.hostName} {
23 hardware ethernet ${machine.ethernetAddress};
24 fixed-address${
25 optionalString (postfix == "6") postfix
26 } ${machine.ipAddress};
27 }
28 '')
29 cfg.machines
30 }
31 '';
32
33 dhcpdService = postfix: cfg:
34 let
35 configFile =
36 if cfg.configFile != null
37 then cfg.configFile
38 else writeConfig postfix cfg;
39 leaseFile = "/var/lib/dhcpd${postfix}/dhcpd.leases";
40 args = [
41 "@${pkgs.dhcp}/sbin/dhcpd" "dhcpd${postfix}" "-${postfix}"
42 "-pf" "/run/dhcpd${postfix}/dhcpd.pid"
43 "-cf" configFile
44 "-lf" leaseFile
45 ] ++ cfg.extraFlags
46 ++ cfg.interfaces;
47 in
48 optionalAttrs cfg.enable {
49 "dhcpd${postfix}" = {
50 description = "DHCPv${postfix} server";
51 wantedBy = [ "multi-user.target" ];
52 after = [ "network.target" ];
53
54 preStart = "touch ${leaseFile}";
55 serviceConfig = {
56 ExecStart = concatMapStringsSep " " escapeShellArg args;
57 Type = "forking";
58 Restart = "always";
59 DynamicUser = true;
60 User = "dhcpd";
61 Group = "dhcpd";
62 AmbientCapabilities = [
63 "CAP_NET_RAW" # to send ICMP messages
64 "CAP_NET_BIND_SERVICE" # to bind on DHCP port (67)
65 ];
66 StateDirectory = "dhcpd${postfix}";
67 RuntimeDirectory = "dhcpd${postfix}";
68 PIDFile = "/run/dhcpd${postfix}/dhcpd.pid";
69 };
70 };
71 };
72
73 machineOpts = { ... }: {
74
75 options = {
76
77 hostName = mkOption {
78 type = types.str;
79 example = "foo";
80 description = lib.mdDoc ''
81 Hostname which is assigned statically to the machine.
82 '';
83 };
84
85 ethernetAddress = mkOption {
86 type = types.str;
87 example = "00:16:76:9a:32:1d";
88 description = lib.mdDoc ''
89 MAC address of the machine.
90 '';
91 };
92
93 ipAddress = mkOption {
94 type = types.str;
95 example = "192.168.1.10";
96 description = lib.mdDoc ''
97 IP address of the machine.
98 '';
99 };
100
101 };
102 };
103
104 dhcpConfig = postfix: {
105
106 enable = mkOption {
107 type = types.bool;
108 default = false;
109 description = lib.mdDoc ''
110 Whether to enable the DHCPv${postfix} server.
111 '';
112 };
113
114 extraConfig = mkOption {
115 type = types.lines;
116 default = "";
117 example = ''
118 option subnet-mask 255.255.255.0;
119 option broadcast-address 192.168.1.255;
120 option routers 192.168.1.5;
121 option domain-name-servers 130.161.158.4, 130.161.33.17, 130.161.180.1;
122 option domain-name "example.org";
123 subnet 192.168.1.0 netmask 255.255.255.0 {
124 range 192.168.1.100 192.168.1.200;
125 }
126 '';
127 description = lib.mdDoc ''
128 Extra text to be appended to the DHCP server configuration
129 file. Currently, you almost certainly need to specify something
130 there, such as the options specifying the subnet mask, DNS servers,
131 etc.
132 '';
133 };
134
135 extraFlags = mkOption {
136 type = types.listOf types.str;
137 default = [];
138 description = lib.mdDoc ''
139 Additional command line flags to be passed to the dhcpd daemon.
140 '';
141 };
142
143 configFile = mkOption {
144 type = types.nullOr types.path;
145 default = null;
146 description = lib.mdDoc ''
147 The path of the DHCP server configuration file. If no file
148 is specified, a file is generated using the other options.
149 '';
150 };
151
152 interfaces = mkOption {
153 type = types.listOf types.str;
154 default = ["eth0"];
155 description = lib.mdDoc ''
156 The interfaces on which the DHCP server should listen.
157 '';
158 };
159
160 machines = mkOption {
161 type = with types; listOf (submodule machineOpts);
162 default = [];
163 example = [
164 { hostName = "foo";
165 ethernetAddress = "00:16:76:9a:32:1d";
166 ipAddress = "192.168.1.10";
167 }
168 { hostName = "bar";
169 ethernetAddress = "00:19:d1:1d:c4:9a";
170 ipAddress = "192.168.1.11";
171 }
172 ];
173 description = lib.mdDoc ''
174 A list mapping Ethernet addresses to IPv${postfix} addresses for the
175 DHCP server.
176 '';
177 };
178
179 authoritative = mkOption {
180 type = types.bool;
181 default = true;
182 description = lib.mdDoc ''
183 Whether the DHCP server shall send DHCPNAK messages to misconfigured
184 clients. If this is not done, clients may be unable to get a correct
185 IP address after changing subnets until their old lease has expired.
186 '';
187 };
188
189 };
190
191in
192
193{
194
195 imports = [
196 (mkRenamedOptionModule [ "services" "dhcpd" ] [ "services" "dhcpd4" ])
197 ] ++ flip map [ "4" "6" ] (postfix:
198 mkRemovedOptionModule [ "services" "dhcpd${postfix}" "stateDir" ] ''
199 The DHCP server state directory is now managed with the systemd's DynamicUser mechanism.
200 This means the directory is named after the service (dhcpd${postfix}), created under
201 /var/lib/private/ and symlinked to /var/lib/.
202 ''
203 );
204
205 ###### interface
206
207 options = {
208
209 services.dhcpd4 = dhcpConfig "4";
210 services.dhcpd6 = dhcpConfig "6";
211
212 };
213
214
215 ###### implementation
216
217 config = mkIf (cfg4.enable || cfg6.enable) {
218
219 systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6;
220
221 };
222
223}