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 = lib.mdDoc "Name of the zone.";
21 };
22 master = mkOption {
23 description = lib.mdDoc "Master=false means slave server";
24 type = types.bool;
25 };
26 file = mkOption {
27 type = types.either types.str types.path;
28 description = lib.mdDoc "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 = lib.mdDoc "List of servers for inclusion in stub and secondary zones.";
33 };
34 slaves = mkOption {
35 type = types.listOf types.str;
36 description = lib.mdDoc "Addresses who may request zone transfers.";
37 default = [ ];
38 };
39 allowQuery = mkOption {
40 type = types.listOf types.str;
41 description = lib.mdDoc ''
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 = lib.mdDoc "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 (lib.mdDoc "BIND domain name server");
119
120
121 package = mkOption {
122 type = types.package;
123 default = pkgs.bind;
124 defaultText = literalExpression "pkgs.bind";
125 description = lib.mdDoc "The BIND package to use.";
126 };
127
128 cacheNetworks = mkOption {
129 default = [ "127.0.0.0/24" ];
130 type = types.listOf types.str;
131 description = lib.mdDoc ''
132 What networks are allowed to use us as a resolver. Note
133 that this is for recursive queries -- all networks are
134 allowed to query zones configured with the `zones` option
135 by default (although this may be overridden within each
136 zone's configuration, via the `allowQuery` option).
137 It is recommended that you limit cacheNetworks to avoid your
138 server being used for DNS amplification attacks.
139 '';
140 };
141
142 blockedNetworks = mkOption {
143 default = [ ];
144 type = types.listOf types.str;
145 description = lib.mdDoc ''
146 What networks are just blocked.
147 '';
148 };
149
150 ipv4Only = mkOption {
151 default = false;
152 type = types.bool;
153 description = lib.mdDoc ''
154 Only use ipv4, even if the host supports ipv6.
155 '';
156 };
157
158 forwarders = mkOption {
159 default = config.networking.nameservers;
160 defaultText = literalExpression "config.networking.nameservers";
161 type = types.listOf types.str;
162 description = lib.mdDoc ''
163 List of servers we should forward requests to.
164 '';
165 };
166
167 forward = mkOption {
168 default = "first";
169 type = types.enum ["first" "only"];
170 description = lib.mdDoc ''
171 Whether to forward 'first' (try forwarding but lookup directly if forwarding fails) or 'only'.
172 '';
173 };
174
175 listenOn = mkOption {
176 default = [ "any" ];
177 type = types.listOf types.str;
178 description = lib.mdDoc ''
179 Interfaces to listen on.
180 '';
181 };
182
183 listenOnIpv6 = mkOption {
184 default = [ "any" ];
185 type = types.listOf types.str;
186 description = lib.mdDoc ''
187 Ipv6 interfaces to listen on.
188 '';
189 };
190
191 directory = mkOption {
192 type = types.str;
193 default = "/run/named";
194 description = lib.mdDoc "Working directory of BIND.";
195 };
196
197 zones = mkOption {
198 default = [ ];
199 type = with types; coercedTo (listOf attrs) bindZoneCoerce (attrsOf (types.submodule bindZoneOptions));
200 description = lib.mdDoc ''
201 List of zones we claim authority over.
202 '';
203 example = {
204 "example.com" = {
205 master = false;
206 file = "/var/dns/example.com";
207 masters = [ "192.168.0.1" ];
208 slaves = [ ];
209 extraConfig = "";
210 };
211 };
212 };
213
214 extraConfig = mkOption {
215 type = types.lines;
216 default = "";
217 description = lib.mdDoc ''
218 Extra lines to be added verbatim to the generated named configuration file.
219 '';
220 };
221
222 extraOptions = mkOption {
223 type = types.lines;
224 default = "";
225 description = lib.mdDoc ''
226 Extra lines to be added verbatim to the options section of the
227 generated named configuration file.
228 '';
229 };
230
231 configFile = mkOption {
232 type = types.path;
233 default = confFile;
234 defaultText = literalExpression "confFile";
235 description = lib.mdDoc ''
236 Overridable config file to use for named. By default, that
237 generated by nixos.
238 '';
239 };
240
241 };
242
243 };
244
245
246 ###### implementation
247
248 config = mkIf cfg.enable {
249
250 networking.resolvconf.useLocalResolver = mkDefault true;
251
252 users.users.${bindUser} =
253 {
254 group = bindUser;
255 description = "BIND daemon user";
256 isSystemUser = true;
257 };
258 users.groups.${bindUser} = {};
259
260 systemd.services.bind = {
261 description = "BIND Domain Name Server";
262 after = [ "network.target" ];
263 wantedBy = [ "multi-user.target" ];
264
265 preStart = ''
266 mkdir -m 0755 -p /etc/bind
267 if ! [ -f "/etc/bind/rndc.key" ]; then
268 ${bindPkg.out}/sbin/rndc-confgen -c /etc/bind/rndc.key -u ${bindUser} -a -A hmac-sha256 2>/dev/null
269 fi
270
271 ${pkgs.coreutils}/bin/mkdir -p /run/named
272 chown ${bindUser} /run/named
273
274 ${pkgs.coreutils}/bin/mkdir -p ${cfg.directory}
275 chown ${bindUser} ${cfg.directory}
276 '';
277
278 serviceConfig = {
279 ExecStart = "${bindPkg.out}/sbin/named -u ${bindUser} ${optionalString cfg.ipv4Only "-4"} -c ${cfg.configFile} -f";
280 ExecReload = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' reload";
281 ExecStop = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' stop";
282 };
283
284 unitConfig.Documentation = "man:named(8)";
285 };
286 };
287}