diff options
| author | David Thompson <davet@gnu.org> | 2015-06-02 08:48:16 -0400 |
|---|---|---|
| committer | David Thompson <dthompson2@worcester.edu> | 2015-07-09 08:23:03 -0400 |
| commit | c1f6a0c2ed8caa5b04aae77e5d2e3a2299305a43 (patch) | |
| tree | e9641afa1af47836057b6fc9d18f10c88da92ace /gnu | |
| parent | 85c3127fa9226ff9efa504dddffcf8442f54488d (diff) | |
gnu: build: Add Linux container module.
* gnu/build/linux-container.scm: New file.
* gnu-system.am (GNU_SYSTEM_MODULES): Add it.
* .dir-locals.el: Add Scheme indent rules for 'call-with-container', and
'container-excursion'.
* tests/containers.scm: New file.
* Makefile.am (SCM_TESTS): Add it.
Diffstat (limited to 'gnu')
| -rw-r--r-- | gnu/build/linux-container.scm | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/gnu/build/linux-container.scm b/gnu/build/linux-container.scm new file mode 100644 index 00000000000..7a03a29d2ce --- /dev/null +++ b/gnu/build/linux-container.scm | |||
| @@ -0,0 +1,260 @@ | |||
| 1 | ;;; GNU Guix --- Functional package management for GNU | ||
| 2 | ;;; Copyright © 2015 David Thompson <davet@gnu.org> | ||
| 3 | ;;; | ||
| 4 | ;;; This file is part of GNU Guix. | ||
| 5 | ;;; | ||
| 6 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | ||
| 7 | ;;; under the terms of the GNU General Public License as published by | ||
| 8 | ;;; the Free Software Foundation; either version 3 of the License, or (at | ||
| 9 | ;;; your option) any later version. | ||
| 10 | ;;; | ||
| 11 | ;;; GNU Guix is distributed in the hope that it will be useful, but | ||
| 12 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | ;;; GNU General Public License for more details. | ||
| 15 | ;;; | ||
| 16 | ;;; You should have received a copy of the GNU General Public License | ||
| 17 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | ||
| 18 | |||
| 19 | (define-module (gnu build linux-container) | ||
| 20 | #:use-module (ice-9 format) | ||
| 21 | #:use-module (ice-9 match) | ||
| 22 | #:use-module (srfi srfi-98) | ||
| 23 | #:use-module (guix utils) | ||
| 24 | #:use-module (guix build utils) | ||
| 25 | #:use-module (guix build syscalls) | ||
| 26 | #:use-module ((gnu build file-systems) #:select (mount-file-system)) | ||
| 27 | #:export (%namespaces | ||
| 28 | run-container | ||
| 29 | call-with-container | ||
| 30 | container-excursion)) | ||
| 31 | |||
| 32 | (define %namespaces | ||
| 33 | '(mnt pid ipc uts user net)) | ||
| 34 | |||
| 35 | (define (call-with-clean-exit thunk) | ||
| 36 | "Apply THUNK, but exit with a status code of 1 if it fails." | ||
| 37 | (dynamic-wind | ||
| 38 | (const #t) | ||
| 39 | thunk | ||
| 40 | (lambda () | ||
| 41 | (primitive-exit 1)))) | ||
| 42 | |||
| 43 | (define (purify-environment) | ||
| 44 | "Unset all environment variables." | ||
| 45 | (for-each unsetenv | ||
| 46 | (match (get-environment-variables) | ||
| 47 | (((names . _) ...) names)))) | ||
| 48 | |||
| 49 | ;; The container setup procedure closely resembles that of the Docker | ||
| 50 | ;; specification: | ||
| 51 | ;; https://raw.githubusercontent.com/docker/libcontainer/master/SPEC.md | ||
| 52 | (define* (mount-file-systems root mounts #:key mount-/sys? mount-/proc?) | ||
| 53 | "Mount the essential file systems and the those in the MOUNTS list relative | ||
| 54 | to ROOT, then make ROOT the new root directory for the process." | ||
| 55 | (define (scope dir) | ||
| 56 | (string-append root dir)) | ||
| 57 | |||
| 58 | (define (bind-mount src dest) | ||
| 59 | (mount src dest "none" MS_BIND)) | ||
| 60 | |||
| 61 | ;; Like mount, but creates the mount point if it doesn't exist. | ||
| 62 | (define* (mount* source target type #:optional (flags 0) options | ||
| 63 | #:key (update-mtab? #f)) | ||
| 64 | (mkdir-p target) | ||
| 65 | (mount source target type flags options #:update-mtab? update-mtab?)) | ||
| 66 | |||
| 67 | ;; The container's file system is completely ephemeral, sans directories | ||
| 68 | ;; bind-mounted from the host. | ||
| 69 | (mount "none" root "tmpfs") | ||
| 70 | |||
| 71 | ;; A proc mount requires a new pid namespace. | ||
| 72 | (when mount-/proc? | ||
| 73 | (mount* "none" (scope "/proc") "proc" | ||
| 74 | (logior MS_NOEXEC MS_NOSUID MS_NODEV))) | ||
| 75 | |||
| 76 | ;; A sysfs mount requires the user to have the CAP_SYS_ADMIN capability in | ||
| 77 | ;; the current network namespace. | ||
| 78 | (when mount-/sys? | ||
| 79 | (mount* "none" (scope "/sys") "sysfs" | ||
| 80 | (logior MS_NOEXEC MS_NOSUID MS_NODEV MS_RDONLY))) | ||
| 81 | |||
| 82 | (mount* "none" (scope "/dev") "tmpfs" | ||
| 83 | (logior MS_NOEXEC MS_STRICTATIME) | ||
| 84 | "mode=755") | ||
| 85 | |||
| 86 | ;; Create essential device nodes via bind-mounting them from the | ||
| 87 | ;; host, because a process within a user namespace cannot create | ||
| 88 | ;; device nodes. | ||
| 89 | (for-each (lambda (device) | ||
| 90 | (when (file-exists? device) | ||
| 91 | ;; Create the mount point file. | ||
| 92 | (call-with-output-file (scope device) | ||
| 93 | (const #t)) | ||
| 94 | (bind-mount device (scope device)))) | ||
| 95 | '("/dev/null" | ||
| 96 | "/dev/zero" | ||
| 97 | "/dev/full" | ||
| 98 | "/dev/random" | ||
| 99 | "/dev/urandom" | ||
| 100 | "/dev/tty" | ||
| 101 | "/dev/ptmx" | ||
| 102 | "/dev/fuse")) | ||
| 103 | |||
| 104 | ;; Setup standard input/output/error. | ||
| 105 | (symlink "/proc/self/fd" (scope "/dev/fd")) | ||
| 106 | (symlink "/proc/self/fd/0" (scope "/dev/stdin")) | ||
| 107 | (symlink "/proc/self/fd/1" (scope "/dev/stdout")) | ||
| 108 | (symlink "/proc/self/fd/2" (scope "/dev/stderr")) | ||
| 109 | |||
| 110 | ;; Mount user-specified file systems. | ||
| 111 | (for-each (lambda (spec) | ||
| 112 | (mount-file-system spec #:root root)) | ||
| 113 | mounts) | ||
| 114 | |||
| 115 | ;; Jail the process inside the container's root file system. | ||
| 116 | (let ((put-old (string-append root "/real-root"))) | ||
| 117 | (mkdir put-old) | ||
| 118 | (pivot-root root put-old) | ||
| 119 | (chdir "/") | ||
| 120 | (umount "real-root" MNT_DETACH) | ||
| 121 | (rmdir "real-root"))) | ||
| 122 | |||
| 123 | (define (initialize-user-namespace pid) | ||
| 124 | "Configure the user namespace for PID." | ||
| 125 | (define proc-dir | ||
| 126 | (string-append "/proc/" (number->string pid))) | ||
| 127 | |||
| 128 | (define (scope file) | ||
| 129 | (string-append proc-dir file)) | ||
| 130 | |||
| 131 | ;; Only root can map more than a single uid/gid. A range of 65536 uid/gids | ||
| 132 | ;; is used to cover 16 bits worth of users and groups, which is sufficient | ||
| 133 | ;; for most cases. | ||
| 134 | ;; | ||
| 135 | ;; See also: http://www.freedesktop.org/software/systemd/man/systemd-nspawn.html#--private-users= | ||
| 136 | (let* ((uid (getuid)) | ||
| 137 | (gid (getgid)) | ||
| 138 | (uid-range (if (zero? uid) 65536 1)) | ||
| 139 | (gid-range (if (zero? gid) 65536 1))) | ||
| 140 | |||
| 141 | ;; Only root can write to the gid map without first disabling the | ||
| 142 | ;; setgroups syscall. | ||
| 143 | (unless (and (zero? uid) (zero? gid)) | ||
| 144 | (call-with-output-file (scope "/setgroups") | ||
| 145 | (lambda (port) | ||
| 146 | (display "deny" port)))) | ||
| 147 | |||
| 148 | ;; Map the user/group that created the container to the root user | ||
| 149 | ;; within the container. | ||
| 150 | (call-with-output-file (scope "/uid_map") | ||
| 151 | (lambda (port) | ||
| 152 | (format port "0 ~d ~d" uid uid-range))) | ||
| 153 | (call-with-output-file (scope "/gid_map") | ||
| 154 | (lambda (port) | ||
| 155 | (format port "0 ~d ~d" gid gid-range))))) | ||
| 156 | |||
| 157 | (define (namespaces->bit-mask namespaces) | ||
| 158 | "Return the number suitable for the 'flags' argument of 'clone' that | ||
| 159 | corresponds to the symbols in NAMESPACES." | ||
| 160 | (apply logior SIGCHLD | ||
| 161 | (map (match-lambda | ||
| 162 | ('mnt CLONE_NEWNS) | ||
| 163 | ('uts CLONE_NEWUTS) | ||
| 164 | ('ipc CLONE_NEWIPC) | ||
| 165 | ('user CLONE_NEWUSER) | ||
| 166 | ('pid CLONE_NEWPID) | ||
| 167 | ('net CLONE_NEWNET)) | ||
| 168 | namespaces))) | ||
| 169 | |||
| 170 | (define (run-container root mounts namespaces thunk) | ||
| 171 | "Run THUNK in a new container process and return its PID. ROOT specifies | ||
| 172 | the root directory for the container. MOUNTS is a list of file system specs | ||
| 173 | that specify the mapping of host file systems into the container. NAMESPACES | ||
| 174 | is a list of symbols that correspond to the possible Linux namespaces: mnt, | ||
| 175 | ipc, uts, user, and net." | ||
| 176 | ;; The parent process must initialize the user namespace for the child | ||
| 177 | ;; before it can boot. To negotiate this, a pipe is used such that the | ||
| 178 | ;; child process blocks until the parent writes to it. | ||
| 179 | (match (pipe) | ||
| 180 | ((in . out) | ||
| 181 | (let ((flags (namespaces->bit-mask namespaces))) | ||
| 182 | (match (clone flags) | ||
| 183 | (0 | ||
| 184 | (call-with-clean-exit | ||
| 185 | (lambda () | ||
| 186 | (close out) | ||
| 187 | ;; Wait for parent to set things up. | ||
| 188 | (read in) | ||
| 189 | (close in) | ||
| 190 | (purify-environment) | ||
| 191 | (when (memq 'mnt namespaces) | ||
| 192 | (mount-file-systems root mounts | ||
| 193 | #:mount-/proc? (memq 'pid namespaces) | ||
| 194 | #:mount-/sys? (memq 'net namespaces))) | ||
| 195 | ;; TODO: Manage capabilities. | ||
| 196 | (thunk)))) | ||
| 197 | (pid | ||
| 198 | (when (memq 'user namespaces) | ||
| 199 | (initialize-user-namespace pid)) | ||
| 200 | ;; TODO: Initialize cgroups. | ||
| 201 | (close in) | ||
| 202 | (write 'ready out) | ||
| 203 | (close out) | ||
| 204 | pid)))))) | ||
| 205 | |||
| 206 | (define* (call-with-container mounts thunk #:key (namespaces %namespaces)) | ||
| 207 | "Run THUNK in a new container process and return its exit status. | ||
| 208 | MOUNTS is a list of file system specs that specify the mapping of host file | ||
| 209 | systems into the container. NAMESPACES is a list of symbols corresponding to | ||
| 210 | the identifiers for Linux namespaces: mnt, ipc, uts, pid, user, and net. By | ||
| 211 | default, all namespaces are used. | ||
| 212 | |||
| 213 | Note that if THUNK needs to load any additional Guile modules, the relevant | ||
| 214 | module files must be present in one of the mappings in MOUNTS and the Guile | ||
| 215 | load path must be adjusted as needed." | ||
| 216 | (call-with-temporary-directory | ||
| 217 | (lambda (root) | ||
| 218 | (let ((pid (run-container root mounts namespaces thunk))) | ||
| 219 | ;; Catch SIGINT and kill the container process. | ||
| 220 | (sigaction SIGINT | ||
| 221 | (lambda (signum) | ||
| 222 | (false-if-exception | ||
| 223 | (kill pid SIGKILL)))) | ||
| 224 | |||
| 225 | (match (waitpid pid) | ||
| 226 | ((_ . status) status)))))) | ||
| 227 | |||
| 228 | (define (container-excursion pid thunk) | ||
| 229 | "Run THUNK as a child process within the namespaces of process PID and | ||
| 230 | return the exit status." | ||
| 231 | (define (namespace-file pid namespace) | ||
| 232 | (string-append "/proc/" (number->string pid) "/ns/" namespace)) | ||
| 233 | |||
| 234 | (match (primitive-fork) | ||
| 235 | (0 | ||
| 236 | (call-with-clean-exit | ||
| 237 | (lambda () | ||
| 238 | (for-each (lambda (ns) | ||
| 239 | (call-with-input-file (namespace-file (getpid) ns) | ||
| 240 | (lambda (current-ns-port) | ||
| 241 | (call-with-input-file (namespace-file pid ns) | ||
| 242 | (lambda (new-ns-port) | ||
| 243 | ;; Joining the namespace that the process | ||
| 244 | ;; already belongs to would throw an error. | ||
| 245 | (unless (= (port->fdes current-ns-port) | ||
| 246 | (port->fdes new-ns-port)) | ||
| 247 | (setns (port->fdes new-ns-port) 0))))))) | ||
| 248 | ;; It's important that the user namespace is joined first, | ||
| 249 | ;; so that the user will have the privileges to join the | ||
| 250 | ;; other namespaces. Furthermore, it's important that the | ||
| 251 | ;; mount namespace is joined last, otherwise the /proc mount | ||
| 252 | ;; point would no longer be accessible. | ||
| 253 | '("user" "ipc" "uts" "net" "pid" "mnt")) | ||
| 254 | (purify-environment) | ||
| 255 | (chdir "/") | ||
| 256 | (thunk)))) | ||
| 257 | (pid | ||
| 258 | (match (waitpid pid) | ||
| 259 | ((_ . status) | ||
| 260 | (status:exit-val status)))))) | ||
