code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017 Julien Lepiller <julien@lepiller.eu>
;;; Copyright © 2018 Oleg Pykhalov <go.wigust@gmail.com>
;;;
;;; 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 <http://www.gnu.org/licenses/>.

(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-26)
  #: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

            dnsmasq-service-type
            dnsmasq-configuration

            ddclient-service-type
            ddclient-configuration))

;;;
;;; Knot DNS.
;;;

(define-record-type* <knot-key-configuration>
  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>
  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>
  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>
  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 (* 2 24 3600)))
  (retry   zone-file-retry
           (default (* 15 60)))
  (expiry  zone-file-expiry
           (default (* 2 7 24 3600)))
  (nx      zone-file-nx
           (default 3600)))
(define-record-type* <knot-keystore-configuration>
  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>
  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 (* 30 24 3600)))
  (propagation-delay    knot-policy-configuration-propagation-delay
                        (default (* 24 3600)))
  (rrsig-lifetime       knot-policy-configuration-rrsig-lifetime
                        (default (* 14 24 3600)))
  (rrsig-refresh        knot-policy-configuration-rrsig-refresh
                        (default (* 7 24 3600)))
  (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 (* 30 24 3600))))

(define-record-type* <knot-zone-configuration>
  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>
  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>
  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-policy-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))))))

;;;
;;; Dnsmasq.
;;;

(define-record-type* <dnsmasq-configuration>
  dnsmasq-configuration make-dnsmasq-configuration
  dnsmasq-configuration?
  (package          dnsmasq-configuration-package
                    (default dnsmasq))  ;package
  (no-hosts?        dnsmasq-configuration-no-hosts?
                    (default #f))       ;boolean
  (port             dnsmasq-configuration-port
                    (default 53))       ;integer
  (local-service?   dnsmasq-configuration-local-service?
                    (default #t))       ;boolean
  (listen-addresses dnsmasq-configuration-listen-address
                    (default '()))      ;list of string
  (resolv-file      dnsmasq-configuration-resolv-file
                    (default "/etc/resolv.conf")) ;string
  (no-resolv?       dnsmasq-configuration-no-resolv?
                    (default #f))       ;boolean
  (servers          dnsmasq-configuration-servers
                    (default '()))      ;list of string
  (cache-size       dnsmasq-configuration-cache-size
                    (default 150))      ;integer
  (negative-cache?  dnsmasq-configuration-negative-cache?
                    (default #t)))      ;boolean

(define dnsmasq-shepherd-service
  (match-lambda
    (($ <dnsmasq-configuration> package
                                no-hosts?
                                port local-service? listen-addresses
                                resolv-file no-resolv? servers
                                cache-size negative-cache?)
     (shepherd-service
      (provision '(dnsmasq))
      (requirement '(networking))
      (documentation "Run the dnsmasq DNS server.")
      (start #~(make-forkexec-constructor
                '(#$(file-append package "/sbin/dnsmasq")
                  "--keep-in-foreground"
                  "--pid-file=/run/dnsmasq.pid"
                  #$@(if no-hosts?
                         '("--no-hosts")
                         '())
                  #$(format #f "--port=~a" port)
                  #$@(if local-service?
                         '("--local-service")
                         '())
                  #$@(map (cut format #f "--listen-address=~a" <>)
                          listen-addresses)
                  #$(format #f "--resolv-file=~a" resolv-file)
                  #$@(if no-resolv?
                         '("--no-resolv")
                         '())
                  #$@(map (cut format #f "--server=~a" <>)
                          servers)
                  #$(format #f "--cache-size=~a" cache-size)
                  #$@(if negative-cache?
                         '()
                         '("--no-negcache")))
                #:pid-file "/run/dnsmasq.pid"))
      (stop #~(make-kill-destructor))))))

(define dnsmasq-service-type
  (service-type
   (name 'dnsmasq)
   (extensions
    (list (service-extension shepherd-root-service-type
                             (compose list dnsmasq-shepherd-service))))
   (default-value (dnsmasq-configuration))
   (description "Run the dnsmasq DNS server.")))

;;;
;;; ddclient
;;;

(define (uglify-field-name field-name)
  (string-delete #\? (symbol->string field-name)))

(define (serialize-field field-name val)
  (format #t "~a=~a\n" (uglify-field-name field-name) val))

(define (serialize-boolean field-name val)
  (serialize-field field-name (if val "yes" "no")))

(define (serialize-integer field-name val)
  (serialize-field field-name (number->string val)))

(define (serialize-string field-name val)
  (if (and (string? val) (string=? val ""))
      ""
      (serialize-field field-name val)))

(define (serialize-list field-name val)
  (if (null? val) "" (serialize-field field-name (string-join val))))

(define (serialize-extra-options extra-options)
  (string-join extra-options "\n" 'suffix))

(define-configuration ddclient-configuration
  (ddclient
   (package ddclient)
   "The ddclient package.")
  (daemon
   (integer 300)
   "The period after which ddclient will retry to check IP and domain name.")
  (syslog
   (boolean #t)
   "Use syslog for the output.")
  (mail
   (string "root")
   "Mail to user.")
  (mail-failure
   (string "root")
   "Mail failed update to user.")
  (pid
   (string "/var/run/ddclient/ddclient.pid")
   "The ddclient PID file.")
  (ssl
   (boolean #t)
   "Enable SSL support.")
  (user
   (string "ddclient")
   "Specifies the user name or ID that is used when running ddclient
program.")
  (group
   (string "ddclient")
   "Group of the user who will run the ddclient program.")
  (secret-file
   (string "/etc/ddclient/secrets.conf")
   "Secret file which will be appended to @file{ddclient.conf} file.  This
file contains credentials for use by ddclient.  You are expected to create it
manually.")
  (extra-options
   (list '())
   "Extra options will be appended to @file{ddclient.conf} file."))

(define (ddclient-account config)
  "Return the user accounts and user groups for CONFIG."
  (let ((ddclient-user (ddclient-configuration-user config))
        (ddclient-group (ddclient-configuration-group config)))
    (list (user-group
           (name ddclient-group)
           (system? #t))
          (user-account
           (name ddclient-user)
           (system? #t)
           (group ddclient-group)
           (comment "ddclientd privilege separation user")
           (home-directory (string-append "/var/run/" ddclient-user))))))

(define (ddclient-activation config)
  "Return the activation GEXP for CONFIG."
  (with-imported-modules '((guix build utils)
                           (ice-9 rdelim))
    #~(begin
        (use-modules (guix build utils)
                     (ice-9 rdelim))
        (let ((ddclient-user
               #$(passwd:uid (getpw (ddclient-configuration-user config))))
              (ddclient-group
               #$(passwd:gid (getpw (ddclient-configuration-group config))))
              (ddclient-secret-file
               #$(ddclient-configuration-secret-file config)))
          ;; 'ddclient' complains about ddclient.conf file permissions, which
          ;; rules out /gnu/store.  Thus we copy the ddclient.conf to /etc.
          (for-each (lambda (dir)
                      (mkdir-p dir)
                      (chmod dir #o700)
                      (chown dir ddclient-user ddclient-group))
                    '("/var/cache/ddclient" "/var/run/ddclient"
                      "/etc/ddclient"))
          (with-output-to-file "/etc/ddclient/ddclient.conf"
            (lambda ()
              (display
               (string-append
                "# Generated by 'ddclient-service'.\n\n"
                #$(with-output-to-string
                    (lambda ()
                      (serialize-configuration config
                                               ddclient-configuration-fields)))
                (if (string-null? ddclient-secret-file)
                    ""
                    (format #f "\n\n# Appended from '~a'.\n\n~a"
                            ddclient-secret-file
                            (with-input-from-file ddclient-secret-file
                              read-string)))))))
          (chmod "/etc/ddclient/ddclient.conf" #o600)
          (chown "/etc/ddclient/ddclient.conf"
                 ddclient-user ddclient-group)))))

(define (ddclient-shepherd-service config)
  "Return a <shepherd-service> for ddclient with CONFIG."
  (let ((ddclient (ddclient-configuration-ddclient config))
        (ddclient-pid (ddclient-configuration-pid config))
        (ddclient-user (ddclient-configuration-user config))
        (ddclient-group (ddclient-configuration-group config)))
    (list (shepherd-service
           (provision '(ddclient))
           (documentation "Run ddclient daemon.")
           (start #~(make-forkexec-constructor
                     (list #$(file-append ddclient "/bin/ddclient")
                           "-foreground"
                           "-file" "/etc/ddclient/ddclient.conf")
                     #:pid-file #$ddclient-pid
                     #:environment-variables
                     (list "SSL_CERT_DIR=/run/current-system/profile\
/etc/ssl/certs"
                           "SSL_CERT_FILE=/run/current-system/profile\
/etc/ssl/certs/ca-certificates.crt")
                     #:user #$ddclient-user
                     #:group #$ddclient-group))
           (stop #~(make-kill-destructor))))))

(define ddclient-service-type
  (service-type
   (name 'ddclient)
   (extensions
    (list (service-extension account-service-type
                             ddclient-account)
          (service-extension shepherd-root-service-type
                             ddclient-shepherd-service)
          (service-extension activation-service-type
                             ddclient-activation)))
   (default-value (ddclient-configuration))
   (description "Configure address updating utility for dynamic DNS services,
ddclient.")))

(define (generate-ddclient-documentation)
  (generate-documentation
   `((ddclient-configuration ,ddclient-configuration-fields))
   'ddclient-configuration))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Arun Isaac <arunisaac@systemreboot.net>
;;;
;;; 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 <http://www.gnu.org/licenses/>.

(define-module (gnu services games)
  #:use-module (gnu services)
  #:use-module (gnu services shepherd)
  #:use-module (gnu packages admin)
  #:use-module (gnu packages games)
  #:use-module (gnu system shadow)
  #:use-module (guix gexp)
  #:use-module (guix modules)
  #:use-module (guix records)
  #:use-module (ice-9 match)
  #:export (wesnothd-configuration
            wesnothd-configuration?
            wesnothd-service-type))

;;;
;;; The Battle for Wesnoth server
;;;

(define-record-type* <wesnothd-configuration>
  wesnothd-configuration make-wesnothd-configuration wesnothd-configuration?
  (package wesnothd-configuration-package
           (default wesnoth-server))
  (port wesnothd-configuration-port
        (default 15000)))

(define %wesnothd-accounts
  (list (user-account
         (name "wesnothd")
         (group "wesnothd")
         (system? #t)
         (comment "Wesnoth daemon user")
         (home-directory "/var/empty")
         (shell (file-append shadow "/sbin/nologin")))
        (user-group
         (name "wesnothd")
         (system? #t))))

(define wesnothd-shepherd-service
  (match-lambda
    (($ <wesnothd-configuration> package port)
     (with-imported-modules (source-module-closure
                             '((gnu build shepherd)))
       (shepherd-service
        (documentation "The Battle for Wesnoth server")
        (provision '(wesnoth-daemon))
        (requirement '(networking))
        (modules '((gnu build shepherd)))
        (start #~(make-forkexec-constructor/container
                  (list #$(file-append package "/bin/wesnothd")
                        "-p" #$(number->string port))))
        (stop #~(make-kill-destructor)))))))

(define wesnothd-service-type
  (service-type
   (name 'wesnothd)
   (description
    "Run The Battle for Wesnoth server @command{wesnothd}.")
   (extensions
    (list (service-extension account-service-type
                             (const %wesnothd-accounts))
          (service-extension shepherd-root-service-type
                             (compose list wesnothd-shepherd-service))))
   (default-value (wesnothd-configuration))))

license: mit


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2016, 2017, 2018 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
;;;
;;; 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 <http://www.gnu.org/licenses/>.

(define-module (gnu services herd)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-9)
  #:use-module (srfi srfi-11)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (ice-9 match)
  #:export (%shepherd-socket-file

            shepherd-error?
            service-not-found-error?
            service-not-found-error-service
            action-not-found-error?
            action-not-found-error-service
            action-not-found-error-action
            action-exception-error?
            action-exception-error-service
            action-exception-error-action
            action-exception-error-key
            action-exception-error-arguments
            unknown-shepherd-error?
            unknown-shepherd-error-sexp

            live-service?
            live-service-provision
            live-service-requirement
            live-service-running

            with-shepherd-action
            current-services
            unload-services
            unload-service
            load-services
            load-services/safe
            start-service
            stop-service))

;;; Commentary:
;;;
;;; This module provides an interface to the GNU Shepherd, similar to the
;;; 'herd' command.  Essentially it implements a subset of the (shepherd comm)
;;; module, but focusing only on the parts relevant to 'guix system
;;; reconfigure'.
;;;
;;; Code:

(define %shepherd-socket-file
  (make-parameter "/var/run/shepherd/socket"))

(define* (open-connection #:optional (file (%shepherd-socket-file)))
  "Open a connection to the daemon, using the Unix-domain socket at FILE, and
return the socket."
  ;; The protocol is sexp-based and UTF-8-encoded.
  (with-fluids ((%default-port-encoding "UTF-8"))
    (let ((sock    (socket PF_UNIX SOCK_STREAM 0))
          (address (make-socket-address PF_UNIX file)))
      (catch 'system-error
        (lambda ()
          (connect sock address)
          (setvbuf sock _IOFBF 1024)
          sock)
        (lambda args
          (close-port sock)
          (apply throw args))))))

(define-syntax-rule (with-shepherd connection body ...)
  "Evaluate BODY... with CONNECTION bound to an open socket to PID 1."
  (let ((connection (open-connection)))
    (dynamic-wind
      (const #t)
      (lambda ()
        body ...)
      (lambda ()
        (close-port connection)))))

(define-condition-type &shepherd-error &error
  shepherd-error?)

(define-condition-type &service-not-found-error &shepherd-error
  service-not-found-error?
  (service service-not-found-error-service))

(define-condition-type &action-not-found-error &shepherd-error
  action-not-found-error?
  (service action-not-found-error-service)
  (action  action-not-found-error-action))

(define-condition-type &action-exception-error &shepherd-error
  action-exception-error?
  (service action-exception-error-service)
  (action  action-exception-error-action)
  (key     action-exception-error-key)
  (args    action-exception-error-arguments))

(define-condition-type &unknown-shepherd-error &shepherd-error
  unknown-shepherd-error?
  (sexp   unknown-shepherd-error-sexp))

(define (raise-shepherd-error error)
  "Raise an error condition corresponding to ERROR, an sexp received by a
shepherd client in reply to COMMAND, a command object.  Return #t if ERROR
does not denote an error."
  (match error
    (('error ('version 0 x ...) 'service-not-found service)
     (raise (condition (&service-not-found-error
                        (service service)))))
    (('error ('version 0 x ...) 'action-not-found action service)
     (raise (condition (&action-not-found-error
                        (service service)
                        (action action)))))
    (('error ('version 0 x ...) 'action-exception action service
             key (args ...))
     (raise (condition (&action-exception-error
                        (service service)
                        (action action)
                        (key key) (args args)))))
    (('error . _)
     (raise (condition (&unknown-shepherd-error (sexp error)))))
    (#f                                           ;not an error
     #t)))

(define (display-message message)
  (format (current-error-port) "shepherd: ~a~%" message))

(define* (invoke-action service action arguments cont)
  "Invoke ACTION on SERVICE with ARGUMENTS.  On success, call CONT with the
list of results (one result per instance with the name SERVICE).  Otherwise
return #f."
  (with-shepherd sock
    (write `(shepherd-command (version 0)
                              (action ,action)
                              (service ,service)
                              (arguments ,arguments)
                              (directory ,(getcwd)))
           sock)
    (force-output sock)

    (match (read sock)
      (('reply ('version 0 _ ...) ('result result) ('error #f)
               ('messages messages))
       (for-each display-message messages)
       (cont result))
      (('reply ('version 0 x ...) ('result y) ('error error)
               ('messages messages))
       (for-each display-message messages)
       (raise-shepherd-error error)
       #f)
      (x
       ;; invalid reply
       #f))))

(define-syntax-rule (with-shepherd-action service (action args ...)
                      result body ...)
  "Invoke ACTION on SERVICE with the given ARGS, and evaluate BODY with RESULT
bound to the action's result."
  (invoke-action service action (list args ...)
                 (lambda (result) body ...)))

(define-syntax alist-let*
  (syntax-rules ()
    "Bind the given KEYs in EXP to the corresponding items in ALIST.  ALIST
is assumed to be a list of two-element tuples rather than a traditional list
of pairs."
    ((_ alist (key ...) exp ...)
     (let ((key (and=> (assoc-ref alist 'key) car)) ...)
       exp ...))))

;; Information about live Shepherd services.
(define-record-type <live-service>
  (live-service provision requirement running)
  live-service?
  (provision    live-service-provision)           ;list of symbols
  (requirement  live-service-requirement)         ;list of symbols
  (running      live-service-running))            ;#f | object

(define (current-services)
  "Return the list of currently defined Shepherd services, represented as
<live-service> objects.  Return #f if the list of services could not be
obtained."
  (with-shepherd-action 'root ('status) results
    ;; We get a list of results, one for each service with the name 'root'.
    ;; In practice there's only one such service though.
    (match results
      ((services _ ...)
       (match services
         ((('service ('version 0 _ ...) _ ...) ...)
          (map (lambda (service)
                 (alist-let* service (provides requires running)
                   (live-service provides requires running)))
               services))
         (x
          #f))))))

(define (unload-service service)
  "Unload SERVICE, a symbol name; return #t on success."
  (with-shepherd-action 'root ('unload (symbol->string service)) result
    (first result)))

(define (%load-file file)
  "Load FILE in the Shepherd."
  (with-shepherd-action 'root ('load file) result
    (first result)))

(define (eval-there exp)
  "Eval EXP in the Shepherd."
  (with-shepherd-action 'root ('eval (object->string exp)) result
    (first result)))

(define (load-services files)
  "Load and register the services from FILES, where FILES contain code that
returns a shepherd <service> object."
  (eval-there `(register-services
                ,@(map (lambda (file)
                         `(primitive-load ,file))
                       files))))

(define (load-services/safe files)
  "This is like 'load-services', but make sure only the subset of FILES that
can be safely reloaded is actually reloaded.

This is done to accommodate the Shepherd < 0.15.0 where services lacked the
'replacement' slot, and where 'register-services' would throw an exception
when passed a service with an already-registered name."
  (eval-there `(let* ((services     (map primitive-load ',files))
                      (slots        (map slot-definition-name
                                         (class-slots <service>)))
                      (can-replace? (memq 'replacement slots)))
                 (define (registered? service)
                   (not (null? (lookup-services (canonical-name service)))))

                 (apply register-services
                        (if can-replace?
                            services
                            (remove registered? services))))))

(define (start-service name)
  (with-shepherd-action name ('start) result
    result))

(define (stop-service name)
  (with-shepherd-action name ('stop) result
    result))

;; Local Variables:
;; eval: (put 'alist-let* 'scheme-indent-function 2)
;; eval: (put 'with-shepherd 'scheme-indent-function 1)
;; eval: (put 'with-shepherd-action 'scheme-indent-function 3)
;; End:

;;; herd.scm ends here


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2016 John Darrington <jmd@gnu.org>
;;;
;;; 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 <http://www.gnu.org/licenses/>.

(define-module (gnu services kerberos)
  #:use-module (gnu services)
  #:use-module (gnu services configuration)
  #:use-module (gnu system pam)
  #:use-module (guix gexp)
  #:use-module (guix records)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (ice-9 match)
  #:export (pam-krb5-configuration
            pam-krb5-configuration?
            pam-krb5-service-type

            krb5-realm
            krb5-realm?

            krb5-configuration
            krb5-configuration?
            krb5-service-type))

(define unset-field (list 'unset-field))

(define (predicate/unset pred)
  (lambda (x) (or (eq? x unset-field) (pred x))))

(define string/unset? (predicate/unset string?))
(define boolean/unset? (predicate/unset boolean?))
(define integer/unset? (predicate/unset integer?))

(define (uglify-field-name field-name)
  "Return FIELD-NAME with all instances of '-' replaced by '_' and any
trailing '?' removed."
  (let ((str (symbol->string field-name)))
    (string-join (string-split (if (string-suffix? "?" str)
                                   (substring str 0 (1- (string-length str)))
                                   str)
                               #\-)
                 "_")))

(define (serialize-field* field-name val)
  (format #t "~a = ~a\n" (uglify-field-name field-name) val))

(define (serialize-string/unset field-name val)
  (unless (eq? val unset-field)
      (serialize-field* field-name val)))

(define (serialize-integer/unset field-name val)
  (unless (eq? val unset-field)
      (serialize-field* field-name val)))

(define (serialize-boolean/unset field-name val)
  (unless (eq? val unset-field)
      (serialize-field* field-name
                        (if val "true" "false"))))

;; An end-point is an address such as "192.168.0.1"
;; or an address port pair ("foobar.example.com" . 109)
(define (end-point? val)
  (match val
    ((? string?) #t)
    (((? string?) . (? integer?)) #t)
    (_ #f)))

(define (serialize-end-point field-name val)
  (serialize-field* field-name
                    (match val
                      ((host . port)
                       ;; The [] are needed in the case of IPv6 addresses
                       (format #f "[~a]:~a" host port))
                      (host
                       (format #f "~a" host)))))

(define (serialize-space-separated-string-list/unset field-name val)
  (unless (eq? val unset-field)
      (serialize-field* field-name (string-join val " "))))

(define (space-separated-string-list? val)
  (and (list? val)
       (and-map (lambda (x)
                  (and (string? x) (not (string-index x #\space))))
                val)))

(define space-separated-string-list/unset?
  (predicate/unset space-separated-string-list?))

(define comma-separated-integer-list/unset?
  (predicate/unset (lambda (val)
                     (and (list? val)
                          (and-map (lambda (x) (integer? x))
                                   val)))))

(define (serialize-comma-separated-integer-list/unset field-name val)
  (unless (eq? val unset-field)
      (serialize-field* field-name
                       (string-drop ; Drop the leading comma
                        (fold
                         (lambda (i prev)
                           (string-append prev "," (number->string i)))
                         "" val) 1))))

(define file-name? (predicate/unset
                    (lambda (val)
                      (string-prefix? "/" val))))

(define (serialize-field field-name val)
  (format #t "~a ~a\n" (uglify-field-name field-name) val))

(define (serialize-string field-name val)
  (serialize-field field-name val))

(define (serialize-file-name field-name val)
  (unless (eq? val unset-field)
    (serialize-string field-name val)))

(define (serialize-space-separated-string-list field-name val)
  (serialize-field field-name (string-join val " ")))

(define (non-negative-integer? val)
  (and (exact-integer? val) (not (negative? val))))

(define (serialize-non-negative-integer/unset field-name val)
  (unless (eq? val unset-field)
    (serialize-field* field-name val)))

(define (free-form-fields? val)
  (match val
    (() #t)
    ((((? symbol?) . (? string)) . val) (free-form-fields? val))
    (_ #f)))

(define (serialize-free-form-fields field-name val)
  (for-each (match-lambda ((k . v) (serialize-field* k v))) val))

(define non-negative-integer/unset? (predicate/unset non-negative-integer?))

(define (realm-list? val)
  (and (list? val)
       (and-map (lambda (x) (krb5-realm? x)) val)))

(define (serialize-realm-list field-name val)
  (format #t "\n[~a]\n" field-name)
  (for-each (lambda (realm)
              (format #t "\n~a = {\n" (krb5-realm-name realm))
              (for-each (lambda (field)
                          (unless (eq? 'name (configuration-field-name field))
                            ((configuration-field-serializer field)
                             (configuration-field-name field)
                             ((configuration-field-getter field)
                              realm)))) krb5-realm-fields)

              (format #t "}\n")) val))

;; For a more detailed explanation of these fields see man 5 krb5.conf
(define-configuration krb5-realm
  (name
   (string/unset unset-field)
   "The name of the realm.")

  (kdc
   (end-point unset-field)
   "The host and port on which the realm's Key Distribution Server listens.")

  (admin-server
   (string/unset unset-field)
   "The Host running the administration server for the realm.")

  (master-kdc
   (string/unset unset-field)
   "If an attempt to get credentials fails because of an invalid password,
the client software will attempt to contact the master KDC.")

  (kpasswd-server
   (string/unset unset-field)
   "The server where password changes are performed.")

  (auth-to-local
   (free-form-fields '())
   "Rules to map between principals and local users.")

  (auth-to-local-names
   (free-form-fields '())
   "Explicit mappings between principal names and local user names.")

  (http-anchors
   (free-form-fields '())
   "Useful only when http proxy is used to access KDC or KPASSWD.")

  ;; The following are useful only for working with V4 services
  (default-domain
    (string/unset unset-field)
    "The domain used to expand host names when translating Kerberos 4 service
principals to Kerberos 5 principals")

  (v4-instance-convert
   (free-form-fields '())
   "Exceptions to the default-domain mapping rule.")

  (v4-realm
   (string/unset unset-field)
   "Used  when the V4 realm name and the V5 realm name are not the same, but
still share the same principal names and passwords"))

;; For a more detailed explanation of these fields see man 5 krb5.conf
(define-configuration krb5-configuration
  (allow-weak-crypto?
   (boolean/unset unset-field)
   "If true, permits access to services which only offer weak encryption.")

  (ap-req-checksum-type
   (non-negative-integer/unset unset-field)
   "The type of the AP-REQ checksum.")

  (canonicalize?
   (boolean/unset unset-field)
   "Should principals in initial ticket requests be canonicalized?")

  (ccache-type
   (non-negative-integer/unset unset-field)
   "The format of the credential cache type.")

  (clockskew
   (non-negative-integer/unset unset-field)
   "Maximum allowable clock skew in seconds (default 300).")

  (default-ccache-name
    (file-name unset-field)
    "The name of the default credential cache.")

  (default-client-keytab-name
    (file-name unset-field)
    "The name of the default keytab for client credentials.")

  (default-keytab-name
    (file-name unset-field)
    "The name of the default keytab file.")

  (default-realm
    (string/unset unset-field)
    "The realm to be accessed if not explicitly specified by clients.")

  (default-tgs-enctypes
    (free-form-fields '())
    "Session key encryption types when making TGS-REQ requests.")

  (default-tkt-enctypes
    (free-form-fields '())
    "Session key encryption types when making AS-REQ requests.")

  (dns-canonicalize-hostname?
   (boolean/unset  unset-field)
   "Whether name lookups will be used to canonicalize host names for use in
service principal names.")

  (dns-lookup-kdc?
   (boolean/unset unset-field)
 "Should DNS SRV records should be used to locate the KDCs and other servers
not appearing in the realm specification")

  (err-fmt
   (string/unset unset-field)
   "Custom error message formatting. If not #f error messages will be formatted
by substituting a normal error message for %M and an error code for %C in the
value.")

  (forwardable?
   (boolean/unset unset-field)
   "Should initial tickets be forwardable by default?")

  (ignore-acceptor-hostname?
   (boolean/unset unset-field)
   "When accepting GSSAPI or krb5 security contexts for host-based service
principals, ignore any hostname passed by the calling application, and allow
clients to authenticate to any service principal in the keytab matching the
service name and realm name.")

  (k5login-authoritative?
   (boolean/unset unset-field)
   "If this flag is true, principals must be listed in a local user's k5login
file to be granted login access, if a ~/.k5login file exists.")

  (k5login-directory
   (string/unset unset-field)
   "If not #f, the library will look for a local user's @file{k5login} file
within the named directory (instead of the user's home directory), with a
file name corresponding to the local user name.")

  (kcm-mach-service
   (string/unset unset-field)
   "The name of the bootstrap service used to contact the KCM daemon for the
KCM credential cache type.")

  (kcm-socket
   (file-name unset-field)
 "Path to the Unix domain socket used to access the KCM daemon for the KCM
credential cache type.")

  (kdc-default-options
   (non-negative-integer/unset unset-field)
   "Default KDC options (logored for multiple values) when requesting initial
tickets.")

  (kdc-timesync
   (non-negative-integer/unset unset-field)
   "Attempt to compensate for clock skew between the KDC and client.")

  (kdc-req-checksum-type
   (non-negative-integer/unset unset-field)
   "The type of checksum to use for the KDC requests. Relevant only for DES
keys")

  (noaddresses?
   (boolean/unset unset-field)
   "If true, initial ticket requests will not be made with address restrictions.
This enables their use across NATs.")

  (permitted-enctypes
   (space-separated-string-list/unset unset-field)
   "All encryption types that are permitted for use in session key encryption.")

  (plugin-base-dir
   (file-name unset-field)
   "The directory where krb5 plugins are located.")

  (preferred-preauth-types
   (comma-separated-integer-list/unset unset-field)
   "The preferred pre-authentication types which the client will attempt before
others.")

  (proxiable?
   (boolean/unset unset-field)
   "Should initial tickets be proxiable by default?")

  (rdns?
   (boolean/unset unset-field)
   "Should reverse DNS lookup be used in addition to forward name lookup to
canonicalize host names for use in service principal names.")

  (realm-try-domains
   (integer/unset unset-field)
   "Should a host's domain components should be used to determine the Kerberos
realm of the host.")

  (renew-lifetime
   (non-negative-integer/unset unset-field)
   "The default renewable lifetime for initial ticket requests.")

  (safe-checksum-type
   (non-negative-integer/unset unset-field)
   "The type of checksum to use for the KRB-SAFE requests.")

  (ticket-lifetime
   (non-negative-integer/unset unset-field)
   "The default lifetime for initial ticket requests.")

  (udp-preference-limit
   (non-negative-integer/unset unset-field)
   "When sending messages to the KDC, the library will try using TCP
before UDP if the size of the message greater than this limit.")

  (verify-ap-rereq-nofail?
   (boolean/unset unset-field)
 "If true, then attempts to verify initial credentials will fail if the client
machine does not have a keytab.")

  (realms
   (realm-list '())
   "The list of realms which clients may access."))

(define (krb5-configuration-file config)
  "Create a Kerberos 5 configuration file based on CONFIG"
  (mixed-text-file "krb5.conf"
                   "[libdefaults]\n\n"
                   (with-output-to-string
                     (lambda ()
                       (serialize-configuration config
                                                krb5-configuration-fields)))))

(define (krb5-etc-service config)
  (list `("krb5.conf" ,(krb5-configuration-file config))))

(define krb5-service-type
  (service-type (name 'krb5)
                (extensions
                 (list (service-extension etc-service-type
                                          krb5-etc-service)))))

(define-record-type* <pam-krb5-configuration>
  pam-krb5-configuration  make-pam-krb5-configuration
  pam-krb5-configuration?
  (pam-krb5               pam-krb5-configuration-pam-krb5
                          (default pam-krb5))
  (minimum-uid            pam-krb5-configuration-minimum-uid
                          (default 1000)))

(define (pam-krb5-pam-service config)
  "Return a PAM service for Kerberos authentication."
  (lambda (pam)
    (define pam-krb5-module
      #~(string-append #$(pam-krb5-configuration-pam-krb5 config)
                       "/lib/security/pam_krb5.so"))

    (let ((pam-krb5-sufficient
           (pam-entry
            (control "sufficient")
            (module pam-krb5-module)
            (arguments
             (list
              (format #f "minimum_uid=~a"
                      (pam-krb5-configuration-minimum-uid config)))))))
      (pam-service
       (inherit pam)
       (auth (cons* pam-krb5-sufficient
                    (pam-service-auth pam)))
       (session (cons* pam-krb5-sufficient
                       (pam-service-session pam)))
       (account (cons* pam-krb5-sufficient
                       (pam-service-account pam)))))))

(define (pam-krb5-pam-services config)
  (list (pam-krb5-pam-service config)))

(define pam-krb5-service-type
  (service-type (name 'pam-krb5)
                (extensions
                 (list
                  (service-extension pam-root-service-type
                                     pam-krb5-pam-services)))))


code:
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015 Alex Kost <alezost@gmail.com>
;;; Copyright © 2015, 2016 Ludovic Courtès <ludo@gnu.org>
;;;
;;; 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 <http://www.gnu.org/licenses/>.

(define-module (gnu services lirc)
  #:use-module (gnu services)
  #:use-module (gnu services shepherd)
  #:use-module (gnu packages lirc)
  #:use-module (guix gexp)
  #:use-module (guix records)
  #:use-module (ice-9 match)
  #:export (lirc-configuration
            lirc-configuation?
            lirc-service
            lirc-service-type))

;;; Commentary:
;;;
;;; LIRC service.
;;;
;;; Code:

(define-record-type* <lirc-configuration>
  lirc-configuration make-lirc-configuration
  lirc-configuation?
  (lirc          lirc-configuration-lirc          ;<package>
                 (default lirc))
  (device        lirc-configuration-device)       ;string
  (driver        lirc-configuration-driver)       ;string
  (config-file   lirc-configuration-file)         ;string | file-like object
  (extra-options lirc-configuration-options       ;list of strings
                 (default '())))

(define %lirc-activation
  #~(begin
      (use-modules (guix build utils))
      (mkdir-p "/var/run/lirc")))

(define lirc-shepherd-service
  (match-lambda
    (($ <lirc-configuration> lirc device driver config-file options)
     (list (shepherd-service
            (provision '(lircd))
            (documentation "Run the LIRC daemon.")
            (requirement '(user-processes))
            (start #~(make-forkexec-constructor
                      (list (string-append #$lirc "/sbin/lircd")
                            "--nodaemon"
                            #$@(if device
                                   #~("--device" #$device)
                                   #~())
                            #$@(if driver
                                   #~("--driver" #$driver)
                                   #~())
                            #$@(if config-file
                                   #~(#$config-file)
                                   #~())
                            #$@options)))
            (stop #~(make-kill-destructor)))))))

(define lirc-service-type
  (service-type (name 'lirc)
                (extensions
                 (list (service-extension shepherd-root-service-type
                                          lirc-shepherd-service)
                       (service-extension activation-service-type
                                          (const %lirc-activation))))))

(define* (lirc-service #:key (lirc lirc)
                       device driver config-file
                       (extra-options '()))
  "Return a service that runs @url{http://www.lirc.org,LIRC}, a daemon that
decodes infrared signals from remote controls.

The daemon will use specified @var{device}, @var{driver} and
@var{config-file} (configuration file name).

Finally, @var{extra-options} is a list of additional command-line options
passed to @command{lircd}."
  (service lirc-service-type
           (lirc-configuration
            (lirc lirc)
            (device device) (driver driver)
            (config-file config-file)
            (extra-options extra-options))))

;;; lirc.scm ends here