-#!/bin/bash
+#!/bin/sh
+# Client script for LXC container images.
#
-# lxc: linux Container library
-
-# Authors:
-# Daniel Lezcano <daniel.lezcano@free.fr>
-
-# 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.
+# Copyright @ Daniel Lezcano <daniel.lezcano@free.fr>
+# Copyright © 2018 Christian Brauner <christian.brauner@ubuntu.com>
+#
+# 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
-# 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.
+ [ "$(cat /proc/self/uid_map)" = "$(cat /proc/1/uid_map)" ] && { echo userns-root; return; }
+ echo yes
+}
-# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+USERNS="$(in_userns)"
install_busybox()
{
- rootfs=$1
- name=$2
- res=0
- tree="\
-$rootfs/selinux \
-$rootfs/dev \
-$rootfs/home \
-$rootfs/root \
-$rootfs/etc \
-$rootfs/etc/init.d \
-$rootfs/bin \
-$rootfs/sbin \
-$rootfs/proc \
-$rootfs/mnt \
-$rootfs/tmp \
-$rootfs/var/log \
-$rootfs/usr/share/udhcpc \
-$rootfs/dev/pts \
-$rootfs/dev/shm \
-$rootfs/lib \
-$rootfs/usr/lib \
-$rootfs/lib64 \
-$rootfs/usr/lib64"
-
- mkdir -p $tree || return 1
- chmod 755 $tree || return 1
-
- pushd $rootfs/dev > /dev/null || return 1
-
- # minimal devices needed for busybox
- mknod tty c 5 0 || res=1
- mknod console c 5 1 || res=1
- chmod 666 tty console || res=1
- mknod tty0 c 4 0 || res=1
- mknod tty1 c 4 0 || res=1
- mknod tty5 c 4 0 || res=1
- chmod 666 tty0 || res=1
- mknod ram0 b 1 0 || res=1
- chmod 600 ram0 || res=1
- mknod null c 1 3 || res=1
- chmod 666 null || res=1
-
- popd > /dev/null
-
- # root user defined
- cat <<EOF >> $rootfs/etc/passwd
+ 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 <<EOF >> "${rootfs}/etc/passwd"
root:x:0:0:root:/root:/bin/sh
EOF
- cat <<EOF >> $rootfs/etc/group
+ cat <<EOF >> "${rootfs}/etc/group"
root:x:0:root
EOF
# mount everything
- cat <<EOF >> $rootfs/etc/init.d/rcS
+ cat <<EOF >> "${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
-
- # mount points
- cat <<EOF >> $rootfs/etc/fstab
-proc /proc proc defaults 0 0
-shm /dev/shm tmpfs defaults 0 0
-EOF
-
- # writable and readable for other
- chmod 644 $rootfs/etc/fstab || return 1
+ # 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 <<EOF >> $rootfs/etc/inittab
+ # launch rcS first then make a console available
+ # and propose a shell on the tty, the last one is
+ # not needed
+ cat <<EOF >> "${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
+ # writable and readable for other
+ chmod 644 "${rootfs}/etc/inittab" || return 1
- cat <<EOF >> $rootfs/usr/share/udhcpc/default.script
-#!/bin/sh
+ # 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 <<EOF >> ${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
- echo nameserver \$i >> /etc/resolv.conf
- done
- ;;
+ 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/usr/share/udhcpc/default.script
+ chmod 744 ${rootfs}/${DEF_SCRIPT}
- return $res
+ return "${res}"
}
configure_busybox()
{
- rootfs=$1
-
- functions="\
- [ [[ addgroup adduser adjtimex ar arp arping ash awk basename \
- brctl bunzip2 bzcat bzip2 cal cat catv chattr chgrp chmod \
- chown chpasswd chpst chroot chrt chvt cksum clear cmp comm \
- cp cpio crond crontab cryptpw cut date dc dd deallocvt \
- delgroup deluser df dhcprelay diff dirname dmesg dnsd dos2unix \
- du dumpkmap dumpleases echo ed egrep eject env envdir envuidgid \
- ether-wake expand expr fakeidentd false fbset fdformat fdisk \
- fetchmail fgrep find findfs fold free freeramdisk fsck \
- fsck.minix ftpget ftpput fuser getopt getty grep gunzip gzip \
- halt hdparm head hexdump hostid hostname httpd hwclock id \
- ifconfig ifdown ifenslave ifup inetd init insmod install ip \
- ipaddr ipcalc ipcrm ipcs iplink iproute iprule iptunnel \
- kbd_mode kill killall killall5 klogd last length less linux32 \
- linux64 linuxrc ln loadfont loadkmap logger login logname \
- logread losetup lpd lpq lpr ls lsattr lsmod lzmacat makedevs \
- md5sum mdev mesg microcom mkdir mkfifo mkfs.minix mknod mkswap \
- mktemp modprobe more mount mountpoint msh mt mv nameif nc \
- netstat nice nmeter nohup nslookup od openvt passwd patch \
- pgrep pidof ping ping6 pipe_progress pivot_root pkill poweroff \
- printenv printf ps pscan pwd raidautorun rdate readahead \
- readlink readprofile realpath reboot renice reset resize rm \
- rmdir rmmod route rpm rpm2cpio run-parts runlevel runsv \
- runsvdir rx script sed sendmail seq setarch setconsole \
- setkeycodes setlogcons setsid setuidgid sh sha1sum slattach \
- sleep softlimit sort split start-stop-daemon stat strings \
- stty su sulogin sum sv svlogd swapoff swapon switch_root \
- sync sysctl syslogd tac tail tar taskset tcpsvd tee telnet \
- telnetd test tftp tftpd time top touch tr traceroute \
- true tty ttysize udhcpc udhcpd udpsvd umount uname uncompress \
- unexpand uniq unix2dos unlzma unzip uptime usleep uudecode \
- uuencode vconfig vi vlock watch watchdog wc wget which \
- who whoami xargs yes zcat zcip"
-
- type busybox >/dev/null
-
- if [ $? -ne 0 ]; then
- echo "busybox executable is not accessible"
- return 1
- fi
-
- file $(which busybox) | grep -q "statically linked"
- if [ $? -ne 0 ]; then
- echo "warning : busybox is not statically linked."
- echo "warning : The template script may not correctly"
- echo "warning : setup the container environment."
- fi
+ rootfs="${1}"
- # copy busybox in the rootfs
- cp $(which busybox) $rootfs/bin
- if [ $? -ne 0 ]; then
- echo "failed to copy busybox in the rootfs"
- return 1
- fi
+ # copy busybox in the rootfs
+ if ! cp "${BUSYBOX_EXE}" "${rootfs}/bin"; then
+ echo "ERROR: Failed to copy busybox binary" 1>&2
+ return 1
+ fi
- # do hardlink to busybox for the different commands
- for i in $functions; do ln $rootfs/bin/busybox $rootfs/bin/$i; done
+ # 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
+ # relink /sbin/init
+ ln "${rootfs}/bin/busybox" "${rootfs}/sbin/init"
- # passwd exec must be setuid
- chmod +s $rootfs/bin/passwd
- touch $rootfs/etc/shadow
- chroot $rootfs /bin/passwd -d root
+ # /etc/fstab must exist for "mount -a"
+ touch "${rootfs}/etc/fstab"
- echo "No password for 'root', please change !"
+ # passwd exec must be setuid
+ chmod +s "${rootfs}/bin/passwd"
+ touch "${rootfs}/etc/shadow"
- return 0
+ return 0
}
copy_configuration()
{
- path=$1
- rootfs=$2
- name=$3
-
-cat <<EOF >> $path/config
-lxc.utsname = $name
-lxc.tty = 1
-lxc.pts = 1
-lxc.rootfs = $rootfs
+ path="${1}"
+ rootfs="${2}"
+ name="${3}"
+
+grep -q "^lxc.rootfs.path" "${path}/config" 2>/dev/null || echo "lxc.rootfs.path = ${rootfs}" >> "${path}/config"
+cat <<EOF >> "${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.aa_profile = 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"
+ 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
+ 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"
}
-usage()
+remap_userns()
{
- cat <<EOF
-$1 -h|--help -p|--path=<path>
+ 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 <<EOF
+LXC busybox image builder
+
+Special arguments:
+
+ [ -h | --help ]: Print this help message and exit.
+
+LXC internal arguments:
+
+ [ --name <name> ]: The container name
+ [ --path <path> ]: The path to the container
+ [ --rootfs <rootfs> ]: The path to the container's rootfs (default: config or <path>/rootfs)
+ [ --mapped-uid <map> ]: A uid map (user namespaces)
+ [ --mapped-gid <map> ]: A gid map (user namespaces)
+
+BUSYBOX template specific arguments:
+
+ [ --busybox-path <path> ]: busybox pathname (default: ${BUSYBOX_EXE})
+
EOF
- return 0
+ return 0
}
-options=$(getopt -o hp:n: -l help,path:,name: -- "$@")
-if [ $? -ne 0 ]; then
- usage $(basename $0)
- exit 1
+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 $0 && exit 0;;
- -p|--path) path=$2; shift 2;;
- -n|--name) name=$2; shift 2;;
- --) shift 1; break ;;
- *) break ;;
- esac
+ 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
-if [ "$(id -u)" != "0" ]; then
- echo "This script should be run as 'root'"
+# 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
-if [ -z "$path" ]; then
- echo "'path' parameter is required"
+# 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
-rootfs=$path/rootfs
+# 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
-install_busybox $rootfs $name
-if [ $? -ne 0 ]; then
- echo "failed to install busybox's rootfs"
- exit 1
+if ! install_busybox "${rootfs}" "${name}"; then
+ echo "ERROR: Failed to install rootfs" 1>&2
+ exit 1
fi
-configure_busybox $rootfs
-if [ $? -ne 0 ]; then
- echo "failed to configure busybox template"
- exit 1
+if ! configure_busybox "${rootfs}"; then
+ echo "ERROR: Failed to configure busybox" 1>&2
+ exit 1
fi
-copy_configuration $path $rootfs $name
-if [ $? -ne 0 ]; then
- echo "failed to write configuration file"
- exit 1
+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