1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.bind;
8
9 bindPkg = config.services.bind.package;
10
11 bindUser = "named";
12
13 bindZoneCoerce = list: builtins.listToAttrs (lib.forEach list (zone: { name = zone.name; value = zone; }));
14
15 bindZoneOptions = { name, config, ... }: {
16 options = {
17 name = mkOption {
18 type = types.str;
19 default = name;
20 description = "Name of the zone.";
21 };
22 master = mkOption {
23 description = "Master=false means slave server";
24 type = types.bool;
25 };
26 file = mkOption {
27 type = types.either types.str types.path;
28 description = "Zone file resource records contain columns of data, separated by whitespace, that define the record.";
29 };
30 masters = mkOption {
31 type = types.listOf types.str;
32 description = "List of servers for inclusion in stub and secondary zones.";
33 };
34 slaves = mkOption {
35 type = types.listOf types.str;
36 description = "Addresses who may request zone transfers.";
37 default = [ ];
38 };
39 allowQuery = mkOption {
40 type = types.listOf types.str;
41 description = ''
42 List of address ranges allowed to query this zone. Instead of the address(es), this may instead
43 contain the single string "any".
44
45 NOTE: This overrides the global-level `allow-query` setting, which is set to the contents
46 of `cachenetworks`.
47 '';
48 default = [ "any" ];
49 };
50 extraConfig = mkOption {
51 type = types.str;
52 description = "Extra zone config to be appended at the end of the zone section.";
53 default = "";
54 };
55 };
56 };
57
58 confFile = pkgs.writeText "named.conf"
59 ''
60 include "/etc/bind/rndc.key";
61 controls {
62 inet 127.0.0.1 allow {localhost;} keys {"rndc-key";};
63 };
64
65 acl cachenetworks { ${concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} };
66 acl badnetworks { ${concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} };
67
68 options {
69 listen-on { ${concatMapStrings (entry: " ${entry}; ") cfg.listenOn} };
70 listen-on-v6 { ${concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6} };
71 allow-query { cachenetworks; };
72 blackhole { badnetworks; };
73 forward ${cfg.forward};
74 forwarders { ${concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
75 directory "${cfg.directory}";
76 pid-file "/run/named/named.pid";
77 ${cfg.extraOptions}
78 };
79
80 ${cfg.extraConfig}
81
82 ${ concatMapStrings
83 ({ name, file, master ? true, slaves ? [], masters ? [], allowQuery ? [], extraConfig ? "" }:
84 ''
85 zone "${name}" {
86 type ${if master then "master" else "slave"};
87 file "${file}";
88 ${ if master then
89 ''
90 allow-transfer {
91 ${concatMapStrings (ip: "${ip};\n") slaves}
92 };
93 ''
94 else
95 ''
96 masters {
97 ${concatMapStrings (ip: "${ip};\n") masters}
98 };
99 ''
100 }
101 allow-query { ${concatMapStrings (ip: "${ip}; ") allowQuery}};
102 ${extraConfig}
103 };
104 '')
105 (attrValues cfg.zones) }
106 '';
107
108in
109
110{
111
112 ###### interface
113
114 options = {
115
116 services.bind = {
117
118 enable = mkEnableOption "BIND domain name server";
119
120
121 package = mkPackageOption pkgs "bind" { };
122
123 cacheNetworks = mkOption {
124 default = [ "127.0.0.0/24" "::1/128" ];
125 type = types.listOf types.str;
126 description = ''
127 What networks are allowed to use us as a resolver. Note
128 that this is for recursive queries -- all networks are
129 allowed to query zones configured with the `zones` option
130 by default (although this may be overridden within each
131 zone's configuration, via the `allowQuery` option).
132 It is recommended that you limit cacheNetworks to avoid your
133 server being used for DNS amplification attacks.
134 '';
135 };
136
137 blockedNetworks = mkOption {
138 default = [ ];
139 type = types.listOf types.str;
140 description = ''
141 What networks are just blocked.
142 '';
143 };
144
145 ipv4Only = mkOption {
146 default = false;
147 type = types.bool;
148 description = ''
149 Only use ipv4, even if the host supports ipv6.
150 '';
151 };
152
153 forwarders = mkOption {
154 default = config.networking.nameservers;
155 defaultText = literalExpression "config.networking.nameservers";
156 type = types.listOf types.str;
157 description = ''
158 List of servers we should forward requests to.
159 '';
160 };
161
162 forward = mkOption {
163 default = "first";
164 type = types.enum ["first" "only"];
165 description = ''
166 Whether to forward 'first' (try forwarding but lookup directly if forwarding fails) or 'only'.
167 '';
168 };
169
170 listenOn = mkOption {
171 default = [ "any" ];
172 type = types.listOf types.str;
173 description = ''
174 Interfaces to listen on.
175 '';
176 };
177
178 listenOnIpv6 = mkOption {
179 default = [ "any" ];
180 type = types.listOf types.str;
181 description = ''
182 Ipv6 interfaces to listen on.
183 '';
184 };
185
186 directory = mkOption {
187 type = types.str;
188 default = "/run/named";
189 description = "Working directory of BIND.";
190 };
191
192 zones = mkOption {
193 default = [ ];
194 type = with types; coercedTo (listOf attrs) bindZoneCoerce (attrsOf (types.submodule bindZoneOptions));
195 description = ''
196 List of zones we claim authority over.
197 '';
198 example = {
199 "example.com" = {
200 master = false;
201 file = "/var/dns/example.com";
202 masters = [ "192.168.0.1" ];
203 slaves = [ ];
204 extraConfig = "";
205 };
206 };
207 };
208
209 extraConfig = mkOption {
210 type = types.lines;
211 default = "";
212 description = ''
213 Extra lines to be added verbatim to the generated named configuration file.
214 '';
215 };
216
217 extraOptions = mkOption {
218 type = types.lines;
219 default = "";
220 description = ''
221 Extra lines to be added verbatim to the options section of the
222 generated named configuration file.
223 '';
224 };
225
226 configFile = mkOption {
227 type = types.path;
228 default = confFile;
229 defaultText = literalExpression "confFile";
230 description = ''
231 Overridable config file to use for named. By default, that
232 generated by nixos.
233 '';
234 };
235
236 };
237
238 };
239
240
241 ###### implementation
242
243 config = mkIf cfg.enable {
244
245 networking.resolvconf.useLocalResolver = mkDefault true;
246
247 users.users.${bindUser} =
248 {
249 group = bindUser;
250 description = "BIND daemon user";
251 isSystemUser = true;
252 };
253 users.groups.${bindUser} = {};
254
255 systemd.services.bind = {
256 description = "BIND Domain Name Server";
257 after = [ "network.target" ];
258 wantedBy = [ "multi-user.target" ];
259
260 preStart = ''
261 mkdir -m 0755 -p /etc/bind
262 if ! [ -f "/etc/bind/rndc.key" ]; then
263 ${bindPkg.out}/sbin/rndc-confgen -c /etc/bind/rndc.key -u ${bindUser} -a -A hmac-sha256 2>/dev/null
264 fi
265
266 ${pkgs.coreutils}/bin/mkdir -p /run/named
267 chown ${bindUser} /run/named
268
269 ${pkgs.coreutils}/bin/mkdir -p ${cfg.directory}
270 chown ${bindUser} ${cfg.directory}
271 '';
272
273 serviceConfig = {
274 ExecStart = "${bindPkg.out}/sbin/named -u ${bindUser} ${optionalString cfg.ipv4Only "-4"} -c ${cfg.configFile} -f";
275 ExecReload = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' reload";
276 ExecStop = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' stop";
277 };
278
279 unitConfig.Documentation = "man:named(8)";
280 };
281 };
282}