From ba69e8f7ce21a81bdd5b99fdb1cc64492443e15c Mon Sep 17 00:00:00 2001 From: Julien Lepiller Date: Mon, 1 May 2017 21:41:45 +0200 Subject: [PATCH] gnu: Add knot-service-type. * gnu/services/dns.scm: New file. * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. * doc/guix.texi (DNS Services): New subsubsection. --- doc/guix.texi | 410 ++++++++++++++++++++++++++++++ gnu/local.mk | 1 + gnu/services/dns.scm | 593 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1004 insertions(+) create mode 100644 gnu/services/dns.scm diff --git a/doc/guix.texi b/doc/guix.texi index aa8b705be6..0d389261a2 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -218,6 +218,7 @@ Services * Messaging Services:: Messaging services. * Kerberos Services:: Kerberos services. * Web Services:: Web servers. +* DNS Services:: DNS daemons. * VPN Services:: VPN daemons. * Network File System:: NFS related services. * Continuous Integration:: The Cuirass service. @@ -8737,6 +8738,7 @@ declaration. * Messaging Services:: Messaging services. * Kerberos Services:: Kerberos services. * Web Services:: Web servers. +* DNS Services:: DNS daemons. * VPN Services:: VPN daemons. * Network File System:: NFS related services. * Continuous Integration:: The Cuirass service. @@ -13520,6 +13522,414 @@ Whether the server should add its configuration to response. @end table @end deftp +@node DNS Services +@subsubsection DNS Services +@cindex DNS (domain name system) +@cindex domain name system (DNS) + +The @code{(gnu services dns)} module provides services related to the +@dfn{domain name system} (DNS). It provides a server service for hosting +an @emph{authoritative} DNS server for multiple zones, slave or master. +This service uses @uref{https://www.knot-dns.cz/, Knot DNS}. + +An example configuration of an authoritative server for two zones, one master +and one slave, is: + +@lisp +(define-zone-entries example.org.zone +;; Name TTL Class Type Data + ("@@" "" "IN" "A" "127.0.0.1") + ("@@" "" "IN" "NS" "ns") + ("ns" "" "IN" "A" "127.0.0.1")) + +(define master-zone + (knot-zone-configuration + (domain "example.org") + (zone (zone-file + (origin "example.org") + (entries example.org.zone))))) + +(define slave-zone + (knot-zone-configuration + (domain "plop.org") + (dnssec-policy "default") + (master (list "plop-master")))) + +(define plop-master + (knot-remote-configuration + (id "plop-master") + (address (list "208.76.58.171")))) + +(operating-system + ;; ... + (services (cons* (service knot-service-type + (knot-confifguration + (remotes (list plop-master)) + (zones (list master-zone slave-zone)))) + ;; ... + %base-services))) +@end lisp + +@deffn {Scheme Variable} knot-service-type +This is the type for the Knot DNS server. + +Knot DNS is an authoritative DNS server, meaning that it can serve multiple +zones, that is to say domain names you would buy from a registrar. This server +is not a resolver, meaning that it can only resolve names for which it is +authoritative. This server can be configured to serve zones as a master server +or a slave server as a per-zone basis. Slave zones will get their data from +masters, and will serve it as an authoritative server. From the point of view +of a resolver, there is no difference between master and slave. + +The following data types are used to configure the Knot DNS server: +@end deffn + +@deftp {Data Type} knot-key-configuration +Data type representing a key. +This type has the following parameters: + +@table @asis +@item @code{id} (default: @code{""}) +An identifier for other configuration fields to refer to this key. IDs must +be unique and must not be empty. + +@item @code{algorithm} (default: @code{#f}) +The algorithm to use. Choose between @code{#f}, @code{'hmac-md5}, +@code{'hmac-sha1}, @code{'hmac-sha224}, @code{'hmac-sha256}, @code{'hmac-sha384} +and @code{'hmac-sha512}. + +@item @code{secret} (default: @code{""}) +The secret key itself. + +@end table +@end deftp + +@deftp {Data Type} knot-acl-configuration +Data type representing an Access Control List (ACL) configuration. +This type has the following parameters: + +@table @asis +@item @code{id} (default: @code{""}) +An identifier for ether configuration fields to refer to this key. IDs must be +unique and must not be empty. + +@item @code{address} (default: @code{'()}) +An ordered list of IP addresses, network subnets, or network ranges represented +with strings. The query must match one of them. Empty value means that +address match is not required. + +@item @code{key} (default: @code{'()}) +An ordered list of references to keys represented with strings. The string +must match a key ID defined in a @code{knot-key-configuration}. No key means +that a key is not require to match that ACL. + +@item @code{action} (default: @code{'()}) +An ordered list of actions that are permitted or forbidden by this ACL. Possible +values are lists of zero or more elements from @code{'transfer}, @code{'notify} +and @code{'update}. + +@item @code{deny?} (default: @code{#f}) +When true, the ACL defines restrictions. Listed actions are forbidden. When +false, listed actions are allowed. + +@end table +@end deftp + +@deftp {Data Type} zone-entry +Data type represnting a record entry in a zone file. +This type has the following parameters: + +@table @asis +@item @code{name} (default: @code{"@@"}) +The name of the record. @code{"@@"} refers to the origin of the zone. Names +are relative to the origin of the zone. For example, in the @code{example.org} +zone, @code{"ns.example.org"} actually refers to @code{ns.example.org.example.org}. +Names ending with a dot are absolute, which means that @code{"ns.example.org."} +refers to @code{ns.example.org}. + +@item @code{ttl} (default: @code{""}) +The Time-To-Live (TTL) of this record. If not set, the default TTL is used. + +@item @code{class} (default: @code{"IN"}) +The class of the record. Knot currently supports only @code{"IN"} and +partially @code{"CH"}. + +@item @code{type} (default: @code{"A"}) +The type of the record. Common types include A (IPv4 address), AAAA (IPv6 +address), NS (Name Server) and MX (Mail eXchange). Many other types are +defined. + +@item @code{data} (default: @code{""}) +The data contained in the record. For instance an IP address associated with +an A record, or a domain name associated with an NS record. Remember that +domain names are relative to the origin unless they end with a dot. + +@end table +@end deftp + +@deftp {Data Type} zone-file +Data type representing the content of a zone file. +This type has the following parameters: + +@table @asis +@item @code{entries} (default: @code{'()}) +The list of entries. The SOA record is taken care of, so you don't need to +put it in the list of entries. This list should probably contain an entry +for your primary authoritative DNS server. Other than using a list of entries +directly, you can use @code{define-zone-entries} to define a object containing +the list of entries more easily, that you can later pass to the @code{entries} +field of the @code{zone-file}. + +@item @code{origin} (default: @code{""}) +The name of your zone. This parameter cannot be empty. + +@item @code{ns} (default: @code{"ns"}) +The domain of your primary authoritative DNS server. The name is relative to +the origin, unless it ends with a dot. It is mandatory that this primary +DNS server corresponds to an NS record in the zone and that it is associated +to an IP address in the list of entries. + +@item @code{mail} (default: @code{"hostmaster"}) +An email address people can contact you at, as the owner of the zone. This +is translated as @code{@@}. + +@item @code{serial} (default: @code{1}) +The serial number of the zone. As this is used to keep track of changes by +both slaves and resolvers, it is mandatory that it @emph{never} decreases. +Always increment it when you make a change in your zone. + +@item @code{refresh} (default: @code{"2d"}) +The frequency at which slaves will do a zone transfer. This value can be +a number of seconds or a number of some unit between: +@itemize +@item m: minute +@item h: hour +@item d: day +@item w: week +@end itemize + +@item @code{retry} (default: @code{"15m"}) +The period after which a slave will retry to contact its master when it fails +to do so a first time. + +@item @code{expiry} (default: @code{"2w"}) +Default TTL of records. Existing records are considered correct for at most +this amount of time. After this period, resolvers will invalidate their cache +and check again that it still exists. + +@item @code{nx} (default: @code{"1h"}) +Default TTL of inexistant records. This delay is usually short because you want +your new domains to reach everyone quickly. + +@end table +@end deftp + +@deftp {Data Type} knot-remote-configuration +Data type representing a remote configuration. +This type has the following parameters: + +@table @asis +@item @code{id} (default: @code{""}) +An identifier for other configuration fields to refer to this remote. IDs must +be unique and must not be empty. + +@item @code{address} (default: @code{'()}) +An ordered list of destination IP addresses. Addresses are tried in sequence. +An optional port can be given with the @@ separator. For instance: +@code{(list "1.2.3.4" "2.3.4.5@@53")}. Default port is 53. + +@item @code{via} (default: @code{'()}) +An ordered list of source IP addresses. An empty list will have Knot choose +an appropriate source IP. An optional port can be given with the @@ separator. +The default is to choose at random. + +@item @code{key} (default: @code{#f}) +A reference to a key, that is a string containing the identifier of a key +defined in a @code{knot-key-configuration} field. + +@end table +@end deftp + +@deftp {Data Type} knot-keystore-configuration +Data type representing a keystore to hold dnssec keys. +This type has the following parameters: + +@table @asis +@item @code{id} (default: @code{""}) +The id of the keystore. It must not be empty. + +@item @code{backend} (default: @code{'pem}) +The backend to store the keys in. Can be @code{'pem} or @code{'pkcs11}. + +@item @code{config} (default: @code{"/var/lib/knot/keys/keys"}) +The configuration string of the backend. An example for the PKCS#11 is: +@code{"pkcs11:token=knot;pin-value=1234 /gnu/store/.../lib/pkcs11/libsofthsm2.so"}. +For the pem backend, the string reprensents a path in the filesystem. + +@end table +@end deftp + +@deftp {Data Type} knot-policy-configuration +Data type representing a dnssec policy. Knot DNS is able to automatically +sign your zones. It can either generate and manage your keys automatically or +use keys that you generate. + +Dnssec is usually implemented using two keys: a Key Signing Key (KSK) that is +used to sign the second, and a Zone Signing Key (ZSK) that is used to sign the +zone. In order to be trusted, the KSK needs to be present in the parent zone +(usually a top-level domain). If your registrar supports dnssec, you will +have to send them your KSK's hash so they can add a DS record in their zone. +This is not automated and need to be done each time you change your KSK. + +The policy also defines the lifetime of keys. Usually, ZSK can be changed +easily and use weaker cryptographic functions (they use lower parameters) in +order to sign records quickly, so they are changed often. The KSK however +requires manual interaction with the registrar, so they are changed less often +and use stronger parameters because they sign only one record. + +This type has the following parameters: + +@table @asis +@item @code{id} (default: @code{""}) +The id of the policy. It must not be empty. + +@item @code{keystore} (default: @code{"default"}) +A reference to a keystore, that is a string containing the identifier of a +keystore defined in a @code{knot-keystore-configuration} field. The +@code{"default"} identifier means the default keystore (a kasp database that +was setup by this service). + +@item @code{manual?} (default: @code{#f}) +Whether the key management is manual or automatic. + +@item @code{single-type-signing?} (default: @code{#f}) +When @code{#t}, use the Single-Type Signing Scheme. + +@item @code{algorithm} (default: @code{"ecdsap256sha256"}) +An algorithm of signing keys and issued signatures. + +@item @code{ksk-size} (default: @code{256}) +The length of the KSK. Note that this value is correct for the default +algorithm, but would be unsecure for other algorithms. + +@item @code{zsk-size} (default: @code{256}) +The length of the ZSK. Note that this value is correct for the default +algorithm, but would be unsecure for other algorithms. + +@item @code{dnskey-ttl} (default: @code{'default}) +The TTL value for DNSKEY records added into zone apex. The special +@code{'default} value means same as the zone SOA TTL. + +@item @code{zsk-lifetime} (default: @code{"30d"}) +The period between ZSK publication and the next rollover initiation. + +@item @code{propagation-delay} (default: @code{"1d"}) +An extra delay added for each key rollover step. This value should be high +enough to cover propagation of data from the master server to all slaves. + +@item @code{rrsig-lifetime} (default: @code{"14d"}) +A validity period of newly issued signatures. + +@item @code{rrsig-refresh} (default: @code{"7d"}) +A period how long before a signature expiration the signature will be refreshed. + +@item @code{nsec3?} (default: @code{#f}) +When @code{#t}, NSEC3 will be used instead of NSEC. + +@item @code{nsec3-iterations} (default: @code{5}) +The number of additional times the hashing is performed. + +@item @code{nsec3-salt-length} (default: @code{8}) +The length of a salt field in octets, which is appended to the original owner +name before hashing. + +@item @code{nsec3-salt-lifetime} (default: @code{"30d"}) +The validity period of newly issued salt field. + +@end table +@end deftp + +@deftp {Data Type} knot-zone-configuration +Data type representing a zone served by Knot. +This type has the following parameters: + +@table @asis +@item @code{domain} (default: @code{""}) +The domain served by this configuration. It must not be empty. + +@item @code{file} (default: @code{""}) +The file where this zone is saved. This parameter is ignored by master zones. +Empty means default location that depends on the domain name. + +@item @code{zone} (default: @code{(zone-file)}) +The content of the zone file. This parameter is ignored by slave zones. It +must contain a zone-file record. + +@item @code{master} (default: @code{'()}) +A list of master remotes. When empty, this zone is a master. When set, this +zone is a slave. This is a list of remotes identifiers. + +@item @code{ddns-master} (default: @code{#f}) +The main master. When empty, it defaults to the first master in the list of +masters. + +@item @code{notify} (default: @code{'()}) +A list of slave remote identifiers. + +@item @code{acl} (default: @code{'()}) +A list of acl identifiers. + +@item @code{semantic-checks?} (default: @code{#f}) +When set, this adds more semantic checks to the zone. + +@item @code{disable-any?} (default: @code{#f}) +When set, this forbids queries of the ANY type. + +@item @code{zonefile-sync} (default: @code{0}) +The delay between a modification in memory and on disk. 0 means immediate +synchronization. + +@item @code{serial-policy} (default: @code{'increment}) +A policy between @code{'increment} and @code{'unixtime}. + +@end table +@end deftp + +@deftp {Data Type} knot-configuration +Data type representing the Knot configuration. +This type has the following parameters: + +@table @asis +@item @code{knot} (default: @code{knot}) +The Knot package. + +@item @code{run-directory} (default: @code{"/var/run/knot"}) +The run directory. This directory will be used for pid file and sockets. + +@item @code{listen-v4} (default: @code{"0.0.0.0"}) +An ip address on which to listen. + +@item @code{listen-v6} (default: @code{"::"}) +An ip address on which to listen. + +@item @code{listen-port} (default: @code{53}) +A port on which to listen. + +@item @code{keys} (default: @code{'()}) +The list of knot-key-configuration used by this configuration. + +@item @code{acls} (default: @code{'()}) +The list of knot-acl-configuration used by this configuration. + +@item @code{remotes} (default: @code{'()}) +The list of knot-remote-configuration used by this configuration. + +@item @code{zones} (default: @code{'()}) +The list of knot-zone-configuration used by this configuration. + +@end table +@end deftp + @node VPN Services @subsubsection VPN Services @cindex VPN (virtual private network) diff --git a/gnu/local.mk b/gnu/local.mk index a97be8b533..0ef6e2af98 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -426,6 +426,7 @@ GNU_SYSTEM_MODULES = \ %D%/services/dbus.scm \ %D%/services/desktop.scm \ %D%/services/dict.scm \ + %D%/services/dns.scm \ %D%/services/kerberos.scm \ %D%/services/lirc.scm \ %D%/services/mail.scm \ diff --git a/gnu/services/dns.scm b/gnu/services/dns.scm new file mode 100644 index 0000000000..2ed7b9e22f --- /dev/null +++ b/gnu/services/dns.scm @@ -0,0 +1,593 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2017 Julien Lepiller +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (gnu services dns) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services shepherd) + #:use-module (gnu system shadow) + #:use-module (gnu packages admin) + #:use-module (gnu packages dns) + #:use-module (guix packages) + #:use-module (guix records) + #:use-module (guix gexp) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-34) + #:use-module (srfi srfi-35) + #:use-module (ice-9 match) + #:use-module (ice-9 regex) + #:export (knot-service-type + knot-acl-configuration + knot-key-configuration + knot-keystore-configuration + knot-zone-configuration + knot-remote-configuration + knot-policy-configuration + knot-configuration + define-zone-entries + zone-file + zone-entry)) + +;;; +;;; Knot DNS. +;;; + +(define-record-type* + knot-key-configuration make-knot-key-configuration + knot-key-configuration? + (id knot-key-configuration-id + (default "")) + (algorithm knot-key-configuration-algorithm + (default #f)); one of #f, or an algorithm name + (secret knot-key-configuration-secret + (default ""))) + +(define-record-type* + knot-acl-configuration make-knot-acl-configuration + knot-acl-configuration? + (id knot-acl-configuration-id + (default "")) + (address knot-acl-configuration-address + (default '())) + (key knot-acl-configuration-key + (default '())) + (action knot-acl-configuration-action + (default '())) + (deny? knot-acl-configuration-deny? + (default #f))) + +(define-record-type* + zone-entry make-zone-entry + zone-entry? + (name zone-entry-name + (default "@")) + (ttl zone-entry-ttl + (default "")) + (class zone-entry-class + (default "IN")) + (type zone-entry-type + (default "A")) + (data zone-entry-data + (default ""))) + +(define-record-type* + zone-file make-zone-file + zone-file? + (entries zone-file-entries + (default '())) + (origin zone-file-origin + (default "")) + (ns zone-file-ns + (default "ns")) + (mail zone-file-mail + (default "hostmaster")) + (serial zone-file-serial + (default 1)) + (refresh zone-file-refresh + (default "2d")) + (retry zone-file-retry + (default "15m")) + (expiry zone-file-expiry + (default "2w")) + (nx zone-file-nx + (default "1h"))) +(define-record-type* + knot-keystore-configuration make-knot-keystore-configuration + knot-keystore-configuration? + (id knot-keystore-configuration-id + (default "")) + (backend knot-keystore-configuration-backend + (default 'pem)) + (config knot-keystore-configuration-config + (default "/var/lib/knot/keys/keys"))) + +(define-record-type* + knot-policy-configuration make-knot-policy-configuration + knot-policy-configuration? + (id knot-policy-configuration-id + (default "")) + (keystore knot-policy-configuration-keystore + (default "default")) + (manual? knot-policy-configuration-manual? + (default #f)) + (single-type-signing? knot-policy-configuration-single-type-signing? + (default #f)) + (algorithm knot-policy-configuration-algorithm + (default "ecdsap256sha256")) + (ksk-size knot-policy-configuration-ksk-size + (default 256)) + (zsk-size knot-policy-configuration-zsk-size + (default 256)) + (dnskey-ttl knot-policy-configuration-dnskey-ttl + (default 'default)) + (zsk-lifetime knot-policy-configuration-zsk-lifetime + (default "30d")) + (propagation-delay knot-policy-configuration-propagation-delay + (default "1d")) + (rrsig-lifetime knot-policy-configuration-rrsig-lifetime + (default "14d")) + (rrsig-refresh knot-policy-configuration-rrsig-refresh + (default "7d")) + (nsec3? knot-policy-configuration-nsec3? + (default #f)) + (nsec3-iterations knot-policy-configuration-nsec3-iterations + (default 5)) + (nsec3-salt-length knot-policy-configuration-nsec3-salt-length + (default 8)) + (nsec3-salt-lifetime knot-policy-configuration-nsec3-salt-lifetime + (default "30d"))) + +(define-record-type* + knot-zone-configuration make-knot-zone-configuration + knot-zone-configuration? + (domain knot-zone-configuration-domain + (default "")) + (file knot-zone-configuration-file + (default "")) ; the file where this zone is saved. + (zone knot-zone-configuration-zone + (default (zone-file))) ; initial content of the zone file + (master knot-zone-configuration-master + (default '())) + (ddns-master knot-zone-configuration-ddns-master + (default #f)) + (notify knot-zone-configuration-notify + (default '())) + (acl knot-zone-configuration-acl + (default '())) + (semantic-checks? knot-zone-configuration-semantic-checks? + (default #f)) + (disable-any? knot-zone-configuration-disable-any? + (default #f)) + (zonefile-sync knot-zone-configuration-zonefile-sync + (default 0)) + (dnssec-policy knot-zone-configuration-dnssec-policy + (default #f)) + (serial-policy knot-zone-configuration-serial-policy + (default 'increment))) + +(define-record-type* + knot-remote-configuration make-knot-remote-configuration + knot-remote-configuration? + (id knot-remote-configuration-id + (default "")) + (address knot-remote-configuration-address + (default '())) + (via knot-remote-configuration-via + (default '())) + (key knot-remote-configuration-key + (default #f))) + +(define-record-type* + knot-configuration make-knot-configuration + knot-configuration? + (knot knot-configuration-knot + (default knot)) + (run-directory knot-configuration-run-directory + (default "/var/run/knot")) + (listen-v4 knot-configuration-listen-v4 + (default "0.0.0.0")) + (listen-v6 knot-configuration-listen-v6 + (default "::")) + (listen-port knot-configuration-listen-port + (default 53)) + (keys knot-configuration-keys + (default '())) + (keystores knot-configuration-keystores + (default '())) + (acls knot-configuration-acls + (default '())) + (remotes knot-configuration-remotes + (default '())) + (policies knot-configuration-policies + (default '())) + (zones knot-configuration-zones + (default '()))) + +(define-syntax define-zone-entries + (syntax-rules () + ((_ id (name ttl class type data) ...) + (define id (list (make-zone-entry name ttl class type data) ...))))) + +(define (error-out msg) + (raise (condition (&message (message msg))))) + +(define (verify-knot-key-configuration key) + (unless (knot-key-configuration? key) + (error-out "keys must be a list of only knot-key-configuration.")) + (let ((id (knot-key-configuration-id key))) + (unless (and (string? id) (not (equal? id ""))) + (error-out "key id must be a non empty string."))) + (unless (memq '(#f hmac-md5 hmac-sha1 hmac-sha224 hmac-sha256 hmac-sha384 hmac-sha512) + (knot-key-configuration-algorithm key)) + (error-out "algorithm must be one of: #f, 'hmac-md5, 'hmac-sha1, +'hmac-sha224, 'hmac-sha256, 'hmac-sha384 or 'hmac-sha512"))) + +(define (verify-knot-keystore-configuration keystore) + (unless (knot-keystore-configuration? keystore) + (error-out "keystores must be a list of only knot-keystore-configuration.")) + (let ((id (knot-keystore-configuration-id keystore))) + (unless (and (string? id) (not (equal? id ""))) + (error-out "keystore id must be a non empty string."))) + (unless (memq '(pem pkcs11) + (knot-keystore-configuration-backend keystore)) + (error-out "backend must be one of: 'pem or 'pkcs11"))) + +(define (verify-knot-policy-configuration policy) + (unless (knot-keystore-configuration? policy) + (error-out "policies must be a list of only knot-policy-configuration.")) + (let ((id (knot-policy-configuration-id policy))) + (unless (and (string? id) (not (equal? id ""))) + (error-out "policy id must be a non empty string.")))) + +(define (verify-knot-acl-configuration acl) + (unless (knot-acl-configuration? acl) + (error-out "acls must be a list of only knot-acl-configuration.")) + (let ((id (knot-acl-configuration-id acl)) + (address (knot-acl-configuration-address acl)) + (key (knot-acl-configuration-key acl)) + (action (knot-acl-configuration-action acl))) + (unless (and (string? id) (not (equal? id ""))) + (error-out "acl id must be a non empty string.")) + (unless (and (list? address) + (fold (lambda (x1 x2) (and (string? x1) (string? x2))) "" address)) + (error-out "acl address must be a list of strings."))) + (unless (boolean? (knot-acl-configuration-deny? acl)) + (error-out "deny? must be #t or #f."))) + +(define (verify-knot-zone-configuration zone) + (unless (knot-zone-configuration? zone) + (error-out "zones must be a list of only knot-zone-configuration.")) + (let ((domain (knot-zone-configuration-domain zone))) + (unless (and (string? domain) (not (equal? domain ""))) + (error-out "zone domain must be a non empty string.")))) + +(define (verify-knot-remote-configuration remote) + (unless (knot-remote-configuration? remote) + (error-out "remotes must be a list of only knot-remote-configuration.")) + (let ((id (knot-remote-configuration-id remote))) + (unless (and (string? id) (not (equal? id ""))) + (error-out "remote id must be a non empty string.")))) + +(define (verify-knot-configuration config) + (unless (package? (knot-configuration-knot config)) + (error-out "knot configuration field must be a package.")) + (unless (string? (knot-configuration-run-directory config)) + (error-out "run-directory must be a string.")) + (unless (list? (knot-configuration-keys config)) + (error-out "keys must be a list of knot-key-configuration.")) + (for-each (lambda (key) (verify-knot-key-configuration key)) + (knot-configuration-keys config)) + (unless (list? (knot-configuration-keystores config)) + (error-out "keystores must be a list of knot-keystore-configuration.")) + (for-each (lambda (keystore) (verify-knot-keystore-configuration keystore)) + (knot-configuration-keystores config)) + (unless (list? (knot-configuration-acls config)) + (error-out "acls must be a list of knot-acl-configuration.")) + (for-each (lambda (acl) (verify-knot-acl-configuration acl)) + (knot-configuration-acls config)) + (unless (list? (knot-configuration-zones config)) + (error-out "zones must be a list of knot-zone-configuration.")) + (for-each (lambda (zone) (verify-knot-zone-configuration zone)) + (knot-configuration-zones config)) + (unless (list? (knot-configuration-policies config)) + (error-out "policies must be a list of knot-policy-configuration.")) + (for-each (lambda (policy) (verify-knot-policy-configuration policy)) + (knot-configuration-policies config)) + (unless (list? (knot-configuration-remotes config)) + (error-out "remotes must be a list of knot-remote-configuration.")) + (for-each (lambda (remote) (verify-knot-remote-configuration remote)) + (knot-configuration-remotes config)) + #t) + +(define (format-string-list l) + "Formats a list of string in YAML" + (if (eq? l '()) + "" + (let ((l (reverse l))) + (string-append + "[" + (fold (lambda (x1 x2) + (string-append (if (symbol? x1) (symbol->string x1) x1) ", " + (if (symbol? x2) (symbol->string x2) x2))) + (car l) (cdr l)) + "]")))) + +(define (knot-acl-config acls) + (with-output-to-string + (lambda () + (for-each + (lambda (acl-config) + (let ((id (knot-acl-configuration-id acl-config)) + (address (knot-acl-configuration-address acl-config)) + (key (knot-acl-configuration-key acl-config)) + (action (knot-acl-configuration-action acl-config)) + (deny? (knot-acl-configuration-deny? acl-config))) + (format #t " - id: ~a\n" id) + (unless (eq? address '()) + (format #t " address: ~a\n" (format-string-list address))) + (unless (eq? key '()) + (format #t " key: ~a\n" (format-string-list key))) + (unless (eq? action '()) + (format #t " action: ~a\n" (format-string-list action))) + (format #t " deny: ~a\n" (if deny? "on" "off")))) + acls)))) + +(define (knot-key-config keys) + (with-output-to-string + (lambda () + (for-each + (lambda (key-config) + (let ((id (knot-key-configuration-id key-config)) + (algorithm (knot-key-configuration-algorithm key-config)) + (secret (knot-key-configuration-secret key-config))) + (format #t " - id: ~a\n" id) + (if algorithm + (format #t " algorithm: ~a\n" (symbol->string algorithm))) + (format #t " secret: ~a\n" secret))) + keys)))) + +(define (knot-keystore-config keystores) + (with-output-to-string + (lambda () + (for-each + (lambda (keystore-config) + (let ((id (knot-keystore-configuration-id keystore-config)) + (backend (knot-keystore-configuration-backend keystore-config)) + (config (knot-keystore-configuration-config keystore-config))) + (format #t " - id: ~a\n" id) + (format #t " backend: ~a\n" (symbol->string backend)) + (format #t " config: \"~a\"\n" config))) + keystores)))) + +(define (knot-policy-config policies) + (with-output-to-string + (lambda () + (for-each + (lambda (policy-config) + (let ((id (knot-policy-configuration-id policy-config)) + (keystore (knot-policy-configuration-keystore policy-config)) + (manual? (knot-policy-configuration-manual? policy-config)) + (single-type-signing? (knot-policy-configuration-single-type-signing? + policy-config)) + (algorithm (knot-policy-configuration-algorithm policy-config)) + (ksk-size (knot-policy-configuration-ksk-size policy-config)) + (zsk-size (knot-policy-configuration-zsk-size policy-config)) + (dnskey-ttl (knot-policy-configuration-dnskey-ttl policy-config)) + (zsk-lifetime (knot-policy-configuration-zsk-lifetime policy-config)) + (propagation-delay (knot-policy-configuration-propagation-delay + policy-config)) + (rrsig-lifetime (knot-policy-configuration-rrsig-lifetime + policy-config)) + (nsec3? (knot-policy-configuration-nsec3? policy-config)) + (nsec3-iterations (knot-policy-configuration-nsec3-iterations + policy-config)) + (nsec3-salt-length (knot-policy-configuration-nsec3-salt-length + policy-config)) + (nsec3-salt-lifetime (knot-policy-configuration-nsec3-salt-lifetime + policy-config))) + (format #t " - id: ~a\n" id) + (format #t " keystore: ~a\n" keystore) + (format #t " manual: ~a\n" (if manual? "on" "off")) + (format #t " single-type-signing: ~a\n" (if single-type-signing? + "on" "off")) + (format #t " algorithm: ~a\n" algorithm) + (format #t " ksk-size: ~a\n" (number->string ksk-size)) + (format #t " zsk-size: ~a\n" (number->string zsk-size)) + (unless (eq? dnskey-ttl 'default) + (format #t " dnskey-ttl: ~a\n" dnskey-ttl)) + (format #t " zsk-lifetime: ~a\n" zsk-lifetime) + (format #t " propagation-delay: ~a\n" propagation-delay) + (format #t " rrsig-lifetime: ~a\n" rrsig-lifetime) + (format #t " nsec3: ~a\n" (if nsec3? "on" "off")) + (format #t " nsec3-iterations: ~a\n" + (number->string nsec3-iterations)) + (format #t " nsec3-salt-length: ~a\n" + (number->string nsec3-salt-length)) + (format #t " nsec3-salt-lifetime: ~a\n" nsec3-salt-lifetime))) + policies)))) + +(define (knot-remote-config remotes) + (with-output-to-string + (lambda () + (for-each + (lambda (remote-config) + (let ((id (knot-remote-configuration-id remote-config)) + (address (knot-remote-configuration-address remote-config)) + (via (knot-remote-configuration-via remote-config)) + (key (knot-remote-configuration-key remote-config))) + (format #t " - id: ~a\n" id) + (unless (eq? address '()) + (format #t " address: ~a\n" (format-string-list address))) + (unless (eq? via '()) + (format #t " via: ~a\n" (format-string-list via))) + (if key + (format #t " key: ~a\n" key)))) + remotes)))) + +(define (serialize-zone-entries entries) + (with-output-to-string + (lambda () + (for-each + (lambda (entry) + (let ((name (zone-entry-name entry)) + (ttl (zone-entry-ttl entry)) + (class (zone-entry-class entry)) + (type (zone-entry-type entry)) + (data (zone-entry-data entry))) + (format #t "~a ~a ~a ~a ~a\n" name ttl class type data))) + entries)))) + +(define (serialize-zone-file zone domain) + (computed-file (string-append domain ".zone") + #~(begin + (call-with-output-file #$output + (lambda (port) + (format port "$ORIGIN ~a.\n" + #$(zone-file-origin zone)) + (format port "@ IN SOA ~a ~a (~a ~a ~a ~a ~a)\n" + #$(zone-file-ns zone) + #$(zone-file-mail zone) + #$(zone-file-serial zone) + #$(zone-file-refresh zone) + #$(zone-file-retry zone) + #$(zone-file-expiry zone) + #$(zone-file-nx zone)) + (format port "~a\n" + #$(serialize-zone-entries (zone-file-entries zone)))))))) + +(define (knot-zone-config zone) + (let ((content (knot-zone-configuration-zone zone))) + #~(with-output-to-string + (lambda () + (let ((domain #$(knot-zone-configuration-domain zone)) + (file #$(knot-zone-configuration-file zone)) + (master (list #$@(knot-zone-configuration-master zone))) + (ddns-master #$(knot-zone-configuration-ddns-master zone)) + (notify (list #$@(knot-zone-configuration-notify zone))) + (acl (list #$@(knot-zone-configuration-acl zone))) + (semantic-checks? #$(knot-zone-configuration-semantic-checks? zone)) + (disable-any? #$(knot-zone-configuration-disable-any? zone)) + (dnssec-policy #$(knot-zone-configuration-dnssec-policy zone)) + (serial-policy '#$(knot-zone-configuration-serial-policy zone))) + (format #t " - domain: ~a\n" domain) + (if (eq? master '()) + ;; This server is a master + (if (equal? file "") + (format #t " file: ~a\n" + #$(serialize-zone-file content + (knot-zone-configuration-domain zone))) + (format #t " file: ~a\n" file)) + ;; This server is a slave (has masters) + (begin + (format #t " master: ~a\n" + #$(format-string-list + (knot-zone-configuration-master zone))) + (if ddns-master (format #t " ddns-master ~a\n" ddns-master)))) + (unless (eq? notify '()) + (format #t " notify: ~a\n" + #$(format-string-list + (knot-zone-configuration-notify zone)))) + (unless (eq? acl '()) + (format #t " acl: ~a\n" + #$(format-string-list + (knot-zone-configuration-acl zone)))) + (format #t " semantic-checks: ~a\n" (if semantic-checks? "on" "off")) + (format #t " disable-any: ~a\n" (if disable-any? "on" "off")) + (if dnssec-policy + (begin + (format #t " dnssec-signing: on\n") + (format #t " dnssec-policy: ~a\n" dnssec-policy))) + (format #t " serial-policy: ~a\n" + (symbol->string serial-policy))))))) + +(define (knot-config-file config) + (verify-knot-configuration config) + (computed-file "knot.conf" + #~(begin + (call-with-output-file #$output + (lambda (port) + (format port "server:\n") + (format port " rundir: ~a\n" #$(knot-configuration-run-directory config)) + (format port " user: knot\n") + (format port " listen: ~a@~a\n" + #$(knot-configuration-listen-v4 config) + #$(knot-configuration-listen-port config)) + (format port " listen: ~a@~a\n" + #$(knot-configuration-listen-v6 config) + #$(knot-configuration-listen-port config)) + (format port "\nkey:\n") + (format port #$(knot-key-config (knot-configuration-keys config))) + (format port "\nkeystore:\n") + (format port #$(knot-keystore-config (knot-configuration-keystores config))) + (format port "\nacl:\n") + (format port #$(knot-acl-config (knot-configuration-acls config))) + (format port "\nremote:\n") + (format port #$(knot-remote-config (knot-configuration-remotes config))) + (format port "\npolicy:\n") + (format port #$(knot-policy-config (knot-configuration-policies config))) + (unless #$(eq? (knot-configuration-zones config) '()) + (format port "\nzone:\n") + (format port "~a\n" + (string-concatenate + (list #$@(map knot-zone-config + (knot-configuration-zones config))))))))))) + +(define %knot-accounts + (list (user-group (name "knot") (system? #t)) + (user-account + (name "knot") + (group "knot") + (system? #t) + (comment "knot dns server user") + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) + +(define (knot-activation config) + #~(begin + (use-modules (guix build utils)) + (define (mkdir-p/perms directory owner perms) + (mkdir-p directory) + (chown directory (passwd:uid owner) (passwd:gid owner)) + (chmod directory perms)) + (mkdir-p/perms #$(knot-configuration-run-directory config) + (getpwnam "knot") #o755) + (mkdir-p/perms "/var/lib/knot" (getpwnam "knot") #o755) + (mkdir-p/perms "/var/lib/knot/keys" (getpwnam "knot") #o755) + (mkdir-p/perms "/var/lib/knot/keys/keys" (getpwnam "knot") #o755))) + +(define (knot-shepherd-service config) + (let* ((config-file (knot-config-file config)) + (knot (knot-configuration-knot config))) + (list (shepherd-service + (documentation "Run the Knot DNS daemon.") + (provision '(knot dns)) + (requirement '(networking)) + (start #~(make-forkexec-constructor + (list (string-append #$knot "/sbin/knotd") + "-c" #$config-file))) + (stop #~(make-kill-destructor)))))) + +(define knot-service-type + (service-type (name 'knot) + (extensions + (list (service-extension shepherd-root-service-type + knot-shepherd-service) + (service-extension activation-service-type + knot-activation) + (service-extension account-service-type + (const %knot-accounts))))))