Subject: [PATCH 2/2] services: kerberos/heimdal.scm: New file, add Heimdal Kerberos services.

From: Felix Lechner <[email-masked]>
Date: Thu, 30 Nov 2023 16:45:12 -0800

Includes detailed documentation and two system tests.

Change-Id: I7b3a9da1340b559f1db8a8156581e73b918cfb78
---
 doc/guix.texi                     | 101 +++++++++++++++-
 gnu/local.mk                      |   3 +
 gnu/services/kerberos.scm         | 120 ++++++++++++++++++-
 gnu/services/kerberos/heimdal.scm | 189 ++++++++++++++++++++++++++++++
 gnu/tests/heimdal-kadmind.scm     |  71 +++++++++++
 gnu/tests/heimdal-kdc.scm         |  71 +++++++++++
 6 files changed, 551 insertions(+), 4 deletions(-)
 create mode 100644 gnu/services/kerberos/heimdal.scm
 create mode 100644 gnu/tests/heimdal-kadmind.scm
 create mode 100644 gnu/tests/heimdal-kdc.scm

diff --git a/doc/guix.texi b/doc/guix.texi
index a5119d2058..ecb85771ad 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -29979,7 +29979,8 @@ Kerberos Services
 Other implementations have not been tested.
 
 @defvar krb5-association-service-type
-A service type for Kerberos 5 clients.
+A service type for Kerberos 5 clients. This service type was previously
+named @code{krb5-service-type}.
 @end defvar
 
 @noindent
@@ -30037,6 +30038,8 @@ Kerberos Services
 
 @deftp {Data Type} krb5-association-configuration
 
+This configuration record was previously named @code{krb5-configuration}.
+
 @table @asis
 @item @code{allow-weak-crypto?} (default: @code{#f})
 If this flag is @code{#t} then services which only offer encryption algorithms
@@ -30059,6 +30062,102 @@ Kerberos Services
 @end deftp

+@subsubheading Heimdal Key Distribution (Kdc) Service
+
+The @code{(gnu services kerberos heimdal)} module provides services
+related to the @dfn{Heimdal} implementation for the authentication
+protocol @dfn{Kerberos}.
+
+This service starts the @dfn{Kerberos Key Distribution Center}
+server. The server will remain running.
+
+Kerberos client programs can obtain the location of this server from a
+configuration file at @file{/etc/krb5.conf}. You may wish to create that
+file separately via the @code{krb5-association-service-type}.
+
+@c %start of fragment
+@deftp {Data Type} heimdal-kdc-configuration
+Available @code{heimdal-kdc-configuration} fields are:
+
+@table @asis
+@item @code{heimdal} (default: @code{heimdal}) (type: file-like)
+The heimdal package to use.
+
+@item @code{config-file} (type: maybe-string)
+Configuration file for Heimdal KDC server.
+
+@item @code{require-preauth?} (default: @code{#t}) (type: boolean)
+Require pre-authentication in the initial AS-REQ for all principals.
+
+@item @code{max-request-size} (type: maybe-non-negative-integer)
+Maximum size of requests the server is willing to handle.
+
+@item @code{enable-http?} (default: @code{#f}) (type: boolean)
+Listen on port 80 and handle requests encapsulated in HTTP.
+
+@item @code{v4-realm} (type: maybe-string)
+Realm for version 4 requests.
+
+@item @code{ports} (default: @code{()}) (type: list-of-strings)
+Ports to listen on.
+
+@item @code{addresses} (default: @code{()}) (type: list-of-strings)
+Addresses to listen on.
+
+@item @code{disable-des?} (default: @code{#f}) (type: boolean)
+Disable all DES encryption types.
+
+@end table
+
+@end deftp
+@c %end of fragment
+
+
+@subsubheading Heimdal Admin (Kadmind) Service
+
+The @code{(gnu services kerberos heimdal)} module provides services
+related to the @dfn{Heimdal} implementation for the authentication
+protocol @dfn{Kerberos}.
+
+This service starts the @dfn{Kerberos Administration} server. The server
+will remain running.
+
+Kerberos client programs can obtain the location of the server from a
+configuration file at @file{/etc/krb5.conf}. You may wish to create that
+file separately via the @code{krb5-association-service-type}.
+
+@c %start of fragment
+@deftp {Data Type} heimdal-kadmind-configuration
+Available @code{heimdal-kadmind-configuration} fields are:
+
+@table @asis
+@item @code{heimdal} (default: @code{heimdal}) (type: file-like)
+The heimdal package to use.
+
+@item @code{config-file} (type: maybe-string)
+Configuration file for Heimdal Kadmind server.
+
+@item @code{key-file} (type: maybe-string)
+Location of master key file.
+
+@item @code{keytab} (type: maybe-string)
+Kerberos keytab to use.
+
+@item @code{realm} (type: maybe-string)
+Kerberos realm to serve.
+
+@item @code{debug?} (default: @code{#f}) (type: boolean)
+Enable debugging.
+
+@item @code{ports} (default: @code{()}) (type: list-of-strings)
+Ports to listen on.
+
+@end table
+
+@end deftp
+@c %end of fragment
+
+
 @subsubheading PAM krb5 Service
 @cindex pam-krb5
 
diff --git a/gnu/local.mk b/gnu/local.mk
index a82372527e..64cda5b8b6 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -695,6 +695,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/services/guix.scm			\
   %D%/services/hurd.scm				\
   %D%/services/kerberos.scm			\
+  %D%/services/kerberos/heimdal.scm		\
   %D%/services/ldap.scm		      		\
   %D%/services/lightdm.scm      		\
   %D%/services/linux.scm			\
@@ -790,6 +791,8 @@ GNU_SYSTEM_MODULES =				\
   %D%/tests/ganeti.scm				\
   %D%/tests/gdm.scm				\
   %D%/tests/guix.scm				\
+  %D%/tests/heimdal-kadmind.scm			\
+  %D%/tests/heimdal-kdc.scm			\
   %D%/tests/monitoring.scm                      \
   %D%/tests/nfs.scm				\
   %D%/tests/image.scm				\
diff --git a/gnu/services/kerberos.scm b/gnu/services/kerberos.scm
index ec9b6c10b5..432f205904 100644
--- a/gnu/services/kerberos.scm
+++ b/gnu/services/kerberos.scm
@@ -421,9 +421,123 @@ (define krb5-association-service-type
 normally expect a configuration file in @file{/etc/krb5.conf}.  This service
 generates such a file.  It does not cause any daemon to be started.")))
 
-(define-deprecated krb-configuration krb5-association-configuration)
-(define-deprecated krb-configuration? krb5-association-configuration?)
-(define-deprecated krb-service-type krb5-association-service-type)
+(define-deprecated krb5-service-type krb5-association-service-type)
+
+(define-deprecated/public-alias
+  krb5-configuration
+  krb5-association-configuration)
+(define-deprecated/public-alias
+  krb5-configuration?
+  krb5-association-configuration?)
+
+(define-deprecated/public-alias
+  krb5-configuration-allow-weak-crypto?
+  krb5-association-configuration-allow-weak-crypto?)
+(define-deprecated/public-alias
+  krb5-configuration-ap-req-checksum-type
+  krb5-association-configuration-ap-req-checksum-type)
+(define-deprecated/public-alias
+  krb5-configuration-canonicalize?
+  krb5-association-configuration-canonicalize?)
+(define-deprecated/public-alias
+  krb5-configuration-ccache-type
+  krb5-association-configuration-ccache-type)
+(define-deprecated/public-alias
+  krb5-configuration-clockskew
+  krb5-association-configuration-clockskew)
+(define-deprecated/public-alias
+  krb5-configuration-default-ccache-name
+  krb5-association-configuration-default-ccache-name)
+(define-deprecated/public-alias
+  krb5-configuration-default-client-keytab-name
+  krb5-association-configuration-default-client-keytab-name)
+(define-deprecated/public-alias
+  krb5-configuration-default-keytab-name
+  krb5-association-configuration-default-keytab-name)
+(define-deprecated/public-alias
+  krb5-configuration-default-realm
+  krb5-association-configuration-default-realm)
+(define-deprecated/public-alias
+  krb5-configuration-default-tgs-enctypes
+  krb5-association-configuration-default-tgs-enctypes)
+(define-deprecated/public-alias
+  krb5-configuration-default-tkt-enctypes
+  krb5-association-configuration-default-tkt-enctypes)
+(define-deprecated/public-alias
+  krb5-configuration-dns-canonicalize-hostname?
+  krb5-association-configuration-dns-canonicalize-hostname?)
+(define-deprecated/public-alias
+  krb5-configuration-dns-lookup-kdc?
+  krb5-association-configuration-dns-lookup-kdc?)
+(define-deprecated/public-alias
+  krb5-configuration-err-fmt
+  krb5-association-configuration-err-fmt)
+(define-deprecated/public-alias
+  krb5-configuration-forwardable?
+  krb5-association-configuration-forwardable?)
+(define-deprecated/public-alias
+  krb5-configuration-ignore-acceptor-hostname?
+  krb5-association-configuration-ignore-acceptor-hostname?)
+(define-deprecated/public-alias
+  krb5-configuration-k5login-authoritative?
+  krb5-association-configuration-k5login-authoritative?)
+(define-deprecated/public-alias
+  krb5-configuration-k5login-directory
+  krb5-association-configuration-k5login-directory)
+(define-deprecated/public-alias
+  krb5-configuration-kcm-mach-service
+  krb5-association-configuration-kcm-mach-service)
+(define-deprecated/public-alias
+  krb5-configuration-kcm-socket
+  krb5-association-configuration-kcm-socket)
+(define-deprecated/public-alias
+  krb5-configuration-kdc-default-options
+  krb5-association-configuration-kdc-default-options)
+(define-deprecated/public-alias
+  krb5-configuration-kdc-timesync
+  krb5-association-configuration-kdc-timesync)
+(define-deprecated/public-alias
+  krb5-configuration-kdc-req-checksum-type
+  krb5-association-configuration-kdc-req-checksum-type)
+(define-deprecated/public-alias
+  krb5-configuration-noaddresses?
+  krb5-association-configuration-noaddresses?)
+(define-deprecated/public-alias
+  krb5-configuration-permitted-enctypes
+  krb5-association-configuration-permitted-enctypes)
+(define-deprecated/public-alias
+  krb5-configuration-plugin-base-dir
+  krb5-association-configuration-plugin-base-dir)
+(define-deprecated/public-alias
+  krb5-configuration-preferred-preauth-types
+  krb5-association-configuration-preferred-preauth-types)
+(define-deprecated/public-alias
+  krb5-configuration-proxiable?
+  krb5-association-configuration-proxiable?)
+(define-deprecated/public-alias
+  krb5-configuration-rdns?
+  krb5-association-configuration-rdns?)
+(define-deprecated/public-alias
+  krb5-configuration-realm-try-domains
+  krb5-association-configuration-realm-try-domains)
+(define-deprecated/public-alias
+  krb5-configuration-renew-lifetime
+  krb5-association-configuration-renew-lifetime)
+(define-deprecated/public-alias
+  krb5-configuration-safe-checksum-type
+  krb5-association-configuration-safe-checksum-type)
+(define-deprecated/public-alias
+  krb5-configuration-ticket-lifetime
+  krb5-association-configuration-ticket-lifetime)
+(define-deprecated/public-alias
+  krb5-configuration-udp-preference-limit
+  krb5-association-configuration-udp-preference-limit)
+(define-deprecated/public-alias
+  krb5-configuration-verify-ap-rereq-nofail?
+  krb5-association-configuration-verify-ap-rereq-nofail?)
+(define-deprecated/public-alias
+  krb5-configuration-realms
+  krb5-association-configuration-realms)
 
 
 
diff --git a/gnu/services/kerberos/heimdal.scm b/gnu/services/kerberos/heimdal.scm
new file mode 100644
index 0000000000..0dc17f6315
--- /dev/null
+++ b/gnu/services/kerberos/heimdal.scm
@@ -0,0 +1,189 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2023 Felix Lechner <[email-masked]>
+;;;
+;;; 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 heimdal)
+  #:use-module (gnu packages kerberos)
+  #:use-module (gnu services)
+  #:use-module (gnu services configuration)
+  #:use-module (gnu services shepherd)
+  #:use-module (guix gexp)
+  #:use-module (guix records)
+  #:use-module (ice-9 match)
+  #:export (heimdal-kdc-configuration
+            heimdal-kdc-service-type
+            heimdal-kadmind-configuration
+            heimdal-kadmind-service-type))
+
+
+;;;
+;;; Heimdal Kdc
+;;;
+
+(define-maybe/no-serialization string)
+
+(define (non-negative-integer? val)
+  (and (exact-integer? val) (not (negative? val))))
+
+(define-maybe/no-serialization non-negative-integer)
+
+(define-configuration/no-serialization heimdal-kdc-configuration
+  (heimdal
+   (file-like heimdal)
+   "The heimdal package to use.")
+  (config-file
+   maybe-string
+   "Configuration file for Heimdal KDC server.")
+  (require-preauth?
+   (boolean #t)
+   "Require pre-authentication in the initial AS-REQ for all principals.")
+  (max-request-size
+   maybe-non-negative-integer
+   "Maximum size of requests the server is willing to handle.")
+  (enable-http?
+   (boolean #f)
+   "Listen on port 80 and handle requests encapsulated in HTTP.")
+  (v4-realm
+   maybe-string
+   "Realm for version 4 requests.")
+  (ports
+   (list-of-strings '())
+   "Ports to listen on.")
+  (addresses
+   (list-of-strings '())
+   "Addresses to listen on.")
+  (disable-des?
+   (boolean #f)
+   "Disable all DES encryption types."))
+
+(define (heimdal-kdc-shepherd-service config)
+  "Return a <shepherd-service> for Heimdal's kdc for CONFIG."
+  (match-record config
+      <heimdal-kdc-configuration> (heimdal config-file require-preauth?
+                                           max-request-size enable-http?
+                                           v4-realm ports addresses
+                                           disable-des?)
+    (shepherd-service
+     (documentation "Run the Heimdal Kerberos KDC daemon (heimdal-kdc).")
+     (provision '(heimdal-kdc))
+     (requirement '(networking))
+     (start #~(make-forkexec-constructor
+               (list #$(file-append heimdal "/libexec/kdc")
+                     #$@(if (maybe-value-set? config-file)
+                            `(,(string-append "--config-file=" (maybe-value config-file)))
+                            '())
+                     #$@(if require-preauth? '() '("--no-require-preauth"))
+                     #$@(if (maybe-value-set? max-request-size)
+                            `(,(string-append
+                                "--max-request-size="
+                                (number->string (maybe-value max-request-size))))
+                            '())
+                     #$@(if enable-http? '("--enable-http") '())
+                     #$@(if (maybe-value-set? v4-realm)
+                            `(,(string-append "--v4-realm=" (maybe-value v4-realm)))
+                            '())
+                     ;; ports parameter is white-space separated
+                     #$@(if (null? ports)
+                            '()
+                            `(,(string-append "--ports=" (string-join ports))))
+                     ;; addresses parameter is white-space separated
+                     #$@(if (null? addresses)
+                            '()
+                            `(,(string-append "--addresses=" (string-join addresses))))
+                     #$@(if disable-des? '("--disable-des") '()))
+               #:log-file "/var/log/kdc-shepherd"))
+     (stop #~(make-kill-destructor)))))
+
+(define heimdal-kdc-service-type
+  (service-type
+   (name 'heimdal-kdc)
+   (description
+    "Run the Heimdal @command{kdc} daemon.")
+   (extensions
+    (list
+     (service-extension shepherd-root-service-type
+                        (compose list heimdal-kdc-shepherd-service))))
+   (default-value (heimdal-kdc-configuration))))
+
+
+;;;
+;;; Heimdal Kadmind
+;;;
+
+(define-configuration/no-serialization heimdal-kadmind-configuration
+  (heimdal
+   (file-like heimdal)
+   "The heimdal package to use.")
+  (config-file
+   maybe-string
+   "Configuration file for Heimdal Kadmind server.")
+  (key-file
+   maybe-string
+   "Location of master key file.")
+  (keytab
+   maybe-string
+   "Kerberos keytab to use.")
+  (realm
+   maybe-string
+   "Kerberos realm to serve.")
+  (debug?
+   (boolean #f)
+   "Enable debugging.")
+  (ports
+   (list-of-strings '())
+   "Ports to listen on."))
+
+(define (heimdal-kadmind-shepherd-service config)
+  "Return a <shepherd-service> for Heimdal's kadmind for CONFIG."
+  (match-record config
+      <heimdal-kadmind-configuration> (heimdal config-file key-file keytab
+                                               realm debug? ports)
+    (shepherd-service
+     (documentation "Run the Heimdal Kerberos admin daemon (heimdal-kadmind).")
+     (provision '(heimdal-kadmind))
+     (requirement '(networking))
+     (start #~(make-forkexec-constructor
+               (list #$(file-append heimdal "/libexec/kadmind")
+                     #$@(if (maybe-value-set? config-file)
+                            `(,(string-append "--config-file=" (maybe-value config-file)))
+                            '())
+                     #$@(if (maybe-value-set? key-file)
+                            `(,(string-append "--key-file=" (maybe-value key-file)))
+                            '())
+                     #$@(if (maybe-value-set? keytab)
+                            `(,(string-append "--keytab=" (maybe-value keytab)))
+                            '())
+                     #$@(if (maybe-value-set? realm)
+                            `(,(string-append "--realm=" (maybe-value realm)))
+                            '())
+                     #$@(if debug? '("--debug") '())
+                     ;; ports parameter is white-space separated
+                     #$@(if (null? ports)
+                            '()
+                            `(,(string-append "--ports=" (string-join ports)))))))
+     (stop #~(make-kill-destructor)))))
+
+(define heimdal-kadmind-service-type
+  (service-type
+   (name 'heimdal-kadmind)
+   (description
+    "Run the Heimdal @command{kadmind} daemon.")
+   (extensions
+    (list
+     (service-extension shepherd-root-service-type
+                        (compose list heimdal-kadmind-shepherd-service))))
+   (default-value (heimdal-kadmind-configuration))))
diff --git a/gnu/tests/heimdal-kadmind.scm b/gnu/tests/heimdal-kadmind.scm
new file mode 100644
index 0000000000..b340017c69
--- /dev/null
+++ b/gnu/tests/heimdal-kadmind.scm
@@ -0,0 +1,71 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2017 Peter Mikkelsen <[email-masked]>
+;;; Copyright © 2022 Bruno Victal <[email-masked]>
+;;; Copyright © 2023 Felix Lechner <[email-masked]>
+;;;
+;;; 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 tests heimdal-kadmind)
+  #:use-module (gnu tests)
+  #:use-module (gnu system)
+  #:use-module (gnu system vm)
+  #:use-module (gnu services)
+  #:use-module (gnu services kerberos heimdal)
+  #:use-module (gnu services networking)
+  #:use-module (guix gexp)
+  #:export (%test-heimdal-kadmind))
+
+(define %heimdal-kadmind-os
+  (simple-operating-system
+   (service dhcp-client-service-type)
+   (service heimdal-kadmind-service-type)))
+
+(define (run-heimdal-kadmind-test)
+  "Run tests in %heimdal-kadmind-os, which has heimdal-kadmind running."
+  (define os
+    (marionette-operating-system
+     %heimdal-kadmind-os
+     #:imported-modules '((gnu services herd))))
+
+  (define vm
+    (virtual-machine os))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (srfi srfi-64)
+                       (gnu build marionette))
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          (test-runner-current (system-test-runner #$output))
+          (test-begin "heimdal-kadmind")
+
+          (test-assert "service is running"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+                (start-service 'heimdal-kadmind))
+             marionette))
+
+          (test-end))))
+  (gexp->derivation "heimdal-kadmind-test" test))
+
+(define %test-heimdal-kadmind
+  (system-test
+   (name "heimdal-kadmind")
+   (description "Test that the heimdal-kadmind runs when started.")
+   (value (run-heimdal-kadmind-test))))
diff --git a/gnu/tests/heimdal-kdc.scm b/gnu/tests/heimdal-kdc.scm
new file mode 100644
index 0000000000..b6424ace9e
--- /dev/null
+++ b/gnu/tests/heimdal-kdc.scm
@@ -0,0 +1,71 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2017 Peter Mikkelsen <[email-masked]>
+;;; Copyright © 2022 Bruno Victal <[email-masked]>
+;;; Copyright © 2023 Felix Lechner <[email-masked]>
+;;;
+;;; 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 tests heimdal-kdc)
+  #:use-module (gnu tests)
+  #:use-module (gnu system)
+  #:use-module (gnu system vm)
+  #:use-module (gnu services)
+  #:use-module (gnu services kerberos heimdal)
+  #:use-module (gnu services networking)
+  #:use-module (guix gexp)
+  #:export (%test-heimdal-kdc))
+
+(define %heimdal-kdc-os
+  (simple-operating-system
+   (service dhcp-client-service-type)
+   (service heimdal-kdc-service-type)))
+
+(define (run-heimdal-kdc-test)
+  "Run tests in %heimdal-kdc-os, which has heimdal-kdc running."
+  (define os
+    (marionette-operating-system
+     %heimdal-kdc-os
+     #:imported-modules '((gnu services herd))))
+
+  (define vm
+    (virtual-machine os))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (srfi srfi-64)
+                       (gnu build marionette))
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          (test-runner-current (system-test-runner #$output))
+          (test-begin "heimdal-kdc")
+
+          (test-assert "service is running"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+                (start-service 'heimdal-kdc))
+             marionette))
+
+          (test-end))))
+  (gexp->derivation "heimdal-kdc-test" test))
+
+(define %test-heimdal-kdc
+  (system-test
+   (name "heimdal-kdc")
+   (description "Test that the heimdal-kdc runs when started.")
+   (value (run-heimdal-kdc-test))))
-- 
2.41.0