view sbin/fjail @ 193:62a24dfb238c

Adjust copyright year
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 20 Aug 2022 13:07:53 +0200
parents 2e7d0c967c36
children 379d3178f3ce
line wrap: on
line source

#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
: 'A very minimal BSD Jail management tool.

:Author:    Franz Glasner
:Copyright: (c) 2019-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: fjail [ 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 [-u] PARENT CHILD

    Create ZFS datasets to be used within a jail

    PARENT must exist already and CHILD must not exist.

    -s        Also create a dataset for freebsd-update data files
    -t        Create a more tiny set of datasets
    -T        Create only an extra tiny set of datasets
    -u        Do not automatically mount newly created datasets

  privs MOUNTPOINT

    Adjust some Unix privileges to mounted jail datasets

  populate MOUNTPOINT BASETXZ

    Populate the jail directory in MOUNTPOINT with the base system in BASETXZ

  copy SOURCE-DATASET DEST-DATASET

    Copy a tree of ZFS datasets with "zfs send -R" and "zfs receive".
    Note that the destination dataset must not exist already.

    -r        Copy the datasets with the -Lec options (aka "raw")
    -u        Do not automatically mount received datasets

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 `zfs mount` to determine the ZFS dataset for a given mountpoint.

    '
    local _mountpoint
    local _ds _mount

    _mountpoint="$1"

    while read -r _ds _mount; do
        if [ "$_mount" = "$_mountpoint" ]; then
            echo $_ds
            return 0
        fi
    done <<EOF__GDSFM
$(zfs mount)
EOF__GDSFM
    return 1
}


_get_dataset_for_varempty() {
    : 'Allow special handling for <mountpoint>/var/empty which may be
    mounted read-only.

    '
    local _mountpoint
    local _ve_mount

    _mountpoint="$1"

    if [ "$_mountpoint" = '/' ]; then
        _ve_mount='/var/empty'
    else
        _ve_mount="${_mountpoint}/var/empty"
    fi

    _get_dataset_for_mountpoint "${_ve_mount}"
}


#
# "datasets" -- create the ZFS dataset tree
#
# command_datasets [ -u ] parent-dataset child-dataset
#
#    -u  do not automatically mount newly created datasets
#
command_datasets() {
    # parent ZFS dataset -- child ZFS dataset name
    local _pds _cds
    # and its mount point
    local _pmp _get _dummy
    # full name of the dataset
    local _ds
    # dynamic ZFS options  -- create cache for freebsd-update  -- use a more tiny layout
    local _zfsopts _fbsdupdate _tiny

    _zfsopts=""
    _fbsdupdate=""
    _tiny="no"
    while getopts "ustT" _opt ; do
        case ${_opt} in
            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
    _get=$(zfs get -H mountpoint "${_pds}" 2>/dev/null) || { echo "ERROR: dataset \`${_pds}' does not exist" >&2; return 1; }
    IFS=$'\t' read _dummy _dummy _pmp _dummy <<EOF
${_get}
EOF
    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 get -H mountpoint "${_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} -o atime=off                                                                      "${_ds}"
    zfs create ${_zfsopts} -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}                                                                           "${_ds}/usr"
        fi
        zfs create ${_zfsopts} -o setuid=off                                                                 "${_ds}/usr/home"
        zfs create ${_zfsopts}                                                                               "${_ds}/usr/local"
    fi
    if [ \( "${_tiny}" = "yes" \) -o \( "${_tiny}" = "extra" \) ]; then
        zfs create ${_zfsopts} -o canmount=off                                                               "${_ds}/var"
    else
        zfs create ${_zfsopts}                                                                               "${_ds}/var"
    fi
    if [ "${_tiny}" != "extra" ]; then
        zfs create ${_zfsopts} -o exec=off -o setuid=off                                                     "${_ds}/var/audit"
        zfs create ${_zfsopts} -o exec=off -o setuid=off                                                     "${_ds}/var/cache"
        zfs create ${_zfsopts} -o exec=off -o setuid=off -o primarycache=metadata -o compression=off         "${_ds}/var/cache/pkg"
        zfs create ${_zfsopts} -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} -o exec=off -o setuid=off                                                 "${_ds}/var/db"
        fi
        zfs create ${_zfsopts} -o exec=off -o setuid=off -o primarycache=metadata -o compression=off         "${_ds}/var/db/freebsd-update"
    fi
    zfs create ${_zfsopts} -o readonly=on -o exec=off -o setuid=off                                          "${_ds}/var/empty"
    zfs create ${_zfsopts} -o exec=off -o setuid=off -o primarycache=metadata                                "${_ds}/var/log"
    zfs create ${_zfsopts} -o exec=off -o setuid=off -o atime=on                                             "${_ds}/var/mail"
    zfs create ${_zfsopts} -o sync=disabled -o exec=off -o setuid=off -o compression=off -o primarycache=all "${_ds}/var/run"
    zfs create ${_zfsopts} -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
}


#
# "copy" -- ZFS copy of datasets
#
# command_copy source-dataset destination-dataset
#
command_copy() {
    # source dataset -- destination dataset
    local _source _dest
    # dynamic ZFS options -- ZFS copy options
    local _zfsopts _zfscopyopts

    _zfsopts=""
    _zfscopyopts=""
    while getopts "ru" _opt ; do
        case ${_opt} in
            r)
                # Use raw datasets
                _zfscopyopts="-Lec"
                ;;
            u)
                # do not mount newly created datasets
                _zfsopts="${_zfsopts} -u"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _source="$1"
    if [ -z "${_source}" ]; then
        echo "ERROR: no source dataset given" >&2
        return 2
    fi
    _dest="$2"
    if [ -z "${_dest}" ]; then
        echo "ERROR: no source dataset given" >&2
        return 2
    fi
    zfs send -R ${_zfscopyopts} -n -v "${_source}" || { echo "ERROR: ZFS operation failed in no-op mode" >&2; return 1; }
    zfs send -R ${_zfscopyopts} "${_source}" | zfs receive ${_zfsopts} "${_dest}"  || { echo "ERROR: ZFS operation failed" >&2; return 1; }
}


#
# "privs" -- adjust privileges
#
# To be used when all ZFS datasets are mounted.
#
command_privs() {
    # mountpoint
    local _mp _d _veds _get _vestatus

    _mp="$1"
    if [ -z "${_mp}" ]; then
        echo "ERROR: no mountpoint given" >&2
        return 2
    fi
    if [ ! -d "${_mp}" ]; then
        echo "ERROR: directory \`${_mp}' does not exist" >&2
        return 1
    fi
    for _d in tmp var/tmp ; do
       chmod 01777 "${_mp}/${_d}"
    done
    chown root:mail "${_mp}/var/mail"
    chmod 0775 "${_mp}/var/mail"

    #
    # Handle <mountpoint>/var/empty specially:
    # make it writeable temporarily if it is mounted read-only:
    #
    _vestatus=""
    _veds=$(_get_dataset_for_varempty "${_mp}")
    if [ $? -eq 0 ]; then
        _get=$(zfs get -H readonly ${_veds} 2>/dev/null) || { echo "ERROR: cannot determine readonly status of ${_mp}/var/empty" >&2; return 1; }
        IFS=$'\t' read _dummy _dummy _vestatus _dummy <<EOF
${_get}
EOF
        if [ "${_vestatus}" = "on" ]; then
            zfs set readonly=off ${_veds} >/dev/null 2>/dev/null || { echo "ERROR: cannot reset readonly-status of ${_mp}/var/empty" >&2; return 1; }
        fi
    fi
    # Set the access rights and the file flags as given in mtree
    chmod 0555 "${_mp}/var/empty"
    chflags schg "${_mp}/var/empty"
    # Reset the read-only status of the mountpoint as it was before
    if [ "${_vestatus}" = "on" ]; then
        zfs set readonly=on ${_veds} >/dev/null 2>/dev/null || { echo "ERROR: cannot reactivate readonly-status of ${_mp}/var/empty" >&2; return 1; }
    fi
}


#
# Global option handling
#
while getopts "Vh" _opt ; do
    case ${_opt} in
        V)
            printf 'fjail 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 "$@"
        ;;
    privs)
        command_privs "$@"
        ;;
    populate)
        command_populate "$@"
        ;;
    copy)
        command_copy "$@"
        ;;
    *)
        echo "ERROR: unknown command \`${command}'" >&2
        exit 2
        ;;
esac