summary refs log tree commit diff
path: root/gnu/services
diff options
context:
space:
mode:
Diffstat (limited to 'gnu/services')
-rw-r--r--gnu/services/certbot.scm185
-rw-r--r--gnu/services/cuirass.scm22
-rw-r--r--gnu/services/linux.scm2
-rw-r--r--gnu/services/networking.scm268
-rw-r--r--gnu/services/shepherd.scm14
-rw-r--r--gnu/services/virtualization.scm680
-rw-r--r--gnu/services/web.scm56
7 files changed, 1050 insertions, 177 deletions
diff --git a/gnu/services/certbot.scm b/gnu/services/certbot.scm
index 0c45471659..f287c8367f 100644
--- a/gnu/services/certbot.scm
+++ b/gnu/services/certbot.scm
@@ -6,6 +6,7 @@
 ;;; Copyright © 2020 Jack Hill <jackhill@jackhill.us>
 ;;; Copyright © 2020 Tobias Geerinckx-Rice <me@tobias.gr>
 ;;; Copyright © 2021 Raghav Gururajan <rg@raghavgururajan.name>
+;;; Copyright © 2024 Carlo Zancanaro <carlo@zancanaro.id.au>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -34,6 +35,7 @@
   #:use-module (guix records)
   #:use-module (guix gexp)
   #:use-module (srfi srfi-1)
+  #:use-module (ice-9 format)
   #:use-module (ice-9 match)
   #:export (certbot-service-type
             certbot-configuration
@@ -63,7 +65,9 @@
   (cleanup-hook        certificate-cleanup-hook
                        (default #f))
   (deploy-hook         certificate-configuration-deploy-hook
-                       (default #f)))
+                       (default #f))
+  (start-self-signed?  certificate-configuration-start-self-signed?
+                       (default #t)))
 
 (define-record-type* <certbot-configuration>
   certbot-configuration make-certbot-configuration
@@ -87,6 +91,45 @@
                           (body
                            (list "return 301 https://$host$request_uri;"))))))
 
+(define (certbot-deploy-hook name deploy-hook-script)
+  "Returns a gexp which creates symlinks for privkey.pem and fullchain.pem
+from /etc/certs/NAME to /etc/letsenctypt/live/NAME.  If DEPLOY-HOOK-SCRIPT is
+not #f then it is run after the symlinks have been created.  This wrapping is
+necessary for certificates with start-self-signed? set to #t, as it will
+overwrite the initial self-signed certificates upon the first successful
+deploy."
+  (program-file
+   (string-append name "-deploy-hook")
+   (with-imported-modules '((gnu services herd)
+                            (guix build utils))
+     #~(begin
+         (use-modules (gnu services herd)
+                      (guix build utils))
+         (mkdir-p #$(string-append "/etc/certs/" name))
+         (chmod #$(string-append "/etc/certs/" name) #o755)
+
+         ;; Create new symlinks
+         (symlink #$(string-append
+                     "/etc/letsencrypt/live/" name "/privkey.pem")
+                  #$(string-append "/etc/certs/" name "/privkey.pem.new"))
+         (symlink #$(string-append
+                     "/etc/letsencrypt/live/" name "/fullchain.pem")
+                  #$(string-append "/etc/certs/" name "/fullchain.pem.new"))
+
+         ;; Rename over the top of the old ones, just in case they were the
+         ;; original self-signed certificates.
+         (rename-file #$(string-append "/etc/certs/" name "/privkey.pem.new")
+                      #$(string-append "/etc/certs/" name "/privkey.pem"))
+         (rename-file #$(string-append "/etc/certs/" name "/fullchain.pem.new")
+                      #$(string-append "/etc/certs/" name "/fullchain.pem"))
+
+         ;; With the new certificates in place, tell nginx to reload them.
+         (with-shepherd-action 'nginx ('reload) result result)
+
+         #$@(if deploy-hook-script
+                (list #~(invoke #$deploy-hook-script))
+                '())))))
+
 (define certbot-command
   (match-lambda
     (($ <certbot-configuration> package webroot certificates email
@@ -118,7 +161,8 @@
                           `("--manual-auth-hook" ,authentication-hook)
                           '())
                       (if cleanup-hook `("--manual-cleanup-hook" ,cleanup-hook) '())
-                      (if deploy-hook `("--deploy-hook" ,deploy-hook) '()))
+                      (list "--deploy-hook"
+                            (certbot-deploy-hook name deploy-hook)))
                      (append
                       (list name certbot "certonly" "-n" "--agree-tos"
                             "--webroot" "-w" webroot
@@ -130,20 +174,51 @@
                           '("--register-unsafely-without-email"))
                       (if server `("--server" ,server) '())
                       (if rsa-key-size `("--rsa-key-size" ,rsa-key-size) '())
-                      (if deploy-hook `("--deploy-hook" ,deploy-hook) '()))))))
+                      (list "--deploy-hook"
+                            (certbot-deploy-hook name deploy-hook)))))))
               certificates)))
        (program-file
         "certbot-command"
         #~(begin
-            (use-modules (ice-9 match))
-            (let ((code 0))
+            (use-modules (ice-9 match)
+                         (ice-9 textual-ports))
+
+            (define (log format-string . args)
+              (apply format #t format-string args)
+              (force-output))
+
+            (define (file-contains? file string)
+              (string-contains (call-with-input-file file
+                                 get-string-all)
+                               string))
+
+            (define (connection-error?)
+              ;; Certbot errors are always exit code 1, so we need to look at
+              ;; the log file to see if there was a connection error.
+              (file-contains? "/var/log/letsencrypt/letsencrypt.log"
+                              "Failed to establish a new connection"))
+
+            (let ((script-code 0))
               (for-each
                (match-lambda
                  ((name . command)
-                  (begin
-                    (format #t "Acquiring or renewing certificate: ~a~%" name)
-                    (set! code (or (apply system* command) code)))))
-               '#$commands) code)))))))
+                  (log "Acquiring or renewing certificate: ~a~%" name)
+                  (cond
+                   ((zero? (status:exit-val (apply system* command)))
+                    (log "Certificate successfully acquired: ~a~%" name))
+                   ((connection-error?)
+                    ;; If we have a connection error, then bail early with
+                    ;; exit code 2. We don't expect this to resolve within the
+                    ;; timespan of this script.
+                    (log "Connection error - bailing out~%")
+                    (exit 2))
+                   (else
+                    ;; If we have any other type of error, then continue but
+                    ;; exit with a failing status code in the end.
+                    (log "Error: ~a - continuing with other domains~%" name)
+                    (set! script-code 1)))))
+               '#$commands)
+              (exit script-code))))))))
 
 (define (certbot-renewal-jobs config)
   (list
@@ -152,11 +227,84 @@
    #~(job '(next-minute-from (next-hour '(0 12)) (list (random 60)))
           #$(certbot-command config))))
 
+(define (certbot-renewal-one-shot config)
+  (list
+   ;; Renew certificates when the system first starts. This is a one-shot
+   ;; service, because the mcron configuration will take care of running this
+   ;; periodically. This is most useful the very first time the system starts,
+   ;; to overwrite our self-signed certificates as soon as possible without
+   ;; user intervention.
+   (shepherd-service
+    (provision '(renew-certbot-certificates))
+    (requirement '(nginx))
+    (one-shot? #t)
+    (start #~(lambda _
+               ;; This needs the network, but there's no reliable way to know
+               ;; if the network is up other than trying. If we fail due to a
+               ;; connection error we retry a number of times in the hope that
+               ;; the network comes up soon.
+               (let loop ((attempt 0))
+                 (let ((code (status:exit-val
+                              (system* #$(certbot-command config)))))
+                   (cond
+                    ((and (= code 2)      ; Exit code 2 means connection error
+                          (< attempt 12)) ; Arbitrarily chosen max attempts
+                     (sleep 10)           ; Arbitrarily chosen retry delay
+                     (loop (1+ attempt)))
+                    ((zero? code)
+                     ;; Success!
+                     #t)
+                    (else
+                     ;; Failure.
+                     #f))))))
+    (auto-start? #t)
+    (documentation "Call certbot to renew certificates.")
+    (actions (list (shepherd-configuration-action (certbot-command config)))))))
+
+(define (generate-certificate-gexp certbot-cert-directory rsa-key-size)
+  (match-lambda
+    (($ <certificate-configuration> name (primary-domain other-domains ...)
+                                    challenge
+                                    csr authentication-hook
+                                    cleanup-hook deploy-hook)
+     (let (;; Arbitrary default subject, with just the
+           ;; right domain filled in. These values don't
+           ;; have any real significance.
+           (subject (string-append
+                     "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN="
+                     primary-domain))
+           (alt-names (if (null? other-domains)
+                          #f
+                          (format #f "subjectAltName=~{DNS:~a~^,~}"
+                                  other-domains)))
+           (directory (string-append "/etc/certs/" (or name primary-domain))))
+       #~(when (not (file-exists? #$directory))
+           ;; We generate self-signed certificates in /etc/certs/{domain},
+           ;; because certbot is very sensitive to its directory
+           ;; structure. It refuses to write over the top of existing files,
+           ;; so we need to use a directory outside of its control.
+           ;;
+           ;; These certificates are overwritten by the certbot deploy hook
+           ;; the first time it successfully obtains a letsencrypt-signed
+           ;; certificate.
+           (mkdir-p #$directory)
+           (chmod #$directory #o755)
+           (invoke #$(file-append openssl "/bin/openssl")
+                   "req" "-x509"
+                   "-newkey" #$(string-append "rsa:" (or rsa-key-size "4096"))
+                   "-keyout" #$(string-append directory "/privkey.pem")
+                   "-out" #$(string-append directory "/fullchain.pem")
+                   "-sha256"
+                   "-days" "1" ; Only one day, because we expect certbot to run
+                   "-nodes"
+                   "-subj" #$subject
+                   #$@(if alt-names
+                          (list "-addext" alt-names)
+                          (list))))))))
+
 (define (certbot-activation config)
   (let* ((certbot-directory "/var/lib/certbot")
-         (certbot-cert-directory "/etc/letsencrypt/live")
-         (script (in-vicinity certbot-directory "renew-certificates"))
-         (message (format #f (G_ "~a may need to be run~%") script)))
+         (certbot-cert-directory "/etc/letsencrypt/live"))
     (match config
       (($ <certbot-configuration> package webroot certificates email
                                   server rsa-key-size default-location)
@@ -166,8 +314,13 @@
              (mkdir-p #$webroot)
              (mkdir-p #$certbot-directory)
              (mkdir-p #$certbot-cert-directory)
-             (copy-file #$(certbot-command config) #$script)
-             (display #$message)))))))
+
+             #$@(let ((rsa-key-size (and rsa-key-size
+                                         (number->string rsa-key-size))))
+                  (map (generate-certificate-gexp certbot-cert-directory
+                                                  rsa-key-size)
+                       (filter certificate-configuration-start-self-signed?
+                               certificates)))))))))
 
 (define certbot-nginx-server-configurations
   (match-lambda
@@ -200,7 +353,9 @@
                        (service-extension activation-service-type
                                           certbot-activation)
                        (service-extension mcron-service-type
-                                          certbot-renewal-jobs)))
+                                          certbot-renewal-jobs)
+                       (service-extension shepherd-root-service-type
+                                          certbot-renewal-one-shot)))
                 (compose concatenate)
                 (extend (lambda (config additional-certificates)
                           (certbot-configuration
diff --git a/gnu/services/cuirass.scm b/gnu/services/cuirass.scm
index fcbd5e08a5..bcdbffa2f3 100644
--- a/gnu/services/cuirass.scm
+++ b/gnu/services/cuirass.scm
@@ -1,6 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2016 Mathieu Lirzin <mthl@gnu.org>
-;;; Copyright © 2016-2023 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2016-2024 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2017, 2020 Mathieu Othacehe <m.othacehe@gmail.com>
 ;;; Copyright © 2017 Jan Nieuwenhuizen <janneke@gnu.org>
 ;;; Copyright © 2018, 2019 Ricardo Wurmus <rekado@elephly.net>
@@ -136,7 +136,9 @@
         (database         (cuirass-configuration-database config))
         (port             (cuirass-configuration-port config))
         (host             (cuirass-configuration-host config))
-        (specs            (cuirass-configuration-specifications config))
+        (config-file      (scheme-file
+                           "cuirass-specs.scm"
+                           (cuirass-configuration-specifications config)))
         (use-substitutes? (cuirass-configuration-use-substitutes? config))
         (one-shot?        (cuirass-configuration-one-shot? config))
         (fallback?        (cuirass-configuration-fallback? config))
@@ -144,13 +146,14 @@
     `(,(shepherd-service
         (documentation "Run Cuirass.")
         (provision '(cuirass))
-        (requirement '(guix-daemon postgres postgres-roles networking))
+        (requirement '(user-processes
+                       guix-daemon
+                       postgres postgres-roles networking))
         (start #~(make-forkexec-constructor
                   (list (string-append #$cuirass "/bin/cuirass")
                         "register"
                         "--cache-directory" #$cache-directory
-                        "--specifications"
-                        #$(scheme-file "cuirass-specs.scm" specs)
+                        "--specifications" #$config-file
                         "--database" #$database
                         "--interval" #$(number->string interval)
                         #$@(if parameters
@@ -172,11 +175,12 @@
                   #:user #$user
                   #:group #$group
                   #:log-file #$main-log-file))
-        (stop #~(make-kill-destructor)))
+        (stop #~(make-kill-destructor))
+        (actions (list (shepherd-configuration-action config-file))))
       ,(shepherd-service
         (documentation "Run Cuirass web interface.")
         (provision '(cuirass-web))
-        (requirement '(cuirass))
+        (requirement '(user-processes cuirass))
         (start #~(make-forkexec-constructor
                   (list (string-append #$cuirass "/bin/cuirass")
                         "web"
@@ -202,7 +206,7 @@
                (shepherd-service
                 (documentation "Run Cuirass remote build server.")
                 (provision '(cuirass-remote-server))
-                (requirement '(avahi-daemon cuirass))
+                (requirement '(user-processes avahi-daemon cuirass))
                 (start #~(make-forkexec-constructor
                           (list (string-append #$cuirass "/bin/cuirass")
                                 "remote-server"
@@ -373,7 +377,7 @@ CONFIG."
     (list (shepherd-service
            (documentation "Run Cuirass remote build worker.")
            (provision '(cuirass-remote-worker))
-           (requirement '(avahi-daemon guix-daemon networking))
+           (requirement '(user-processes avahi-daemon guix-daemon networking))
            (start #~(make-forkexec-constructor
                      (list (string-append #$cuirass "/bin/cuirass")
                            "remote-worker"
diff --git a/gnu/services/linux.scm b/gnu/services/linux.scm
index 9ee0d93030..9955a11e64 100644
--- a/gnu/services/linux.scm
+++ b/gnu/services/linux.scm
@@ -291,7 +291,7 @@ more information)."
      (shepherd-service
       (documentation "Load kernel modules.")
       (provision '(kernel-module-loader))
-      (requirement '())
+      (requirement '(udev))
       (one-shot? #t)
       (modules `((srfi srfi-1)
                  (srfi srfi-34)
diff --git a/gnu/services/networking.scm b/gnu/services/networking.scm
index 7c114fa53c..8e64e529ab 100644
--- a/gnu/services/networking.scm
+++ b/gnu/services/networking.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013-2023 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013-2024 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2015 Mark H Weaver <mhw@netris.org>
 ;;; Copyright © 2016, 2018, 2020 Efraim Flashner <efraim@flashner.co.il>
 ;;; Copyright © 2016 John Darrington <jmd@gnu.org>
@@ -21,6 +21,7 @@
 ;;; Copyright © 2022, 2023 Andrew Tropin <andrew@trop.in>
 ;;; Copyright © 2023 Declan Tsien <declantsien@riseup.net>
 ;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu>
+;;; Copyright © 2023 muradm <mail@muradm.net>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -78,6 +79,7 @@
   #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-43)
   #:use-module (ice-9 match)
+  #:use-module (ice-9 string-fun)
   #:use-module (json)
   #:re-export (static-networking-service
                static-networking-service-type)
@@ -171,6 +173,8 @@
             network-manager-configuration-vpn-plugins
             network-manager-service-type
 
+            connman-general-configuration
+            connman-general-configuration?
             connman-configuration
             connman-configuration?
             connman-configuration-connman
@@ -266,6 +270,14 @@
 ;;;
 ;;; Code:
 
+(define %unroutable-ipv4
+  ;; Unroutable address, as per <https://www.rfc-editor.org/rfc/rfc5737>.
+  "203.0.113.1")
+
+(define %unroutable-ipv6
+  ;; Unroutable address, as per <https://www.rfc-editor.org/rfc/rfc6666>.
+  "0100::")
+
 (define facebook-host-aliases
   ;; This is the list of known Facebook hosts to be added to /etc/hosts if you
   ;; are to block it.
@@ -278,7 +290,8 @@
     (append-map (lambda (name)
                   (map (lambda (addr)
                          (host addr name))
-                       (list "127.0.0.1" "::1"))) domains)))
+                       (list %unroutable-ipv4 %unroutable-ipv6)))
+                domains)))
 
 (define-deprecated %facebook-host-aliases
   block-facebook-hosts-service-type
@@ -1326,6 +1339,241 @@ wireless networking."))))
 ;;; Connman
 ;;;
 
+(define (connman-general-configuration-field-name field-name)
+  (let* ((str->camel (lambda (s)
+                       (string-concatenate
+                        (map string-capitalize (string-split s #\-)))))
+         (str (if (symbol? field-name)
+                  (str->camel (symbol->string field-name))
+                  field-name)))
+    (cond
+     ((string-suffix? "?" str) (connman-general-configuration-field-name
+                                (string-drop-right str 1)))
+     ((string-contains str "RegulatoryDomain") (connman-general-configuration-field-name
+                                                (string-replace-substring str "RegulatoryDomain" "Regdom")))
+     ((string-contains str "Url") (connman-general-configuration-field-name
+                                   (string-replace-substring str "Url" "URL")))
+     ((string-contains str "Ip") (connman-general-configuration-field-name
+                                  (string-replace-substring str "Ip" "IP")))
+     ((string-contains str "6To4") (connman-general-configuration-field-name
+                                    (string-replace-substring str "6To4" "6to4")))
+     (#t str))))
+
+(define (connman-general-configuration-serialize-string field-name value)
+  (let ((param (connman-general-configuration-field-name field-name)))
+    #~(string-append #$param " = " #$value "\n")))
+
+(define (connman-general-configuration-serialize-number field-name value)
+  (connman-general-configuration-serialize-string
+   field-name (number->string value)))
+
+(define (connman-general-configuration-serialize-list field-name value)
+  (connman-general-configuration-serialize-string
+   field-name (string-join value ",")))
+
+(define (connman-general-configuration-serialize-boolean field-name value)
+  (connman-general-configuration-serialize-string
+   field-name (if value "true" "false")))
+
+(define-maybe boolean (prefix connman-general-configuration-))
+(define-maybe number (prefix connman-general-configuration-))
+(define-maybe string (prefix connman-general-configuration-))
+(define-maybe list (prefix connman-general-configuration-))
+
+(define-configuration connman-general-configuration
+  (input-request-timeout
+   maybe-number
+   "Set input request timeout.  Default is 120 seconds.  The request for inputs
+like passphrase will timeout after certain amount of time.  Use this setting to
+increase the value in case of different user interface designs.")
+  (browser-launch-timeout
+   maybe-number
+   "Set browser launch timeout.  Default is 300 seconds.  The request for
+launching a browser for portal pages will timeout after certain  amount  of
+time.  Use this setting to increase the value in case of different user
+interface designs.")
+  (background-scanning?
+   maybe-boolean
+   "Enable background scanning.  Default is true.  If wifi is disconnected, the
+background scanning will follow a simple back off mechanism from 3s up to 5
+minutes.  Then, it will stay in 5 minutes unless user specifically asks for
+scanning through a D-Bus call.  If so, the mechanism will start again from
+3s.  This feature activates also the background scanning while being connected,
+which is required for roaming on wifi.  When @code{background-scanning?} is false,
+ConnMan will not perform any scan regardless of wifi is connected or not,
+unless it is requested by the user through a D-Bus call.")
+  (use-gateways-as-timeservers?
+   maybe-boolean
+   "Assume that service gateways also function as timeservers.  Default is false.")
+  (fallback-timeservers
+   maybe-list
+   "List of Fallback timeservers.  These timeservers are used for NTP sync
+when there are no timeservers set by the user or by the service, and when
+@code{use-gateways-as-timeservers?} is @code{#f}.  These can contain a mixed
+combination of fully qualified domain names, IPv4 and IPv6 addresses.")
+  (fallback-nameservers
+   maybe-list
+   "List of fallback nameservers appended to the list of nameservers given
+by the service.  The nameserver entries must be in numeric format,
+host names are ignored.")
+  (default-auto-connect-technologies
+   maybe-list
+   "List of technologies that are marked autoconnectable by default.  The
+default value for this entry when empty is @code{\"ethernet\"}, @code{\"wifi\"},
+@code{\"cellular\"}.  Services that are automatically connected must have been
+set up and saved to storage beforehand.")
+  (default-favourite-technologies
+   maybe-list
+   "List of technologies that are marked favorite by default.  The default
+value for this entry when empty is @code{\"ethernet\"}.  Connects to services
+from this technology even if not setup and saved to storage.")
+  (always-connected-technologies
+   maybe-list
+   "List of technologies which are always connected regardless of
+preferred-technologies setting (@code{auto-connect?} @code{#t}).  The default
+value is empty and this feature is disabled unless explicitly enabled.")
+  (preferred-technologies
+   maybe-list
+   "List of preferred technologies from the most preferred one to the least
+preferred one.  Services of the listed technology type will be tried one by
+one in the order given, until one of them gets connected or they are all
+tried.  A service of a preferred technology type in state 'ready' will get
+the default route when compared to another preferred type further down the
+list with state 'ready' or with a non-preferred type; a service of a
+preferred technology type in state 'online' will get the default route when
+compared to either a non-preferred type or a preferred type further down
+in the list.")
+  (network-interface-blacklist
+   maybe-list
+   "List of blacklisted network interfaces.  Found interfaces will be
+compared to the list and will not be handled by ConnMan, if their first
+characters match any of the list entries.  Default value is @code{\"vmnet\"},
+@code{\"vboxnet\"}, @code{\"virbr\"}, @code{\"ifb\"}.")
+  (allow-hostname-updates?
+   maybe-boolean
+   "Allow ConnMan to change the system hostname.  This can happen for
+example if we receive DHCP hostname option.  Default value is @code{#t}.")
+  (allow-domainname-updates?
+   maybe-boolean
+   "Allow connman to change the system domainname.  This can happen for
+example if we receive DHCP domainname option.  Default value is @code{#t}.")
+  (single-connected-technology?
+   maybe-boolean
+   "Keep only a single connected technology at any time.  When a new
+service is connected by the user or a better one is found according to
+preferred-technologies, the new service is kept connected  and all the
+other previously connected services are disconnected.  With this setting
+it does not matter whether the previously connected services are
+in 'online' or 'ready' states, the newly connected service is the only
+one that will be kept connected.  A service connected by the user will
+be used until going out of network coverage.  With this setting enabled
+applications will notice more network breaks than normal.  Note this
+options can't be used with VPNs.  Default value is @code{#f}.")
+  (tethering-technologies
+   maybe-list
+   "List of technologies that are allowed to enable tethering.  The
+default value is @code{\"wifi\"}, @code{\"bluetooth\"},
+@code{\"gadget\"}.  Only those technologies listed here are used for
+tethering.  If one wants to tether ethernet, then add @code{\"ethernet\"}
+in the list.  Note that if ethernet tethering is enabled, then a DHCP
+server is started on all ethernet interfaces.  Tethered ethernet should
+never be connected to corporate or home network as it will disrupt normal
+operation of these networks.  Due to this ethernet is not tethered by
+default.  Do not activate ethernet tethering unless you really know
+what you are doing.")
+  (persistent-tethering-mode?
+   maybe-boolean
+   "Restore earlier tethering status when returning from offline mode,
+re-enabling a technology, and after restarts and reboots.  Default
+value is @code{#f}.")
+  (enable-6to4?
+   maybe-boolean
+   "Automatically enable anycast 6to4 if possible.  This is not
+recommended, as the use of 6to4 will generally lead to a severe
+degradation of connection quality.  See RFC6343.  Default value
+is @code{#f} (as recommended by RFC6343 section 4.1).")
+  (vendor-class-id
+   maybe-string
+   "Set DHCP option 60 (Vendor Class ID) to the given string.  This
+option can be used by DHCP servers to identify specific clients
+without having to rely on MAC address ranges, etc.")
+  (enable-online-check?
+   maybe-boolean
+   "Enable or disable use of HTTP GET as an online status check.  When
+a service is in a READY state, and is selected as default, ConnMan will
+issue an HTTP GET request to verify that end-to-end connectivity is
+successful.  Only then the service will be transitioned to ONLINE
+state.  If this setting is false, the default service will remain
+in READY state.  Default value is @code{#t}.")
+  (online-check-ipv4-url
+   maybe-string
+   "IPv4 URL used during the online status check.  Please refer to
+the README for more detailed  information.  Default value is
+@url{http://ipv4.connman.net/online/status.html}.")
+  (online-check-ipv6-url
+   maybe-string
+   "IPv6 URL used during the online status check.  Please refer to
+the README for more detailed  information.  Default value is
+@url{http://ipv6.connman.net/online/status.html}.")
+  (online-check-initial-interval
+   maybe-number
+   "Range of intervals between two online check requests.  Please
+refer to the README for more detailed information.  Default value
+is @samp{1}.")
+  (online-check-max-interval
+   maybe-number
+   "Range of intervals between two online check requests.  Please
+refer to the README for more detailed information.  Default value
+is @samp{1}.")
+  (enable-online-to-ready-transition?
+   maybe-boolean
+   "WARNING: This is an experimental feature.  In addition to
+@code{enable-online-check} setting, enable or disable use of HTTP GET
+to detect the loss of end-to-end connectivity.  If this setting is
+@code{#f}, when the default service transitions to ONLINE state, the
+HTTP GET request is no more called until next cycle, initiated by a
+transition of the default service to DISCONNECT state.  If this
+setting is @code{#t}, the HTTP GET request keeps being called to
+guarantee that end-to-end connectivity is still successful.  If not,
+the default service will transition to READY state, enabling another
+service to become the default one, in replacement.  Default value
+is @code{#f}.")
+  (auto-connect-roaming-services?
+   maybe-boolean
+   "Automatically connect roaming services.  This is not recommended
+unless you know you won't have any billing problem.  Default value
+is @code{#f}.")
+  (address-conflict-detection?
+   maybe-boolean
+   "Enable or disable the implementation of IPv4 address conflict
+detection according to RFC5227.  ConnMan will send probe ARP packets
+to see if an IPv4 address is already in use before assigning the
+address to an interface.  If an address conflict occurs for a
+statically configured address, an IPv4LL address will be chosen
+instead (according to RFC3927).  If an address conflict occurs for
+an address offered via DHCP, ConnMan sends a DHCP DECLINE once
+and for the second conflict resorts to finding an IPv4LL
+address.  Default value is @code{#f}.")
+  (localtime
+   maybe-string
+   "Path to localtime file.  Defaults to @file{/etc/localtime}.")
+  (regulatory-domain-follows-timezone?
+   maybe-boolean
+   "Enable regulatory domain to be changed along timezone changes.
+With this option set to true each time the timezone changes the first
+present ISO3166 country code is read from
+@file{/usr/share/zoneinfo/zone1970.tab} and set as regulatory domain
+value.  Default value is @code{#f}.")
+  (resolv-conf
+   maybe-string
+   "Path to resolv.conf file.  If the file does not exist, but
+intermediate directories exist, it will be created.  If this option
+is not set, it tries to write into @file{/var/run/connman/resolv.conf}
+if it fails (@file{/var/run/connman} does not exist or is not
+writeable).  If you do not want to update resolv.conf, you can
+set @file{/dev/null}.")
+  (prefix connman-general-configuration-))
+
 (define-record-type* <connman-configuration>
   connman-configuration make-connman-configuration
   connman-configuration?
@@ -1337,7 +1585,9 @@ wireless networking."))))
                 (default #f))
   (iwd?         connman-configuration-iwd?
                 (default #f)
-                (sanitize warn-iwd?-field-deprecation)))
+                (sanitize warn-iwd?-field-deprecation))
+  (general-configuration connman-configuration-general-configuration
+                         (default (connman-general-configuration))))
 
 (define (connman-activation config)
   (let ((disable-vpn? (connman-configuration-disable-vpn? config)))
@@ -1350,10 +1600,17 @@ wireless networking."))))
 
 (define (connman-shepherd-service config)
   (match-record config <connman-configuration> (connman shepherd-requirement
-                                                disable-vpn? iwd?)
+                                                disable-vpn? iwd?
+                                                general-configuration)
     (let ((iwd? (or iwd?  ; TODO: deprecated field, remove later.
                     (and shepherd-requirement
-                         (memq 'iwd shepherd-requirement)))))
+                         (memq 'iwd shepherd-requirement))))
+          (config (mixed-text-file
+                   "main.conf"
+                   "[General]\n"
+                   (serialize-configuration
+                    general-configuration
+                    connman-general-configuration-fields))))
       (list (shepherd-service
              (documentation "Run Connman")
              (provision '(connman networking))
@@ -1365,6 +1622,7 @@ wireless networking."))))
              (start #~(make-forkexec-constructor
                        (list (string-append #$connman
                                             "/sbin/connmand")
+                             (string-append "--config=" #$config)
                              "--nodaemon"
                              "--nodnsproxy"
                              #$@(if disable-vpn? '("--noplugin=vpn") '())
diff --git a/gnu/services/shepherd.scm b/gnu/services/shepherd.scm
index 5ebac129ce..f5bcde721f 100644
--- a/gnu/services/shepherd.scm
+++ b/gnu/services/shepherd.scm
@@ -55,6 +55,8 @@
             shepherd-service-canonical-name
             shepherd-service-requirement
             shepherd-service-one-shot?
+            shepherd-service-respawn-limit
+            shepherd-service-respawn-delay
             shepherd-service-respawn?
             shepherd-service-start
             shepherd-service-stop
@@ -211,6 +213,10 @@ DEFAULT is given, use it as the service's default value."
                  (default #f))
   (respawn?      shepherd-service-respawn?             ;Boolean
                  (default #t))
+  (respawn-limit shepherd-service-respawn-limit
+                 (default #f))
+  (respawn-delay shepherd-service-respawn-delay
+                 (default #f))
   (start         shepherd-service-start)               ;g-expression (procedure)
   (stop          shepherd-service-stop                 ;g-expression (procedure)
                  (default #~(const #f)))
@@ -309,6 +315,14 @@ stored."
                        #:one-shot? '#$(shepherd-service-one-shot? service)
 
                        #:respawn? '#$(shepherd-service-respawn? service)
+                       #$@(if (shepherd-service-respawn-limit service)
+                              `(#:respawn-limit
+                                ,(shepherd-service-respawn-limit service))
+                              '())
+                       #$@(if (shepherd-service-respawn-delay service)
+                              `(#:respawn-delay
+                                ,(shepherd-service-respawn-delay service))
+                              '())
                        #:start #$(shepherd-service-start service)
                        #:stop #$(shepherd-service-stop service)
                        #:actions
diff --git a/gnu/services/virtualization.scm b/gnu/services/virtualization.scm
index f0f0ab3bf1..7b04ddb35e 100644
--- a/gnu/services/virtualization.scm
+++ b/gnu/services/virtualization.scm
@@ -1,6 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2017 Ryan Moe <ryan.moe@gmail.com>
-;;; Copyright © 2018, 2020-2023 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2018, 2020-2024 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2020, 2021, 2023 Janneke Nieuwenhuizen <janneke@gnu.org>
 ;;; Copyright © 2021 Timotej Lazar <timotej.lazar@araneo.si>
 ;;; Copyright © 2022 Oleg Pykhalov <go.wigust@gmail.com>
@@ -36,6 +36,7 @@
   #:use-module (gnu services base)
   #:use-module (gnu services configuration)
   #:use-module (gnu services dbus)
+  #:use-module (gnu services mcron)
   #:use-module (gnu services shepherd)
   #:use-module (gnu services ssh)
   #:use-module (gnu services)
@@ -43,6 +44,8 @@
   #:use-module (gnu system hurd)
   #:use-module (gnu system image)
   #:use-module (gnu system shadow)
+  #:autoload   (gnu system vm) (linux-image-startup-command
+                                virtualized-operating-system)
   #:use-module (gnu system)
   #:use-module (guix derivations)
   #:use-module (guix gexp)
@@ -55,12 +58,20 @@
   #:autoload   (guix self) (make-config.scm)
   #:autoload   (guix platform) (platform-system)
 
+  #:use-module ((srfi srfi-1) #:hide (partition))
   #:use-module (srfi srfi-9)
+  #:use-module (srfi srfi-19)
   #:use-module (srfi srfi-26)
   #:use-module (rnrs bytevectors)
   #:use-module (ice-9 match)
 
-  #:export (%hurd-vm-operating-system
+  #:export (virtual-build-machine
+            virtual-build-machine-service-type
+
+            %virtual-build-machine-operating-system
+            %virtual-build-machine-default-vm
+
+            %hurd-vm-operating-system
             hurd-vm-configuration
             hurd-vm-configuration?
             hurd-vm-configuration-os
@@ -996,7 +1007,7 @@ specified, the QEMU default path is used."))
 ;;; Secrets for guest VMs.
 ;;;
 
-(define (secret-service-shepherd-services port)
+(define (secret-service-shepherd-services address)
   "Return a Shepherd service that fetches sensitive material at local PORT,
 over TCP.  Reboot upon failure."
   ;; This is a Shepherd service, rather than an activation snippet, to make
@@ -1018,7 +1029,7 @@ over TCP.  Reboot upon failure."
                          "receiving secrets from the host...~%")
                  (force-output (current-error-port))
 
-                 (let ((sent (secret-service-receive-secrets #$port)))
+                 (let ((sent (secret-service-receive-secrets #$address)))
                    (unless sent
                      (sleep 3)
                      (reboot))))))
@@ -1039,9 +1050,13 @@ over TCP.  Reboot upon failure."
 boot time.  This service is meant to be used by virtual machines (VMs) that
 can only be accessed by their host.")))
 
-(define (secret-service-operating-system os)
+(define* (secret-service-operating-system os
+                                          #:optional
+                                          (address
+                                           #~(make-socket-address
+                                              AF_INET INADDR_ANY 1004)))
   "Return an operating system based on OS that includes the secret-service,
-that will be listening to receive secret keys on port 1004, TCP."
+that will be listening to receive secret keys on ADDRESS."
   (operating-system
     (inherit os)
     (services
@@ -1049,7 +1064,7 @@ that will be listening to receive secret keys on port 1004, TCP."
      ;; activation: that requires entropy and thus takes time during boot, and
      ;; those keys are going to be overwritten by secrets received from the
      ;; host anyway.
-     (cons (service secret-service-type 1004)
+     (cons (service secret-service-type address)
            (modify-services (operating-system-user-services os)
              (openssh-service-type
               config => (openssh-configuration
@@ -1062,6 +1077,498 @@ that will be listening to receive secret keys on port 1004, TCP."
 
 
 ;;;
+;;; Offloading-as-a-service.
+;;;
+
+(define-record-type* <virtual-build-machine>
+  virtual-build-machine make-virtual-build-machine
+  virtual-build-machine?
+  this-virtual-build-machine
+  (name        virtual-build-machine-name
+               (default 'build-vm))
+  (image       virtual-build-machine-image
+               (thunked)
+               (default
+                 (virtual-build-machine-default-image
+                  this-virtual-build-machine)))
+  (qemu        virtual-build-machine-qemu
+               (default qemu-minimal))
+  (cpu         virtual-build-machine-cpu
+               (thunked)
+               (default
+                 (qemu-cpu-model-for-date
+                  (virtual-build-machine-systems this-virtual-build-machine)
+                  (virtual-build-machine-date this-virtual-build-machine))))
+  (cpu-count   virtual-build-machine-cpu-count
+               (default 4))
+  (memory-size virtual-build-machine-memory-size  ;integer (MiB)
+               (default 2048))
+  (date        virtual-build-machine-date
+               ;; Default to a date "in the past" assuming a common use case
+               ;; is to rebuild old packages.
+               (default (make-date 0 0 00 00 01 01 2020 0)))
+  (port-forwardings virtual-build-machine-port-forwardings
+                    (default
+                      `((,%build-vm-ssh-port . 22)
+                        (,%build-vm-secrets-port . 1004))))
+  (systems     virtual-build-machine-systems
+               (default (list (%current-system))))
+  (auto-start? virtual-build-machine-auto-start?
+               (default #f)))
+
+(define %build-vm-ssh-port
+  ;; Default host port where the guest's SSH port is forwarded.
+  11022)
+
+(define %build-vm-secrets-port
+  ;; Host port to communicate secrets to the build VM.
+  ;; FIXME: Anyone on the host can talk to it; use virtio ports or AF_VSOCK
+  ;; instead.
+  11044)
+
+(define %x86-64-intel-cpu-models
+  ;; List of release date/CPU model pairs representing Intel's x86_64 models.
+  ;; The list is taken from
+  ;; <https://en.wikipedia.org/wiki/List_of_Intel_CPU_microarchitectures>.
+  ;; CPU model strings are those found in 'qemu-system-x86_64 -cpu help'.
+  (letrec-syntax ((cpu-models (syntax-rules ()
+                                ((_ (date model) rest ...)
+                                 (alist-cons (date->time-utc
+                                              (string->date date "~Y-~m-~d"))
+                                             model
+                                             (cpu-models rest ...)))
+                                ((_)
+                                 '()))))
+    (reverse
+     (cpu-models ("2006-01-01" "core2duo")
+                 ("2010-01-01" "Westmere")
+                 ("2008-01-01" "Nehalem")
+                 ("2011-01-01" "SandyBridge")
+                 ("2012-01-01" "IvyBridge")
+                 ("2013-01-01" "Haswell")
+                 ("2014-01-01" "Broadwell")
+                 ("2015-01-01" "Skylake-Client")))))
+
+(define (qemu-cpu-model-for-date systems date)
+  "Return the QEMU name of a CPU model for SYSTEMS that was current at DATE."
+  (if (any (cut string-prefix? "x86_64-" <>) systems)
+      (let ((time (date->time-utc date)))
+        (any (match-lambda
+               ((release-date . model)
+                (and (time<? release-date time)
+                     model)))
+             %x86-64-intel-cpu-models))
+      ;; TODO: Add models for other architectures.
+      "host"))
+
+(define (virtual-build-machine-ssh-port config)
+  "Return the host port where CONFIG has its VM's SSH port forwarded."
+  (any (match-lambda
+         ((host-port . 22) host-port)
+         (_ #f))
+       (virtual-build-machine-port-forwardings config)))
+
+(define (virtual-build-machine-secrets-port config)
+  "Return the host port where CONFIG has its VM's secrets port forwarded."
+  (any (match-lambda
+         ((host-port . 1004) host-port)
+         (_ #f))
+       (virtual-build-machine-port-forwardings config)))
+
+(define %minimal-vm-syslog-config
+  ;; Minimal syslog configuration for a VM.
+  (plain-file "vm-syslog.conf" "\
+# Log most messages to the console, which goes to the serial
+# output, allowing the host to log it.
+*.info;auth.notice;authpriv.none       -/dev/console
+
+# The rest.
+*.=debug                               -/var/log/debug
+authpriv.*;auth.info                    /var/log/secure
+"))
+
+(define %virtual-build-machine-operating-system
+  (operating-system
+    (host-name "build-machine")
+    (bootloader (bootloader-configuration         ;unused
+                 (bootloader grub-minimal-bootloader)
+                 (targets '("/dev/null"))))
+    (file-systems (cons (file-system              ;unused
+                          (mount-point "/")
+                          (device "none")
+                          (type "tmpfs"))
+                        %base-file-systems))
+    (users (cons (user-account
+                  (name "offload")
+                  (group "users")
+                  (supplementary-groups '("kvm"))
+                  (comment "Account used for offloading"))
+                 %base-user-accounts))
+    (services (cons* (service static-networking-service-type
+                              (list %qemu-static-networking))
+                     (service openssh-service-type
+                              (openssh-configuration
+                               (openssh openssh-sans-x)))
+
+                     ;; Run GC once per hour.
+                     (simple-service 'perdiodic-gc mcron-service-type
+                                     (list #~(job "12 * * * *"
+                                                  "guix gc -F 2G")))
+
+                     (modify-services %base-services
+                       ;; By default, the secret service introduces a
+                       ;; pre-initialized /etc/guix/acl file in the VM.  Thus,
+                       ;; clear 'authorize-key?' so that it's not overridden
+                       ;; at activation time.
+                       (guix-service-type config =>
+                                          (guix-configuration
+                                           (inherit config)
+                                           (authorize-key? #f)))
+                       (syslog-service-type config =>
+                                            (syslog-configuration
+                                             (config-file
+                                              %minimal-vm-syslog-config)))
+                       (delete mingetty-service-type)
+                       (delete console-font-service-type))))))
+
+(define %default-virtual-build-machine-image-size
+  ;; Size of the default disk image of virtual build machines.  It should be
+  ;; large enough to let users build a few things.
+  (* 20 (expt 2 30)))
+
+(define (virtual-build-machine-default-image config)
+  (let* ((type (lookup-image-type-by-name 'mbr-raw))
+         (base (os->image %virtual-build-machine-operating-system
+                          #:type type)))
+    (image (inherit base)
+           (name (symbol-append 'build-vm-
+                                (virtual-build-machine-name config)))
+           (format 'compressed-qcow2)
+           (partition-table-type 'mbr)
+           (volatile-root? #f)
+           (shared-store? #f)
+           (size %default-virtual-build-machine-image-size)
+           (partitions (match (image-partitions base)
+                         ((root)
+                          ;; Increase the size of the root partition to match
+                          ;; that of the disk image.
+                          (let ((root-size (- size (* 50 (expt 2 20)))))
+                            (list (partition
+                                   (inherit root)
+                                   (size root-size))))))))))
+
+(define (virtual-build-machine-account-name config)
+  (string-append "build-vm-"
+                 (symbol->string
+                  (virtual-build-machine-name config))))
+
+(define (virtual-build-machine-accounts config)
+  (let ((name (virtual-build-machine-account-name config)))
+    (list (user-group (name name) (system? #t))
+          (user-account
+           (name name)
+           (group name)
+           (supplementary-groups '("kvm"))
+           (comment "Privilege separation user for the virtual build machine")
+           (home-directory "/var/empty")
+           (shell (file-append shadow "/sbin/nologin"))
+           (system? #t)))))
+
+(define (build-vm-shepherd-services config)
+  (define transform
+    (compose secret-service-operating-system
+             operating-system-with-locked-root-account
+             operating-system-with-offloading-account
+             (lambda (os)
+               (virtualized-operating-system os #:full-boot? #t))))
+
+  (define transformed-image
+    (let ((base (virtual-build-machine-image config)))
+      (image
+       (inherit base)
+       (operating-system
+         (transform (image-operating-system base))))))
+
+  (define command
+    (linux-image-startup-command transformed-image
+                                 #:qemu
+                                 (virtual-build-machine-qemu config)
+                                 #:cpu
+                                 (virtual-build-machine-cpu config)
+                                 #:cpu-count
+                                 (virtual-build-machine-cpu-count config)
+                                 #:memory-size
+                                 (virtual-build-machine-memory-size config)
+                                 #:port-forwardings
+                                 (virtual-build-machine-port-forwardings
+                                  config)
+                                 #:date
+                                 (virtual-build-machine-date config)))
+
+  (define user
+    (virtual-build-machine-account-name config))
+
+  (list (shepherd-service
+         (documentation "Run the build virtual machine service.")
+         (provision (list (virtual-build-machine-name config)))
+         (requirement '(user-processes))
+         (modules `((gnu build secret-service)
+                    (guix build utils)
+                    ,@%default-modules))
+         (start
+          (with-imported-modules (source-module-closure
+                                  '((gnu build secret-service)
+                                    (guix build utils)))
+            #~(lambda arguments
+                (let* ((pid  (fork+exec-command (append #$command arguments)
+                                                #:user #$user
+                                                #:group "kvm"
+                                                #:environment-variables
+                                                ;; QEMU tries to write to /var/tmp
+                                                ;; by default.
+                                                '("TMPDIR=/tmp")))
+                       (port #$(virtual-build-machine-secrets-port config))
+                       (root #$(virtual-build-machine-secret-root config))
+                       (address (make-socket-address AF_INET INADDR_LOOPBACK
+                                                     port)))
+                  (catch #t
+                    (lambda _
+                      (if (secret-service-send-secrets address root)
+                          pid
+                          (begin
+                            (kill (- pid) SIGTERM)
+                            #f)))
+                    (lambda (key . args)
+                      (kill (- pid) SIGTERM)
+                      (apply throw key args)))))))
+         (stop #~(make-kill-destructor))
+         (actions
+          (list (shepherd-action
+                 (name 'configuration)
+                 (documentation
+                  "Display the configuration of this virtual build machine.")
+                 (procedure
+                  #~(lambda (_)
+                      (format #t "CPU: ~a~%"
+                              #$(virtual-build-machine-cpu config))
+                      (format #t "number of CPU cores: ~a~%"
+                              #$(virtual-build-machine-cpu-count config))
+                      (format #t "memory size: ~a MiB~%"
+                              #$(virtual-build-machine-memory-size config))
+                      (format #t "initial date: ~a~%"
+                              #$(date->string
+                                 (virtual-build-machine-date config))))))))
+         (auto-start? (virtual-build-machine-auto-start? config)))))
+
+(define (authorize-guest-substitutes-on-host)
+  "Return a program that authorizes the guest's archive signing key (passed as
+an argument) on the host."
+  (define not-config?
+    (match-lambda
+      ('(guix config) #f)
+      (('guix _ ...) #t)
+      (('gnu _ ...) #t)
+      (_ #f)))
+
+  (define run
+    (with-extensions (list guile-gcrypt)
+      (with-imported-modules `(((guix config) => ,(make-config.scm))
+                               ,@(source-module-closure
+                                  '((guix pki)
+                                    (guix build utils))
+                                  #:select? not-config?))
+        #~(begin
+            (use-modules (ice-9 match)
+                         (ice-9 textual-ports)
+                         (gcrypt pk-crypto)
+                         (guix pki)
+                         (guix build utils))
+
+            (match (command-line)
+              ((_ guest-config-directory)
+               (let ((guest-key (string-append guest-config-directory
+                                               "/signing-key.pub")))
+                 (if (file-exists? guest-key)
+                     ;; Add guest key to the host's ACL.
+                     (let* ((key (string->canonical-sexp
+                                  (call-with-input-file guest-key
+                                    get-string-all)))
+                            (acl (public-keys->acl
+                                  (cons key (acl->public-keys (current-acl))))))
+                       (with-atomic-file-replacement %acl-file
+                         (lambda (_ port)
+                           (write-acl acl port))))
+                     (format (current-error-port)
+                             "warning: guest key missing from '~a'~%"
+                             guest-key)))))))))
+
+  (program-file "authorize-guest-substitutes-on-host" run))
+
+(define (initialize-build-vm-substitutes)
+  "Initialize the Hurd VM's key pair and ACL and store it on the host."
+  (define run
+    (with-imported-modules '((guix build utils))
+      #~(begin
+          (use-modules (guix build utils)
+                       (ice-9 match))
+
+          (define host-key
+            "/etc/guix/signing-key.pub")
+
+          (define host-acl
+            "/etc/guix/acl")
+
+          (match (command-line)
+            ((_ guest-config-directory)
+             (setenv "GUIX_CONFIGURATION_DIRECTORY"
+                     guest-config-directory)
+             (invoke #+(file-append guix "/bin/guix") "archive"
+                     "--generate-key")
+
+             (when (file-exists? host-acl)
+               ;; Copy the host ACL.
+               (copy-file host-acl
+                          (string-append guest-config-directory
+                                         "/acl")))
+
+             (when (file-exists? host-key)
+               ;; Add the host key to the childhurd's ACL.
+               (let ((key (open-fdes host-key O_RDONLY)))
+                 (close-fdes 0)
+                 (dup2 key 0)
+                 (execl #+(file-append guix "/bin/guix")
+                        "guix" "archive" "--authorize"))))))))
+
+  (program-file "initialize-build-vm-substitutes" run))
+
+(define* (build-vm-activation secret-directory
+                              #:key
+                              offloading-ssh-key
+                              (offloading? #t))
+  (with-imported-modules '((guix build utils))
+    #~(begin
+        (use-modules (guix build utils))
+
+        (define secret-directory
+          #$secret-directory)
+
+        (define ssh-directory
+          (string-append secret-directory "/etc/ssh"))
+
+        (define guix-directory
+          (string-append secret-directory "/etc/guix"))
+
+        (define offloading-ssh-key
+          #$offloading-ssh-key)
+
+        (unless (file-exists? ssh-directory)
+          ;; Generate SSH host keys under SSH-DIRECTORY.
+          (mkdir-p ssh-directory)
+          (invoke #$(file-append openssh "/bin/ssh-keygen")
+                  "-A" "-f" secret-directory))
+
+        (unless (or (not #$offloading?)
+                    (file-exists? offloading-ssh-key))
+          ;; Generate a user SSH key pair for the host to use when offloading
+          ;; to the guest.
+          (mkdir-p (dirname offloading-ssh-key))
+          (invoke #$(file-append openssh "/bin/ssh-keygen")
+                  "-t" "ed25519" "-N" ""
+                  "-f" offloading-ssh-key)
+
+          ;; Authorize it in the guest for user 'offloading'.
+          (let ((authorizations
+                 (string-append ssh-directory
+                                "/authorized_keys.d/offloading")))
+            (mkdir-p (dirname authorizations))
+            (copy-file (string-append offloading-ssh-key ".pub")
+                       authorizations)
+            (chmod (dirname authorizations) #o555)))
+
+        (unless (file-exists? guix-directory)
+          (invoke #$(initialize-build-vm-substitutes)
+                  guix-directory))
+
+        (when #$offloading?
+          ;; Authorize the archive signing key from GUIX-DIRECTORY in the host.
+          (invoke #$(authorize-guest-substitutes-on-host) guix-directory)))))
+
+(define (virtual-build-machine-offloading-ssh-key config)
+  "Return the name of the file containing the SSH key of user 'offloading'."
+  (string-append "/etc/guix/offload/ssh/virtual-build-machine/"
+                 (symbol->string
+                  (virtual-build-machine-name config))))
+
+(define (virtual-build-machine-activation config)
+  "Return a gexp to activate the build VM according to CONFIG."
+  (build-vm-activation (virtual-build-machine-secret-root config)
+                       #:offloading? #t
+                       #:offloading-ssh-key
+                       (virtual-build-machine-offloading-ssh-key config)))
+
+(define (virtual-build-machine-secret-root config)
+  (string-append "/etc/guix/virtual-build-machines/"
+                 (symbol->string
+                  (virtual-build-machine-name config))))
+
+(define (check-vm-availability config)
+  "Return a Scheme file that evaluates to true if the service corresponding to
+CONFIG, a <virtual-build-machine>, is up and running."
+  (define service-name
+    (virtual-build-machine-name config))
+
+  (scheme-file "check-build-vm-availability.scm"
+               #~(begin
+                   (use-modules (gnu services herd)
+                                (srfi srfi-34))
+
+                   (guard (c ((service-not-found-error? c) #f))
+                     (->bool (live-service-running
+                              (current-service '#$service-name)))))))
+
+(define (build-vm-guix-extension config)
+  (define vm-ssh-key
+    (string-append
+     (virtual-build-machine-secret-root config)
+     "/etc/ssh/ssh_host_ed25519_key.pub"))
+
+  (define host-ssh-key
+    (virtual-build-machine-offloading-ssh-key config))
+
+  (guix-extension
+   (build-machines
+    (list #~(if (primitive-load #$(check-vm-availability config))
+                (list (build-machine
+                       (name "localhost")
+                       (port #$(virtual-build-machine-ssh-port config))
+                       (systems
+                        '#$(virtual-build-machine-systems config))
+                       (user "offloading")
+                       (host-key (call-with-input-file #$vm-ssh-key
+                                   (@ (ice-9 textual-ports)
+                                      get-string-all)))
+                       (private-key #$host-ssh-key)))
+                '())))))
+
+(define virtual-build-machine-service-type
+  (service-type
+   (name 'build-vm)
+   (extensions (list (service-extension shepherd-root-service-type
+                                        build-vm-shepherd-services)
+                     (service-extension guix-service-type
+                                        build-vm-guix-extension)
+                     (service-extension account-service-type
+                                        virtual-build-machine-accounts)
+                     (service-extension activation-service-type
+                                        virtual-build-machine-activation)))
+   (description
+    "Create a @dfn{virtual build machine}: a virtual machine (VM) that builds
+can be offloaded to.  By default, the virtual machine starts with a clock
+running at some point in the past.")
+   (default-value (virtual-build-machine))))
+
+
+;;;
 ;;; The Hurd in VM service: a Childhurd.
 ;;;
 
@@ -1243,24 +1750,26 @@ is added to the OS specified in CONFIG."
            (source-module-closure '((gnu build secret-service)
                                     (guix build utils)))
          #~(lambda ()
-             (let ((pid  (fork+exec-command #$vm-command
-                                            #:user "childhurd"
-                                            ;; XXX TODO: use "childhurd" after
-                                            ;; updating Shepherd
-                                            #:group "kvm"
-                                            #:environment-variables
-                                            ;; QEMU tries to write to /var/tmp
-                                            ;; by default.
-                                            '("TMPDIR=/tmp")))
-                   (port #$(hurd-vm-port config %hurd-vm-secrets-port))
-                   (root #$(hurd-vm-configuration-secret-root config)))
+             (let* ((pid  (fork+exec-command #$vm-command
+                                             #:user "childhurd"
+                                             ;; XXX TODO: use "childhurd" after
+                                             ;; updating Shepherd
+                                             #:group "kvm"
+                                             #:environment-variables
+                                             ;; QEMU tries to write to /var/tmp
+                                             ;; by default.
+                                             '("TMPDIR=/tmp")))
+                    (port #$(hurd-vm-port config %hurd-vm-secrets-port))
+                    (root #$(hurd-vm-configuration-secret-root config))
+                    (address (make-socket-address AF_INET INADDR_LOOPBACK
+                                                  port)))
                (catch #t
                  (lambda _
                    ;; XXX: 'secret-service-send-secrets' won't complete until
                    ;; the guest has booted and its secret service server is
                    ;; running, which could take 20+ seconds during which PID 1
                    ;; is stuck waiting.
-                   (if (secret-service-send-secrets port root)
+                   (if (secret-service-send-secrets address root)
                        pid
                        (begin
                          (kill (- pid) SIGTERM)
@@ -1284,136 +1793,13 @@ is added to the OS specified in CONFIG."
          (shell (file-append shadow "/sbin/nologin"))
          (system? #t))))
 
-(define (initialize-hurd-vm-substitutes)
-  "Initialize the Hurd VM's key pair and ACL and store it on the host."
-  (define run
-    (with-imported-modules '((guix build utils))
-      #~(begin
-          (use-modules (guix build utils)
-                       (ice-9 match))
-
-          (define host-key
-            "/etc/guix/signing-key.pub")
-
-          (define host-acl
-            "/etc/guix/acl")
-
-          (match (command-line)
-            ((_ guest-config-directory)
-             (setenv "GUIX_CONFIGURATION_DIRECTORY"
-                     guest-config-directory)
-             (invoke #+(file-append guix "/bin/guix") "archive"
-                     "--generate-key")
-
-             (when (file-exists? host-acl)
-               ;; Copy the host ACL.
-               (copy-file host-acl
-                          (string-append guest-config-directory
-                                         "/acl")))
-
-             (when (file-exists? host-key)
-               ;; Add the host key to the childhurd's ACL.
-               (let ((key (open-fdes host-key O_RDONLY)))
-                 (close-fdes 0)
-                 (dup2 key 0)
-                 (execl #+(file-append guix "/bin/guix")
-                        "guix" "archive" "--authorize"))))))))
-
-  (program-file "initialize-hurd-vm-substitutes" run))
-
-(define (authorize-guest-substitutes-on-host)
-  "Return a program that authorizes the guest's archive signing key (passed as
-an argument) on the host."
-  (define not-config?
-    (match-lambda
-      ('(guix config) #f)
-      (('guix _ ...) #t)
-      (('gnu _ ...) #t)
-      (_ #f)))
-
-  (define run
-    (with-extensions (list guile-gcrypt)
-      (with-imported-modules `(((guix config) => ,(make-config.scm))
-                               ,@(source-module-closure
-                                  '((guix pki)
-                                    (guix build utils))
-                                  #:select? not-config?))
-        #~(begin
-            (use-modules (ice-9 match)
-                         (ice-9 textual-ports)
-                         (gcrypt pk-crypto)
-                         (guix pki)
-                         (guix build utils))
-
-            (match (command-line)
-              ((_ guest-config-directory)
-               (let ((guest-key (string-append guest-config-directory
-                                               "/signing-key.pub")))
-                 (if (file-exists? guest-key)
-                     ;; Add guest key to the host's ACL.
-                     (let* ((key (string->canonical-sexp
-                                  (call-with-input-file guest-key
-                                    get-string-all)))
-                            (acl (public-keys->acl
-                                  (cons key (acl->public-keys (current-acl))))))
-                       (with-atomic-file-replacement %acl-file
-                         (lambda (_ port)
-                           (write-acl acl port))))
-                     (format (current-error-port)
-                             "warning: guest key missing from '~a'~%"
-                             guest-key)))))))))
-
-  (program-file "authorize-guest-substitutes-on-host" run))
-
 (define (hurd-vm-activation config)
   "Return a gexp to activate the Hurd VM according to CONFIG."
-  (with-imported-modules '((guix build utils))
-    #~(begin
-        (use-modules (guix build utils))
-
-        (define secret-directory
-          #$(hurd-vm-configuration-secret-root config))
-
-        (define ssh-directory
-          (string-append secret-directory "/etc/ssh"))
-
-        (define guix-directory
-          (string-append secret-directory "/etc/guix"))
-
-        (define offloading-ssh-key
-          #$(hurd-vm-configuration-offloading-ssh-key config))
-
-        (unless (file-exists? ssh-directory)
-          ;; Generate SSH host keys under SSH-DIRECTORY.
-          (mkdir-p ssh-directory)
-          (invoke #$(file-append openssh "/bin/ssh-keygen")
-                  "-A" "-f" secret-directory))
-
-        (unless (or (not #$(hurd-vm-configuration-offloading? config))
-                    (file-exists? offloading-ssh-key))
-          ;; Generate a user SSH key pair for the host to use when offloading
-          ;; to the guest.
-          (mkdir-p (dirname offloading-ssh-key))
-          (invoke #$(file-append openssh "/bin/ssh-keygen")
-                  "-t" "ed25519" "-N" ""
-                  "-f" offloading-ssh-key)
-
-          ;; Authorize it in the guest for user 'offloading'.
-          (let ((authorizations
-                 (string-append ssh-directory
-                                "/authorized_keys.d/offloading")))
-            (mkdir-p (dirname authorizations))
-            (copy-file (string-append offloading-ssh-key ".pub")
-                       authorizations)
-            (chmod (dirname authorizations) #o555)))
-
-        (unless (file-exists? guix-directory)
-          (invoke #$(initialize-hurd-vm-substitutes)
-                  guix-directory))
-
-        (when #$(hurd-vm-configuration-offloading? config)
-          ;; Authorize the archive signing key from GUIX-DIRECTORY in the host.
-          (invoke #$(authorize-guest-substitutes-on-host) guix-directory)))))
+  (build-vm-activation (hurd-vm-configuration-secret-root config)
+                       #:offloading?
+                       (hurd-vm-configuration-offloading? config)
+                       #:offloading-ssh-key
+                       (hurd-vm-configuration-offloading-ssh-key config)))
 
 (define (hurd-vm-configuration-offloading-ssh-key config)
   "Return the name of the file containing the SSH key of user 'offloading'."
diff --git a/gnu/services/web.scm b/gnu/services/web.scm
index 05fd71f994..406117c457 100644
--- a/gnu/services/web.scm
+++ b/gnu/services/web.scm
@@ -16,6 +16,7 @@
 ;;; Copyright © 2020, 2021 Alexandru-Sergiu Marton <brown121407@posteo.ro>
 ;;; Copyright © 2022 Simen Endsjø <simendsjo@gmail.com>
 ;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu>
+;;; Copyright © 2023 Miguel Ángel Moreno <mail@migalmoreno.com>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -36,6 +37,7 @@
   #:use-module (gnu services)
   #:use-module (gnu services shepherd)
   #:use-module (gnu services admin)
+  #:use-module (gnu services configuration)
   #:use-module (gnu services getmail)
   #:use-module (gnu services mail)
   #:use-module (gnu system pam)
@@ -47,6 +49,7 @@
   #:use-module (gnu packages patchutils)
   #:use-module (gnu packages php)
   #:use-module (gnu packages python)
+  #:use-module (gnu packages python-web)
   #:use-module (gnu packages gnupg)
   #:use-module (gnu packages guile)
   #:use-module (gnu packages logging)
@@ -240,6 +243,13 @@
 
             varnish-service-type
 
+            whoogle-service-type
+            whoogle-configuration
+            whoogle-configuration-package
+            whoogle-configuration-host
+            whoogle-configuration-port
+            whoogle-configuration-environment-variables
+
             patchwork-database-configuration
             patchwork-database-configuration?
             patchwork-database-configuration-engine
@@ -1605,6 +1615,52 @@ files.")
 
 
 ;;;
+;;; Whoogle
+;;;
+
+(define-configuration/no-serialization whoogle-configuration
+  (package
+    (package whoogle-search)
+    "The @code{whoogle-search} package to use.")
+  (host
+   (string "127.0.0.1")
+   "The host address to run Whoogle on.")
+  (port
+   (integer 5000)
+   "The port to run Whoogle on.")
+  (environment-variables
+   (list-of-strings '())
+   "A list of strings specifying environment variables used to configure
+Whoogle."))
+
+(define (whoogle-shepherd-service config)
+  (match-record config <whoogle-configuration>
+    (package host port environment-variables)
+    (list
+     (shepherd-service
+      (provision '(whoogle-search))
+      (start #~(make-forkexec-constructor
+                (list (string-append #$package "/bin/whoogle-search")
+                      "--host" #$host "--port" #$(number->string port))
+                #:environment-variables
+                (append (list "CONFIG_VOLUME=/var/cache/whoogle-search")
+                        '#$environment-variables)))
+      (stop #~(make-kill-destructor))
+      (documentation "Run a @code{whoogle-search} instance.")))))
+
+(define whoogle-service-type
+  (service-type
+   (name 'whoogle-search)
+   (extensions
+    (list (service-extension shepherd-root-service-type
+                             whoogle-shepherd-service)
+          (service-extension profile-service-type
+                             (compose list whoogle-configuration-package))))
+   (default-value (whoogle-configuration))
+   (description "Set up the @code{whoogle-search} metasearch engine.")))
+
+
+;;;
 ;;; Patchwork
 ;;;