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