diff sbin/ftjail @ 233:f745d3a216a6

Erste Basis für "ftjail": Management von Thin Jails
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 05 Sep 2022 09:45:13 +0200
parents
children 8682cfa74f6a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sbin/ftjail	Mon Sep 05 09:45:13 2022 +0200
@@ -0,0 +1,468 @@
+#!/bin/sh
+# -*- indent-tabs-mode: nil; -*-
+: 'A very minimal BSD Thin Jail management tool.
+
+:Author:    Franz Glasner
+:Copyright: (c) 2022 Franz Glasner.
+            All rights reserved.
+:License:   BSD 3-Clause "New" or "Revised" License.
+            See LICENSE for details.
+            If you cannot find LICENSE see
+            <https://opensource.org/licenses/BSD-3-Clause>
+:ID:        @(#)@@PKGORIGIN@@ $HGid$
+
+'
+
+set -eu
+
+VERSION="@@VERSION@@"
+
+USAGE='
+USAGE: ftjail [ OPTIONS ] COMMAND [ COMMAND OPTIONS ] [ ARG ... ]
+
+OPTIONS:
+
+  -V    Print the program name and version number to stdout and exit
+
+  -h    Print this help message to stdout and exit
+
+COMMANDS:
+
+  datasets [OPTIONS] PARENT-BASE PARENT-SKELETON NAME
+
+    Create ZFS datasets for the ro base and the rw skeleton to be used
+    within for jails
+
+    PARENT-BASE and PARENT-SKELETON must exist already and NAME must
+    not exist.
+
+    The datasets will not be mounted.
+
+ENVIRONMENT:
+
+  All environment variables that affect "zfs" are effective also.
+
+DESCRIPTION:
+
+  All commands with the exception of "populate" require ZFS as
+  filesystem.
+'
+
+# Reset to standard umask
+umask 0022
+
+
+_get_dataset_for_mountpoint() {
+    : 'Use `mount -t zfs -p` to determine the ZFS dataset for a given mountpoint.
+
+    '
+    local _mountpoint
+    local _ds _mount _rest
+
+    _mountpoint="$1"
+
+    mount -t zfs -p \
+    | {
+        while IFS=' '$'\t' read -r _ds _mount _rest ; do
+            if [ "$_mount" = "$_mountpoint" ]; then
+                echo "${_ds}"
+                return 0
+            fi
+        done
+        return 1
+    }
+}
+
+
+#
+# "datasets" -- create the ZFS dataset tree
+#
+command_datasets() {
+    # parent ZFS dataset -- child ZFS dataset name
+    local _pds _cds
+    # and its mount point
+    local _pmp _get
+    # full name of the dataset
+    local _ds
+    # dynamic ZFS options  -- create cache for freebsd-update  -- use a more tiny layout
+    local _zfsopts _fbsdupdate _tiny _zfsnoauto _varempty_ro
+
+    _zfsopts=""
+    _fbsdupdate=""
+    _tiny="no"
+    _zfsnoauto=""
+    _varempty_ro="-o readonly=on"
+    while getopts "oustAT" _opt ; do
+        case ${_opt} in
+            A)
+                #
+                # set canmount=noauto where otherwise canmount=on would have been set
+                # or inherited
+                #
+                _zfsnoauto="-o canmount=noauto"
+                ;;
+            o)
+                # Clear out the default setting of creating var/empty as read-only dataset
+                _varempty_ro=""
+                ;;
+            t)
+                # use a more tiny layout
+                _tiny="yes"
+                ;;
+            T)  # extra tiny layout
+                _tiny="extra"
+                ;;
+            u)
+                # do not mount newly created datasets
+                _zfsopts="${_zfsopts} -u"
+                ;;
+            s)
+                # create also a dataset for freebsd-update data
+                _fbsdupdate="yes"
+                ;;
+            \?|:)
+                return 2;
+                ;;
+        esac
+    done
+    shift $((OPTIND-1))
+    OPTIND=1
+
+    _pds="$1"
+    if [ -z "${_pds}" ]; then
+        echo "ERROR: no parent dataset given" >&2
+        return 2
+    fi
+    _pmp=$(zfs list -H -o mountpoint -t filesystem "${_pds}" 2>/dev/null) || { echo "ERROR: dataset \`${_pds}' does not exist" >&2; return 1; }
+    case "${_pmp}" in
+        none)
+            echo "ERROR: dataset \`${_pds}' has no mountpoint" >&2
+            return 1
+            ;;
+        legacy)
+            echo "ERROR: dataset \`${_pds}' has a \`${_mp}' mountpoint" >&2
+            return 1
+            ;;
+        *)
+            # VOID
+            ;;
+    esac
+    _cds="$2"
+    if [ -z "${_cds}" ]; then
+        echo "ERROR: no child dataset given" >&2
+        return 2
+    fi
+    _ds="${_pds}/${_cds}"
+    echo "Resulting new root dataset is \`${_ds}' at mountpoint \`${_pmp}/${_cds}'"
+    if zfs list -H -o mountpoint -t filesystem "${_ds}" >/dev/null 2>/dev/null; then
+        echo "ERROR: dataset \`${_ds}' does already exist" >&2
+        return 1
+    fi
+
+    #
+    # NOTE: For BEs these directory will be *excluded* from the BE
+    #
+    #   /tmp
+    #   /usr/home
+    #   /usr/ports
+    #   /usr/src
+    #   /var/audit
+    #   /var/crash
+    #   /var/log
+    #   /var/mail
+    #   /var/tmp
+    #
+    zfs create ${_zfsopts} ${_zfsnoauto} -o atime=off                                                        "${_ds}"
+    zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o setuid=off                                      "${_ds}/tmp"
+    if [ "${_tiny}" != "extra" ]; then
+        if [ "${_tiny}" = "yes" ]; then
+            zfs create ${_zfsopts} -o canmount=off                                                           "${_ds}/usr"
+        else
+            zfs create ${_zfsopts} ${_zfsnoauto}                                                             "${_ds}/usr"
+        fi
+        zfs create ${_zfsopts} ${_zfsnoauto} -o setuid=off                                                   "${_ds}/usr/home"
+        zfs create ${_zfsopts} ${_zfsnoauto}                                                                 "${_ds}/usr/local"
+    fi
+    if [ \( "${_tiny}" = "yes" \) -o \( "${_tiny}" = "extra" \) ]; then
+        zfs create ${_zfsopts} -o canmount=off                                                               "${_ds}/var"
+    else
+        zfs create ${_zfsopts} ${_zfsnoauto}                                                                 "${_ds}/var"
+    fi
+    if [ "${_tiny}" != "extra" ]; then
+        zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off                                                      "${_ds}/var/audit"
+        zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off                                                      "${_ds}/var/cache"
+        zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata -o compression=off          "${_ds}/var/cache/pkg"
+        zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o compression=off                                   "${_ds}/var/crash"
+    fi
+    if [ "$_fbsdupdate" = "yes" ]; then
+        if [ \( "${_tiny}" = "yes" \) -o \( "${_tiny}" = "extra" \) ]; then
+            zfs create ${_zfsopts} -o canmount=off -o exec=off -o setuid=off                                 "${_ds}/var/db"
+        else
+            zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off                                   "${_ds}/var/db"
+        fi
+        zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata -o compression=off          "${_ds}/var/db/freebsd-update"
+    fi
+    zfs create ${_zfsopts} ${_zfsnoauto} ${_varempty_ro} -o exec=off -o setuid=off                                          "${_ds}/var/empty"
+    zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata                                 "${_ds}/var/log"
+    zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o atime=on                                              "${_ds}/var/mail"
+    zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o exec=off -o setuid=off -o compression=off -o primarycache=all  "${_ds}/var/run"
+    zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o setuid=off                                                     "${_ds}/var/tmp"
+}
+
+
+#
+# "populate" -- populate the datasets with content from a FreeBSD base.txz
+#
+# command_populate mountpoint basetxz
+#
+command_populate() {
+    # MOUNTPOINT -- base.txz
+    local _mp _basetxz
+
+    _mp="$1"
+    _basetxz="$2"
+
+    if [ -z "${_mp}" ]; then
+        echo "ERROR: no mountpoint given" >&2
+        return 2
+    fi
+    if [ -z "${_basetxz}" ]; then
+        echo "ERROR: no base.txz given" >&2
+        return 2
+    fi
+    if [ ! -d "${_mp}" ]; then
+        echo "ERROR: mountpoint \`${_mp}' does not exist" >&2
+        return 1
+    fi
+    if [ ! -r "${_basetxz}" ]; then
+        echo "ERROR: file \`${_basetxz}' is not readable" >&2
+        return 1
+    fi
+
+    #
+    # Handle /var/empty separately later: could be already there and
+    # mounted read-only.
+    #
+    tar -C "${_mp}" --exclude=./var/empty -xJp -f "${_basetxz}" || { echo "ERROR: tar encountered errors" >&2; return 1; }
+    if [ -d "${_mp}/var/empty" ]; then
+        #
+        # If /var/empty exists already try to extract with changing the
+        # flags (e.g. `schg'). But be ignore errors here.
+        #
+        tar -C "${_mp}" -xJp -f "${_basetxz}" ./var/empty || { echo "tar warnings for handling ./var/empty ignored because ./var/empty exists already" >&2; }
+    else
+        # Just extract /var/empty normally
+        tar -C "${_mp}" -xJp -f "${_basetxz}" ./var/empty || { echo "ERROR: tar encountered errors" >&2; return 1; }
+    fi
+
+    find "${_mp}/boot" -type f -delete
+}
+
+
+#
+# "mount" -- recursively mount a dataset including subordinate datasets
+#
+# command_mount dataset mountpoint
+#
+command_mount() {
+    local _dsname _mountpoint
+    local _name _mp _canmount _mounted
+    local _rootds_mountpoint _relative_mp _real_mp
+    local _dry_run _mount_outside _mount_natural
+
+    _dry_run=""
+    _mount_outside=""
+    _mount_natural=""
+    while getopts "ONnu" _opt ; do
+        case ${_opt} in
+            O)
+                _mount_outside="yes"
+                ;;
+            N)
+                _mount_natural="yes"
+                ;;
+            n|u)
+                _dry_run="yes"
+                ;;
+            \?|:)
+                return 2;
+                ;;
+        esac
+    done
+    shift $((OPTIND-1))
+    OPTIND=1
+
+    _dsname="${1-}"
+    _mountpoint="${2-}"
+
+    if [ -z "${_dsname}" ]; then
+        echo "ERROR: no dataset given" >&2
+        return 2
+    fi
+
+    _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}")"  || \
+        { echo "ERROR: root dataset does not exist" >&2; return 1; }
+
+    if [ -z "${_mountpoint}" ]; then
+        if [ "${_mount_natural}" = "yes" ]; then
+            _mountpoint="${_rootds_mountpoint}"
+        else
+            echo "ERROR: no mountpoint given" >&2
+            return 2
+        fi
+    else
+        if [ "${_mount_natural}" = "yes" ]; then
+            echo "ERROR: Cannot have a custom mountpoint when \"-O\" is given" >&2
+            return 2
+        fi
+    fi
+
+    # Eventually remove a trailing slash
+    _mountpoint="${_mountpoint%/}"
+    if [ -z "${_mountpoint}" ]; then
+        echo "ERROR: would mount over the root filesystem" >&2
+        return 1
+    fi
+
+    zfs list -H -o name,mountpoint,canmount,mounted -s mountpoint -t filesystem -r "${_dsname}" \
+    | {
+        while IFS=$'\t' read -r _name _mp _canmount _mounted ; do
+            # Skip filesystems that are already mounted
+            [ "${_mounted}" = "yes" ] && continue
+            # Skip filesystems that must not be mounted
+            [ "${_canmount}" = "off" ] && continue
+            case "${_mp}" in
+                "none"|"legacy")
+                    # Do nothing for filesystem with unset or legacy mountpoints
+                    ;;
+                "${_rootds_mountpoint}"|"${_rootds_mountpoint}/"*)
+                    #
+                    # Handle only mountpoints that have a mountpoint below
+                    # the parent datasets mountpoint
+                    #
+
+                    # Determine the mountpoint relative to the parent mountpoint
+                    _relative_mp="${_mp#${_rootds_mountpoint}}"
+                    # Eventually remove a trailing slash
+                    _relative_mp="${_relative_mp%/}"
+                    # The real effective full mountpoint
+                    _real_mp="${_mountpoint}${_relative_mp}"
+
+                    #
+                    # Consistency and sanity check: computed real mountpoint must
+                    # be equal to the configured mountpoint when no custom mountpoint
+                    # is given.
+                    #
+                    if [ "${_mount_natural}" = "yes" ]; then
+                        if [ "${_real_mp}" != "${_mp}" ]; then
+                            echo "ERROR: mountpoint mismatch" >&2
+                            return 1
+                        fi
+                    fi
+
+                    if [ "${_dry_run}" = "yes" ]; then
+                        echo "Would mount ${_name} on ${_real_mp}"
+                    else
+                        mkdir -p "${_real_mp}" 1> /dev/null 2> /dev/null || \
+                            { echo "ERROR: cannot create mountpoint ${_real_mp}" >&2; return 1; }
+                        echo "Mounting ${_name} on ${_real_mp}"
+                        mount -t zfs "${_name}" "${_real_mp}" || return 1
+                    fi
+                    ;;
+                *)
+                    if [ "${_mount_outside}" = "yes" ]; then
+                        if [ "${_dry_run}" = "yes" ]; then
+                            echo "Would mount ${_name} on configured ZFS dataset mountpoint ${_mp}"
+                        else
+                            echo "Mounting ${_name} on configured ZFS dataset mountpoint ${_mp}"
+                            zfs mount "${_name}" || return 1
+                        fi
+                    else
+                        echo "Skipping ${_name} because its configured ZFS mountpoint is not relative to given root dataset" 2>&1
+                    fi
+                    ;;
+            esac
+        done
+
+        return 0
+    }
+}
+
+
+#
+# "umount" -- Recursively unmount ZFS datasets
+#
+# command_umount dataset
+#
+command_umount() {
+    local _dsname
+    local _name _mp _rest
+    local _rootds_mountpoint
+
+    _dsname="${1-}"
+    [ -z "${_dsname}" ] && \
+        { echo "ERROR: no dataset given" >&2; return 2; }
+
+    # Just determine whether the given dataset name exists
+    _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}")" || { echo "ERROR: dataset not found" >&2; return 1; }
+
+    mount -t zfs -p \
+    | grep -E "^${_dsname}(/|\s)" \
+    | sort -n -r \
+    | {
+        while IFS=' '$'\t' read -r _name _mp _rest ; do
+            echo "Umounting ${_name} on ${_mp}"
+            umount "${_mp}" || return 1
+        done
+        return 0
+    }
+}
+
+
+#
+# Global option handling
+#
+while getopts "Vh" _opt ; do
+    case ${_opt} in
+        V)
+            printf 'ftjail v%s (rv:%s)\n' "${VERSION}" '@@HGREVISION@@'
+            exit 0
+            ;;
+        h)
+            echo "${USAGE}"
+            exit 0
+            ;;
+        \?)
+            exit 2;
+            ;;
+        *)
+            echo "ERROR: option handling failed" >&2
+            exit 2
+            ;;
+    esac
+done
+
+#
+# Reset the Shell's option handling system to prepare for handling
+# command-local options.
+#
+shift $((OPTIND-1))
+OPTIND=1
+
+test $# -gt 0 || { echo "ERROR: no command given" >&2; exit 2; }
+
+command="$1"
+shift
+
+case "${command}" in
+    datasets)
+        command_datasets "$@"
+        ;;
+    populate)
+        command_populate "$@"
+        ;;
+    *)
+        echo "ERROR: unknown command \`${command}'" >&2
+        exit 2
+        ;;
+esac