1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.pdns-recursor;
12
13 oneOrMore = type: with types; either type (listOf type);
14 valueType =
15 with types;
16 oneOf [
17 int
18 str
19 bool
20 path
21 ];
22 configType = with types; attrsOf (nullOr (oneOrMore valueType));
23
24 toBool = val: if val then "yes" else "no";
25 serialize =
26 val:
27 with types;
28 if str.check val then
29 val
30 else if int.check val then
31 toString val
32 else if path.check val then
33 toString val
34 else if bool.check val then
35 toBool val
36 else if builtins.isList val then
37 (concatMapStringsSep "," serialize val)
38 else
39 "";
40
41 settingsFormat = pkgs.formats.yaml { };
42
43 mkDefaultAttrs = mapAttrs (n: v: mkDefault v);
44
45 mkForwardZone = mapAttrsToList (
46 zone: uri: {
47 inherit zone;
48 forwarders = [ uri ];
49 }
50 );
51
52 configFile =
53 if cfg.old-settings != { } then
54 # Convert recursor.conf to recursor.yml and merge it
55 let
56 conf = pkgs.writeText "recursor.conf" (
57 concatStringsSep "\n" (mapAttrsToList (name: val: "${name}=${serialize val}") cfg.old-settings)
58 );
59
60 yaml = settingsFormat.generate "recursor.yml" cfg.yaml-settings;
61 in
62 pkgs.runCommand "recursor-merged.yml" { } ''
63 ${pkgs.pdns-recursor}/bin/rec_control show-yaml --config ${conf} > override.yml
64 ${pkgs.yq-go}/bin/yq '. *= load("override.yml")' ${yaml} > $out
65 ''
66 else
67 settingsFormat.generate "recursor.yml" cfg.yaml-settings;
68
69in
70{
71 options.services.pdns-recursor = {
72 enable = mkEnableOption "PowerDNS Recursor, a recursive DNS server";
73
74 dns.address = mkOption {
75 type = oneOrMore types.str;
76 default = [
77 "::"
78 "0.0.0.0"
79 ];
80 description = ''
81 IP addresses Recursor DNS server will bind to.
82 '';
83 };
84
85 dns.port = mkOption {
86 type = types.port;
87 default = 53;
88 description = ''
89 Port number Recursor DNS server will bind to.
90 '';
91 };
92
93 dns.allowFrom = mkOption {
94 type = types.listOf types.str;
95 default = [
96 "127.0.0.0/8"
97 "10.0.0.0/8"
98 "100.64.0.0/10"
99 "169.254.0.0/16"
100 "192.168.0.0/16"
101 "172.16.0.0/12"
102 "::1/128"
103 "fc00::/7"
104 "fe80::/10"
105 ];
106 example = [
107 "0.0.0.0/0"
108 "::/0"
109 ];
110 description = ''
111 IP address ranges of clients allowed to make DNS queries.
112 '';
113 };
114
115 api.address = mkOption {
116 type = types.str;
117 default = "0.0.0.0";
118 description = ''
119 IP address Recursor REST API server will bind to.
120 '';
121 };
122
123 api.port = mkOption {
124 type = types.port;
125 default = 8082;
126 description = ''
127 Port number Recursor REST API server will bind to.
128 '';
129 };
130
131 api.allowFrom = mkOption {
132 type = types.listOf types.str;
133 default = [
134 "127.0.0.1"
135 "::1"
136 ];
137 example = [
138 "0.0.0.0/0"
139 "::/0"
140 ];
141 description = ''
142 IP address ranges of clients allowed to make API requests.
143 '';
144 };
145
146 exportHosts = mkOption {
147 type = types.bool;
148 default = false;
149 description = ''
150 Whether to export names and IP addresses defined in /etc/hosts.
151 '';
152 };
153
154 forwardZones = mkOption {
155 type = types.attrs;
156 default = { };
157 description = ''
158 DNS zones to be forwarded to other authoritative servers.
159 '';
160 };
161
162 forwardZonesRecurse = mkOption {
163 type = types.attrs;
164 example = {
165 eth = "[::1]:5353";
166 };
167 default = { };
168 description = ''
169 DNS zones to be forwarded to other recursive servers.
170 '';
171 };
172
173 dnssecValidation = mkOption {
174 type = types.enum [
175 "off"
176 "process-no-validate"
177 "process"
178 "log-fail"
179 "validate"
180 ];
181 default = "validate";
182 description = ''
183 Controls the level of DNSSEC processing done by the PowerDNS Recursor.
184 See <https://doc.powerdns.com/md/recursor/dnssec/> for a detailed explanation.
185 '';
186 };
187
188 serveRFC1918 = mkOption {
189 type = types.bool;
190 default = true;
191 description = ''
192 Whether to directly resolve the RFC1918 reverse-mapping domains:
193 `10.in-addr.arpa`,
194 `168.192.in-addr.arpa`,
195 `16-31.172.in-addr.arpa`
196 This saves load on the AS112 servers.
197 '';
198 };
199
200 old-settings = mkOption {
201 type = configType;
202 default = { };
203 example = literalExpression ''
204 {
205 loglevel = 8;
206 log-common-errors = true;
207 }
208 '';
209 description = ''
210 Older PowerDNS Recursor settings. Use this option to configure
211 Recursor settings not exposed in a NixOS option or to bypass one.
212 See the full documentation at
213 <https://doc.powerdns.com/recursor/settings.html>
214 for the available options.
215
216 ::: {.warning}
217 This option is provided for backward compatibility only
218 and will be removed in the next release of NixOS.
219 :::
220 '';
221 };
222
223 yaml-settings = mkOption {
224 type = settingsFormat.type;
225 default = { };
226 example = literalExpression ''
227 {
228 loglevel = 8;
229 log-common-errors = true;
230 }
231 '';
232 description = ''
233 PowerDNS Recursor settings. Use this option to configure Recursor
234 settings not exposed in a NixOS option or to bypass one.
235 See the full documentation at
236 <https://doc.powerdns.com/recursor/yamlsettings.html>
237 for the available options.
238 '';
239 };
240
241 luaConfig = mkOption {
242 type = types.lines;
243 default = "";
244 description = ''
245 The content Lua configuration file for PowerDNS Recursor. See
246 <https://doc.powerdns.com/recursor/lua-config/index.html>.
247 '';
248 };
249 };
250
251 config = mkIf cfg.enable {
252
253 environment.etc."/pdns-recursor/recursor.yml".source = configFile;
254
255 networking.resolvconf.useLocalResolver = lib.mkDefault true;
256
257 services.pdns-recursor.yaml-settings = {
258 incoming = mkDefaultAttrs {
259 listen = cfg.dns.address;
260 port = cfg.dns.port;
261 allow_from = cfg.dns.allowFrom;
262 };
263
264 webservice = mkDefaultAttrs {
265 address = cfg.api.address;
266 port = cfg.api.port;
267 allow_from = cfg.api.allowFrom;
268 };
269
270 recursor = mkDefaultAttrs {
271 forward_zones = mkForwardZone cfg.forwardZones;
272 forward_zones_recurse = mkForwardZone cfg.forwardZonesRecurse;
273 export_etc_hosts = cfg.exportHosts;
274 serve_rfc1918 = cfg.serveRFC1918;
275 lua_config_file = pkgs.writeText "recursor.lua" cfg.luaConfig;
276 daemon = false;
277 write_pid = false;
278 };
279
280 dnssec = mkDefaultAttrs {
281 validation = cfg.dnssecValidation;
282 };
283
284 logging = mkDefaultAttrs {
285 timestamp = false;
286 disable_syslog = true;
287 };
288 };
289
290 systemd.packages = [ pkgs.pdns-recursor ];
291
292 systemd.services.pdns-recursor = {
293 restartTriggers = [ config.environment.etc."/pdns-recursor/recursor.yml".source ];
294 wantedBy = [ "multi-user.target" ];
295 };
296
297 users.users.pdns-recursor = {
298 isSystemUser = true;
299 group = "pdns-recursor";
300 description = "PowerDNS Recursor daemon user";
301 };
302
303 users.groups.pdns-recursor = { };
304
305 warnings = lib.optional (cfg.old-settings != { }) ''
306 pdns-recursor has changed its configuration file format from pdns-recursor.conf
307 (mapped to `services.pdns-recursor.old-settings`) to the newer pdns-recursor.yml
308 (mapped to `services.pdns-recursor.yaml-settings`).
309
310 Support for the older format will be removed in a future version, so please migrate
311 your settings over. See <https://doc.powerdns.com/recursor/yamlsettings.html>.
312 '';
313
314 };
315
316 imports = [
317 (mkRemovedOptionModule [
318 "services"
319 "pdns-recursor"
320 "extraConfig"
321 ] "To change extra Recursor settings use services.pdns-recursor.settings instead.")
322
323 (mkRenamedOptionModule
324 [
325 "services"
326 "pdns-recursor"
327 "settings"
328 ]
329 [
330 "services"
331 "pdns-recursor"
332 "old-settings"
333 ]
334 )
335 ];
336
337 meta.maintainers = with lib.maintainers; [ rnhmjoj ];
338
339}