#!/bin/sh # Client script for LXC container images. # # Copyright @ Daniel Lezcano # Copyright © 2018 Christian Brauner # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library 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 # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA LXC_MAPPED_UID= LXC_MAPPED_GID= BUSYBOX_EXE=`which busybox` # Make sure the usual locations are in PATH export PATH=$PATH:/usr/sbin:/usr/bin:/sbin:/bin in_userns() { [ -e /proc/self/uid_map ] || { echo no; return; } while read -r line; do fields="$(echo "$line" | awk '{ print $1 " " $2 " " $3 }')" if [ "${fields}" = "0 0 4294967295" ]; then echo no; return; fi if echo "${fields}" | grep -q " 0 1$"; then echo userns-root; return; fi done < /proc/self/uid_map [ "$(cat /proc/self/uid_map)" = "$(cat /proc/1/uid_map)" ] && { echo userns-root; return; } echo yes } USERNS="$(in_userns)" install_busybox() { rootfs="${1}" name="${2}" res=0 fstree="\ ${rootfs}/selinux \ ${rootfs}/dev \ ${rootfs}/home \ ${rootfs}/root \ ${rootfs}/etc \ ${rootfs}/etc/init.d \ ${rootfs}/bin \ ${rootfs}/usr/bin \ ${rootfs}/sbin \ ${rootfs}/usr/sbin \ ${rootfs}/proc \ ${rootfs}/sys \ ${rootfs}/mnt \ ${rootfs}/tmp \ ${rootfs}/var/log \ ${rootfs}/var/run \ ${rootfs}/usr/share/udhcpc \ ${rootfs}/dev/pts \ ${rootfs}/dev/shm \ ${rootfs}/lib \ ${rootfs}/usr/lib \ ${rootfs}/lib64 \ ${rootfs}/usr/lib64" # shellcheck disable=SC2086 mkdir -p ${fstree} || return 1 # shellcheck disable=SC2086 chmod 755 ${fstree} || return 1 # minimal devices needed for busybox if [ "${USERNS}" = "yes" ]; then for dev in tty console tty0 tty1 ram0 null urandom; do echo "lxc.mount.entry = /dev/${dev} dev/${dev} none bind,optional,create=file 0 0" >> "${path}/config" done else mknod -m 666 "${rootfs}/dev/tty" c 5 0 || res=1 mknod -m 666 "${rootfs}/dev/console" c 5 1 || res=1 mknod -m 666 "${rootfs}/dev/tty0" c 4 0 || res=1 mknod -m 666 "${rootfs}/dev/tty1" c 4 0 || res=1 mknod -m 666 "${rootfs}/dev/tty5" c 4 0 || res=1 mknod -m 600 "${rootfs}/dev/ram0" b 1 0 || res=1 mknod -m 666 "${rootfs}/dev/null" c 1 3 || res=1 mknod -m 666 "${rootfs}/dev/zero" c 1 5 || res=1 mknod -m 666 "${rootfs}/dev/urandom" c 1 9 || res=1 fi # make /tmp accessible to any user (with sticky bit) chmod 1777 "${rootfs}/tmp" || return 1 # root user defined cat <> "${rootfs}/etc/passwd" root:x:0:0:root:/root:/bin/sh EOF cat <> "${rootfs}/etc/group" root:x:0:root EOF # mount everything cat <> "${rootfs}/etc/init.d/rcS" #!/bin/sh /bin/syslogd /bin/mount -a /bin/udhcpc EOF # executable chmod 744 "${rootfs}/etc/init.d/rcS" || return 1 # launch rcS first then make a console available # and propose a shell on the tty, the last one is # not needed cat <> "${rootfs}/etc/inittab" ::sysinit:/etc/init.d/rcS tty1::respawn:/bin/getty -L tty1 115200 vt100 console::askfirst:/bin/sh EOF # writable and readable for other chmod 644 "${rootfs}/etc/inittab" || return 1 # Look for the pathname of "default.script" from the help of udhcpc DEF_SCRIPT=`${BUSYBOX_EXE} udhcpc -h 2>&1 | grep -- '-s,--script PROG' | cut -d'/' -f2- | cut -d')' -f1` DEF_SCRIPT_DIR=`dirname /${DEF_SCRIPT}` mkdir -p ${rootfs}/${DEF_SCRIPT_DIR} chmod 644 ${rootfs}/${DEF_SCRIPT_DIR} || return 1 cat <> ${rootfs}/${DEF_SCRIPT} #!/bin/sh case "\$1" in deconfig) ip addr flush dev \$interface ;; renew|bound) # flush all the routes if [ -n "\$router" ]; then ip route del default 2> /dev/null fi # check broadcast if [ -n "\$broadcast" ]; then broadcast="broadcast \$broadcast" fi # add a new ip address ip addr add \$ip/\$mask \$broadcast dev \$interface if [ -n "\$router" ]; then ip route add default via \$router dev \$interface fi [ -n "\$domain" ] && echo search \$domain > /etc/resolv.conf for i in \$dns ; do grep "nameserver \$i" /etc/resolv.conf > /dev/null 2>&1 if [ \$? -ne 0 ]; then echo nameserver \$i >> /etc/resolv.conf fi done ;; esac exit 0 EOF chmod 744 ${rootfs}/${DEF_SCRIPT} return "${res}" } configure_busybox() { rootfs="${1}" # copy busybox in the rootfs if ! cp "${BUSYBOX_EXE}" "${rootfs}/bin"; then echo "ERROR: Failed to copy busybox binary" 1>&2 return 1 fi # symlink busybox for the commands it supports # it would be nice to just use "chroot $rootfs busybox --install -s /bin" # but that only works right in a chroot with busybox >= 1.19.0 ( cd "${rootfs}/bin" || return 1 ./busybox --list | grep -v busybox | xargs -n1 ln -s busybox ) # relink /sbin/init ln "${rootfs}/bin/busybox" "${rootfs}/sbin/init" # /etc/fstab must exist for "mount -a" touch "${rootfs}/etc/fstab" # passwd exec must be setuid chmod +s "${rootfs}/bin/passwd" touch "${rootfs}/etc/shadow" return 0 } copy_configuration() { path="${1}" rootfs="${2}" name="${3}" grep -q "^lxc.rootfs.path" "${path}/config" 2>/dev/null || echo "lxc.rootfs.path = ${rootfs}" >> "${path}/config" cat <> "${path}/config" lxc.signal.halt = SIGUSR1 lxc.signal.reboot = SIGTERM lxc.uts.name = "${name}" lxc.tty.max = 1 lxc.pty.max = 1 lxc.cap.drop = sys_module mac_admin mac_override sys_time # When using LXC with apparmor, uncomment the next line to run unconfined: #lxc.apparmor.profile = unconfined lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed lxc.mount.entry = shm /dev/shm tmpfs defaults 0 0 EOF libdirs="\ lib \ usr/lib \ lib64 \ usr/lib64" for dir in ${libdirs}; do if [ -d "/${dir}" ] && [ -d "${rootfs}/${dir}" ]; then echo "lxc.mount.entry = /${dir} ${dir} none ro,bind 0 0" >> "${path}/config" fi done echo "lxc.mount.entry = /sys/kernel/security sys/kernel/security none ro,bind,optional 0 0" >> "${path}/config" } remap_userns() { path="${1}" if [ -n "$LXC_MAPPED_UID" ] && [ "$LXC_MAPPED_UID" != "-1" ]; then chown "${LXC_MAPPED_UID}" "${path}/config" > /dev/null 2>&1 chown -R root "${path}/rootfs" > /dev/null 2>&1 fi if [ -n "$LXC_MAPPED_GID" ] && [ "$LXC_MAPPED_GID" != "-1" ]; then chgrp "${LXC_MAPPED_GID}" "${path}/config" > /dev/null 2>&1 chgrp -R root "${path}/rootfs" > /dev/null 2>&1 fi } usage() { cat < ]: The container name [ --path ]: The path to the container [ --rootfs ]: The path to the container's rootfs (default: config or /rootfs) [ --mapped-uid ]: A uid map (user namespaces) [ --mapped-gid ]: A gid map (user namespaces) BUSYBOX template specific arguments: [ --busybox-path ]: busybox pathname (default: ${BUSYBOX_EXE}) EOF return 0 } if ! options=$(getopt -o hp:n: -l help,rootfs:,path:,name:,mapped-uid:,mapped-gid:,busybox-path: -- "$@"); then usage exit 1 fi eval set -- "$options" while true do case "$1" in -h|--help) usage && exit 0;; -n|--name) name=$2; shift 2;; -p|--path) path=$2; shift 2;; --rootfs) rootfs=$2; shift 2;; --mapped-uid) LXC_MAPPED_UID=$2; shift 2;; --mapped-gid) LXC_MAPPED_GID=$2; shift 2;; --busybox-path) BUSYBOX_EXE=$2; shift 2;; --) shift 1; break ;; *) break ;; esac done # Check that we have all variables we need if [ -z "${name}" ] || [ -z "${path}" ]; then echo "ERROR: Please pass the name and path for the container" 1>&2 exit 1 fi # Make sure busybox is present if [ -z "${BUSYBOX_EXE}" ]; then echo "ERROR: Please pass a pathname for busybox binary" 1>&2 exit 1 fi if [ ! -x "${BUSYBOX_EXE}" ]; then echo "ERROR: Failed to find busybox binary (${BUSYBOX_EXE})" 1>&2 exit 1 fi # detect rootfs config="$path/config" if [ -z "$rootfs" ]; then if grep -q '^lxc.rootfs.path' "${config}" 2> /dev/null ; then rootfs=$(awk -F= '/^lxc.rootfs.path =/{ print $2 }' "${config}") else rootfs="${path}/rootfs" fi fi if ! install_busybox "${rootfs}" "${name}"; then echo "ERROR: Failed to install rootfs" 1>&2 exit 1 fi if ! configure_busybox "${rootfs}"; then echo "ERROR: Failed to configure busybox" 1>&2 exit 1 fi if ! copy_configuration "${path}" "${rootfs}" "${name}"; then echo "ERROR: Failed to write config file" 1>&2 exit 1 fi if ! remap_userns "${path}"; then echo "ERROR: Failed to change idmappings" 1>&2 exit 1 fi