;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2015 David Thompson ;;; Copyright © 2017, 2018, 2019 Ludovic Courtès ;;; ;;; This file is part of GNU Guix. ;;; ;;; GNU Guix is free software; you can redistribute it and/or modify it ;;; under the terms of the GNU General Public License as published by ;;; the Free Software Foundation; either version 3 of the License, or (at ;;; your option) any later version. ;;; ;;; GNU Guix is distributed in the hope that it will be useful, but ;;; WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; GNU General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with GNU Guix. If not, see . (define-module (gnu build linux-container) #:use-module (ice-9 format) #:use-module (ice-9 match) #:use-module (ice-9 rdelim) #:use-module (srfi srfi-98) #:use-module (guix build utils) #:use-module (guix build syscalls) #:use-module (gnu system file-systems) ; #:use-module ((gnu build file-systems) #:select (mount-file-system)) #:export (user-namespace-supported? unprivileged-user-namespace-supported? setgroups-supported? %namespaces run-container call-with-container container-excursion container-excursion*)) (define (user-namespace-supported?) "Return #t if user namespaces are supported on this system." (file-exists? "/proc/self/ns/user")) (define (unprivileged-user-namespace-supported?) "Return #t if user namespaces can be created by unprivileged users." (let ((userns-file "/proc/sys/kernel/unprivileged_userns_clone")) (if (file-exists? userns-file) (eqv? #\1 (call-with-input-file userns-file read-char)) #t))) (define (;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2017, 2018 Clément Lassieur <clement@lassieur.org> ;;; Copyright © 2017-2018, 2021-2022 Ludovic Courtès <ludo@gnu.org> ;;; Copyright © 2018 Efraim Flashner <efraim@flashner.co.il> ;;; ;;; 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 messaging) #:use-module (gnu tests) #:use-module (gnu system) #:use-module (gnu system vm) #:use-module (gnu services) #:use-module (gnu services messaging) #:use-module (gnu services networking) #:use-module (gnu packages messaging) #:use-module (guix gexp) #:use-module (guix store) #:use-module (guix modules) #:export (%test-prosody %test-bitlbee %test-quassel)) (define (run-xmpp-test name xmpp-service pid-file create-account) "Run a test of an OS running XMPP-SERVICE, which writes its PID to PID-FILE." (define os (marionette-operating-system (simple-operating-system (service dhcp-client-service-type) xmpp-service) #:imported-modules '((gnu services herd)))) (define port 15222) (define vm (virtual-machine (operating-system os) (port-forwardings `((,port . 5222))))) (define username "alice") (define server "localhost") (define jid (string-append username "@" server)) (define password "correct horse battery staple") (define message "hello world") (define witness "/tmp/freetalk-witness") (define script.ft (scheme-file "script.ft" #~(begin (define (handle-received-message time from nickname message) (define (touch file-name) (call-with-output-file file-name (const #t))) (when (equal? message #$message) (touch #$witness))) (add-hook! ft-message-receive-hook handle-received-message) (ft-set-jid! #$jid) (ft-set-password! #$password) (ft-set-server! #$server) (ft-set-port! #$port) (ft-set-sslconn! #f) (ft-connect-blocking) (ft-send-message #$jid #$message) (ft-set-daemon) (ft-main-loop)))) (define test (with-imported-modules '((gnu build marionette)) #~(begin (use-modules (gnu build marionette) (srfi srfi-64)) (define marionette (make-marionette (list #$vm))) (define (host-wait-for-file file) ;; Wait until FILE exists in the host. (let loop ((i 60)) (cond ((file-exists? file) #t) ((> i 0) (begin (sleep 1)) (loop (- i 1))) (else (error "file didn't show up" file))))) (test-runner-current (system-test-runner #$output)) (test-begin "xmpp") ;; Wait for XMPP service to be up and running. (test-assert "service running" (marionette-eval '(begin (use-modules (gnu services herd)) (start-service 'xmpp-daemon)) marionette)) ;; Check XMPP service's PID. (test-assert "service process id" (let ((pid (number->string (wait-for-file #$pid-file marionette)))) (marionette-eval `(file-exists? (string-append "/proc/" ,pid)) marionette))) ;; Alice sends an XMPP message to herself, with Freetalk. (test-assert "client-to-server communication" (let ((freetalk-bin (string-append #$freetalk "/bin/freetalk"))) (marionette-eval '(system* #$create-account #$jid #$password) marionette) ;; Freetalk requires write access to $HOME. (setenv "HOME" "/tmp") (system* freetalk-bin "-s" #$script.ft) (host-wait-for-file #$witness))) (test-end)))) (gexp->derivation name test)) (define %create-prosody-account (program-file "create-account" #~(begin (use-modules (ice-9 match)) (match (command-line) ((command jid password) (let ((password-input (format #f "\"~a~%~a\"" password password)) (prosodyctl #$(file-append prosody "/bin/prosodyctl"))) (system (string-join `("echo" ,password-input "|" ,prosodyctl "adduser" ,jid) " ")))))))) (define %test-prosody (let* ((config (prosody-configuration (insecure-sasl-mechanisms '()) (virtualhosts (list (virtualhost-configuration (domain "localhost"))))))) (system-test (name "prosody") (description "Connect to a running Prosody daemon.") (value (run-xmpp-test name (service prosody-service-type config) (prosody-configuration-pidfile config) %create-prosody-account))))) ;;; ;;; BitlBee. ;;; (define (run-bitlbee-test) (define os (marionette-operating-system (simple-operating-system (service dhcp-client-service-type) (service bitlbee-service-type (bitlbee-configuration (interface "0.0.0.0")))) #:imported-modules (source-module-closure '((gnu services herd))))) (define vm (virtual-machine (operating-system os) (port-forwardings `((6667 . 6667))))) (define test (with-imported-modules '((gnu build marionette)) #~(begin (use-modules (ice-9 rdelim) (srfi srfi-64) (gnu build marionette)) (define marionette (make-marionette (list #$vm))) (test-runner-current (system-test-runner #$output)) (test-begin "bitlbee") (test-assert "service started" (marionette-eval '(begin (use-modules (gnu services herd)) (start-service 'bitlbee)) marionette)) (test-assert "connect" (let* ((address (make-socket-address AF_INET INADDR_LOOPBACK 6667)) (sock (socket AF_INET SOCK_STREAM 0))) (connect sock address) ;; See <https://tools.ietf.org/html/rfc1459>. (->bool (string-contains (pk 'message (read-line sock)) "BitlBee")))) (test-end)))) (gexp->derivation "bitlbee-test" test)) (define %test-bitlbee (system-test (name "bitlbee") (description "Connect to a BitlBee IRC server.") (value (run-bitlbee-test)))) (define (run-quassel-test) (define os (marionette-operating-system (simple-operating-system (service dhcp-client-service-type) (service quassel-service-type)) #:imported-modules (source-module-closure '((gnu services herd))))) (define vm (virtual-machine (operating-system os) (port-forwardings `((4242 . 4242))))) (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 "quassel") (test-assert "service started" (marionette-eval '(begin (use-modules (gnu services herd)) (start-service 'quassel)) marionette)) (test-assert "certificate file" (marionette-eval '(file-exists? "/var/lib/quassel/quasselCert.pem") marionette)) (test-end)))) (gexp->derivation "quassel-test" test)) (define %test-quassel (system-test (name "quassel") (description "Connect to a quassel IRC server.") (value (run-quassel-test)))) le-recursively tmp-dir)))))) (define* (call-with-container mounts thunk #:key (namespaces %namespaces) (host-uids 1) (guest-uid 0) (guest-gid 0) (process-spawned-hook (const #t))) "Run THUNK in a new container process and return its exit status; call PROCESS-SPAWNED-HOOK with the PID of the new process that has been spawned. MOUNTS is a list of objects that specify file systems to mount inside the container. NAMESPACES is a list of symbols corresponding to the identifiers for Linux namespaces: mnt, ipc, uts, pid, user, and net. By default, all namespaces are used. HOST-UIDS is the number of host user identifiers to map into the container's user namespace, if there is one. By default, only a single uid/gid, that of the current user, is mapped into the container. The host user that creates the container is the root user (uid/gid 0) within the container. Only root can map more than a single uid/gid. GUEST-UID and GUEST-GID specify the first UID (respectively GID) that host UIDs (respectively GIDs) map to in the namespace. Note that if THUNK needs to load any additional Guile modules, the relevant module files must be present in one of the mappings in MOUNTS and the Guile load path must be adjusted as needed." (call-with-temporary-directory (lambda (root) (let ((pid (run-container root mounts namespaces host-uids thunk #:guest-uid guest-uid #:guest-gid guest-gid))) ;; Catch SIGINT and kill the container process. (sigaction SIGINT (lambda (signum) (false-if-exception (kill pid SIGKILL)))) (process-spawned-hook pid) (match (waitpid pid) ((_ . status) status)))))) (define (container-excursion pid thunk) "Run THUNK as a child process within the namespaces of process PID and return the exit status." (define (namespace-file pid namespace) (string-append "/proc/" (number->string pid) "/ns/" namespace)) (match (primitive-fork) (0 (call-with-clean-exit (lambda () (for-each (lambda (ns) (let ((source (namespace-file (getpid) ns)) (target (namespace-file pid ns))) ;; Joining the namespace that the process already ;; belongs to would throw an error so avoid that. ;; XXX: This /proc interface leads to TOCTTOU. (unless (string=? (readlink source) (readlink target)) (call-with-input-file source (lambda (current-ns-port) (call-with-input-file target (lambda (new-ns-port) (setns (fileno new-ns-port) 0)))))))) ;; It's important that the user namespace is joined first, ;; so that the user will have the privileges to join the ;; other namespaces. Furthermore, it's important that the ;; mount namespace is joined last, otherwise the /proc mount ;; point would no longer be accessible. '("user" "ipc" "uts" "net" "pid" "mnt")) (purify-environment) (chdir "/") (thunk)))) (pid (match (waitpid pid) ((_ . status) (status:exit-val status)))))) (define (container-excursion* pid thunk) "Like 'container-excursion', but return the return value of THUNK." (match (pipe) ((in . out) (match (container-excursion pid (lambda () (close-port in) (write (thunk) out) (close-port out))) (0 (close-port out) (let ((result (read in))) (close-port in) result)) (_ ;maybe PID died already (close-port out) (close-port in) #f)))))