Message ID | 20240127121040.7156-2-soeren@soeren-tempel.net |
---|---|
State | New |
Headers | show |
Series | [bug#68757] services: dns: Add unbound service | expand |
Hi Sören, soeren@soeren-tempel.net skribis: > From: Sören Tempel <soeren@soeren-tempel.net> > > This allows using Unbound as a local DNSSEC-enabled resolver. This > commit also allows configuration of the Unbound DNS resolver via a > Scheme API. Conceptually, the Unbound configuration consists of several > "sections" that contain key-value pairs (see unbound.conf(5)). The > configuration sections are modeled in Scheme using record-type fields, > where each field expects a list of pairs. > > A sample configuration, which uses a DoT forwarder, looks as follows: > > (service unbound-service-type > (unbound-configuration > (forward-zone > '((name . ".") > (forward-addr . "149.112.112.112#dns.quad9.net") > (forward-addr . "2620:fe::9#dns.quad9.net") > (forward-tls-upstream . yes))))) > > * gnu/service/dns.scm (serialize-list): New procedure. > * gnu/service/dns.scm (unbound-configuration): New record. > * gnu/service/dns.scm (unbound-config-file): New procedure. > * gnu/service/dns.scm (unbound-shepherd-service): New procedure. > * gnu/service/dns.scm (unbound-account-service): New constant. > * gnu/service/dns.scm (unbound-service-type): New services. > > Signed-off-by: Sören Tempel <soeren@soeren-tempel.net> Nice! Some comments: • Please document the service in doc/guix.texi. Make sure to include an example like the one above in the introduction, with explanations (you take remove the example from the commit log though). • Unless it’s too hard, please provide a system test (the service for knot lacks one for some reason, so there’s a precedent, but the general rule is that system services should always have associated tests.) > +(define-configuration unbound-configuration I recommend adding an “escape hatch” by which users may provide raw strings (or a file-like object) that gets inserted into the config file. > + (server > + (maybe-list '((interface . "127.0.0.1") > + (interface . "::1") > + > + ;; TLS certificate bundle for DNS over TLS. > + (tls-cert-bundle . "/etc/ssl/certs/ca-certificates.crt") > + > + (hide-identity . yes) > + (hide-version . yes))) Please use Scheme booleans #t and #f instead of 'yes and 'no. > + "The server section of the configuration.") > + (remote-control > + (maybe-list '((control-enable . yes) > + (control-interface . "/run/unbound.sock"))) > + "Configuration of the remote control facility.") For ‘remote-control’ and ‘server’, it’s not clear to me why we resort to alists instead of records (or fields within this record type); it looks inconsistent. Could you consider turning them into records or fields? > + (documentation "Unbound daemon.") “Run the Unbound DNS resolver” maybe? > + (provision '(unbound dns)) > + (requirement '(networking)) Add 'user-processes. However, does it really need ‘networking’? (See <https://issues.guix.gnu.org/66306>.) > + (shell "/run/current-system/profile/sbin/nologin")))) Rather (file-append …) as is done in other services. > +(define unbound-service-type > + (service-type (name 'unbound) > + (description "Run the unbound DNS resolver.") s/unbound/Unbound/ TIA, Ludo’.
Ludovic Courtès <ludo@gnu.org> wrote: > Hi Sören, Hi Ludovic, > For ‘remote-control’ and ‘server’, it’s not clear to me why we resort to > alists instead of records (or fields within this record type); it looks > inconsistent. > > Could you consider turning them into records or fields? Prior to submitting this patch I was experimenting with both records and alists for the Unbound configuration abstraction. Unbound has **a lot** of configuration options and new options are constantly getting added by upstream, see unbound.conf(5). Therefore, supporting them through a record type with fields for each configuration option requires a lot of code. Furthermore, it will require constant maintenance to keep up with new upstream options. I looked at prior art and noticed that the Nix service configuration for unbound just uses a plain hash with string keys [1]. This seemed like a good way to deal with the complexity of unbound.conf, hence I opted for a similar approach here. I don't think it's feasible to model the configuration using a record type with several hundred fields and, as rde uses an alist-based approach for services with similar complexity, I don't think its unheard of in the Guix world either. While it is not as “type safe” as a record-based approach (e.g. you can create semantically invalid unbound configurations), it offers good forwards compatibility and requires less Scheme code. In theory, it would be possible to model sections with less options (e.g. the ‘remote-control’ or ‘server’ option) using records. However, using alists for some sections and records for others seems inconsistent to me. Please let me know what you think so I can revise this accordingly. > I recommend adding an “escape hatch” by which users may provide raw > strings (or a file-like object) that gets inserted into the config file. I think at the moment, it should be possible to express all possible unbound configurations using the alist-based approach. If not, I would consider it this a bug in the Scheme abstraction. As such, I don't think there is a need for an “escape hatch” right now (see also: my comment on records and forwards compatibility above). However, if this is a common idiom then I can add such an escape hatch. The other things you mentioned seem obvious to me and I will just implement them as suggested in a v2 revision of the patch. Thanks for the feedback! Greetings, Sören [1]: https://github.com/NixOS/nixpkgs/blob/0a37316d6cfea44280f4470b6867a711a24606bd/nixos/modules/services/networking/unbound.nix#L102-L126
Hi, Sören Tempel <soeren@soeren-tempel.net> skribis: > Prior to submitting this patch I was experimenting with both records and > alists for the Unbound configuration abstraction. Unbound has **a lot** > of configuration options and new options are constantly getting added by > upstream, see unbound.conf(5). Therefore, supporting them through a > record type with fields for each configuration option requires a lot of > code. Furthermore, it will require constant maintenance to keep up with > new upstream options. Right. > I looked at prior art and noticed that the Nix service configuration for > unbound just uses a plain hash with string keys [1]. This seemed like a > good way to deal with the complexity of unbound.conf, hence I opted for > a similar approach here. I don't think it's feasible to model the > configuration using a record type with several hundred fields and, as rde > uses an alist-based approach for services with similar complexity, I > don't think its unheard of in the Guix world either. While it is not as > “type safe” as a record-based approach (e.g. you can create semantically > invalid unbound configurations), it offers good forwards compatibility > and requires less Scheme code. > > In theory, it would be possible to model sections with less options > (e.g. the ‘remote-control’ or ‘server’ option) using records. However, > using alists for some sections and records for others seems inconsistent > to me. > > Please let me know what you think so I can revise this accordingly. The usual approach for services in Guix is to have a record for the most common options (or for all the options if that doing so can be automated, as was done with Dovecot) and an “escape hatch” that lets users insert raw config text. Key/value alists are not a common idiom. I would suggest sticking to this model as much as possible. Perhaps key/value alists would be preferable as an escape hatch than raw strings? Now, I don’t use Unbound, so I can only give general advice based on what’s usually done in Guix. Maybe ‘knot-service-type’ is a useful source of inspiration. HTH! Ludo’.
diff --git a/gnu/services/dns.scm b/gnu/services/dns.scm index 6608046909..224a4d4c32 100644 --- a/gnu/services/dns.scm +++ b/gnu/services/dns.scm @@ -3,6 +3,7 @@ ;;; Copyright © 2020 Pierre Langlois <pierre.langlois@gmx.com> ;;; Copyright © 2021 Maxime Devos <maximedevos@telenet.be> ;;; Copyright © 2022 Remco van 't Veer <remco@remworks.net> +;;; Copyright © 2024 Sören Tempel <soeren@soeren-tempel.net> ;;; ;;; This file is part of GNU Guix. ;;; @@ -52,7 +53,19 @@ (define-module (gnu services dns) knot-resolver-configuration dnsmasq-service-type - dnsmasq-configuration)) + dnsmasq-configuration + + unbound-service-type + unbound-configuration + unbound-configuration? + unbound-configuration-server + unbound-configuration-remote-control + unbound-configuration-forward-zone + unbound-configuration-stub-zone + unbound-configuration-auth-zone + unbound-configuration-view + unbound-configuration-python + unbound-configuration-dynlib)) ;;; ;;; Knot DNS. @@ -897,3 +910,103 @@ (define dnsmasq-service-type dnsmasq-activation))) (default-value (dnsmasq-configuration)) (description "Run the dnsmasq DNS server."))) + + +;;; +;;; Unbound. +;;; + +(define-maybe list) + +(define (serialize-list field-name lst) + ;; Ensure that strings within the unbound configuration + ;; are not enclosed in double quotes by the serialization. + (define (->string obj) + (if (string? obj) + obj + (object->string obj))) + + #~(string-append + #$(string-append (symbol->string field-name) ":\n") + #$(apply string-append + (map + (lambda (pair) + (string-append "\t" + (symbol->string (car pair)) + ": " + (->string (cdr pair)) + "\n")) + lst)))) + +(define-configuration unbound-configuration + (server + (maybe-list '((interface . "127.0.0.1") + (interface . "::1") + + ;; TLS certificate bundle for DNS over TLS. + (tls-cert-bundle . "/etc/ssl/certs/ca-certificates.crt") + + (hide-identity . yes) + (hide-version . yes))) + "The server section of the configuration.") + (remote-control + (maybe-list '((control-enable . yes) + (control-interface . "/run/unbound.sock"))) + "Configuration of the remote control facility.") + (forward-zone + maybe-list + "Configuration of nameservers to forward queries to.") + (stub-zone + maybe-list + "Configuration of stub zones.") + (auth-zone + maybe-list + "Zones for which unbound should response as an authority server.") + (view + maybe-list + "Configuration of view clauses.") + (python + maybe-list + "Configuration of the Python module.") + (dynlib + maybe-list + "Dynamic library module configuration.")) + +(define (unbound-config-file config) + (mixed-text-file "unbound.conf" + (serialize-configuration + config + unbound-configuration-fields))) + +(define (unbound-shepherd-service config) + (let ((config-file (unbound-config-file config))) + (list (shepherd-service + (documentation "Unbound daemon.") + (provision '(unbound dns)) + (requirement '(networking)) + (actions (list (shepherd-configuration-action config-file))) + (start #~(make-forkexec-constructor + (list (string-append #$unbound "/sbin/unbound") + "-d" "-p" "-c" #$config-file))) + (stop #~(make-kill-destructor)))))) + +(define unbound-account-service + (list (user-group (name "unbound") (system? #t)) + (user-account + (name "unbound") + (group "unbound") + (system? #t) + (comment "Unbound daemon user") + (home-directory "/var/empty") + (shell "/run/current-system/profile/sbin/nologin")))) + +(define unbound-service-type + (service-type (name 'unbound) + (description "Run the unbound DNS resolver.") + (extensions + (list (service-extension account-service-type + (const unbound-account-service)) + (service-extension shepherd-root-service-type + unbound-shepherd-service))) + (compose concatenate) + (default-value (unbound-configuration))))