Really disposable (RAM based) qubes

Original forum link
https://forum.qubes-os.org/t/21532
Original poster
qubist
Created at
2023-10-16 19:19:23
Posts count
194
Likes count
346

Hi,

While researching, I found @unman's GitHub repo and another user's improvement of the original bash script. After noticing some issues (with log paths), I allowed myself the liberty to fix them and add further flexibility to the script. One essential new thing is that no log files are saved on the file system at all as they exist only as symlinks to /dev/null. Additionally, now names are not fixed and one can use the script based on one's own templates and preferences.

I still don't know how to work with git, so I will just share my version here in case someone finds it useful:

#!/bin/bash
#
# Create, launch, and clean up a RAM based disposable qube in Qubes OS's dom0
#
# Inspired by:
# https://github.com/unman/notes/Really_Disposable_Qubes.md
# https://github.com/kennethrrosen/qubes-shadow-dvm/

set -euo pipefail

# Display error message and notification
error()
{
    local -a args=('-e')
    local -r light_red='\033[1;31m'
    local -r nocolor='\033[0m'
    args+=("${@}")
    args[1]="${light_red}${args[1]}${nocolor}"
    echo "${args[@]}" 1>&2

    notify-send --expire-time 5000 \
            --icon='/usr/share/icons/Adwaita/256x256/legacy/dialog-error.png' \
            "${0##*/}" \
            "${*}"
}

# Prevent two processes from trying to create the same qube
readonly pidfile="/run/user/${UID}/${0##*/}.pid"
if [ -f "${pidfile}" ]; then
    error "Another ${0##*/} instance is currently running."
    exit 1
fi


if [ $# -eq 0 ]; then
    cat >&2 <<-EOF
    Usage: ${0##*/} [options] -c <command>
     -c, --command             Command to execute inside the Qube
    Optional [defaults]:
     -q, --qubename            Qube name [rdispN], where N is 100-9999
     -d, --tempdir             Mountpoint for the RAM drive
                                 [${HOME}/tmp/<qubename>]
     -s, --tempsize            RAM drive size (1G, 2G ...) [1G]
     -p, --property name=value Sets domain's properties.
                               If not set, these defaults are used:
                                 include_in_backups=false
                                 netvm= (i.e. none)
                                 memory=1000
                                 template_for_dispvms=false
                                 default_dispvm= (i.e. none)
                                 label - based on netvm:
                                   - netvm= (i.e .none) => gray
                                   - netvm=sys-whonix => purple
                                   - netvm=<any other> => red
                               Use label after netvm to override.
                               Last set values override previous ones.
                               See man qvm-prefs for all properties.

    EXAMPLE: Launch Tor browser in a RAM based whonix disposable:
     ${0##*/} -p template=whonix-ws-16-dvm -p netvm=sys-whonix -c torbrowser
    EOF
    exit 1
fi

tempdir_root="${HOME}/tmp"
tempsize='1G'
properties=('include_in_backups=false'
        'template_for_dispvms=false'
        'netvm='
        'memory=1000'
        'default_dispvm='
        'label=gray')
# Generate Qubes OS style random name: rdisp100-rdisp9999
# making sure it does not duplicate existing VM name
while : ; do
    qube_name=$(/usr/bin/shuf --input-range=100-9999 --head-count=1)
    qube_name="rdisp${qube_name}"
    if ! qvm-check "${qube_name}" > /dev/null 2>&1; then
        break
    fi
done

set +u
while : ; do
    case "${1}" in
        -q | --qubename)
            if qvm-check "${2}" > /dev/null 2>&1; then
                error "${2}" "already exists. Exiting."
                exit 1
            fi
            qube_name="${2}"
                        shift 2
                        ;;
        -c | --command)
                        command_to_run="${2}"
                        shift 2
                        ;;
        -d | --tempdir)
            if [ -d "${2}" ]; then
                error "${2}:" 'The directory exists'
                exit 1
            fi
            tempdir="${2}"
            shift 2
            ;;
        -s | --tempsize)
            # TODO: Validate size value
            # Show error message if size exceeds
            # available memory
            tempsize="${2}"
            shift 2
            ;;
        -p | --property)
            if ! grep -qiE '^[^=]+=[^=]*$' <<< "${2}"; then
                error 'Wrong property pattern:' \
                    'Usage: --property name=value'
                exit 1
            fi
            if [[ "${2}" == 'netvm=sys-whonix' ]]; then
                properties+=( 'label=purple' )
            elif [[ "${2}" =~ netvm=.+ ]]; then
                properties+=( 'label=red' )
            fi
            if [[ "${2}" =~ template=.+ ]]; then
                template=$(echo "${2}" \
                            | sed -r 's/^template=//g')
                shift 2
                continue
            fi
            properties+=( "${2}" )
            shift 2
            ;;
        --) # End of all options
            shift
            break;
            ;;
        -*)
            error 'Unknown option:' "${1}"
            exit 1
            ;;
        *)  # No more options
            break
            ;;
    esac
done

cleanup()
{
    local exit_code="${1}"
    set +e
    qvm-kill "${qube_name}"
    qvm-remove --force "${qube_name}"
    qvm-pool remove "${pool_name}"
    sudo umount "${pool_name}"

    # Leave no trace on file system
    find "${HOME}/.config/menus/applications-merged" \
        -regextype posix-egrep \
        -regex \
        ".*\/user-qubes-(disp)?vm-directory(_|-)${qube_name}\.menu$" \
        -delete
    sudo rm -rf "${tempdir}" \
        "/run/qubes/audio-control.${qube_name}"
    for file in "${logfiles[@]}"; do
        sudo rm -rf "${file}" "${file}.old"
    done
    # Remove the root of temp directories
    rmdir --ignore-fail-on-non-empty "${tempdir_root}"
    notify-send --expire-time 5000 \
            --icon='/usr/share/icons/Adwaita/scalable/emblems/emblem-default-symbolic.svg' \
            "${qube_name}" \
            "Remnants cleared"
    rm -f "${pidfile}"
    exit "${exit_code}"
}

if [[ $(qvm-prefs "${template}" template_for_dispvms) != True ]]; then
    error "${template}" 'is not a disposable template'
    cleanup 1 > /dev/null 2>&1;
fi

set -u
notify-send --expire-time 10000 \
         --icon='/usr/share/icons/hicolor/scalable/apps/xfce4-timer-plugin.svg' \
         "${0##*/}" \
         "Attempting to create ${qube_name}"

tempdir="${tempdir_root}/${qube_name}"
if [ -d "${tempdir}" ]; then
    error "${tempdir}" 'already exists. Exiting.'
    exit 1
fi
pool_name="ram_pool_${qube_name}"
if qvm-pool info "${pool_name}" > /dev/null 2>&1; then
    error "${pool_name}" 'already exists. Exiting.'
    exit 1
fi

logdir='/var/log'
logfiles=("${logdir}/libvirt/libxl/${qube_name}.log"
      "${logdir}/qubes/guid.${qube_name}.log"
      "${logdir}/qubes/qrexec.${qube_name}.log"
      "${logdir}/qubes/qubesdb.${qube_name}.log"
      "${logdir}/xen/console/guest-${qube_name}.log")

main()
{
    sudo swapoff --all
    mkdir --parents "${tempdir}"

    sudo mount --types tmpfs \
           --options size="${tempsize}" \
           "${pool_name}" \
           "${tempdir}"
    qvm-pool add "${pool_name}" \
         file \
         --option revisions_to_keep=1 \
         --option dir_path="${tempdir}" \
         --option ephemeral_volatile=True

    # Create void symlinks to prevent log saving
    for file in "${logfiles[@]}"; do
        sudo ln -sfT /dev/null "${file}"
    done

    qvm-clone  --quiet -P "${pool_name}" "${template}" "${qube_name}" \
           || cleanup 1
    qvm-volume config "${qube_name}:root" rw False
    local property
    local prop_name
    local prop_value
    for property in "${properties[@]}"; do
        prop_name=$(echo "${property}" \
                     | sed -r 's/(^--property=)//g' \
                 | sed -r 's/=[^=]*$//g')
        prop_value=$(echo "${property}" \
                                 | sed -r 's/(^--property=)//g' \
                 | sed -r 's/[^=]+=//g')
        qvm-prefs "${qube_name}" "${prop_name}" "${prop_value}" \
              || cleanup 1
    done
    unset property prop_name prop_value
    # Process locking is necessary only during qube creation
    rm -f "${pidfile}"
    set +e
    qvm-run "${qube_name}" "${command_to_run}"
    set -e
    cleanup 0
}

touch "${pidfile}"
trap 'cleanup' SIGINT SIGTERM

main "${@}"
For example usage start the script in console without arguments.

Here is also a second script which cleans up remnants of ANY qubes (not only those created by the script, i.e. RAM based, but also "traditional" ones). Some users observed that a system shutdown does not allow the script from above to complete, so remnants need to be cleaned manually (now with the help of this script). This second script is also a workaround for this issue. [b]Use it with caution![/b]

#!/bin/bash
#
# Remove RAM pools, logs and menu files for non-existing qubes

set -euo pipefail

readonly light_green='\033[1;32m'
readonly yellow='\033[1;33m'
readonly light_blue='\033[1;34m'
readonly light_purple='\033[1;35m'
readonly light_cyan='\033[1;36m'
readonly white='\033[1;37m'
readonly no_color='\033[0m'
readonly bg_black='\033[40m'
readonly bg_red='\033[41m'

readonly logdir='/var/log'
readonly tempdir_root="${HOME}/tmp"
readonly menudir="${HOME}/.config/menus/applications-merged"

pause()
{
    local answer
    local message
    message+="${bg_red}${white}Yes${no_color}/"
    message+="${bg_black}${yellow}No${no_color}/"
    message+="${light_green}Quit${no_color}? "
    while true; do
        read -r -n 1 -p "$(echo -e "${message}")" answer
        case "${answer}" in
            [Yy]* ) echo; break;;
            [Nn]* ) echo; return 1;;
            [Qq]* ) echo -e "\nExitting. Bye.\n"; exit;;
            * ) echo -e "\nPlease answer (y)es, (n)o or (q)uit.";;
        esac
    done
}

notice()
{
    printf "${light_blue}%s${no_color}\n" "${@}"
}

remove_unused_pools()
{
    [ -z "${1}" ] && return
    local pools_list="${1}"
    readarray -t pools_list < <(echo "${pools_list}")
    for pool_name in "${pools_list[@]}"; do
        qube_name=$(echo "${pool_name}" | sed -r 's/^ram_pool_//')
        if qvm-check "${qube_name}" > /dev/null 2>&1; then
            # The qube exists
            continue
        fi
        local pool_mountpoint
        pool_mountpoint=$(qvm-pool info "${pool_name}" \
                               | grep -E '^dir_path' \
                       | sed -r 's/^dir_path\s+//g')

        printf "%s ${light_cyan}%s${no_color} " \
            "Remove pool" "${pool_name}"
        printf "%s ${light_cyan}%s${no_color}: " \
            "with dir_path" "${pool_mountpoint}"
        ! pause && continue
        qvm-pool remove "${pool_name}"
        set +e
        sudo umount "${pool_mountpoint}"
        set -e
        sudo rm -rf "${pool_mountpoint}"
    done
}

remove_qubes_files()
{
    [ -z "${1}" ] && return
    local qubes_list="${1}"
    readarray -t qubes_list < <(echo "${qubes_list}")
    local qube_name
    local decoded_qube_name

    for qube_name in "${qubes_list[@]}"; do
        decoded_qube_name=$(echo "${qube_name}" \
            | sed -r 's/_d/-/g' \
            | sed -r 's/_u/_/g'
        )
        if qvm-check "${decoded_qube_name}" > /dev/null 2>&1; then
            # The qube exists
            continue
        fi
        printf "\n%s ${light_purple}%s${no_color} %s\n" \
            "Qube" "${decoded_qube_name}" "does not exist"
        #readarray -d '' files < <(find "${dir}" -name "$input" -print0)
        # NOTE: It's good that qube names are simple
        # and won't need regex escaping
        local log_pattern="${qube_name}\.log((\.old)|(-[0-9]{8}))?(\.gz)?"
        local menu_pattern="user-qubes-(disp)?vm-directory(_|-)${qube_name}\.menu"

        local -A targets
        targets=(["${logdir}/libvirt/libxl"]="${log_pattern}"
             ["${logdir}/qubes"]="((guid|qrexec|qubesdb)\.)?${log_pattern}"
             ["${logdir}/xen/console"]="guest-${log_pattern}"
             ["${menudir}"]="${menu_pattern}")
        if [ -d "${tempdir_root}" ]; then
            targets+=(["${tempdir_root}"]="${qube_name}")
        fi
        local remnant
        local search_dir
        local -a remnants=()
        for search_dir in "${!targets[@]}"; do
            local intermediate_results
            mapfile -d $'\0' intermediate_results \
                < <(sudo find "${search_dir}" \
                         -regextype posix-egrep \
                     -regex ".*\/${targets[${search_dir}]}$" \
                     -print0)
            remnants+=("${intermediate_results[@]}")
            unset intermediate_results
        done
        if [[ "${#remnants[@]}" == 0 ]]; then
            echo 'No files found'
            continue
        fi
        for remnant in "${remnants[@]}"; do
            local message="Delete ${light_cyan}${remnant}${no_color}: "
            message=$(echo -e "${message}" \
                       | sed -r "s/${qube_name}/\\${light_purple}${qube_name}\\${light_cyan}/g")
            echo -ne "${message}"
            ! pause && continue
            sudo rm -rf "${remnant}"
        done
    done
}

warning=$(cat <<-EOF
WARNING!!!
This script searches for remnants of ANY non-existing qubes.
You will be asked to confirm each change individually.
Be careful and think twice before confirming anything!
You have been warned.
EOF
)
readonly warning

printf "${bg_red}${white}%s${no_color}\n" "${warning}"
echo 'If there are no remnants, you will not have to do anything.'
echo 'Continue?'
pause || exit 1

existing_qubes=$(qvm-ls --fields=name \
                    --raw-data \
            | sort)
readonly existing_qubes

# Look for qubes in the logs
all_qube_names=$(sudo find "${logdir}/qubes/" \
                       "${logdir}/libvirt/libxl/" \
               -type f \
               -regextype posix-egrep \
               -regex '.*\.log((\.old)|(-[0-9]{8}))?(\.gz)?$' \
               -exec basename "{}" \; \
               | sed -r 's/\.log((\.old)|(-[0-9]{8}))?(\.gz)?$//g' \
               | sed -r 's/^(guid|qrexec|qubesdb)\.//g' \
               | sort \
               | uniq)$'\n'
all_qube_names+=$(sudo find "${logdir}/xen/console/" \
                        -type f \
                -regextype posix-egrep \
                -regex '.*\/guest-.*\.log((\.old)|(-[0-9]{8}))?(\.gz)?$' \
                -exec basename "{}" \; \
                | sed -r 's/\.log((\.old)|(-[0-9]{8}))?(\.gz)?$//g' \
                | sed -r 's/^guest-//g' \
                | sort \
                | uniq)$'\n'

# Look for qubes in the RAM pools
set +e
ram_pools=$(qvm-pool list \
                 | grep -Eio '^ram_pool_[^ ]+' \
             | sort \
             | uniq)
set -e
all_qube_names+=$(echo "${ram_pools}" | sed -r 's/^ram_pool_//g')$'\n'

# Look for qubes in mountpoints
if [ -d "${tempdir_root}" ]; then
    all_qube_names+=$(find "${tempdir_root}" \
                       -mindepth 1 \
                   -maxdepth 1 \
                   -type d \
                   -exec basename "{}" \; \
                   | sort \
                   | uniq)$'\n'
fi

# Look for qubes in menus directory
all_qube_names+=$(find "${menudir}" \
                   -regextype posix-egrep \
               -regex '.*\/user-qubes-.*\.menu$' \
               -exec basename "{}" \; \
               | sed -r 's/\.menu$//g' \
               | sed -r 's/^user-qubes-(disp)?vm-directory(_|-)//g' \
               | sort \
               | uniq)$'\n'

all_qube_names=$(echo "${all_qube_names}" \
                  | sed -r 's/^(Domain-0|libxl-driver)$//g' \
              | sed -r '/^\s*$/d' \
              | sort \
              | uniq)
set +e
qubes_to_remove=$(diff --new-line-format='' \
                   --unchanged-line-format='' \
               <(echo "${all_qube_names}") \
               <(echo "${existing_qubes}") \
               | sed -r '/^\s*$/d')
set -e

notice 'Checking for unused RAM qube pools'
remove_unused_pools "${ram_pools}"
notice 'Finished checking for unused RAM qube pools'

set +e
qubes_to_remove=$(echo "${qubes_to_remove}" | sed -r '/^\s*$/d')
set -e
notice 'Checking for files with no corresponding qubes'
remove_qubes_files "${qubes_to_remove}"
notice 'Finished checking for qube file remnants'

if [[ -d "${tempdir_root}" && -z "$(ls -A "${tempdir_root}")" ]]; then
    printf "%s ${light_cyan}%s${no_color}: " 'Delete' "${tempdir_root}"
    pause && rmdir "${tempdir_root}"
fi
notice 'Done'

One more script to monitor RAM pool and volume usage. Could be useful for optimizing memory related qube settings (e.g. run watch -c pool-usage in a dom0 console while working with various qubes):

#!/bin/bash

set -euo pipefail
/usr/bin/renice -n 19 $$ > /dev/null 2>&1

readonly light_green='\033[1;32m'
readonly light_purple='\033[1;35m'
readonly white='\033[1;37m'
readonly no_color='\033[0m'
readonly bg_red='\033[41m'

echo -ne "volatile volume: ${white}${bg_red}non ephemeral${no_color}"
echo -e "${light_green} ephemeral${no_color}"

output()
{
    local text="${1}"
    local ephemeral="${2}"
    local size="${3}"
    local usage="${4}"
    local color="${white}${bg_red}"
    [[ "${ephemeral}" =~ 'True' ]] && color="${light_green}"

    local percent
    local limit=80
    percent=$(awk -v usage="${usage}" -v size="${size}" \
               'BEGIN {printf "%3.2f", 100*usage/size}')
    local percent_color="${no_color}"
    if [ "${percent%.*}" -gt "${limit%.*}" ]; then
        percent_color="${white}${bg_red}"
    fi
    local size_gb
    size_gb=$(awk -v size="${size}" \
               'BEGIN {printf "%2.2f", size/1024/1024/1024}')
    printf "${color}%-18s${no_color} " "${text}"
    printf "${percent_color}%6.2f%%${no_color} of %5.2f GiB\n" \
        "${percent}" "${size_gb}"
}

main()
{
    local ram_pools
    mapfile -t ram_pools < <(qvm-pool list \
                         | grep -Eo '^ram_pool_[^ ]+')
    for pool in "${ram_pools[@]}"; do
        local qube
        qube="${pool#ram_pool_}"
        printf "${light_purple}%s${no_color}\n" "${qube}"
        local ephemeral_volatile=''
        ephemeral_volatile=$(qvm-pool info "${pool}" \
                                  | grep -E '^ephemeral_volatile')
        local size
        size=$(qvm-pool info "${pool}" \
                    | grep -E '^size' \
                    | grep -Eo '[0-9]+')
        local usage
        usage=$(qvm-pool info "${pool}" \
                     | grep -E '^usage' \
                 | grep -Eo '[0-9]+')
        output "${pool}" "${ephemeral_volatile}" "${size}" "${usage}"

        local volume
        for volume in 'volatile' 'private'; do
            local ephemeral=''
            ephemeral=$(qvm-volume info "${qube}:${volume}" ephemeral)
            size=$(qvm-volume info "${qube}:${volume}" size)
            usage=$(qvm-volume info "${qube}:${volume}" usage)
            output "${volume}" "${ephemeral}" "${size}" "${usage}"
        done
    done
}

main "${@}"

Your comments and suggestions are welcome.

-- Edits: - @barto's fixes - added -l, --label option - fix whitespaces - added -k, --kernel and -e, --ephemeral options, removed unnecessary wait. Now even forcefully killed qubes should get proper cleanup. - removed ephemeral until there is more clarity about it - reworked to allow all properties supported by qvm-create - added a second script for easy cleanup (when necessary) - script 1: improved checks, now pool names match custom qube name; script 2: find more logs, added qube name colorization in log file paths - New feature: automatic label based on netvm value (custom label is still possible); added process locking to prevent 2+ instances trying to create the same qube. - fixed a bug with auto labeling. It works as expected now. - After clarifications on GitHub and here, [b]reworked[/b] to use AppVMs instead. The DVM template is copied to RAM and runs from there. - made AppVM's root volume read-only - added pool-usage script - fixed a typo bug in the cleanup script - added icons to notifications; decreased default tempsize to 1 GiB - updated filename patterns for ram-qube and remove-qube-remnants script to clean up correctly in Qubes OS 4.2.0