1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.services.bind;
10
11 bindPkg = config.services.bind.package;
12
13 bindUser = "named";
14
15 bindZoneCoerce =
16 list:
17 builtins.listToAttrs (
18 lib.forEach list (zone: {
19 name = zone.name;
20 value = zone;
21 })
22 );
23
24 bindZoneOptions =
25 { name, config, ... }:
26 {
27 options = {
28 name = lib.mkOption {
29 type = lib.types.str;
30 default = name;
31 description = "Name of the zone.";
32 };
33 master = lib.mkOption {
34 description = "Master=false means slave server";
35 type = lib.types.bool;
36 };
37 file = lib.mkOption {
38 type = lib.types.either lib.types.str lib.types.path;
39 description = "Zone file resource records contain columns of data, separated by whitespace, that define the record.";
40 };
41 masters = lib.mkOption {
42 type = lib.types.listOf lib.types.str;
43 description = "List of servers for inclusion in stub and secondary zones.";
44 };
45 slaves = lib.mkOption {
46 type = lib.types.listOf lib.types.str;
47 description = "Addresses who may request zone transfers.";
48 default = [ ];
49 };
50 allowQuery = lib.mkOption {
51 type = lib.types.listOf lib.types.str;
52 description = ''
53 List of address ranges allowed to query this zone. Instead of the address(es), this may instead
54 contain the single string "any".
55 '';
56 default = [ "any" ];
57 };
58 extraConfig = lib.mkOption {
59 type = lib.types.lines;
60 description = "Extra zone config to be appended at the end of the zone section.";
61 default = "";
62 };
63 };
64 };
65
66 confFile = pkgs.writeText "named.conf" ''
67 include "/etc/bind/rndc.key";
68 controls {
69 inet 127.0.0.1 allow {localhost;} keys {"rndc-key";};
70 };
71
72 acl cachenetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} };
73 acl badnetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} };
74
75 options {
76 listen-on { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOn} };
77 listen-on-v6 { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6} };
78 allow-query-cache { cachenetworks; };
79 blackhole { badnetworks; };
80 forward ${cfg.forward};
81 forwarders { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
82 directory "${cfg.directory}";
83 pid-file "/run/named/named.pid";
84 ${cfg.extraOptions}
85 };
86
87 ${cfg.extraConfig}
88
89 ${lib.concatMapStrings (
90 {
91 name,
92 file,
93 master ? true,
94 slaves ? [ ],
95 masters ? [ ],
96 allowQuery ? [ ],
97 extraConfig ? "",
98 }:
99 ''
100 zone "${name}" {
101 type ${if master then "master" else "slave"};
102 file "${file}";
103 ${
104 if master then
105 ''
106 allow-transfer {
107 ${lib.concatMapStrings (ip: "${ip};\n") slaves}
108 };
109 ''
110 else
111 ''
112 masters {
113 ${lib.concatMapStrings (ip: "${ip};\n") masters}
114 };
115 ''
116 }
117 allow-query { ${lib.concatMapStrings (ip: "${ip}; ") allowQuery}};
118 ${extraConfig}
119 };
120 ''
121 ) (lib.attrValues cfg.zones)}
122 '';
123
124in
125
126{
127
128 ###### interface
129
130 options = {
131
132 services.bind = {
133
134 enable = lib.mkEnableOption "BIND domain name server";
135
136 package = lib.mkPackageOption pkgs "bind" { };
137
138 cacheNetworks = lib.mkOption {
139 default = [
140 "127.0.0.0/24"
141 "::1/128"
142 ];
143 type = lib.types.listOf lib.types.str;
144 description = ''
145 What networks are allowed to use us as a resolver. Note
146 that this is for recursive queries -- all networks are
147 allowed to query zones configured with the `zones` option
148 by default (although this may be overridden within each
149 zone's configuration, via the `allowQuery` option).
150 It is recommended that you limit cacheNetworks to avoid your
151 server being used for DNS amplification attacks.
152 '';
153 };
154
155 blockedNetworks = lib.mkOption {
156 default = [ ];
157 type = lib.types.listOf lib.types.str;
158 description = ''
159 What networks are just blocked.
160 '';
161 };
162
163 ipv4Only = lib.mkOption {
164 default = false;
165 type = lib.types.bool;
166 description = ''
167 Only use ipv4, even if the host supports ipv6.
168 '';
169 };
170
171 forwarders = lib.mkOption {
172 default = config.networking.nameservers;
173 defaultText = lib.literalExpression "config.networking.nameservers";
174 type = lib.types.listOf lib.types.str;
175 description = ''
176 List of servers we should forward requests to.
177 '';
178 };
179
180 forward = lib.mkOption {
181 default = "first";
182 type = lib.types.enum [
183 "first"
184 "only"
185 ];
186 description = ''
187 Whether to forward 'first' (try forwarding but lookup directly if forwarding fails) or 'only'.
188 '';
189 };
190
191 listenOn = lib.mkOption {
192 default = [ "any" ];
193 type = lib.types.listOf lib.types.str;
194 description = ''
195 Interfaces to listen on.
196 '';
197 };
198
199 listenOnIpv6 = lib.mkOption {
200 default = [ "any" ];
201 type = lib.types.listOf lib.types.str;
202 description = ''
203 Ipv6 interfaces to listen on.
204 '';
205 };
206
207 directory = lib.mkOption {
208 type = lib.types.str;
209 default = "/run/named";
210 description = "Working directory of BIND.";
211 };
212
213 zones = lib.mkOption {
214 default = [ ];
215 type =
216 with lib.types;
217 coercedTo (listOf attrs) bindZoneCoerce (attrsOf (lib.types.submodule bindZoneOptions));
218 description = ''
219 List of zones we claim authority over.
220 '';
221 example = {
222 "example.com" = {
223 master = false;
224 file = "/var/dns/example.com";
225 masters = [ "192.168.0.1" ];
226 slaves = [ ];
227 extraConfig = "";
228 };
229 };
230 };
231
232 extraConfig = lib.mkOption {
233 type = lib.types.lines;
234 default = "";
235 description = ''
236 Extra lines to be added verbatim to the generated named configuration file.
237 '';
238 };
239
240 extraOptions = lib.mkOption {
241 type = lib.types.lines;
242 default = "";
243 description = ''
244 Extra lines to be added verbatim to the options section of the
245 generated named configuration file.
246 '';
247 };
248
249 configFile = lib.mkOption {
250 type = lib.types.path;
251 default = confFile;
252 defaultText = lib.literalExpression "confFile";
253 description = ''
254 Overridable config file to use for named. By default, that
255 generated by nixos.
256 '';
257 };
258
259 };
260
261 };
262
263 ###### implementation
264
265 config = lib.mkIf cfg.enable {
266
267 networking.resolvconf.useLocalResolver = lib.mkDefault true;
268
269 users.users.${bindUser} = {
270 group = bindUser;
271 description = "BIND daemon user";
272 isSystemUser = true;
273 };
274 users.groups.${bindUser} = { };
275
276 systemd.tmpfiles.settings."bind" = lib.mkIf (cfg.directory != "/run/named") {
277 ${cfg.directory} = {
278 d = {
279 user = bindUser;
280 group = bindUser;
281 age = "-";
282 };
283 };
284 };
285 systemd.services.bind = {
286 description = "BIND Domain Name Server";
287 after = [ "network.target" ];
288 wantedBy = [ "multi-user.target" ];
289
290 preStart = ''
291 if ! [ -f "/etc/bind/rndc.key" ]; then
292 ${bindPkg.out}/sbin/rndc-confgen -c /etc/bind/rndc.key -a -A hmac-sha256 2>/dev/null
293 fi
294 '';
295
296 serviceConfig = {
297 Type = "forking"; # Set type to forking, see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=900788
298 ExecStart = "${bindPkg.out}/sbin/named ${lib.optionalString cfg.ipv4Only "-4"} -c ${cfg.configFile}";
299 ExecReload = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' reload";
300 ExecStop = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' stop";
301 User = bindUser;
302 RuntimeDirectory = "named";
303 RuntimeDirectoryPreserve = "yes";
304 ConfigurationDirectory = "bind";
305 ReadWritePaths = [
306 (lib.mapAttrsToList (
307 name: config: if (lib.hasPrefix "/" config.file) then ("-${dirOf config.file}") else ""
308 ) cfg.zones)
309 cfg.directory
310 ];
311 CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
312 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
313 # Security
314 NoNewPrivileges = true;
315 # Sandboxing
316 ProtectSystem = "strict";
317 ReadOnlyPaths = "/sys";
318 ProtectHome = true;
319 PrivateTmp = true;
320 PrivateDevices = true;
321 PrivateMounts = true;
322 ProtectHostname = true;
323 ProtectClock = true;
324 ProtectKernelTunables = true;
325 ProtectKernelModules = true;
326 ProtectKernelLogs = true;
327 ProtectControlGroups = true;
328 ProtectProc = "invisible";
329 ProcSubset = "pid";
330 RemoveIPC = true;
331 RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6 AF_NETLINK" ];
332 LockPersonality = true;
333 MemoryDenyWriteExecute = true;
334 RestrictRealtime = true;
335 RestrictSUIDSGID = true;
336 RestrictNamespaces = true;
337 # System Call Filtering
338 SystemCallArchitectures = "native";
339 SystemCallFilter = "~@mount @debug @clock @reboot @resources @privileged @obsolete acct modify_ldt add_key adjtimex clock_adjtime delete_module fanotify_init finit_module get_mempolicy init_module io_destroy io_getevents iopl ioperm io_setup io_submit io_cancel kcmp kexec_load keyctl lookup_dcookie migrate_pages move_pages open_by_handle_at perf_event_open process_vm_readv process_vm_writev ptrace remap_file_pages request_key set_mempolicy swapoff swapon uselib vmsplice";
340 };
341
342 unitConfig.Documentation = "man:named(8)";
343 };
344 };
345}