#!/bin/bash ## vim:sw=2:sts=2:ts=2:et:spell: ## ## Copyright (C) 2023 - 2023 ENCRYPTED SUPPORT LP ## See the file COPYING for copying conditions. ########################## ## BEGIN DEFAULT VALUES ## ########################## if test "${BASH_SOURCE-}" != "${0}"; then ## Script was sourced. ## This is useful for other programs / scripts to be able to `source` the ## functions of this script for code re-use. dist-installer-gui will do this. dist_installer_cli_was_sourced="true" else ## Script was executed. dist_installer_cli_was_sourced="false" # shellcheck disable=SC2317 set -o errexit set -o nounset fi set_globals() { ## Version is commit based: https://github.com/Whonix/usability-misc version="20.3-1" me="${0##*/}" # shellcheck disable=SC2034 all_args="${*}" start_time="$(date +%s)" dialog_title="License agreement (scroll with arrows)" license=" Please do NOT continue unless you understand everything! DISCLAIMER OF WARRANTY. . THE PROGRAM IS PROVIDED WITHOUT ANY WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE AND MERCHANTABILITY. THE PROGRAM IS BEING DELIVERED OR MADE AVAILABLE 'AS IS', 'WITH ALL FAULTS' AND WITHOUT WARRANTY OR REPRESENTATION. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. . LIMITATION OF LIABILITY. . UNDER NO CIRCUMSTANCES SHALL ANY COPYRIGHT HOLDER OR ITS AFFILIATES, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, FOR ANY DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL OR PUNITIVE DAMAGES ARISING FROM, OUT OF OR IN CONNECTION WITH THE USE OR INABILITY TO USE THE PROGRAM OR OTHER DEALINGS WITH THE PROGRAM(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), WHETHER OR NOT ANY COPYRIGHT HOLDER OR SUCH OTHER PARTY RECEIVES NOTICE OF ANY SUCH DAMAGES AND WHETHER OR NOT SUCH DAMAGES COULD HAVE BEEN FORESEEN. . INDEMNIFICATION. . IF YOU CONVEY A COVERED WORK AND AGREE WITH ANY RECIPIENT OF THAT COVERED WORK THAT YOU WILL ASSUME ANY LIABILITY FOR THAT COVERED WORK, YOU HEREBY AGREE TO INDEMNIFY, DEFEND AND HOLD HARMLESS THE OTHER LICENSORS AND AUTHORS OF THAT COVERED WORK FOR ANY DAMAGES, DEMANDS, CLAIMS, LOSSES, CAUSES OF ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABILITY ARISING FROM, RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. " ## https://www.whonix.org/wiki/Main/Project_Signing_Key#cite_note-7 adrelanos_signify="untrusted comment: Patrick Schleizer adrelanos@whonix.org signify public key RWQ6KRormNEETq+M8IysxRe/HAWlqZRlO8u7ACIiv5poAW0ztsirOjCQ" ## https://www.whonix.org/wiki/KVM/Project_Signing_Key#cite_note-4 hulahoop_signify="untrusted comment: signify public key RWT2GZDQkp1NtTAC1IoQHUsyb/AQ2LIQF82cygQU+riOpPWSq730A/rq" ## https://www.virtualbox.org/wiki/Linux_Downloads oracle_pgp="-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.12 (GNU/Linux) mQINBFcZ9OEBEACSvycoAEIKJnyyIpZ9cZLCWa+rHjXJzPymndnPOwZr9lksZVYs 12YnsEy7Uj48rTB6mipbIuDDH9VBybJzpu3YjY7PFTkYAeW6WAPeJ8RcSGXsDvc0 fQ8c+7/2V1bpNofc9vDSdvcM/U8ULQcNeEa6DI4/wgy2sWLXpi1DYhuUOSU10I97 KHPwmpWQPsLtLHEeodeOTvnmSvLX1RRl32TPFLpLdjTpkEGS7XrOEXelqzMBQXau VUwanUzQ2VkzKnh4WecmKFT7iekOFVHiW0355ErL2RZvEDfjMjeIOOa/lPmW7y4F fHMU3a3sT3EzpD9ST/JGhrmaZ+r5yQD4s4hn1FheYFUtUN0dqHe9KgPDecUGgh4w rGnm0nUQsmQLKGSFXskqt26IiERdRt1eXpR9C5yufCVZfYpSsoG/mIHAt9opXFqi ryJqzx5pfQkOLTz9WErThHK1399jyXJwYGKLyddHFQEdy3u3ELM8Kfp7SZD/ERVq t2oA8jsr24IOyL16cydzfSP2kAV1r30bsF/1Q4qq6ii/KfDLaI0KEliBLQuB9jrA 6XQ69VLtkNPgiWzVMclg+qW1pA8ptXqXLMxi4h5EmE5GOhsihuwkwhhBmFqGT1RJ EGlc/uiHWQskOW3nhQ3Epd6xhCUImy8Eu83YRxS6QriH6K8z5LgRSdg9nwARAQAB tElPcmFjbGUgQ29ycG9yYXRpb24gKFZpcnR1YWxCb3ggYXJjaGl2ZSBzaWduaW5n IGtleSkgPGluZm9AdmlydHVhbGJveC5vcmc+iQI3BBMBCgAhBQJXGfThAhsDBQsJ CAcDBRUKCQgLBRYDAgEAAh4BAheAAAoJEKL2g8UpgK7P49QP/39dH+lFqlD9ruCV apBKVPmWTiwWbqmjxAV35PzG9reO7zHeZHil7vQ6UCb6FGMgZaYzcj4Sl9xVxfbH Zk7lMgyLDuNMTTG4c6WUxQV9UH4i75E1IBm9lOJw64bpbpfEezUF/60PAFIiFBvD 34qUAoVKe49PbvuTy98er5Kw6Kea880emWxU6I1Q1ZA80+o2dFEEtQc+KCgfWFgd O757WrqbTj6gjQjBAD5B4z5SwBYMg1/TiAYF0oa+a32LNhQIza/5H3Y+ufMfO3tY B/z1jLj8ee5lhjrv0jWvvfUUeIlq5pNoOmtNYFS+TdkO0rsqEC6AD0JRTKsRHOBu eSj7SLt8gmqy7eEzRCMlYIvoQEzt0/JuTQNJjHCuxH1scV13Q3bK6SmxqlY46tf5 Ljni9Z4lLJ7MB1BF2MkHuwQ7OcaEgUQBZSudzPkpRnY0AktiQYYP4Q1uDp+vfvFp GTkY1pqz3z2XD66fLz0ea5WIBBb3X/uq9zdHu8BTwDCiZlWLaDR5eQoZWWe+u+5J NUx1wcBpC1Hr2AnmuXBCRq+bzd8iaB8qxWfpCAFZBksSIW2aGhigSeYdx1jpjOob xog4qbuo5w1IUh8YLHwQ6uM12CqwC1nZadLxG0Fj4KoYbvp0T5ryBM3XD+TVGjKB m/QHLqabxZBbuJT0Cw2dRtW/ty5ZuQINBFcZ9OEBEADEY+YveerQjzzy5nA1FjQG XSaPcjy4JlloRxrUyqlATA0AIuK7cwc7PVrpstV8mR9qb38fdeIoY1z1dD3wnQzJ lbDfZhS5nGMzk9AANd6eJ2KcWI3qLeB//4fr2pPS0piOG4qyW4IhY4KeuCwusE6d IyDBg2XEdpG1IesSDaqNsvLZjPFEBNiCIkqrC7XSmoPNwHkKGj5LeD1wAE914cn2 a04IlbXiT46V9jjJFnNem/Co0u+2e2J3oReNmHvbb62OC57rqeBxqBplXg9tvJk/ w0A3bXxUrfz83tY6vDYoFdwJDudaJJWQjvqpYnySXMJYT6KoE4Xgl5fNcbNIVUpU k74BcWD9PZVadSMN7FWZzMfVsbTMmUA22tuDKD6hrF6ysCelex4YO44kSH7dhXu5 ANtZ2BFfRZvdjTQoblOI8C9cy/iX74vvG8OZarFG+u/kon3+xcAgY5KceUVbostO 0n3V8iK0gMQWH8sR8vXH+oV4GqHUEQURax2XM2Tt7Ra5XGcQaYDIkNPKSVVVtTk5 3OU/bNoBofAbwd4eOZOf9ag5ZVIIaoubMOEiveGYde4AEVE7krSNcYh/C48iCVKr eOyS26AVA15dAvnKTAqxJqICUSQ9zjGfTp1obhXCkMAy0m+AxNVEfSzFznQLHtWK zEGr+zCsvj1R8/qlMpHBXQARAQABiQIfBBgBCgAJBQJXGfThAhsMAAoJEKL2g8Up gK7PKpQP+wY9zLgnJeqrvNowmd70afd8SVge9BvhLh60cdG+piM5ZuEV5ZmfTFoX XPHzOo2dgt6VYTE9JO72Jv7MyzJj3zw3G/IcJQ6VuQwzfKkFTD+IeOiXX2I2lX1y nFv24rs1MTZ4Px1NJai7fdyXLiCl3ToYBmLafFpfbsVEwJ8U9bCDrHE4KTVc9IXO KQ5/86JaIPN+JJLHJoO2EBQC08Cw3oxTDFVcWZ/IWvEFeqyqRSyoFMoDkjLYsqHS we1kEoMmM2qN20otpKYq8R+bIEI5KKuJvAts/1xKE2cHeRvwl5kcFw/S3QQjKj+b LCVTSRZ6EgcDDmsAPKt7o01wmu+P3IjDoiyMZJQZpZIA2pYDxruY+OLXpcmw78Gq lTXb4Q9Vf47sAE8HmHfkh/wrdDeEiY9TQErzCBCufYbQj7sgttGoxAt12N+pUepM MBceAsnqkF6aEa4n8dUTdS2/nijnyUZ2rDVzikmKc0JlrZEKaw8orDzg8fXzfHIc pTrXCmFLX5BzNQ4ezAlw0NZG/qvhSBCuAkFiibfQUal8KLYwswvGJFghuQHsVTkf gF8Op7Br7loTNnp3yiI0jo2D+7DBFqtiSHCq1fIgktmKQoVLCfd3wlBJ/o9cguT4 Y3B83Y34PxuSIq2kokIGo8JhqfqPB/ohtTLHg/o9RhP8xmfvALRD =Rv7/ -----END PGP PUBLIC KEY BLOCK-----" } ## colors get_colors(){ ## Disable colors if some environment variables are present. if test -n "${NO_COLOR:-}" || test -n "${ANSI_COLORS_DISABLED:-}"; then # shellcheck disable=SC2034 nocolor="" bold="" nobold="" underline="" nounderline="" red="" green="" yellow="" #blue="" magenta="" cyan="" return 0 fi # shellcheck disable=SC2034 nocolor="\033[0m" bold="\033[1m" nobold="\033[22m" underline="\033[4m" nounderline="\033[24m" red="\033[31m" green="\033[32m" yellow="\033[33m" #blue="\033[34m" magenta="\033[35m" cyan="\033[36m" } ######################## ## END DEFAULT VALUES ## ######################## ################ ## BEGIN MISC ## ################ ## This is just a simple wrapper around 'command -v' to avoid ## spamming '>/dev/null' throughout this function. This also guards ## against aliases and functions. ## https://github.com/dylanaraps/pfetch/blob/pfetch#L53 has(){ _cmd="$(command -v "${1}")" 2>/dev/null || return 1 [ -x "${_cmd}" ] || return 1 } dirname() { ## Usage: dirname "path" ## If '$1' is empty set 'dir' to '.', else '$1'. dir=${1:-.} ## Strip all trailing forward-slashes '/' from ## the end of the string. # ## "${dir##*[!/]}": Remove all non-forward-slashes ## from the start of the string, leaving us with only ## the trailing slashes. ## "${dir%%"${}"}": Remove the result of the above ## substitution (a string of forward slashes) from the ## end of the original string. dir=${dir%%"${dir##*[!/]}"} ## If the variable *does not* contain any forward slashes ## set its value to '.'. [ "${dir##*/*}" ] && dir=. ## Remove everything *after* the last forward-slash '/'. dir=${dir%/*} ## Again, strip all trailing forward-slashes '/' from ## the end of the string (see above). dir=${dir%%"${dir##*[!/]}"} ## Print the resulting string and if it is empty, ## print '/'. printf '%s\n' "${dir:-/}" } ## Capitalize only the first char of a string. capitalize_first_char(){ echo "${1:-}" | awk '{$1=toupper(substr($1,0,1))substr($1,2)}1' } ## Block running as root. not_as_root(){ test "$(id -u)" = "0" && die 1 "\ ${underline}Non-Root Check:${nounderline} Running as root detected. - You are currently running this installer with root privileges. - This installer should not be run as root. - Please run as normal user." return 0 } ## Wrapper that supports su, sudo, doas root_cmd(){ test -z "${1:-}" && die 1 "${underline}root_cmd function:${nounderline} Failed to pass arguments to root_cmd." : "${root_cmd_loglevel:=notice}" case "${sucmd}" in su) ## Thanks to Congelli501 for su to not mess with quotes. ## https://stackoverflow.com/a/32966744/2605155 cmd="$(which -- "${1}")" shift log_run "$root_cmd_loglevel" su root -s "${cmd}" -- "${@}" ;; sudo) log_run "$root_cmd_loglevel" sudo -- "${@}" ;; doas) log_run "$root_cmd_loglevel" doas -u root -- "${@}" ;; *) die 1 "${underline}root_cmd function:${nounderline} root_cmd does not support sucmd: '${sucmd}'" ;; esac } ## Check if variable is integer is_integer(){ printf %d "${1}" >/dev/null 2>&1 || return 1 } ## Checks if the target is valid. ## Address range from 0.0.0.0 to 255.255.255.255. Port ranges from 0 to 65535 ## this is not perfect but it is better than nothing is_addr_port(){ addr_port="${1}" port="${addr_port##*:}" addr="${addr_port%%:*}" ## Support only IPv4 x.x.x.x:y if [ "$(echo "${addr_port}" | tr -cd "." | wc -c)" != 3 ] || [ "$(echo "${addr_port}" | tr -cd ":" | wc -c)" != 1 ] || [ "${port}" = "${addr}" ]; then die 2 "${underline}is_addr_port test:${nounderline} Invalid address:port assignment: ${addr_port}" fi is_integer "${port}" || die 2 "${underline}is_addr_port test:${nounderline} Invalid port '${port}', not an integer." if [ "${port}" -gt 0 ] && [ "${port}" -le 65535 ]; then true "is_addr_port test: Valid port: '${port}'" else die 2 "${underline}is_addr_port test:${nounderline} Invalid port '${port}', not within range: 0-65535." fi for quad in $(printf '%s\n' "${addr}" | tr "." " "); do is_integer "${quad}" || die 2 "${underline}is_addr_port test:${nounderline} Invalid address '${addr}', '${quad}' is not an integer." if [ "${quad}" -ge 0 ] && [ "${quad}" -le 255 ]; then true "Valid quad '${quad}'" else die 2 "${underline}is_addr_port test:${nounderline} Invalid address '${addr}', '${quad}' not within range: 0-255." fi done } ## Get host os and other necessary information. get_os(){ ## Source: pfetch: https://github.com/dylanaraps/pfetch/blob/master/pfetch os="$(uname -s)" kernel="$(uname -r)" arch="$(uname -m)" distro="" distro_version="" debian_testing_or_unstable_detected="" case ${os} in Linux*) if has lsb_release; then distro=$(lsb_release --short --description || lsb_release -sd) distro_version=$(lsb_release --short --release || lsb_release -sr) elif test -f /etc/os-release; then while IFS='=' read -r key val; do case "${key}" in (PRETTY_NAME) distro=${val} ;; (VERSION_ID) distro_version=${val} ;; esac done < /etc/os-release else has crux && distro=$(crux) has guix && distro='Guix System' fi distro=${distro##[\"\']} distro=${distro%%[\"\']} case ${PATH} in (*/bedrock/cross/*) distro='Bedrock Linux' ;; esac if [ "${WSLENV:-}" ]; then distro="${distro}${WSLENV+ on Windows 10 [WSL2]}" elif [ -z "${kernel%%*-Microsoft}" ]; then distro="${distro} on Windows 10 [WSL1]" fi ;; Haiku) distro=$(uname -sv);; Minix|DragonFly) distro="${os} ${kernel}";; SunOS) IFS='(' read -r distro _ < /etc/release;; OpenBSD*) distro="$(uname -sr)";; FreeBSD) distro="${os} $(freebsd-version)";; *) distro="${os} ${kernel}";; esac ## Debian testing /etc/os-release does not contain VERSION_ID. if echo "${distro}" | grep --quiet "/sid" ; then debian_testing_or_unstable_detected=true fi distro_derivative_name_pretty="(No derivative detected.)" distro_derivative_version="(No derivative detected.)" if test -f /usr/share/kicksecure/marker; then distro_derivative_name_pretty="Kicksecure" distro_derivative_version=$(cat /etc/kicksecure_version) elif test -f /usr/share/whonix/marker; then distro_derivative_name_pretty="Whonix" distro_derivative_version=$(cat /etc/whonix_version) fi log notice "Architecture detected: '${arch}'" log notice "System detected: '${os}'" log notice "Distribution/Derivative name detected: '${distro}' / '${distro_derivative_name_pretty}'" log notice "Distribution/Derivative version detected: '${distro_version}' / '${distro_derivative_version}'" if [ "$debian_testing_or_unstable_detected" = "true" ]; then log notice "Debian testing or unstable detection: 'success'" if test "${oracle_repo}" = "1"; then log error "You are attempting to use '--oracle-repo' on Debian testing. This is impossible." if test "${ci}" = "1"; then die 0 "${underline}Distribution Test Result:${nounderline} Oracle doesn't provide a Debian testing or unstable repository. Skipped on CI to avoid breaking the CI testing." else die 101 "${underline}Distribution Test Result:${nounderline} Oracle doesn't provide a Debian testing or unstable repository." fi fi log info "Not attempting to use '--oracle-repo' on Debian testing, good." ## In Debian testing distro_version was previously observed as 'n/a' or empty, because ## Debian testing /etc/os-release does not contain VERSION_ID. return 0 fi log info "Debian testing or unstable detection: 'not detected'" ## This at last so the user can hopefully post his system info from the ## logs before the error below. if [ -z "${distro_version}" ]; then if test -f /etc/os-release; then log notice "Contents of /etc/os-release:" cat /etc/os-release || true else log notice "/etc/os-release file not found." fi die 101 "${underline}Distribution Check:${nounderline} Failed to find distribution version." ## it will fail later on get_host_pkgs if the system is not supported. ## but distro version needs to be checked here because it can occur ## frequently when the release of the distribution is still unstable. ## Also because we check for distribution version to abort if necessary. fi distro_version_without_dot="$(echo "${distro_version}" | tr -d ".")" is_integer "${distro_version_without_dot}" || die 101 "${underline}Distribution Check:${nounderline} Distribution version without dot is still not a number: '${distro_version_without_dot}'" } get_distro() { true "distro: ${distro}" case "${os}" in Linux*) case "${distro}" in "Debian"*|"Kali"*|"Tails"*|"Kicksecure"|"Whonix") debian_derivative_detected=1 ;; "Linux Mint"*|"LinuxMint"*|"mint"*) ubuntu_derivative_detected=1 debian_derivative_detected=1 ;; *"buntu"*) ubuntu_derivative_detected=1 debian_derivative_detected=1 if [ "${distro_version_without_dot}" -lt 2204 ]; then die 101 "${underline}Distribution Check:${nounderline} Minimal '${distro}' required version is '22.04', yours is '${distro_version}'." fi ;; "Fedora"*|"CentOS"*|"rhel"*|"Redhat"*|"Red hat") fedora_derivative_detected=1 ;; "Arch"*|"Artix"*|"ArcoLinux"*) claim_unsupported_distro known "${distro}" ;; *) claim_unsupported_distro unknown "${distro}" ;; esac ;; "OpenBSD"*) claim_unsupported_distro known "${distro}" ;; "NetBSD"*) claim_unsupported_distro known "${distro}" ;; "FreeBSD"*|"HardenedBSD"*|"DragonFly"*) claim_unsupported_distro known "${distro}" ;; *) claim_unsupported_distro unknown "${distro}" ;; esac if [ ! "$fedora_derivative_detected" = "1" ]; then return 0 fi if ! test "${ci}" = "1"; then return 0 fi if [ ! "${onion}" = "1" ]; then return 0 fi die 0 "${underline}Distribution Test Result:${nounderline} Fedora on CI does not run the Tor systemd service. Skipped on CI to avoid breaking the CI testing." } check_not_qubes_template() { if ! test -f /run/qubes/this-is-templatevm; then true "INFO: Not running inside a Qubes Template." return 0 fi qubes_template_detected=true user_warned_potential_startup_issue=true log warn "${underline}QubesOS Template Detection Test:${nounderline} 'Template detected' - The installer has detected being run inside a Qubes Template." if test "${virtualbox_only}" = "1"; then true "INFO: Running inside a Qubes Template. Continuing via '--virtualbox-only' option." return 0 fi ## Downloading non-Qubes VM images in Qubes Template makes no sense. Not even for development purposes. ## Installing VirtualBox inside Qubes however makes sense for development purposes. ## For example, this permits building non-Qubes VM images inside Qubes VMs. die 1 "${underline}QubesOS Template Detection Test:${nounderline} Only virtualbox-installer-cli ('--virtualbox-only') allowed in Qubes Template." } ############## ## END MISC ## ############## ########################## ## BEGIN OPTION PARSING ## ########################## ## Begin parsing options. ## function should be called before the case statement to assign the options ## to a temporary variable begin_optparse(){ ## options ended test -z "${1:-}" && return 1 shift_n="" ## save opt orig for error message to understand which opt failed opt_orig="${1}" # shellcheck disable=SC2034 ## need to pass the second positional parameter cause maybe it is an argument arg_possible="${2}" clean_opt "${1}" || return 1 } ## Get arguments from options that require them. ## if option requires argument, check if it was provided, if true, assign the ## arg to the opt. If $arg was already assigned, and if valid, will use it for ## the key value ## usage: get_arg key get_arg(){ true "BEGIN get_arg(): '${*}'" ## if argument is empty or starts with '-', fail as it possibly is an option case "${arg:-}" in ""|-*) die 2 "Option '${opt_orig}' requires an argument." ;; esac set_arg "${1}" "${arg}" ## shift positional argument two times, as this option demands argument, ## unless they are separated by equal sign '=' ## shift_n default value was assigned when trimming dashes '--' from the ## options. If shift_n is equal to zero, '--option arg', if shift_n is not ## equal to zero, '--option=arg' if test -z "${shift_n}"; then shift_n=2 fi true "END get_arg(): '${*}'" } ## Single source to set opts, can later be used to print the options parsed ## usage: set_arg variable value set_arg(){ ## Check if variable had already a value assigned. Expanding to empty value ## is necessary to avoid eval failing because of unset parameter if variable ## didn't have a value assigned before. # shellcheck disable=SC2016 eval previous_value="$(printf '${%s:-}' "${1}")" ## Escaping quotes is needed because else it fails if the argument is quoted # shellcheck disable=SC2140 eval "${1}"="\"${2}\"" ## variable used for --getopt if test -z "${arg_saved:-}"; then arg_saved="${1}=\"${2}\"" else if test -z "${previous_value:-}"; then ## If didn't add to the end of the list arg_saved="${arg_saved}\n${1}=\"${2}\"" else ## If had, replace existing value. arg_saved="$(printf %s"${arg_saved}" | sed "s|^${1}=.*$|${1}=\"${2}\"|")" fi fi true "END set_arg(): '${*}'" } ## Clean options. ## '--option=value' should shift once and '--option value' should shift twice ## but at this point it is not possible to be sure if option requires an ## argument, reset shift to zero, at the end, if it is still 0, it will be ## assigned to one, has to be zero here so we can check later if option ## argument is separated by space ' ' or equal sign '=' clean_opt(){ case "${opt_orig}" in "") ## options ended return 1 ;; --) ## stop option parsing shift 1 return 1 ;; --*=*) ## long option '--sleep=1' opt="${opt_orig%=*}" opt="${opt#*--}" arg="${opt_orig#*=}" shift_n=1 ;; -*=*) ## short option '-s=1' opt="${opt_orig%=*}" opt="${opt#*-}" arg="${opt_orig#*=}" shift_n=1 ;; --*) ## long option '--sleep 1' opt="${opt_orig#*--}" arg="${arg_possible}" ;; -*) ## short option '-s 1' opt="${opt_orig#*-}" arg="${arg_possible}" ;; *) ## not an option usage 2 ;; esac } ## Check if argument is within range ## usage: ## $ range_arg key "1" "2" "3" "4" "5" ## $ range_arg key "a" "b" "c" "A" "B" "C" range_arg(){ key="${1:-}" eval var='$'"${key}" shift 1 list="${*:-}" #range="${list#"${1} "}" if [ -n "${var:-}" ]; then success=0 for tests in ${list:-}; do ## only evaluate if matches all chars [ "${var:-}" = "${tests}" ] && success=1 && break done ## if not within range, fail and show the fixed range that can be used if [ ${success} -eq 0 ]; then die 2 "Option '${key}' cannot be '${var:-}'. Possible values: '${list}'" fi fi } ## check if option has value, if not, error out ## this is intended to be used with required options check_opt_filled(){ key="${1}" eval val='$'"${key:-}" ! test -n "${val}" && die 2 "${key} is missing." } ######################## ## END OPTION PARSING ## ######################## ################### ## BEGIN LOGGING ## ################### ## Logging mechanism with easy customization of message format as well as ## standardization on how the messages are delivered. ## usage: log [info|notice|warn|error] "X occurred." log(){ ## Avoid clogging output if log() is working alright. if test "${xtrace}" = "1"; then true "Removing xtrace for log() function." set +o xtrace fi log_type="${1}" ## capitalize log level log_type_up="$(echo "${log_type}" | tr "[:lower:]" "[:upper:]")" shift 1 ## escape printf reserved char '%' # shellcheck disable=SC2001 log_content="$(echo "${*}" | sed "s/%/%%/g")" ## set formatting based on log level case "${log_type}" in bug) log_color="${yellow}" ;; error) log_color="${red}" ;; warn) log_color="${magenta}" ;; info) log_color="${cyan}" ;; notice) log_color="${green}" ;; null) true ;; *) log bug "Unsupported log type specified: '${log_type}'" die 1 "Please report this bug." esac ## uniform log format log_color="${bold}${log_color}" log_full="${me}: [${log_color}${log_type_up}${nocolor}]: ${log_content}" ## error logs are the minimum and should always be printed, even if ## failing to assign a correct log type ## send bugs and error to stdout and stderr case "${log_type}" in bug) #printf %s"${log_full:+$log_full }Please report this bug.\n" 1>&2 printf %s"${log_full}" 1>&2 return 0 ;; error) printf %s"${log_full}\n" 1>&2 return 0 ;; null) true ;; esac ## reverse importance order is required, excluding 'error' all_log_levels="warn notice info debug null" if echo " ${all_log_levels} " | grep -o ".* ${log_level} " \ | grep -q " ${log_type}" then case "${log_type}" in warn) ## send warning to stdout and stderr printf %s"${log_full}\n" 1>&2 ;; null) true ;; *) printf %s"${log_full}\n" ;; esac fi if test "${xtrace}" = "1"; then set -o xtrace fi } ## For one liners 'log error; die' ## 'log' should not handle exits, because then it would not be possible ## to log consecutive errors on multiple lines, making die more suitable ## usage: die # "msg" ## where '#' is the exit code. die(){ log error "${2}" if test "${allow_errors}" = "1"; then log warn "Skipping termination because of with code '${1}' due to 'allow_errors' setting." return 0 fi case "${1}" in 106|107) true ;; *) log error "Installer aborting." ;; esac exit "${1}" } ## Wrapper to log command before running to avoid duplication of code log_run(){ level="${1}" shift ## Extra spaces appearing when breaking log_run on multiple lines. command_without_extrarenous_spaces="$(echo "${@}" | tr -s " ")" if test "${dry_run}" = "1"; then log "${level}" "Skipping command: $ ${command_without_extrarenous_spaces}" return 0 fi ## TODO: Still an issue? CI expects no output from root_cmd() which calls log_run(). if test "${run_background}" = "1"; then log "${level}" "Background command starting: $ ${command_without_extrarenous_spaces} &" "${@}" & background_pid="$!" disown "$background_pid" else log "${level}" "Command executing: $ ${command_without_extrarenous_spaces}" "${@}" || return 1 fi } ## Useful to get runtime mid run to log easily get_elapsed_time(){ printf '%s\n' "$(($(date +%s) - start_time))" } ## Log elapsed time, the name explains itself. log_time(){ log info "Time elapsed: $(get_elapsed_time)s." } ## Wrapper to end the exit trap. end_exit(){ ## Reset exit trap. trap - EXIT HUP INT QUIT ABRT ALRM TERM ## Kill tail PID. if test -n "${tail_pid:-}"; then ## Sleep less than a second so the file descriptors have enough time to ## output all the logs to the screen before the background job is killed. sleep 0.3 # shellcheck disable=SC2086 kill -9 ${tail_pid} fi ## Exit with desired exit code. exit "${last_exit}" } ## Handle exit trap with line it failed and its exit code. handle_exit(){ true "BEGIN handle_exit() with args: $*" last_exit="${1}" line_number="${2:-0}" log_time ## Exit without errors. test "${last_exit}" = "0" && end_exit ## Virtual Machine expected start issues. test "${last_exit}" = "106" && end_exit ## Virtual Machine unexpected start issues. test "${last_exit}" = "107" && end_exit ## Exit with errors. # shellcheck disable=SC3028 if test -n "${BASH_COMMAND:-}"; then # shellcheck disable=SC2039,3028,3054 log notice "Executed script, function, command executed: '${0}' '${FUNCNAME[1]}' '${BASH_COMMAND}'" else log notice "Executed script: '${0}'" fi ## some shells have a bug that displays line 1 as LINENO if test "${line_number}" -gt 2; then log error "Installer aborted due to an error." log error "No need to panic. Nothing is broken. Just some rare condition has been hit." log error "A solution likely exists for this issue." log error "If the issue does not recur on retry, it can be safely ignored as transient." log error "Consult ${guest_pretty} News and the User Help Forum for assistance." log error "Please report this bug if it has not been already." echo "" log error "An error occurred at line: '${line_number}'" ## ideas from https://unix.stackexchange.com/questions/39623/trap-err-and-echoing-the-error-line ## Simple version that doesn't indicate the error line. # pr -tn "${0}" | tail -n+$((line_number - 3)) | head -n3 ## Easy version for wasting resources and better readability. line_above="$(pr -tn "${0}" | tail -n+$((line_number - 4)) | head -n4)" line_error="$(pr -tn "${0}" | tail -n+$((line_number)) | head -n1)" line_below="$(pr -tn "${0}" | tail -n+$((line_number + 1)) | head -n4)" printf '%s\n*%s\n%s\n' "${line_above}" "${line_error}" "${line_below}" ## Too complex. # awk 'NR>L-4 && NR>>":""),$0 }' L="${line_number}" "${0}" >&2 echo "" log error "Please include the user log and the debug log in your bug report." log error "(For file locations where to find these logs, see above.)" echo "" else if [ "${last_exit}" -gt 128 ] && [ "${last_exit}" -lt 193 ]; then signal_code=$((last_exit-128)) signal_caught="$(kill -l "${signal_code}")" log error "Signal received: '${signal_caught}'" fi fi ## Print exit code. log error "Installer exited with code: '${last_exit}'" end_exit } ################# ## END LOGGING ## ################# ########################### ## BEGIN SCRIPT SPECIFIC ## ########################### claim_unsupported_distro(){ status="${1}" distro="${2}" log error "At this time, your Operating System is unsupported by the ${guest_pretty} Installer." log error "Visit the following URL to check support for manual installation:" log error " ${url_version_domain}/wiki/Virtualbox" die 101 "${underline}Distribution Check:${nounderline} Unsupported (${status} '${distro}') system." } ## Get necessary packages for your host system to be able to set the guest. get_host_pkgs(){ log notice "Package Installation: installation of hypervisor-required packages... Please wait, as this could take a while..." if [ "$ubuntu_derivative_detected" = "1" ]; then install_package_debian_common install_virtualbox_ubuntu elif [ "$debian_derivative_detected" = "1" ]; then install_package_debian_common install_virtualbox_debian elif [ "$fedora_derivative_detected" = "1" ]; then install_package_fedora_common install_virtualbox_fedora else die 1 "Operating system not found in get_host_pkgs. Please report this bug. Debug log required." fi } get_independent_host_pkgs(){ ## Platform independent packages if has signify-openbsd; then ## fix Debian unconventional naming signify(){ # shellcheck disable=SC2317 signify-openbsd "${@}" } fi has timeout || die 1 "${underline}Packages Installed Check:${nounderline} Timeout utility is missing." has curl || install_pkg curl has rsync || install_pkg rsync has mokutil || install_pkg mokutil has pgrep || install_pkg procps ## Install openssl and ca-certificates. ## openssl is required: ## Otherwise there will be an error message by rsync-ssl: ## Failed to find on your path: openssl stunnel4 stunnel ## ca-certificates is required: ## Otherwise rsync-ssl will fail TLS verification. ## XXX: Strictly speaking packages openssl ca-certificates are only required ## if using clearnet. I.e. not required when using --onion. install_pkg openssl ca-certificates while true; do has systemd-detect-virt && nested_virt_tool="systemd-detect-virt" && break test_pkg virt-what && nested_virt_tool="virt-what" && break install_pkg virt-what && nested_virt_tool="virt-what" && break break done } nested_virtualization_test() { #nested_virtualization_detected="" if test -z "${nested_virt_tool:-}"; then ## No hard fail, not a requirement, good to have only. user_warned_potential_startup_issue=true log warn "${underline}Nested Virtualization Test:${nounderline} Detection tool for nested virtualization is missing." else if test "${dry_run}" = "1"; then log notice "Nested Virtualization Test: Skipping test due to dry_run mode." else ## Check if we are a guest of virtualization. if root_cmd "${nested_virt_tool:-}" >/dev/null 2>&1; then #nested_virtualization_detected=true user_warned_potential_startup_issue=true log warn "${underline}Nested Virtualization Test:${nounderline} Nested virtualization detected. - This might be a user error. - This installer is designed to run on the host operating system. - This installer is not designed to be run inside virtual machines. - For more information about nested virtualization, refer to: ${url_version_domain}/wiki/Nested_Virtualization" fi fi fi if test -f /usr/share/qubes/marker-vm; then #nested_virtualization_detected=true user_warned_potential_startup_issue=true log warn "${underline}QubesOS Detection Test:${nounderline} 'Qubes detected' - The installer is not designed for execution within Qubes. - Useful only for development purposes. - It is recommended to use Qubes-Whonix instead." fi } secure_boot_test() { local mokutil_output if mokutil_output=$(mokutil --sb-state 2>&1) ; then secure_boot_status="'enabled' (mokutil_output: '$mokutil_output')" else secure_boot_status="'disabled' (mokutil_output: '$mokutil_output')" fi log info "Secure Boot Check Result: $secure_boot_status Ok." } kernel_modules_load() { local modules_disabled ## Does usually not require root but might on some hardened systems. if ! modules_disabled=$(root_cmd_loglevel=null root_cmd cat /proc/sys/kernel/modules_disabled) ; then kernel_modules_can_be_load=maybe user_warned_potential_startup_issue=true log warn "${underline}Kernel Module Load Check Result:${nounderline} 'failed' (failed to cat /proc/sys/kernel/modules_disabled)" return 0 fi if [ "$modules_disabled" = "0" ]; then kernel_modules_can_be_load=true log info "Kernel Module Load Check Result: 'yes' - modules can be load. (sysctl kernel.modules_disabled=0 is set.)" return 0 fi kernel_modules_can_be_load=false log info "${underline}Kernel Module Load Check Result:${nounderline} 'no' - modules cannot be load. (sysctl kernel.modules_disabled=1 is set.)" } kernel_modules_signed_only() { local sig_enforce ## Does usually not require root but might on some hardened systems. if ! sig_enforce=$(root_cmd_loglevel=null root_cmd cat /sys/module/module/parameters/sig_enforce) ; then modules_signed_only=maybe user_warned_potential_startup_issue=true log warn "${underline}Kernel Module Signature Enforcement Check Result:${nounderline} 'failed' (failed to cat /sys/module/module/parameters/sig_enforce)" return 0 fi if [ "$sig_enforce" = "Y" ]; then modules_signed_only=true log info "Kernel Module Signature Enforcement Check Result: 'yes' - only signed modules can be load. (kernel parameter module.sig_enforce=1 might be set.)" return 0 fi modules_signed_only=false log info "Kernel Module Signature Enforcement Check Result: 'no' - unsigned modules can be load." } need_reboot_check_first() { ## Debian: /var/run/reboot-required ## Fedora: Does not have this. if ! test -f /var/run/reboot-required ; then log info "Reboot Check Result: File /var/run/reboot-required does not exist, good." return 0 fi ## Building for an old kernel might result in missing kernel headers for that kernel ## In result, VirtualBox would fail to start. die 1 "${underline}Reboot Check Result:${nounderline} Your system reports that a reboot is required. Please reboot your system and restart this installer. Debugging information: file /var/run/reboot-required exists." } need_reboot_check_second() { true "fedora_derivative_detected: $fedora_derivative_detected" if [ ! "$fedora_derivative_detected" = "1" ]; then return 0 fi ## Fedora 38: Part of dnf-utils. Not installed by default (on CI). ## Fedora 39: Part of dnf-plugins-core. Not installed by default (on CI). ## Therefore this can only run after install_package_fedora_common. if root_cmd /usr/bin/needs-restarting --reboothint ; then true "INFO: No reboot required." return 0 fi if test "${ci}" = "1"; then true "INFO: Ignore need_reboot_check_second because running on CI." return 0 fi die 1 "${underline}Reboot Check Result:${nounderline} Your system reports that a reboot is required. Please reboot your system and restart this installer. Debugging information: needs-restarting reported that a reboot is required." } update_sources(){ log notice "Updating package list..." ## global console_write_command if tty >/dev/null >&1 ; then console_write_command="tee /dev/tty" else ## github actions CI issue: ## tee: /dev/tty: No such device or address ## https://github.com/actions/runner/issues/241 console_write_command="cat" fi #root_cmd ${pkg_mngr_update} 2>&1 || true #update_sources_error ## result: OK: command_without_extrarenous_spaces="sudo -- apt-get update --yes --error-on=any" ## But if root_cmd is run in a subshell using $(root_cmd ...) then command_without_extrarenous_spaces ## won't be updated. local simulate_only_maybe="" if test "${noupdate}" = "1"; then ## Too slow to run over and over again during testing. simulate_only_maybe=true log warn "Package List Update: Simulate only, via '--noupdate' option." fi # shellcheck disable=SC2086 if update_output=$(root_cmd ${simulate_only_maybe} ${pkg_mngr_update} 2>&1 | $console_write_command) ; then true "INFO: Exit code is zero but that does not guarantee in case of dnf that there is no error." if echo "$update_output" | grep --quiet --ignore-case "Error:" ; then log error "${underline}Package List Update:${nounderline} Exit code was 0 but 'Error:' found in output." update_sources_error else true "INFO: No error found in update output, ok." return 0 fi else log error "${underline}Package List Update:${nounderline} Non-zero exit code. Stop." update_sources_error fi } package_manager_issue_extra_help_text="The user is advised to attempt to debug this with the following steps: 1. Run above command as root (with sudo). 2. If there is an issue, use search engines, documentation and if needed contact the support of your operating system. 3. Once this has been fixed fixed, re-run this installer." update_sources_error() { die 1 "${underline}Package List Update:${nounderline} Could not update package lists. - This issue is most likely not caused by this installer. - This is most likely a package manager configuration or network issue. This is the command which the installer has just run that failed: ${sucmd} ${pkg_mngr_update} ${package_manager_issue_extra_help_text}" } check_upgrades_simulation() { # shellcheck disable=SC2086 if upgrade_simulate_output=$(root_cmd ${pkg_mngr_upgrade_check} ${install_pkg_fasttrack_extra_args_maybe} 2>&1 | $console_write_command) ; then true "INFO: Exit code is zero but that does not guarantee in case of dnf that there is no error." if echo "$upgrade_simulate_output" | grep --quiet --ignore-case "Error:" ; then log error "${underline}Package Upgrade Simulation:${nounderline} Exit code was 0 but 'Error:' found in output." upgrade_simulate_sources_error fi else log error "${underline}Package Upgrade Simulation:${nounderline} Non-zero exit code. Stop." upgrade_simulate_sources_error fi true "INFO: No error found in upgrade_simulate_output output, ok." local number_upgradable number_upgradable="$(${pkg_mngr_upgradable_check} 2>/dev/null| wc -l)" ## First line is just reports. number_upgradable="$((number_upgradable-1))" if test "${dry_run}" = "1"; then number_upgradable=0 fi if [ "$number_upgradable" = "0" ]; then log info "Package Upgrade Simulation: No packages require upgrading, ok." return 0 fi if test "${noupdate}" = "1"; then log warn "Package Upgrade Simulation: Package upgrades available but proceeding anyhow via '--noupdate' option." return 0 fi die 1 "${underline}Package Upgrade Simulation:${nounderline} Package upgrades available. '${number_upgradable}' packages can be upgraded. Please upgrade your operating system first. Otherwise this installer cannot proceed. ${sucmd} ${pkg_mngr_upgrade_install} ${install_pkg_fasttrack_extra_args_maybe} ${package_manager_issue_extra_help_text}" } upgrade_simulate_sources_error() { die 1 "${underline}Package Upgrade Simulation:${nounderline} Failed. - This issue is most likely not caused by this installer. - This is most likely a package manager configuration or network issue. This is the command which the installer has just run that failed: ${sucmd} ${pkg_mngr_upgrade_check} ${install_pkg_fasttrack_extra_args_maybe} ${package_manager_issue_extra_help_text}" } ## Install package only if not installed already. install_pkg(){ pkgs="${*}" special_args="" pkg_not_installed="" for arg in ${pkgs}; do case "${arg}" in --target-release=*) special_args="${special_args} ${arg}" ;; *) ## Test if package exists as a binary or a library, using different tools. # shellcheck disable=SC2086 if ${pkg_mngr_check_installed} "${arg}" >/dev/null 2>&1; then log info "Package already installed: '${arg}'" continue fi if has "${arg}"; then log info "Program already installed: '${arg}'" continue fi pkg_not_installed="${pkg_not_installed} ${arg}" ;; esac done if test -n "${pkg_not_installed}"; then if test "${dry_run}" = "1"; then log notice "Skipping installation of the following packages via '--dry-run' option: '${pkg_not_installed}'" return 0 fi log notice "Installing package(s): '${pkg_not_installed}'" log info "special_args: '${special_args}'" # shellcheck disable=SC2086 if ! root_cmd ${pkg_mngr_install} ${special_args} ${pkg_not_installed} ; then if echo "${pkg_not_installed}" | grep --quiet --ignore-case virtualbox ; then virtualbox_installation_failure_debug fi die 1 "${underline}Package Installation:${nounderline} Failed to install package: '${pkg_not_installed}'" fi ## Test if installation succeeded. test_pkg "${pkg_not_installed}" fi } ## Used to test for a 2nd time if packages exist or not, if not, ## install_pkg() failed above and best thing to do is abort because of missing ## dependencies. test_pkg(){ pkgs="${*}" pkg_not_installed="" for pkg in ${pkgs}; do if ! has "${pkg}" && ! ${pkg_mngr_check_installed} "${pkg}" >/dev/null 2>&1 then pkg_not_installed="${pkg_not_installed} ${pkg}" fi done if test -n "${pkg_not_installed}"; then if test "${dry_run}" = "1"; then log error "Failed to locate package(s) and ignoring via '--dry-run' option: '${pkg_not_installed}'" else log error "Failed to locate package(s): '${pkg_not_installed}'" return 1 fi fi } check_vm_running_general() { case "${guest}" in whonix) check_vm_running_virtualbox "${guest_full_vm_name_gateway}" check_vm_running_virtualbox "${guest_full_vm_name_workstation}" ;; kicksecure) check_vm_running_virtualbox "${guest_full_vm_name_kicksecure}" ;; esac } ## Abort if user wants to reimport a VM that is running. ## This function is called before attempting to reimport an image. check_vm_running_virtualbox(){ vm="${1}" ## Paused state should be considered as running. Instead of grepping ## possible states, grep VM from list of running VMs. # vboxmanage list runningvms | grep -q "^\"${vm}\" " if vboxmanage showvminfo "${vm}" 2>&1 | \ grep -qE "^State:[[:space:]]+(running|paused)" then log error "Cannot proceed. You have the following VM running: ${vm}" die 1 "${underline}VM Running Check:${nounderline} Please turn it off before re-running this installer." fi } ## Check if VM exists on VirtualBox check_vm_registered_virtualbox(){ local extra_message extra_message="$1" case "${guest}" in whonix) ## Test if machine exists. workstation_exists=0 gateway_exists=0 if vboxmanage showvminfo "${guest_full_vm_name_gateway}" >/dev/null 2>&1 ; then gateway_exists=1 log info "Existing VM Check Result $extra_message: guest '${guest_full_vm_name_gateway}' exists." fi if vboxmanage showvminfo "${guest_full_vm_name_workstation}" >/dev/null 2>&1 ; then workstation_exists=1 log info "Existing VM Check Result $extra_message: guest '${guest_full_vm_name_workstation}' exists." fi ## Find discrepancies. if test "${workstation_exists}" = "0" && test "${gateway_exists}" = "1" ; then log info "Existing VM Check Result $extra_message: Gateway exists but Workstation doesn't." fi if test "${workstation_exists}" = "1" && test "${gateway_exists}" = "0" ; then log info "Existing VM Check Result $extra_message: Workstation exists but Gateway doesn't." fi vm_or_vms_already_existing_test_result=false if test "${workstation_exists}" = "1" || test "${gateway_exists}" = "1" ; then ## If either one of the guests exists, proceed. vm_or_vms_already_existing_test_result=true fi if test "${vm_or_vms_already_existing_test_result}" = "false" ; then ## Both guests are still non-existing. Therefore return from this function. log info "Existing VM Check Result $extra_message: None existing yet, ok." return 0 fi log notice "Existing VM Check Result $extra_message: Virtual Machine(s) have been imported previously." ;; kicksecure) vm_or_vms_already_existing_test_result=false if ! vboxmanage showvminfo "${guest_full_vm_name_kicksecure}" >/dev/null 2>&1 ; then log info "Existing VM Check Result $extra_message: None existing yet, ok." return 0 fi log notice "Existing VM Check Result $extra_message: Virtual Machine(s) were imported previously." vm_or_vms_already_existing_test_result=true ;; esac } ## Check if VM exists using hypervisor tools. check_vm_exists_general(){ if test "${virtualbox_only}" = "1"; then return 0 fi if [ "${hypervisor}" = "kvm" ]; then return 0 fi check_vm_registered_virtualbox "$1" } check_vm_file_exists_virtualbox_general() { ## '/home/user/VirtualBox VMs/Kicksecure-Xfce/Kicksecure-Xfce.vbox' ## '/home/user/VirtualBox VMs/Whonix-Gateway-Xfce/Whonix-Gateway-Xfce.vbox' ## '/home/user/VirtualBox VMs/Whonix-Workstation-Xfce/Whonix-Workstation-Xfce.vbox' if [ -z "${HOME+x}" ]; then log warn "VM Import Check: Skip testing if there is an extraneous '.vbox' file because environment variable HOME is unset." return 0 fi local file_name_list=() case "${guest}" in whonix) if test "${import_only}" = "gateway" ; then file_name_list+=("$HOME/VirtualBox VMs/${guest_full_vm_name_gateway}/${guest_full_vm_name_gateway}.vbox") elif test "${import_only}" = "workstation" ; then file_name_list+=("$HOME/VirtualBox VMs/${guest_full_vm_name_workstation}/${guest_full_vm_name_workstation}.vbox") else file_name_list+=("$HOME/VirtualBox VMs/${guest_full_vm_name_gateway}/${guest_full_vm_name_gateway}.vbox") file_name_list+=("$HOME/VirtualBox VMs/${guest_full_vm_name_workstation}/${guest_full_vm_name_workstation}.vbox") fi ;; kicksecure) file_name_list+=("$HOME/VirtualBox VMs/${guest_full_vm_name_kicksecure}/${guest_full_vm_name_kicksecure}.vbox") ;; esac local file_name_item for file_name_item in "${file_name_list[@]}" ; do if test -e "$file_name_item" ; then log warn "VM Import Check: Inconsistent state. This might have happened by previously using VirtualBox GUI 'Remove...' with 'Remove only' instead of 'Delete all files'. File '$file_name_item' exists but there is no associated VM registered in VirtualBox." if test -z "${import_only}"; then die 1 "VM Import Check: Aborting because of inconsistent state and missing '--import-only' option." fi if test "${destroy_existing_guest}" != "1"; then die 1 "VM Import Check: Aborting because ofinconsistent state and missing '--destroy-existing-guest' option." fi log warn "VM superfluous '.vbox' File Deletion: Removing file '$file_name_item' via '--import-only=${import_only}' and '--destroy-existing-guest' option." log_run warn rm -f -- "${file_name_item}" fi done } vm_delete_kicksecure() { if test "${vm_or_vms_already_existing_test_result}" = "true"; then log_run notice vboxmanage unregistervm "${guest_full_vm_name_kicksecure}" --delete else log notice "VM Deletion: Kicksecure VM does not exist, no need to delete, ok." fi } vm_delete_gateway() { if test "${gateway_exists}" = "1"; then log_run notice vboxmanage unregistervm "${guest_full_vm_name_gateway}" --delete else log notice "VM Deletion: Gateway VM does not exist, no need to delete, ok." fi } vm_delete_workstation() { if test "${workstation_exists}" = "1"; then log_run notice vboxmanage unregistervm "${guest_full_vm_name_workstation}" --delete else log notice "VM Deletion: Workstation VM does not exist, no need to delete, ok." fi } vm_delete_maybe() { case "${guest}" in whonix) if test "${destroy_existing_guest}" = "1"; then ## '--destroy-existing-guest' option is set. if test "${import_only}" = "gateway" ; then log warn "VM Deletion: 'yes' - Deleting previously imported gateway via '--import-only=gateway' option... (If it exists.)" vm_delete_gateway elif test "${import_only}" = "workstation" ; then log warn "VM Deletion: 'yes' - Deleting previously imported workstation via '--import-only=workstation' option... (If it exists.)" vm_delete_workstation elif test "${import_only}" = "both" ; then log warn "VM Deletion: 'yes' - Deleting previously imported gateway via '--import-only=both' option... (If it exists.)" vm_delete_gateway log warn "VM Deletion: 'yes' - Deleting previously imported workstation via '--import-only=both' option... (If it exists.)" vm_delete_workstation else die 1 "VM Deletion: 'no' - Not deleting previously any imported VMs because '--import-only' option is not set..." fi else ## '--destroy-existing-guest' option not yet. if test -n "${import_only}" ; then if test "${gateway_exists}" = "1" && test "${import_only}" = "gateway" ; then die 1 "${underline}Existing VM Check Result:${nounderline} '--import-only' option was set to 'gateway', but it already exists and '--destroy-existing-guest' option was not set." elif test "${workstation_exists}" = "1" && test "${import_only}" = "workstation" ; then die 1 "${underline}Existing VM Check Result:${nounderline} '--import-only' option was set to 'workstation', but it already exists and '--destroy-existing-guest' option was not set." fi else log info "Existing VM Check: Neither '--destroy-existing-guest' nor '--import-only' option was set, ok." fi fi ;; kicksecure) if test "${destroy_existing_guest}" = "1"; then ## If VMs exists and '--destroy-existing-guest' is set, remove VMs as they are gonna ## be imported later by main. log warn "VM Deletion: 'yes' - Deleting previously imported Virtual Machine(s) via '--destroy-existing-guest' option. (If it exists.)" vm_delete_kicksecure else log notice "VM Deletion: 'no' - Not deleting previously any imported VMs because neither using '--destroy-existing-guest' option." fi ;; esac } ## Check if guest should start or not check_guest_boot(){ if test "${virtualbox_only}" = "1"; then return 0 fi if [ "${hypervisor}" = "kvm" ]; then return 0 fi ## Refresh variables vm_or_vms_already_existing_test_result, gateway_exists and workstation_exists. check_vm_exists_general "(check after import)" case "${guest}" in whonix) if test "${gateway_exists}" = "1" ; then log notice "Available guest: '${guest_full_vm_name_gateway}'" fi if test "${workstation_exists}" = "1" ; then log notice "Available guest: '${guest_full_vm_name_workstation}'" fi ;; kicksecure) log notice "Available guest: '${guest_full_vm_name_kicksecure}'" ;; esac if [ "$kernel_module_has_been_load" = "false" ]; then ## Kernel modules have not been load. user_warned_potential_startup_issue=true log warn "Debugging information: Secure Boot Status: '$secure_boot_status' Kernel module has been load: '$kernel_module_has_been_load' Kernel module can be load: '$kernel_modules_can_be_load' (modules can be load. (sysctl kernel.modules_disabled=0 is set.) Only signed kernel modules can be load: '$modules_signed_only' Kernel module vboxdrv signer: '$kernel_module_signer' kernel_module_modprobe_output ($sucmd modprobe vboxdrv): '$kernel_module_modprobe_output'" if [ "$kernel_modules_can_be_load" = "true" ]; then log error "${underline}VirtualBox Installation Result:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor} - because kernel modules have not been load yet. - Kernel modules can be load in theory, should be loaded by now, but are not. - This is a VirtualBox installation issue." if ! test "${ci}" = "1"; then virtualbox_start_failed fi else log error "${underline}VirtualBox Installation Result:${nounderline} ${red}${bold}'REBOOT REQUIRED'${nobold}${nocolor} - because kernel module loading is disabled on your system: - The system administrator might have disabled module loading. - Module loading might have been disabled by Kicksecure package security-misc. - You can re-run this installer again after reboot." if ! test "${ci}" = "1"; then virtualbox_start_failed fi fi fi log notice "${underline}VirtualBox Installation Result:${nounderline} 'success'" log info "VM Start: Checking if user wants to start Virtual Machine(s) now..." if test "${qubes_template_detected}" = "true"; then log notice "${underline}VirtualBox Startup Check:${nounderline} Not starting VirtualBox because running inside a Qubes Template." ## Avoid needlessly starting VirtualBox inside a Qubes Template. ## That would not be useful, would create unnecessary files inside the Template's home folder and might be confusing. ## What instead might be useful for development purposes is to install VirtualBox inside a Qubes Template for the purpose ## of using a Qubes App Qube for building VirtualBox VM images. ## Run end_installer here, because no VMs were downloaded anyhow if a Qubes Template was detected. end_installer elif test "${no_boot}" = "1"; then log notice "${underline}VirtualBox Startup Check:${nounderline} User declined to start VirtualBox via '--no-boot' option." log notice "VirtualBox can be started manually." ## Not running end_installer here, because that happens below and we want the following output messages. #end_installer elif test "${non_interactive}" = "1"; then log notice "${underline}VirtualBox Startup Check:${nounderline} Starting VirtualBox automatically via '--non-interactive' option." if test "${virtualbox_only}" = "1"; then start_virtualbox_gui end_installer fi else if test "${virtualbox_only}" = "1"; then log notice "${bold}Question:${nobold} Do you want to start VirtualBox now? [y/n] (default: yes): " else log notice "${bold}Question:${nobold} Do you want to start VirtualBox and the ${guest_pretty} Virtual Machine(s) now? [y/n] (default: yes): " fi printf '%s' "Your answer: " local response read -r response log notice "User replied: '${response}'" case ${response} in ""|[Yy]|[Yy][Ee][Ss]) log notice "${underline}VirtualBox Startup Check:${nounderline} User accepted to start VirtualBox." if test "${virtualbox_only}" = "1"; then start_virtualbox_gui end_installer fi ;; *) log notice "${underline}VirtualBox Startup Check:${nounderline} User declined to start VirtualBox." end_installer ;; esac fi log info "Virtual Machine(s) already exist." if test "${redownload}" != "1"; then log notice "Hint: If you would like to redownload the image, read about '--redownload' option (safe)." fi if test "${destroy_existing_guest}" != "1"; then log notice "Hint: If you would like to delete a VM and re-import, read about '--destroy-existing-guest' option (danger)." fi if test "${no_boot}" = "1"; then ## Skip guest boot log notice "${underline}VM Startup Check:${nounderline} User declined to start Virtual Machine(s) via '--no-boot' option." log notice "Virtual Machine(s) can be started manually." end_installer fi if test "${non_interactive}" = "1"; then ## start guest without interaction log notice "${underline}VM Startup Check:${nounderline} VM start agreed by the user via '--non-interactive' setting." ## Try to start VMs before trying to start VirtualBox Manager, ## because if it fails, it is less confusing to avoid starting VirtualBox Manager. start_guest start_virtualbox_gui end_installer fi case ${response} in ""|[Yy]|[Yy][Ee][Ss]) log notice "${underline}VM Startup Check:${nounderline} User accepted to start Virtual Machine(s)." start_guest start_virtualbox_gui ;; *) log notice "${underline}VM Startup Check:${nounderline} User declined to start Virtual Machine(s)." log notice "The following Virtual Machine(s) can be started manually: '${guest_pretty}'" ;; esac end_installer } import_guest(){ if test "${virtualbox_only}" = "1"; then return 0 fi if test "${no_import}" = "1"; then log notice "VM Import: Not importing guest via '--import-only' option." end_installer fi case "${hypervisor}" in virtualbox) import_virtualbox ;; kvm) import_kvm ;; esac } extract_vm_name_from_virtualbox_ova() { local output="$1" local system_number="$2" local a b c d a=$(echo "$output" | grep --max-count=1 --after-context=4 "Virtual system $system_number:") b=$(echo "$a" | grep "Suggested VM name") c=$(echo "$b" | grep -oP '"\K[^"]+') ## Take first word only because VirtualBox would suggest name "Whonix-Gateway-Xfce 1" in case, ## a VM "Whonix-Gateway-Xfce" is already registered in VirtualBox. d=$(echo "$c" | awk '{print $1}') echo "$d" } ## Import VirtualBox images import_virtualbox(){ ## Check how many systems to import. ## vsys 0: gateway ## vsys 1: workstation case "${guest}" in whonix) ## if importing whonix, import 2 virtual systems vbox_arg_normal="--vsys 0 --eula accept --vsys 1 --eula accept" ;; kicksecure) vbox_arg_normal="--vsys 0 --eula accept" ;; esac vbox_arg_general="${vbox_arg_normal}" vm_purge="purgeme" case "${import_only}" in workstation) vbox_arg_importonly="--vsys 0 --eula accept --vmname ${vm_purge} --vsys 1 --eula accept" vbox_arg_general="${vbox_arg_importonly}" ;; gateway) vbox_arg_importonly="--vsys 0 --eula accept --vsys 1 --eula accept --vmname ${vm_purge}" vbox_arg_general="${vbox_arg_importonly}" ;; "") ;; esac local do_continue if test "${vm_or_vms_already_existing_test_result}" = "true" ; then if test -n "${import_only}"; then do_continue=yes log info "VM Import Check Result: Continue via '--import-only' option." else do_continue=no log info "VM Import Check Result: Skipping import because at least 1 VM already exists but '--import-only' option is not set." fi else do_continue=yes log info "VM Import Check Result: Continue because no VM(s) exist yet." fi if [ "$do_continue" = "no" ]; then log notice "VM Import Check Result: Skipping to import, because Virtual Machine(s) have been imported previously and not using '--import-only' option." return 0 fi check_vm_file_exists_virtualbox_general local output # TODO: vboxmanage localce needs to be C so output can be grepped. #lc_all="$LC_ALL" #export LC_ALL=C # shellcheck disable=SC2086 output=$(log_run notice vboxmanage import "${directory_prefix}/${guest_file}.${guest_file_ext}" ${vbox_arg_normal} --dry-run) || \ die 105 "${underline}VM Import:${nounderline} Failed to import virtual machines." log info "OUTPUT: $output" #export LC_ALL="$lc_all" case "${guest}" in whonix) vsys_0_actual=$(extract_vm_name_from_virtualbox_ova "$output" 0) || true vsys_1_actual=$(extract_vm_name_from_virtualbox_ova "$output" 1) || true if [ ! "${guest_full_vm_name_gateway}" = "${vsys_0_actual}" ]; then die 105 "${underline}VM Import:${nounderline} BUG: vsys0 is actually '${vsys_0_actual}', not '${guest_full_vm_name_gateway}'." fi if [ ! "${guest_full_vm_name_workstation}" = "$vsys_1_actual" ]; then die 105 "${underline}VM Import:${nounderline} BUG: vsys1 is actually '${vsys_1_actual}', not '${guest_full_vm_name_workstation}'." fi log info "vsys_0_actual: ${vsys_0_actual}" log info "vsys_1_actual: ${vsys_1_actual}" ;; kicksecure) vsys_0_actual=$(extract_vm_name_from_virtualbox_ova "$output" 0) || true if [ ! "${guest_full_vm_name_kicksecure}" = "${vsys_0_actual}" ]; then die 105 "${underline}VM Import:${nounderline} BUG: vsys0 is actually '${vsys_0_actual}', not '${guest_full_vm_name_kicksecure}'." fi log info "vsys_0_actual: ${vsys_0_actual}" ;; esac log notice "VM Import: Importing Virtual Machine(s)..." ## import VirtualBox image # shellcheck disable=SC2086 log_run notice vboxmanage import "${directory_prefix}/${guest_file}.${guest_file_ext}" ${vbox_arg_general} || \ die 105 "${underline}VM Import:${nounderline} Failed to import virtual machines." ## VirtualBox does not accept any command to import a single virtual system ## out from an ova with multiple ones. ## https://forums.virtualbox.org/viewtopic.php?f=1&t=107965 if test -n "${import_only}" && test "${import_only}" != "both"; then log_run notice vboxmanage unregistervm "${vm_purge}" --delete || die 1 "${underline}VM Import:${nounderline} Failed to remove extraneous VM '${vm_purge}'." fi log notice "VM Import: 'success'" log notice "You can now open the VirtualBox application and start using: '${guest_pretty}'" } ## Import KVM images import_kvm(){ ## placeholder log notice "KVM import feature does not exist. Ending run." exit 0 } start_guest(){ if test "${virtualbox_only}" = "1"; then return 0 fi case "${hypervisor}" in virtualbox) start_virtualbox_vm ;; kvm) start_kvm_vm ;; esac } ## Start the hypervisor with the desired guest start_virtualbox_vm() { log notice "Virtual Machine Startup: Starting Virtual Machine(s)..." ## 'virtualbox' is called after 'vboxmanage' because it hangs till the app ## is closed, while 'vboxmanage startvm' exits after starting the VMs. case "${guest}" in whonix) if test "${gateway_exists}" = "1" ; then log_run notice vboxmanage startvm "${guest_full_vm_name_gateway}" || virtualbox_start_failed fi if test "${workstation_exists}" = "1" ; then log_run notice vboxmanage startvm "${guest_full_vm_name_workstation}" || virtualbox_start_failed fi ;; kicksecure) log_run notice vboxmanage startvm "${guest_full_vm_name_kicksecure}" || virtualbox_start_failed ;; esac log notice "${underline}Virtual Machine Startup Result:${nounderline} 'success'" } start_virtualbox_gui() { if pgrep 'VirtualBox$' >/dev/null ; then log notice "VirtualBox Manager Startup Result: Not starting duplicate VirtualBox GUI because already running." return 0 fi run_background=1 log_run notice virtualbox if ! test "${dry_run}" = "1"; then log notice "VirtualBox Manager Startup Result: Launched VirtualBox GUI into the background. (pid: '$background_pid')" fi run_background="" } virtualbox_start_failed() { local likely_cause likely_cause="" if [ "$user_warned_potential_startup_issue" = "true" ]; then likely_cause="- This is likely happening due to the warnings that have been reported above." fi die 106 "${underline}Virtual Machine Startup Result:${nounderline} ${red}${bold}FAIL${nobold}${nocolor} $likely_cause - The installer succeeded with download and import, but - failed to start the virtual machines (VMs). - The root cause for this issue is likely not the installer. - This issue would likely also happen if the user tried to manually start the VMs. - Resolving these issues (as per documentation hyperlinks above) would likely resolve this issue. ${url_version_domain}/wiki/VirtualBox/Troubleshooting" } ## Detect if repository is configured in the sources list. get_pattern_sources_debian(){ file="${1}" pattern="${2}" grep -v "#" "${file}" | grep -q -E "${pattern}" || return 1 } write_sources_debian(){ url="${1}" file="${2}" echo "${url}" | root_cmd tee "${file}" || die 1 "${underline}Sources List Writer:${nounderline} Failed to write to file: '${file}'" } ## https://stackoverflow.com/a/54239534 ## "apt list --installed $pkg" does not fail if package is not installed. check_installed_debian(){ status="$(dpkg-query --show --showformat='${db:Status-Status}' "$1" 2>&1)" if test "$?" != 0 || test "$status" != "installed"; then return 1 fi return 0 } install_package_fedora_common(){ pkg_mngr="dnf" pkg_mngr_install="${pkg_mngr} install --assumeyes --setopt=install_weak_deps=False" ## Fedora lacks an equivalent to Debian's '--error-on=any'. #pkg_mngr_update="${pkg_mngr} update --assumeyes" ## 'dnf makecache' does exit non-zero if a repository is unavailable. ## --assumeyes good or bad idea? For now not needed. So not using. #pkg_mngr_update="${pkg_mngr} makecache" pkg_mngr_update="${pkg_mngr} --assumeyes update" pkg_mngr_check_installed="${pkg_mngr} list installed" pkg_mngr_upgrade_check="/bin/true 'dnf does not seem to support an alternative to apt-get full-upgrade --simulate, skipping.'" pkg_mngr_upgradable_check="/bin/true 'dnf does not seem to support an alternative to apt-get full-upgrade --simulate, skipping.'" pkg_mngr_upgrade_install="${pkg_mngr} upgrade" update_sources #check_upgrades_simulation if test "${virtualbox_only}" = "1"; then return 0 fi ## TODO: remove dnf-utils when removing Fedora 38 support from CI. install_pkg torsocks redhat-lsb-core dnf-plugins-core dnf-utils install_signify signify } install_package_debian_common(){ pkg_mngr="apt-get" pkg_mngr_install="${pkg_mngr} install --yes --no-install-recommends" pkg_mngr_update="${pkg_mngr} update --yes --error-on=any" pkg_mngr_check_installed="check_installed_debian" pkg_mngr_upgrade_check="${pkg_mngr} full-upgrade --simulate" pkg_mngr_upgradable_check="apt list --upgradable" pkg_mngr_upgrade_install="${pkg_mngr} full-upgrade" ## 'dpkg --audit' does not return non-zero exit code on failure. dpkg_audit_output="$(dpkg --audit 2>&1)" if test -n "${dpkg_audit_output}"; then die 1 "${underline}DPKG Audit Test Result:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor} - What happened in simple terms? This installer has performed a check to ensure it's sane to proceed, which has failed. - What exactly happened? The command 'dpkg --audit' generated output, signaling a system issue. - What was the expected behavior? The 'dpkg --audit' command should execute without producing any output. - Is this installer to blame for the issue? It's unlikely that this installer is the root cause of the issue. - So, what might have caused it? The issue likely originates from pre-existing DPKG / APT package management system issues on your machine. - What steps should you take now? Start by running the following command yourself: dpkg --audit - What should you do after that? Analyze the output of the command to identify and address the underlying problem. Once resolved, you can re-run this installer. - Do you have any suggestions? The following link might offer helpful insights: https://www.kicksecure.com/wiki/Operating_System_Software_and_Updates#Broken_APT If that doesn't resolve the issue, consider reaching out to your operating system's support team for assistance." fi ## Dumping all sources.list files could have privacy implications. ## Hence only doing it for '--dev'. if test "${dev}" = "1"; then if [ -f /etc/apt/sources.list ]; then cat /etc/apt/sources.list fi for file in /etc/apt/sources.list.d/*; do if [ -f "$file" ]; then cat "$file" fi done fi update_sources check_upgrades_simulation install_pkg lsb-release if test "${virtualbox_only}" = "1"; then return 0 fi install_pkg torsocks install_signify signify-openbsd } add_user_to_vbox_group(){ id_of_user="$(id --name --user)" || die 1 "${underline}Linux user ID check:${nounderline} Failed to run: 'id --name --user'" if id -nG "${id_of_user}" | grep -qw "${virtualbox_linux_user_group}\$"; then log info "Linux Group Configuration: Account '${id_of_user}' is already a member of the Linux group 'vboxusers'." return 0 fi if test "${debian_derivative_detected}" = "1"; then root_cmd adduser "${id_of_user}" "${virtualbox_linux_user_group}" || \ die 1 "${underline}Linux Group Configuration: adduser to Linux user group virtualbox:${nounderline} Failed to add user '$id_of_user' to group '${virtualbox_linux_user_group}'." else root_cmd usermod --append --groups "${virtualbox_linux_user_group}" "${id_of_user}" || \ die 1 "${underline}Linux Group Configuration: adduser to Linux user group virtualbox:${nounderline} Failed to add user '$id_of_user' to group '${virtualbox_linux_user_group}'." fi } ## End installation of VirtualBox on Fedora and derived systems. install_virtualbox_fedora_common_end(){ if ! has vboxmanage ; then if test "${dry_run}" = "1"; then log error "Failed to locate 'vboxmanage' program. Ignoring via '--dry-run' option." else die 1 "${underline}vboxmanage test:${nounderline} Failed to locate 'vboxmanage' program." fi fi add_user_to_vbox_group } ## End installation of VirtualBox on Debian and derived systems. install_virtualbox_debian_common_end(){ if ! has vboxmanage ; then if test "${dry_run}" = "1"; then log error "Failed to locate 'vboxmanage' program. - Ignoring via '--dry-run' option." else die 1 "${underline}vboxmanage test:${nounderline} Failed to locate 'vboxmanage' program." fi fi add_user_to_vbox_group } kernel_modules_check() { if ! [ "${hypervisor}" = "virtualbox" ]; then return 0 fi if ! test -e /dev/vboxdrv ; then kernel_module_has_been_load=false user_warned_potential_startup_issue=true log warn "${underline}VirtualBox Kernel Module Loaded Test Result${nounderline}: 'no' - file /dev/vboxdrv does not exist. This probably means that the vboxdrv kernel module has not been load yet." else kernel_module_has_been_load=true log info "VirtualBox Kernel Module Loaded Test Result: 'yes' - file /dev/vboxdrv exists." fi ## Debugging. log info "Command executing: $ $sucmd -- modinfo --field signer vboxdrv" if kernel_module_signer=$(root_cmd_loglevel=null root_cmd modinfo --field signer vboxdrv 2>&1) ; then log info "VirtualBox Kernel Module Signer: '$kernel_module_signer'" else user_warned_potential_startup_issue=true log warn "${underline}VirtualBox Kernel Module Signer${nounderline}: '$kernel_module_signer'" fi ## Debugging. ## modprobe should be automatic but if it fails, figure out why. log notice "Command executing: $ $sucmd -- modprobe vboxdrv" if kernel_module_modprobe_output=$(root_cmd_loglevel=null root_cmd modprobe vboxdrv 2>&1) ; then log info "VirtualBox Kernel Module modprobe output: '$kernel_module_modprobe_output'" else user_warned_potential_startup_issue=true log warn "${underline}VirtualBox Kernel modprobe output${nounderline}: '$kernel_module_modprobe_output'" fi } install_repositories_for_virtualbox_on_debian(){ distro_codename_real=$(lsb_release --short --codename) distro_codename_common_use="${distro_codename_real}" # log info "VirtualBox Package Availability Test: Checking if package 'virtualbox-qt' is already installable from an already enabled repository..." # if ${pkg_mngr_install} --simulate install virtualbox-qt 1>/dev/null 2>/dev/null ; then # ## Package 'virtualbox' is installable on Debian unstable ("sid"). # log notice "VirtualBox Package Availability Test Result: 'success' - Package 'virtualbox-qt' is already installable from an already enabled repository. No need to add any extra repositories." # return 0 # fi # log info "VirtualBox Package Availability Test Result: Not yet Available. Enabling additional repository..." if test "${dev}" = "1"; then distro_codename_kicksecure_use="${distro_codename_common_use}-developers" else distro_codename_kicksecure_use="${distro_codename_common_use}" fi oracle_found="" oracle_clearnet="download.virtualbox.org" #oracle_onion="" oracle_file_debsource="/etc/apt/sources.list.d/oracle.list" oracle_prefix_debsource="deb [signed-by=/usr/share/keyrings/oracle-virtualbox-2016.asc] " oracle_suffix_debsource="/virtualbox/debian ${distro_codename_common_use} contrib" unstable_found="" unstable_clearnet="deb.debian.org" unstable_onion="2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion" unstable_file_debsource="/etc/apt/sources.list.d/unstable.list" ## CI /etc/apt/sources.list.d/debian.sources uses: ## Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg ## So we have to use signed-by here too, otherwise we get an apt-get error: ## E: Conflicting values set for option Signed-By regarding source unstable_prefix_debsource="deb [signed-by=/usr/share/keyrings/debian-archive-keyring.gpg] " unstable_suffix_debsource="/debian/ unstable main contrib non-free" backports_found="" backports_clearnet="deb.debian.org" backports_onion="2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion" backports_file_debsource="/etc/apt/sources.list.d/backports.list" backports_prefix_debsource="deb " backports_suffix_debsource="/debian/ ${distro_codename_common_use}-backports main contrib non-free" fasttrack_found="" fasttrack_clearnet="fasttrack.debian.net" fasttrack_onion="5phjdr2nmprmhdhw4fdqfxvpvt363jyoeppewju2oqllec7ymnolieyd.onion" fasttrack_file_debsource="/etc/apt/sources.list.d/fasttrack.list" fasttrack_prefix_debsource="deb " fasttrack_suffix_debsource="/debian/ ${distro_codename_common_use}-fasttrack main contrib non-free" kicksecure_found="" kicksecure_clearnet="deb.kicksecure.com" kicksecure_onion="deb.w5j6stm77zs6652pgsij4awcjeel3eco7kvipheu6mtr623eyyehj4yd.onion" kicksecure_file_debsource="/etc/apt/sources.list.d/derivative.list" kicksecure_prefix_debsource="deb [signed-by=/usr/share/keyrings/derivative.asc] " kicksecure_suffix_debsource=" ${distro_codename_kicksecure_use} main contrib non-free" apt_torified="" apt_onion="" ## TODO: ## - handle case if using Debian mirrors ## - add support for Deb822-style Format parsing ## - write sources.list files in one-line-style format or Deb822-style format? ## https://forums.kicksecure.com/t/apt-sources-list-one-line-format-versus-deb822-style-format/267 ## - unit tests for file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list; do test -f "${file}" || continue if get_pattern_sources_debian "${file}" "${oracle_clearnet}" then oracle_found=1 fi if get_pattern_sources_debian "${file}" "${kicksecure_clearnet}|${kicksecure_onion}" then kicksecure_found=1 fi if get_pattern_sources_debian "${file}" "(${unstable_clearnet}|${unstable_onion})/debian.* (unstable|sid)" then unstable_found=1 fi ## Need to check if not only unstable repository or sid codename is enabled but also check if the 'contrib' suite is enabled, ## because VirtualBox is only available from 'contrib'. if get_pattern_sources_debian "${file}" "(${unstable_clearnet}|${unstable_onion})/debian.* (unstable|sid).*contrib" then unstable_found_with_contrib=1 fi if get_pattern_sources_debian "${file}" "(${backports_clearnet}|${backports_onion})/debian/? ${distro_codename_common_use}-backports" then backports_found=1 fi if get_pattern_sources_debian "${file}" "${fasttrack_clearnet}|${fasttrack_onion}" then fasttrack_found=1 fi if get_pattern_sources_debian "${file}" "tor://|tor\+"; then apt_torified=1 fi if get_pattern_sources_debian "${file}" "\.onion"; then apt_onion=1 fi done if test "${apt_torified}" = "1"; then ## If apt-transport-tor is not installed, we shouldn't, because we got ## a false positive that updates should be torified. This can happen if ## user configured the sources list to be torified but hasn't installed ## apt-transport-tor. It is better to abort, the user should configure ## tor by other means to ensure a working connection (bridges, proxy etc). log info "APT is supposed to be torified, checking if package apt-transport-tor is installed." test_pkg apt-transport-tor || die 1 "${underline}Torified APT Test Result:${nounderline} APT is supposed to be torified, but package apt-transport-tor is not installed. Please install." fi ## If user has onion repositories configured, prefer it. if test "${apt_onion}"; then protocol_prefix_debsource="tor+http://" connection_type_debsource="onion" if test "${oracle_repo}" = "1"; then log warn "Oracle doesn't provide onion repositories." oracle_domain_debsource="${oracle_clearnet}" if test "${apt_torified}"; then log warn "Fallback Oracle repository to torified clearnet" protocol_prefix_debsource="tor+https://" else log warn "Fallback Oracle repository to clearnet" protocol_prefix_debsource="https://" fi fi kicksecure_domain_debsource="${kicksecure_onion}" unstable_domain_debsource="${unstable_onion}" fasttrack_domain_debsource="${fasttrack_onion}" backports_domain_debsource="${backports_onion}" ## If user has torified repositories configured, prefer it. elif test "${apt_torified}"; then protocol_prefix_debsource="tor+https://" connection_type_debsource="torified clearnet" oracle_domain_debsource="${oracle_clearnet}" kicksecure_domain_debsource="${kicksecure_clearnet}" unstable_domain_debsource="${unstable_clearnet}" fasttrack_domain_debsource="${fasttrack_clearnet}" backports_domain_debsource="${backports_clearnet}" ## If user doesn't have torified repositories, use clearnet one. else protocol_prefix_debsource="https://" connection_type_debsource="clearnet" oracle_domain_debsource="${oracle_clearnet}" kicksecure_domain_debsource="${kicksecure_clearnet}" unstable_domain_debsource="${unstable_clearnet}" fasttrack_domain_debsource="${fasttrack_clearnet}" backports_domain_debsource="${backports_clearnet}" fi kicksecure_url="${kicksecure_prefix_debsource} ${protocol_prefix_debsource}${kicksecure_domain_debsource}${kicksecure_suffix_debsource}" unstable_url="${unstable_prefix_debsource} ${protocol_prefix_debsource}${unstable_domain_debsource}${unstable_suffix_debsource}" fasttrack_url="${fasttrack_prefix_debsource} ${protocol_prefix_debsource}${fasttrack_domain_debsource}${fasttrack_suffix_debsource}" backports_url="${backports_prefix_debsource} ${protocol_prefix_debsource}${backports_domain_debsource}${backports_suffix_debsource}" if test "${oracle_repo}" = "1"; then oracle_url="${oracle_prefix_debsource} ${protocol_prefix_debsource}${oracle_domain_debsource}${oracle_suffix_debsource}" install_oracle_repository_debian return 0 fi log info "Installing packages required for 'backports' and 'fasttrack' repository..." install_pkg ca-certificates fasttrack-archive-keyring case "${distro_codename_real}" in bullseye) install_backports_and_fasttrack_repository_debian return 0 ;; bookworm) install_backports_and_fasttrack_repository_debian return 0 ;; trixie) install_unstable_repository_debian return 0 ;; esac die 1 "${underline}Repository Add:${nounderline} Unsupported distribution codename: '${distro_codename_real}'!" } install_oracle_repository_fedora(){ oracle_found="" if dnf repolist --all virtualbox | grep -q "."; then oracle_found="1" fi if dnf repolist --disabled virtualbox | grep -q "."; then dnf config-manager --set-enabled virtualbox fi if test "${oracle_found}" = "1"; then log info "Oracle Repository: Skipped adding Oracle repository because it was already found." else log notice "Oracle Repository: Adding Oracle's clearnet repository to /etc/yum.repos.d/oracle.repo" if test -f /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle; then log info "Oracle Repository: Key /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle already exists." else echo "${oracle_pgp}" | \ tee "${log_dir_cur}/oracle-virtualbox-2016.asc" >/dev/null echo "${oracle_pgp}" | \ root_cmd tee /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle >/dev/null ## Optional: the key will be imported when trying to use the repository root_cmd rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-oracle fi ## Based on: ## https://download.virtualbox.org/virtualbox/rpm/fedora/virtualbox.repo # shellcheck disable=SC2016 echo '[virtualbox] name=Fedora $releasever - $basearch - VirtualBox baseurl=https://download.virtualbox.org/virtualbox/rpm/fedora/$releasever/$basearch enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-oracle ' | root_cmd tee /etc/yum.repos.d/oracle.repo ## dnf does not have a command to accept add the key to its database. ## https://bugzilla.redhat.com/show_bug.cgi?id=1768206 ## Workaround to accept the key with --assumeyes without accepting other things. ## Does not work. #root_cmd dnf --assumeyes --cacheonly search VirtualBox #root_cmd dnf --assumeyes module list fi } install_oracle_repository_debian(){ if test "${oracle_found}" = "1"; then log info "Oracle Repository: Skipped adding Oracle repository because it was already found." else log notice "Oracle Repository: Adding Oracle's ${connection_type_debsource} repository to ${oracle_file_debsource}" if test -f /usr/share/keyrings/oracle.asc ; then log info "Oracle Repository: Key /usr/share/keyrings/oracle.asc already exists." else echo "${oracle_pgp}" | \ tee "${log_dir_cur}/oracle-virtualbox-2016.asc" >/dev/null echo "${oracle_pgp}" | \ root_cmd tee /usr/share/keyrings/oracle-virtualbox-2016.asc >/dev/null fi write_sources_debian "${oracle_url}" "${oracle_file_debsource}" fi } install_kicksecure_repository_debian() { ## Contains file: ## /usr/share/extrepo/offline-data/debian/bullseye/whonix.asc ## Same as: kicksecure.asc ## (Which is not yet in the extrepo-offline-data package as of Debian 12.0.) install_pkg extrepo-offline-data ## Not using extrepo directly because it does not support torified and/or onion repositories: ## https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1037254 if test -f /usr/share/keyrings/derivative.asc ; then log info "Kicksecure Repository: Key /usr/share/keyrings/derivative.asc already exists." log info "Kicksecure Repository: Not copying /usr/share/extrepo/offline-data/debian/bullseye/whonix.asc to /usr/share/keyrings/derivative.asc." else root_cmd cp --verbose /usr/share/extrepo/offline-data/debian/bullseye/whonix.asc /usr/share/keyrings/derivative.asc fi if test "${kicksecure_found}" = "1"; then log info "Kicksecure Repository: Skipped adding Kicksecure repository because it was already found." else log notice "Kicksecure Repository: Adding Kicksecure's ${connection_type_debsource} repository to ${kicksecure_file_debsource}" write_sources_debian "${kicksecure_url}" "${kicksecure_file_debsource}" fi } install_unstable_repository_debian() { if test "${unstable_found}" = "1" && test "${unstable_found_with_contrib}" = "1"; then log info "APT Repository Configuration: Skipped adding additional APT repositories because 'unstable' (with 'contrib') were already found." return 0 fi log notice "APT Preferences Configuration Writer: Adding APT pinning configuration to prefer testing over unstable." file="/etc/apt/preferences.d/40-installer-dist-pinning" echo "\ ## This file was created by: $0 ## It can be safely deleted if you know what you are doing. Package: * Pin: release a=testing Pin-Priority: 700 Package: * Pin: release a=unstable Pin-Priority: 650 " | root_cmd tee "$file" || die 1 "${underline}APT Preferences Configuration Writer:${nounderline} Failed to write to file: '$file'" log notice "APT Repository Configuration: Adding 'unstable' ${connection_type_debsource} repository to ${unstable_file_debsource}" write_sources_debian "${unstable_url}" "${unstable_file_debsource}" } install_backports_and_fasttrack_repository_debian() { if test "${backports_found}" = "1"; then log info "APT Repository Configuration: Skipped adding 'backports' repository because it was already found." else log notice "APT Repository Configuration: Adding 'backports' ${connection_type_debsource} repository to ${backports_file_debsource}" write_sources_debian "${backports_url}" "${backports_file_debsource}" fi if test "${fasttrack_found}" = "1"; then log info "APT Repository Configuration: Skipped adding 'fasttrack' repository because it was already found." else log notice "APT Repository Configuration: Adding 'fasttrack' ${connection_type_debsource} repository to ${fasttrack_file_debsource}" write_sources_debian "${fasttrack_url}" "${fasttrack_file_debsource}" fi ## virtualbox-guest-additions-iso is available from Debian stable and fasttrack. ## Debian stable version however is outdated. ## If not using APT with option '--target-release=bookworm-fasttrack' then ## APT would download an outdated version from Debian stable instead the ## newer version from Debian fasttrack. install_pkg_fasttrack_extra_args_maybe="--target-release=${distro_codename_common_use}-fasttrack" } virtualbox_installation_failure_debug() { virtualbox_dkms_main_folder=/var/lib/dkms/virtualbox if ! test -d "$virtualbox_dkms_main_folder" ; then log notice "VirtualBox Installation Debug: folder '$virtualbox_dkms_main_folder' does not exist." return 0 fi ## TODO: review # shellcheck disable=SC2012 log_run notice ls --format=single-column --sort=time "$virtualbox_dkms_main_folder" # shellcheck disable=SC2012 latest_folder=$(ls --format=single-column --sort=time "$virtualbox_dkms_main_folder" | head --lines=1) if [ "$latest_folder" = "" ]; then log notice "VirtualBox Installation Debug: latest_folder in '$virtualbox_dkms_main_folder' is empty, could not be determined." log_run notice ls -la "$virtualbox_dkms_main_folder" || true return 0 fi log notice "VirtualBox Installation Debug: latest_folder: '$latest_folder'" virtualbox_dkms_latest_folder="${virtualbox_dkms_main_folder}/${latest_folder}" if ! test -d "$virtualbox_dkms_latest_folder" ; then log notice "VirtualBox Installation Debug: virtualbox_dkms_latest_folder '$virtualbox_dkms_latest_folder' is not a directory." log_run notice ls -la "$virtualbox_dkms_latest_folder" || true return 0 fi make_log="${virtualbox_dkms_latest_folder}/build/make.log" if ! test -f "$make_log" ; then log notice "VirtualBox Installation Debug: make_log '$make_log' does not exist." log_run notice ls -la "$virtualbox_dkms_latest_folder" || true return 0 fi if ! test -r "$make_log" ; then log notice "VirtualBox Installation Debug: make_log '$make_log' not readable." log_run notice ls -la "$virtualbox_dkms_latest_folder" || true log_run notice ls -la "$make_log" || true return 0 fi log_run notice cat "$make_log" || true } ## Install VirtualBox on Fedora install_virtualbox_fedora(){ install_pkg kernel-headers kernel-devel dkms get_virtualbox_version_fedora(){ ## Too many issues with auto detection. Therefore hardcoded. virtualbox_qt_package_name="VirtualBox-7.0" return 0 # log info "VirtualBox Version Detection: Running 'dnf search VirtualBox-*'. This can take a while..." # ## --cacheonly does not work. # ## Maybe dnf would need to be run with root_cmd. # virtualbox_version=$(dnf search "VirtualBox-*") # virtualbox_version=$(echo "$virtualbox_version" | grep "^VirtualBox-[[:digit:]]\{1,2\}\.[[:digit:]]\{1,2\}\.") # virtualbox_version=$(echo "$virtualbox_version" | tail -1) # virtualbox_version=$(echo "$virtualbox_version" | cut -d " " -f1) # virtualbox_version=$(echo "$virtualbox_version" | cut -d "-" -f2-) # virtualbox_version=$(echo "$virtualbox_version" | cut -d "." -f1) # virtualbox_qt_package_name=VirtualBox-"${virtualbox_version}" # if test -z "$virtualbox_version"; then # ## return non-zero late to avoid unbound variable virtualbox_qt_package_name. # return 1 # fi } has_virtualbox_qt=0 if get_virtualbox_version_fedora ; then test_pkg "${virtualbox_qt_package_name}" 2>/dev/null && has_virtualbox_qt=1 else true "INFO: get_virtualbox_version_fedora failed the first time which is normal is the repository has not been enabled yet." fi ## Guard against adding extraneous repositories. if test "${has_virtualbox_qt}" != "1"; then log notice "VirtualBox Installation: Preparing to install VirtualBox..." install_oracle_repository_fedora ## Does not work. #root_cmd dnf makecache --assumeyes '--disablerepo=*' --enablerepo=virtualbox update_sources #check_upgrades_simulation get_virtualbox_version_fedora || die 2 "\ ${underline}VirtualBox Package Version Detection:${nounderline} 'FAIL'" fi ## This is to make sure loglevel info message is shown: ## Package already installed: $virtualbox_qt_package_name install_pkg "$virtualbox_qt_package_name" install_virtualbox_fedora_common_end } ## Install VirtualBox on Debian ## See also comments for install_virtualbox_fedora. install_virtualbox_debian(){ get_virtualbox_version_debian(){ if test "${oracle_repo}" = "1"; then virtualbox_version=$(apt-cache search --names-only --quiet "^virtualbox-[[:digit:]]{1,2}\.[[:digit:]]{1,2}$") virtualbox_version=$(echo "$virtualbox_version" | tail -1) virtualbox_version=$(echo "$virtualbox_version" | cut -d " " -f1) virtualbox_version=$(echo "$virtualbox_version" | cut -d "-" -f2-) virtualbox_qt_package_name=virtualbox-"${virtualbox_version}" ## Package virtualbox-guest-additions-iso is unavailable from Oracle repository. virtualbox_guest_additions_iso_package_name="" if test -z "$virtualbox_version"; then ## return non-zero late to avoid unbound variable virtualbox_qt_package_name. return 1 fi else virtualbox_qt_package_name=virtualbox-qt ## virtualbox-guest-additions-iso is Freedom Software: ## https://www.kicksecure.com/wiki/Dev/VirtualBox#VirtualBox_Guest_Additions_ISO_Freedom_vs_Non-Freedom ## Provides file: /usr/share/virtualbox/VBoxGuestAdditions.iso ## Useful to easily install VirtualBox guest additions in custom VirtualBox VMs. virtualbox_guest_additions_iso_package_name="virtualbox-guest-additions-iso" fi } if test "${oracle_repo}" = "1"; then ## Workaround for undeclared dependencies bug by virtualbox.org (Oracle) repository. ## ## udev: ## https://www.virtualbox.org/ticket/21804 ## ## gcc: ## virtualbox-"${virtualbox_version}" by virtualbox.org (Oracle) `Recommends:` `gcc` ## Otherwise kernel modules will not be compiled during installation. install_pkg gcc udev fi linux_image="linux-image-$(dpkg --print-architecture)" linux_headers="linux-headers-$(dpkg --print-architecture)" ## Doing here and now below because we $install_pkg_fasttrack_extra_args_maybe should not be used here. install_pkg "${linux_image}" "${linux_headers}" log notice "VirtualBox Installation: Preparing to install VirtualBox..." install_repositories_for_virtualbox_on_debian update_sources check_upgrades_simulation get_virtualbox_version_debian || die 2 "${underline}VirtualBox Package Version Detection:${nounderline} 'FAIL'" ## Cannot quote $virtualbox_guest_additions_iso_package_name because is unset in case of using oracle_repo. # shellcheck disable=SC2086 install_pkg $install_pkg_fasttrack_extra_args_maybe "$virtualbox_qt_package_name" $virtualbox_guest_additions_iso_package_name install_virtualbox_debian_common_end } ## Install VirtualBox on Ubuntu install_virtualbox_ubuntu(){ install_pkg virtualbox-qt virtualbox-guest-additions-iso linux-image-generic linux-headers-generic install_virtualbox_debian_common_end } ## Helper to install signify on different systems. install_signify(){ pkg_name="${1:-signify}" has "${pkg_name}" && return 0 install_pkg "${pkg_name}" } ## Test if user accepts the license, if not, abort. check_license(){ if [ "${non_interactive}" = "1" ]; then log notice "License Check: 'success' - User agreement confirmed via '--non-interactive' option." return 0 fi log notice "The license will be shown in 5 seconds." log notice "(Use '-n' or '--non-interactive' for non-interactive mode.)" test "${dry_run}" != "1" && sleep 5 local dialog_box dialog_box="" ## 'whiptail' is the Debian version of Dialog with much less features. ## Dialog types problems: ## - whiptail does not allow to set default option with scrolltext on. ## - dialog leaves empty lines on exit. while true; do has dialog && dialog_box="dialog" && break has whiptail && dialog_box="whiptail" && break break done case "${dialog_box}" in dialog) ## output-fd to stdout because currently 'main 2>&1 | tee file' if used ## and makes the dialog fail to recognize the characters as it is ## receiving from stderr and writing to stdout. dialog --erase-on-exit --no-shadow \ --title "${dialog_title}" \ --yes-label "Agree" \ --no-label "Disagree" \ --output-fd 1 \ --yesno "${license}" 640 480 || return 1 log notice "${underline}License Check:${nounderline} 'success' - User has accepted the license." ;; whiptail) ## When text is too long and scrolltext is needed, the yesno box ## does not display a default item. (note: --default-item is for items ## in the box to be selected as menu for example, not for buttons). whiptail \ --scrolltext \ --title "${dialog_title}" \ --yes-button "Agree" \ --no-button "Disagree" \ --yesno "${license}" 24 80 || return 1 log notice "${underline}License Check:${nounderline} 'success' - User has accepted the license." ;; *) printf '%s\n' "${license}" printf '%s' "Do you accept the license(s)? (yes/no): " read -r license_agreement case "${license_agreement}" in [yY][eE][sS]) log notice "${underline}License Check:${nounderline} 'success' - User has accepted the license." ;; *) log warn "${underline}License Check:${nounderline} User replied: '${license_agreement}'" return 1 ;; esac ;; esac } get_su_cmd(){ while true; do has sudo && sucmd=sudo && break has doas && sucmd=doas && break has su && sucmd=su && break test -z "${sucmd}" && { die 1 "${underline}get_su_cmd:${nounderline} Failed to find program to run commands with administrative (\"root\") privileges. This installer requires either one of the following programs to be installed: - sudo (recommended) - doas - su" } case "${sucmd}" in sudo) :;; *) log warn "Using sucmd '$sucmd'. Consider installation of sudo instead to cache your passwords instead of typing them every time.";; esac done log info "Testing root_cmd function..." root_cmd echo "test" || die 1 "${underline}get_su_cmd:${nounderline} Failed to run test command as root." if test "${ci}" = "1"; then root_output="$(timeout --kill-after 5 5 sudo --non-interactive -- test -d /usr 2>&1)" if test -n "${root_output}"; then log error "sudo output: '${root_output}'" die 1 "${underline}get_su_cmd:${nounderline} Unexpected non-empty output for sudo test in CI mode." fi return 0 fi ## Other su cmds do not have an option that does the same. if test "${sucmd}" = "sudo"; then if ! timeout --kill-after 5 5 sudo --non-interactive -- test -d /usr; then log warn "Credential Caching Status: 'No' - Without credential caching, this installer will prompt for sudo authorization multiple times. Consider configuring sudo credential caching to streamline the installation process." return 0 fi ## Used by dist-installer-gui. # shellcheck disable=SC2034 credential_caching_status=yes log info "Credential Caching Status: 'Yes'" root_output="$(timeout --kill-after 5 5 sudo -- test -d /usr 2>&1)" if test -n "${root_output}"; then log error "sudo output: '${root_output}'" die 1 "${underline}get_su_cmd:${nounderline} Unexpected non-empty output for sudo test in normal mode." fi fi } get_checkhash_cmd(){ if test "${virtualbox_only}" = "1"; then return 0 fi while true; do has sha512sum && checkhash="sha512sum --check --strict --warn" && break has shasum && checkhash="shasum --algorithm 512 --check --strict --warn" && break ## TODO: Does it have an equivalent to --check? Compatible file format? #has sha512 && checkhash="sha512 -c" && break #has digest && checkhash="digest -a sha512 -c" && break ## TODO: How to make openssl check the file without workarounds? #has openssl && checkhash="openssl dgst -sha512 -r" && break test -z "${checkhash}" && { die 1 "${underline}get_checkhash_cmd:${nounderline} Failed to find program that checks SHA512 hash sum." } done } get_transfer_cmd(){ ## curl|rsync transfer_utility=rsync case "${transfer_utility}" in rsync) rsync=1 ;; curl) curl=1 ;; esac ## 45m transfer_max_time_large_file="2700" ## 3m transfer_max_time_small_file="180" ## 10m transfer_io_timeout="600" ## 3m transfer_connect_timeout="180" transfer_size_test_connection="200K" transfer_size_small_file="2K" transfer_size_large_file="3G" case ${transfer_utility} in curl) ## Maximum time in seconds that we allow the whole operation to take. ## Option works but as rsync doesn't have it, we are using timeout ## utility from coreutils. #transfer_max_time_opt="--max-time" ## Curl does not have I/O timeout. transfer_io_timeout_opt="" ## Maximum time in seconds that we allow curl's connection to take. ## This only limits the connection phase, so if curl's connect in the ## given period, it will continue. transfer_connect_timeout_opt="--connect-timeout ${transfer_connect_timeout}" ## curl max-filesize is not a definitive barrier: ## The file size is not always known prior to download, and for ## such files this option has no effect even if the file transfer ends ## up being larger than this given limit. transfer_size_opt="--max-filesize" transfer_dryrun_opt="" transfer_output_dir_opt="--output-dir" transfer_output_file_opt="--remote-name" transfer_verbosity_opt="" transfer_speed_optimization_opt="" ;; rsync*) ## Rsync does not have an option to set maximum time for of operation. ## Option works but as rsync doesn't have it, we are using timeout ## utility from coreutils. #transfer_max_time_opt="" ## If no data is transferred in the specified time, rsync will exit. transfer_io_timeout_opt="--timeout ${transfer_io_timeout}" ## Amount of time the client will wait for its connection to a server ## to succeed. ## Error when using this option: ## The --contimeout option may only be used when connecting to an ## rsync daemon. #transfer_connect_timeout_opt="--contimeout ${transfer_connect_timeout}" transfer_size_opt="--max-size" transfer_dryrun_opt="--dry-run" transfer_output_dir_opt="" transfer_output_file_opt="" transfer_verbosity_opt="--no-motd --progress --verbose --verbose" transfer_speed_optimization_opt="--compress --partial" ;; esac } ## Get utilities from a pool of known utilities and use the first one found. get_utilities(){ get_su_cmd get_checkhash_cmd get_transfer_cmd } ## Set default traps set_trap(){ log info "Current PATH: '${PATH}'" ## Sometimes ps is not available, default to sh. curr_shell="$(cat /proc/$$/comm)" ## Get current shell from current process ## If the process if the file name, get its shell from shebang ## sometimes the process name is the base name of the script with some ## missing letters. case "${0##*/}" in ${curr_shell}*) ## necessary glob because /bin/sh makes the file name ## appear with one letter less shebang="$(head -1 "${0}")" curr_shell="${shebang##*/}" ;; esac ## TODO: This is not shown on CI. log info "Current shell: '${curr_shell}'" case "${curr_shell}" in *bash|*ksh|*zsh) # shellcheck disable=SC2039 if test "${curr_shell}" = "bash"; then # shellcheck disable=SC2039,3040 set -o errtrace set -o pipefail fi # shellcheck disable=SC2039,3047 trap 'handle_exit $? ${LINENO:-}' ERR ;; esac trap 'handle_exit $? ${LINENO:-}' EXIT HUP INT QUIT ABRT ALRM TERM } ## Check if system status is supported get_system_stat(){ if [ "${arch}" != "x86_64" ]; then die 101 "${underline}Architecture Check:${nounderline} Only supported architecture is 'x86_64', yours is: '${arch}'." fi ## https://www.whonix.org/wiki/RAM#Whonix_RAM_and_VRAM_Defaults ## TODO ## min_ram_mb not used currently because less than total 4GB is too low ## already # shellcheck disable=SC2034 case "${interface}" in xfce) min_ram_mb="3328" ;; cli) min_ram_mb="1024" ;; esac if test "${virtualbox_only}" = "1"; then # shellcheck disable=SC2034 min_ram_mb="1024" fi ## 4GB RAM machine reports 3844Mi and 4031MB ## /proc/meminfo replies in kB ## https://ux.stackexchange.com/a/13850 total_mem_kB="$(awk '/MemTotal/{print $2}' /proc/meminfo)" ## convert kB to MB total_mem="$((total_mem_kB/1000))" ## capped to 4200MB to report that 4GB RAM on the host is too little if [ "${total_mem}" -lt "4200" ]; then user_warned_potential_startup_issue=true log warn "${underline}Minimum RAM Check:${nounderline} Your systems has a low amount of total RAM: '${total_mem} MB'" if test "${virtualbox_only}" != "1"; then log warn " - For more information, refer to:" log warn " ${url_version_domain}/wiki/RAM" fi fi df_output="$(df --output=avail -BG "${directory_prefix}")" free_space_available="$(echo "$df_output" | awk '/G$/{print substr($1, 1, length($1)-1)}')" ## TODO: do not hardcode 10 GB - Kicksecure vs Whonix free_space_required="10" ## Free space require set to approximately double the image size (2023-03-21) case "${guest}" in whonix) case "${interface}" in xfce) free_space_required="5" ;; cli) free_space_required="3" ;; esac ;; kicksecure) case "${interface}" in xfce) free_space_required="2" ;; cli) free_space_required="1" ;; esac ;; esac if test "${virtualbox_only}" = "1"; then free_space_required="1" fi if test "${dev}" = "1"; then free_space_required="1" fi if [ "${free_space_available}" -lt "$free_space_required" ]; then die 101 "\ ${underline}Free Disk Space Check Result:${nounderline} Insufficient free disk space! - available: '${free_space_available}G' - required : '${free_space_required}G' Debugging information: - Command to test the available free space on ${directory_prefix}: df --output=avail -BG \"${directory_prefix}\" - Available free space result: $df_output" fi } ## Sanity checks that should be called before execution of main pre_check(){ get_os get_distro check_not_qubes_template get_system_stat need_reboot_check_first get_host_pkgs need_reboot_check_second get_independent_host_pkgs kernel_modules_check nested_virtualization_test secure_boot_test kernel_modules_load kernel_modules_signed_only ## onion=1 -- Always torify onion connections. ## onion=* -- Only torify clearnet if SOCKS proxy is specified. case "${onion}" in 1) torify_conn;; *) test -n "${socks_proxy}" && torify_conn;; esac ## Functions below are difficult to emulate if test "${dry_run}" = "1"; then log info "Skipping rest or pre_check() because via '--dry-run' option." return 0 fi get_virtualization } ## Generate SOCKS credentials for stream isolation get_proxy_cred(){ test "${transfer_utility}" != "curl" && return 0 test -z "${transfer_proxy_suffix:-}" && return 0 proxy_user="anonym" proxy_pass="${1:?}" printf '%s' "--proxy-user ${proxy_user}:${proxy_pass}" } ## Test if can connect to SOCKS proxy and expect the correct Tor reply. check_tor_proxy(){ log notice "Testing SOCKS proxy: '${proxy}'" expected_response_header="HTTP/1.0 501 Tor is not an HTTP Proxy" log info "Expected response header:" log info "'$expected_response_header'" cmd_check_proxy="env UTW_DEV_PASSTHROUGH=1 curl --silent --show-error --max-time 3 --head http://${proxy}" log info "Command used to check if proxy is functional:" log info "$cmd_check_proxy" # shellcheck disable=SC2086 actual_response_header="$(${cmd_check_proxy} 2>&1)" parsed_response_header=$(echo "${actual_response_header}" | head -1) parsed_response_header=$(echo "${parsed_response_header}" | tr -d "\r") ## Globs are necessary to match patterns in the event the header has more ## characters them expected but still has the expected string. ## Rsync header response example: ## bad response from proxy -- HTTP/1.0 501 Tor is not an HTTP Proxy case "${parsed_response_header}" in *"${expected_response_header}"*) log info "Received header:" log info "'${parsed_response_header}'" log notice "Connected to Tor SOCKS proxy successfully." return 0 ;; *) log error "\ Unexpected proxy response, maybe not a Tor proxy? Debugging information: - Command used to check if proxy is functional: '$cmd_check_proxy' - Expected response header: '$expected_response_header' - Actual received header: '${actual_response_header}' - Parsed received header: '${parsed_response_header}'" return 1 esac } ## Set transference proxy depending on transfer utility. ## usage: set_transfer_proxy ${proxy} set_transfer_proxy(){ proxy_port="${1##*:}" proxy_addr="${1%%:*}" ## Used for transfers that only curl can do. curl_transfer_proxy="--proxy socks5h://${1}" ## Set transfer proxy per utility. case "${transfer_utility}" in curl) transfer_proxy_prefix="" transfer_proxy_suffix="--proxy socks5h://${1}" ;; rsync*) transfer_proxy_suffix="" transfer_proxy_prefix="torsocks --isolate --address ${proxy_addr} --port ${proxy_port}" ;; esac } ## Useful to test if it is a SOCKS proxy before attempting to make requests. ## If connection to proxy fails, abort to avoid leaks. torify_conn(){ if ! has tor; then log warn "System tor binary (little-t-tor) was not found on the system." log warn "Unless your SOCKS connection is made available by the Tor Browser" log warn " or by your uplink network, the proxy check mail fail." log warn "The installer with torified connections depends on a working SOCKS proxy," log warn " it won't configure the proxy, only establish the connection." log warn "If the proxy connection fails, try installation of the 'tor' package on your system." fi ## curl and many other viable applications do not support SOCKS proxy to ## connect with Unix Domain Socket: ## https://curl.se/mail/archive-2021-03/0013.html if test -n "${socks_proxy:-}"; then proxy="${socks_proxy}" set_transfer_proxy "${proxy}" elif test -n "${TOR_SOCKS_PORT:-}"; then proxy="${TOR_SOCKS_HOST:-127.0.0.1}:${TOR_SOCKS_PORT}" set_transfer_proxy "${proxy}" else ## Stream Isolation will be enforced get_proxy_cred() log warn "Missing SOCKS proxy for torified connections." log warn "Trying Tor defaults: system Tor (little-t-tor) (port: 9050) and TBB (Tor Browser Bundle) (port: 9150)." proxy="127.0.0.1:9050" set_transfer_proxy ${proxy} if ! check_tor_proxy; then proxy="127.0.0.1:9150" set_transfer_proxy ${proxy} else return 0 fi fi check_tor_proxy || die 2 "\ ${underline}Check Tor Proxy:${nounderline} 'FAIL' Unable to connect to Tor SOCKS proxy. - This issue is unlikely caused by this installer. - It is more probable that the problem stems from absent software, improper configuration, or network issues. Please note that in order to torify connections, an already functional Tor connection is needed: - A) A pre-installed and running system Tor (little-t-tor), or, - B) A pre-installed and running TBB (Tor Browser Bundle). Additional details: - This installer does not support setting up a functional Tor connection. This task needs to be performed by the system administrator. - When running the above cmd_check_proxy manually, ensure it includes the expected_response_header." } ## Set version by user input or by querying the API get_version(){ log notice "Version Detection: Detecting guest version..." if test -n "${guest_version:-}"; then log notice "Version Detection: 'skipped' - Autodetection using API not required via '--dry_run' or '--dev' option." return 0 fi log info "Version Detection: Acquiring guest version using API..." log info "Version Detection: API host: ${1}" cmd_raw_version="curl ${curl_transfer_proxy:-} $(get_proxy_cred version) \ ${curl_opt_ssl:-} \ --max-time ${transfer_max_time_small_file} \ --max-filesize ${transfer_size_small_file} \ --url ${1}" ## this is necessary because we log will not be printed as the command is ## assigned to a variable at 'raw_version=$()'. if test "${dry_run}" = "1"; then # shellcheck disable=SC2086 log_run notice ${cmd_raw_version} return 0 fi # shellcheck disable=SC2086 raw_version="$(${cmd_raw_version})" ## First line only. guest_version="$(echo "$raw_version" | head -n 1)" # shellcheck disable=SC2046,SC2086 guest_version="$(printf '%s\n' "${guest_version}" | sed "s/<.*//")" ## Distrust the API version ## Block anything that is not made purely out of numbers and dots ## Not printing queried version to avoid it showing a very long version ## that could inhibit the user from seeing the error message. ## The user would still see a failed exit code. [ "${guest_version%%*[^0-9.]*}" ] || die 1 "${underline}Version Check Result:${nounderline} Invalid guest version: contains unexpected characters." ## block string containing more than 12 chars [ "${#guest_version}" -le 12 ] || die 1 "${underline}Version Check Result:${nounderline} Invalid guest version: contains more than 12 characters." } ## Helper for download_files() to make it less repetitive. ## usage: get_file small|large $url get_file(){ size="${1}" url="${2}" ## Round is only used to get a different password every time. test -z "${round:-}" && round=10 round=$((round+1)) case "${size}" in small) download_opt_prefix="timeout --foreground ${transfer_max_time_small_file}" download_opt="${transfer_size_opt} ${transfer_size_small_file}" ;; large) download_opt_prefix="timeout --foreground ${transfer_max_time_large_file}" download_opt="${transfer_speed_optimization_opt} ${transfer_size_opt} ${transfer_size_large_file}" ;; *) log bug "Missing size option for get_file()." ;; esac # shellcheck disable=SC2046,SC2086 download_opt_full="${download_opt_prefix} ${transfer_proxy_prefix:-} ${transfer_utility} ${transfer_verbosity_opt} ${transfer_proxy_suffix:-} $(get_proxy_cred ${round}) ${transfer_connect_timeout_opt:-} ${transfer_io_timeout_opt:-} ${download_opt} ${transfer_output_file_opt} ${url} ${transfer_output_dir_opt} ${directory_prefix}" # shellcheck disable=SC2086 log_run notice ${download_opt_full} || return 1 } files_already_downloaded_check() { test -f "${directory_prefix}/${guest_file}.${guest_file_ext}" test -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.sig" test -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" } ## Check if files were already downloaded, if not, try to download everything ## and only if succeeds, set download flag. download_files(){ log_time log notice "Download: Files will be stored in the directory: '${directory_prefix}'" get_file large "${url_guest_file}.${guest_file_ext}" || return 1 get_file small "${url_guest_file}.${guest_file_ext}.sha512sums.sig" || return 1 get_file small "${url_guest_file}.${guest_file_ext}.sha512sums" || return 1 log info "Download: Checking if files exists locally..." if ! files_already_downloaded_check ; then die 103 "${underline}Download:${nounderline} Failed to download files." fi log_time } ## https://en.wikipedia.org/wiki/X86_virtualization get_virtualization(){ local virt_flag brand ## Check if virtualization is enabled. ## Check CPU flags for capability virt_flag="$(root_cmd grep -m1 -w '^flags[[:blank:]]*:' /proc/cpuinfo | grep -wo -E '(vmx|svm)' || true)" case "${virt_flag:=none}" in vmx) brand=intel;; svm) brand=amd;; *) brand=unknown;; esac # if compgen -G "/sys/kernel/iommu_groups/*/devices/*" > /dev/null; then # log notice "${brand}'s I/O Virtualization Technology is enabled in the BIOS/UEFI" # else # log warn "${brand}'s I/O Virtualization Technology is not enabled in the BIOS/UEFI" # fi case "${virt_flag:=}" in vmx|svm) log notice "Virtualization Support Test Result: 'success'" log info "cpu_brand: '${brand}' virt_flag: '${virt_flag}'" virt_detection_success=true return 0 ;; none) user_warned_potential_startup_issue=true log warn "${underline}Virtualization Support Test:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor} - No virtualization flag found." virt_detection_success=false ;; *) user_warned_potential_startup_issue=true log warn "${underline}Virtualization Support Test:${nounderline} ${red}${bold}'FAIL'${nobold}${nocolor} - Unknown virtualization flag. (Use '--log-level=info' for potentially privacy sensitive details.)" log info "cpu_brand: '${brand}' virt_flag: '${virt_flag}'" virt_detection_success=false ;; esac if [ "$virt_detection_success" = "false" ]; then user_warned_potential_startup_issue=true log warn " - The virtualization detection feature of this installer may not be flawless and could potentially fail to detect virtualization support (this is known as a 'false negative')." if [ "${hypervisor}" = "virtualbox" ]; then log warn " - Refer to user documentation on how to enable virtualization:" log warn " ${url_version_domain}/wiki/VirtualBox/Troubleshooting#Enable_VT-x_in_BIOS" fi return 0 ## Let's not hard fail here, let the user do it later. #return 101 fi ## msr is blocked by security-misc. If no other solution is found, ## remove the rest of of this function. ## $ modprobe msr ## /bin/disabled-msr-by-security-misc: ERROR: This CPU MSR kernel module is disabled by package security-misc by default. See the configuration file /etc/modprobe.d/30_security-misc.conf | args: ## modprobe: ERROR: ../libkmod/libkmod-module.c:990 command_do() Error running install command '/bin/disabled-msr-by-security-misc' for module msr: retcode 1 ## modprobe: ERROR: could not insert 'msr': Invalid argument install_pkg msr-tools # https://bazaar.launchpad.net/~cpu-checker-dev/cpu-checker/trunk/view/head:/kvm-ok # kvm-ok - check whether the CPU we're running on supports KVM acceleration # Copyright (C) 2008-2010 Canonical Ltd. # # Authors: # Dustin Kirkland # Kees Cook # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ## Print verdict verdict() { case "${1}" in 0) log notice "Virtualization can be used." log warn "Virtualization availability can be a false negative." return 0 ;; 1) user_warned_potential_startup_issue=true log warn "Virtualization can NOT be used." log warn "Virtualization availability can be a false negative." return 0 ## let's not hard fail here, let the user do it later. #return 1 ;; 2) user_warned_potential_startup_issue=true log warn "Virtualization can be used, but not enabled." log warn "Virtualization availability can be a false negative." return 0 ## let's not hard fail here, let the user do it later. #return 1 ;; esac } ## Check CPU flags for capability virt=$(root_cmd grep -m1 -w '^flags[[:blank:]]*:' /proc/cpuinfo | grep -wo -E '(vmx|svm)') || true if test -z "${virt}"; then log error "Your CPU does not support Virtualization." verdict 1 fi [ "${virt}" = "vmx" ] && brand="intel" [ "${virt}" = "svm" ] && brand="amd" ## Now, check that the device exists if test -e /dev/kvm; then log notice "Device /dev/kvm exists" verdict 0 else user_warned_potential_startup_issue=true log warn "Device /dev/kvm does not exist" log warn "hint: '${sucmd} modprobe kvm_$brand'" fi ## Prepare MSR access msr="/dev/cpu/0/msr" if root_cmd test ! -r "${msr}"; then root_cmd modprobe msr || die 1 "${underline}modprobe:${nounderline} Could not add module 'msr' to the kernel." fi if root_cmd test ! -r "${msr}"; then log error "Cannot read: '${msr}'" return 1 fi log notice "Your CPU supports Virtualization extensions." virt_disabled=0 ## check brand-specific registers if [ "${virt}" = "vmx" ]; then virt_bit=$(root_cmd rdmsr --bitfield 0:0 0x3a 2>/dev/null || true) if [ "${virt_bit}" = "1" ]; then ## and FEATURE_CONTROL_VMXON_ENABLED_OUTSIDE_SMX clear (no tboot) virt_bit=$(root_cmd rdmsr --bitfield 2:2 0x3a 2>/dev/null || true) [ "${virt_bit}" = "0" ] && virt_disabled=1 fi elif [ "${virt}" = "svm" ]; then virt_bit=$(root_cmd rdmsr --bitfield 4:4 0xc0010114 2>/dev/null || true) [ "${virt_bit}" = "1" ] && virt_disabled=1 else log error "Unknown virtualization extension: '${virt}'" verdict 1 fi if [ "${virt_disabled}" -eq 1 ]; then user_warned_potential_startup_issue=true log warn "'${virt}' is disabled by your BIOS" log warn "Enter your BIOS setup and enable Virtualization Technology (VT)," log warn " and then reboot your system." verdict 2 fi verdict 0 } ######################### ## END SCRIPT SPECIFIC ## ######################### ################ ## BEGIN MAIN ## ################ get_download_links(){ ## Set upstream links as base, especially for API. ## clearnet project domain site_clearnet_whonix="whonix.org" ## onion project domain site_onion_whonix="dds6qkxpwdeubwucdiaord2xgbbeyds25rbsgr73tbfpqpt4a6vjwsyd.onion" ## clearnet project domain site_clearnet_kicksecure="kicksecure.com" ## onion project domain site_onion_kicksecure="w5j6stm77zs6652pgsij4awcjeel3eco7kvipheu6mtr623eyyehj4yd.onion" case "${guest}" in whonix) site_clearnet="${site_clearnet_whonix}" site_onion="${site_onion_whonix}" ;; kicksecure) site_clearnet="${site_clearnet_kicksecure}" site_onion="${site_onion_kicksecure}" ;; *) ## Variables need to be set for --virtualbox-only. site_clearnet="${site_onion_kicksecure}" site_onion="${site_clearnet_kicksecure}" ;; esac if test -z "${mirror}"; then ## No mirror chosen, use default values. mirror=0 fi ## ${variable+string} means use string as value if variable is not empty. ## get download links by mirror of choice. case "${mirror}" in 0) site_download_clearnet="${curl+download.}${site_clearnet}${rsync+/${guest}}" site_download_onion="${curl+download.}${site_onion}${rsync+/${guest}}" ;; 1) site_download_clearnet="mirrors.dotsrc.org/${guest}" site_download_onion="dotsrccccbidkzg7oc7oj4ugxrlfbt64qebyunxbrgqhxiwj3nl6vcad.onion/${guest}" ;; 2) site_download_clearnet="quantum-mirror.hu/mirrors/pub/${guest}" site_download_onion="" ;; *) ## range_arg should have catch this error before, just safeguarding. log bug "Invalid mirror number: '${mirror}'" ;; esac case "${transfer_utility}" in curl) protocol_prefix_clearnet="https" protocol_prefix_onion="http" ;; rsync) protocol_prefix_clearnet="rsync" protocol_prefix_onion="rsync" ;; esac ## clearnet download url url_download_clearnet="${protocol_prefix_clearnet}://${site_download_clearnet}" ## onion download url url_download_onion="${protocol_prefix_onion}://${site_download_onion}" case "${onion}" in 1) log info "Onion preferred." curl_opt_ssl="" ## Used to test internet connection. url_origin="${protocol_prefix_onion}://www.${site_onion}" ## URL to download files from. test -n "${site_download_onion}" || die 1 "${underline}Mirror Selection:${nounderline} Mirror ${mirror} doesn't provide an onion service." url_download="${url_download_onion}" ## Used to query version number. url_version_domain="http://www.${site_onion}" ;; *) log info "Clearnet preferred." test "${transfer_utility}" = "rsync" && transfer_utility="rsync-ssl" curl_opt_ssl="--tlsv1.3 --proto =https" ## Used to test internet connection. url_origin="${protocol_prefix_clearnet}://www.${site_clearnet}" ## URL to download files from. url_download="${url_download_clearnet}" ## Used to query version number. url_version_domain="https://www.${site_clearnet}" ;; esac case "${hypervisor}" in virtualbox) if test "${testers}" = "1"; then url_version_template="VersionTesters" else url_version_template="VersionNew" fi ## image signer signify_key="${adrelanos_signify}" signify_signer="adrelanos" ## url directory to find files of the selected hypervisor url_domain="${url_download}/ova" ## image file extension guest_file_ext="ova" ## function to call when importing guest ;; kvm) if test "${testers}" = "1"; then die 1 "${underline}Version Selection:${nounderline} KVM does not have testers version." #url_version_template="" else url_version_template="Version_KVM" fi ## image signer signify_key="${hulahoop_signify}" signify_signer="hulahoop" ## url directory to find files of the selected hypervisor url_domain="${url_download}/libvirt" ## image file extension guest_file_ext="Intel_AMD64.qcow2.libvirt.xz" ## function to call when importing guest ## TODO ;; esac url_version_prefix="w/index.php?title=Template:" url_version_suffix="&stable=0&action=raw" url_version="${url_version_domain}/${url_version_prefix}${url_version_template}${url_version_suffix}" } ## Test if files should be downloaded should_download(){ if test "${virtualbox_only}" = "1"; then ## 'return 1' so the result of should_download is "no". return 1 fi if test "${dry_run}" = "1"; then log notice "Download: Creating download flag via '--dry-run' option." log_run notice touch "${download_flag}" return 0 fi if test "${redownload}" = "1"; then ## Do not print further messages as it was already printed before. ## Occurs if the should_download() function was called more than once. test "${download_msg_done:-}" = "1" && return 0 download_msg_done=1 ## Download if redownload option is set. log notice "Download: Re-downloading files via '--redownload' option." return 0 elif test -f "${download_flag:-}"; then ## Do not download if flag exists. log notice "Download: Skipping download because download and integrity check previously succeeded." return 1 fi ## Download as no obstacles prohibit it. return 0 } ## Check signature of signed checksum. check_signature(){ signify_checksum_file="${1}" log info "Signify key:\n${signify_key}" log info "Verifying file: '${signify_checksum_file}'" signify_pub_file="${log_dir_cur}/${signify_signer}.pub" echo "${signify_key}" | tee "${signify_pub_file}" >/dev/null log_run info signify -V -p "${signify_pub_file}" \ -m "${signify_checksum_file}" || return 1 log info "Signify Signature Verification: 'success'" } ## Check hash sum. check_hash(){ shafile="${1}" dir="$(dirname "${shafile}")" log info "Checking SHA512 checksum: '${shafile}" ## $checkhash needs to be executed on the same folder as the compared file. log info "Changing to directory: '${dir}'" cd "${dir}" # shellcheck disable=SC2086 log_run info ${checkhash} "${shafile}" || return 1 log info "SHA512 Hash Verification: 'success'" } check_signature_test(){ log info "Unit testing signature, expecting non-zero exit code." rm -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" cp "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" echo "" | tee "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" >/dev/null if ! check_signature "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" 2>/dev/null; then rm -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" log info "Received expected non-zero exit code from unit test." else rm -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" die 104 "${underline}SHA512 Hash Verification (unit test):${nounderline} 'FAIL' - received a zero as exit code, expected non-zero." fi } check_hash_test(){ log info "Unit testing checksum, expecting non-zero exit code." rm -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" cp "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" echo "" | tee "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" >/dev/null if ! check_hash "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" 2>/dev/null; then rm -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" log info "Received expected non-zero exit code from unit test." else rm -f "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums.test" die 104 "${underline}SHA512 Hash Verification (unit test):${nounderline} 'FAIL' - received a zero as exit code, expected non-zero." fi } ## Check integrity of files check_integrity(){ if test "${virtualbox_only}" = "1"; then return 0 fi if test "${dry_run}" = "1"; then log notice "Integrity Check: Skipping integrity checks via '--dry-run' option." return 0 fi log notice "Integrity Check: Performing integrity checks..." check_signature "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" || die 104 "${underline}Signify Signature Verification:${nounderline} 'FAIL'" check_hash "${directory_prefix}/${guest_file}.${guest_file_ext}.sha512sums" \ || die 104 "${underline}SHA512 Hash Verification:${nounderline} 'FAIL'" check_signature_test check_hash_test log_run info touch "${download_flag}" log notice "Integrity Check Result: 'success'" } ## Self explanatory name, make everything after option parsing. main(){ ############### ## BEGIN PRE ## ############### log notice "Saving user log to: '${log_file_user}'" if test -f "${log_file_debug}"; then log notice "Saving debug log to: '${log_file_debug}'" fi log info "Starting main function." guest_pretty="$(capitalize_first_char "${guest}")" case "${guest}" in whonix) guest_full_vm_name_gateway="${guest_pretty}-Gateway-${interface_name}" guest_full_vm_name_workstation="${guest_pretty}-Workstation-${interface_name}" ;; kicksecure) guest_full_vm_name_kicksecure="${guest_pretty}-${interface_name}" ;; esac case "${hypervisor}" in virtualbox) hypervisor_pretty="VirtualBox" ;; kvm) hypervisor_pretty="KVM" ;; *) hypervisor_pretty="${hypervisor}" ;; esac log info "Parsed options:" for item in ${arg_saved}; do log info " ${item}" done if test "${virtualbox_only}" = "1"; then log notice "${underline}Installer:${nounderline} ${bold}'VirtualBox Installer'${nobold}" else log notice "${underline}Installer:${nounderline} ${bold}'${guest_pretty} ${interface_name} for ${hypervisor_pretty} Installer'${nobold}" fi if test "${non_interactive}" != "1"; then log notice "If you wish to cancel installation, press Ctrl+C." fi ## The license function sleeps for some seconds to give time to abort check_license || die 100 "${underline}License Check:${nounderline} User declined the license." pre_check log_time ############# ## END PRE ## ############# #################### ## BEGIN DOWNLOAD ## #################### ## Skip making internet requests if flag already exists and user ## specified the desired version. ## If version is set, use it now to set the download flag path. if test -n "${guest_version}"; then guest_file="${guest_pretty}-${interface_name}-${guest_version}" download_flag="${directory_prefix}/${guest_file}.${guest_file_ext}.flag" fi if should_download; then if test "${dry_run}" != "1"; then log notice "Connectivity Test: Testing internet connection to '${url_origin}'..." cmd_check_internet="timeout --foreground ${transfer_max_time_small_file} ${transfer_proxy_prefix:-} ${transfer_utility} ${transfer_proxy_suffix:-} ${transfer_dryrun_opt} ${transfer_size_opt} ${transfer_size_test_connection} ${url_origin}" log info "Executing: $ ${cmd_check_internet}" ${cmd_check_internet} >/dev/null || die $? "${underline}Connectivity Test Result:${nounderline} 'FAIL' - Cannot connect, perhaps no internet?" log notice "Connectivity Test Result: 'success'" fi get_version "${url_version}" log notice "Version Detection Result: '${guest_version}'" guest_file="${guest_pretty}-${interface_name}-${guest_version}" download_flag="${directory_prefix}/${guest_file}.${guest_file_ext}.flag" url_domain="${url_domain}/${guest_version:?}" url_guest_file="${url_domain}/${guest_file}" ## Check again for download flag after version was queried. if should_download; then download_files || die 103 "${underline}Download:${nounderline} Failed to download files." fi else if test "${virtualbox_only}" = "1"; then true "INFO: Skip showing version via '--virtualbox-only' option." else log notice "Version Detection Result: '${guest_version}'" fi fi ################## ## END DOWNLOAD ## ################## ########################################## ## BEGIN VERIFICATION, IMPORT AND START ## ########################################## check_integrity check_vm_running_general check_vm_exists_general "(check before import)" vm_delete_maybe import_guest check_guest_boot ######################################## ## END VERIFICATION, IMPORT AND START ## ######################################## } ## Print usage message and exit with set exit code, depending if usage was ## called by [-h|--help] or because user tried and invalid option. usage(){ printf %s"Usage: ${me} [options...] User Options: -g, --guest= Specify the guest. Options: kicksecure, whonix (default) -u, --guest-version= Specify guest version, or query from API if not provided. -i, --interface= Choose the interface. Options: cli, xfce (default) -m, --hypervisor= Select the virtualization platform. Options: kvm, virtualbox (default) --oracle-repo Use Oracle's repository for VirtualBox -o, --onion Enable downloading files via onion. -s, --socks-proxy= Set TCP SOCKS proxy for onion client connections. (Defaults to TOR_SOCKS_HOST:TOR_SOCKS_PORT; if unset, attempts to use TBB (Tor Browser Bundle) proxy at port 9150, or system Tor (little-t-tor) proxy at port 9050.) -l, --log-level= Choose log verbosity. Options: debug, info, notice (default), warn, error. -V, --version Display version information and exit. -h, --help Show this help message and exit. Developer options: --no-show-errors Suppress error messages. --allow-errors Continue execution despite errors. Dirty mode. Use with caution. --mirror= Choose a download mirror by index. Defaults to mirror 0 for clearnet and mirror 0 for onion if unspecified. Mirror indices: 0 [DE] download.whonix.org (onion available) 1 [DK] mirrors.dotsrc.org (onion available) 2 [HU] quantum-mirror.hu --redownload Re-download the guest image, even if previously successful. --import-only= Select specific VM to import. Only if guest is Whonix. Options: workstation, gateway. --no-import Skip guest import. Default behavior is to import. --destroy-existing-guest Deletes any existing virtual machine(s) and re-imports them. Warning: This action poses a risk of data loss as it involves a complete reinstallation of the VM(s). Proceed with caution. -k, --no-boot Do not boot the guest after setup. Default is to start. -P, --directory-prefix= Specify the absolute path for the directory where files will be saved. Ensure that the directory already exists and is readable and writable by the user. If this directory is changed and previously downloaded files are not moved to the new directory, the download will restart. Default: \$HOME/dist-installer-cli-download. -n, --non-interactive Enable non-interactive mode; license will be accepted. -D, --dev Activate development mode. Downloads an empty image. --noupdate Skip package manager list update. For development only. --ci Enable Continuous Integration (CI) mode. --testers Download the tester's version. -d, --dry-run Simulate execution; log commands without executing. --virtualbox-only Restrict actions to downloading and installing VirtualBox. -t, --getopt Display parsed options and exit. File name: The default file name is dist-installer-cli. Basic options can be set by using file name following the format 'guest-installer-interface'. Names not adhering to this format or the default are rejected. Command-line options take precedence over file name settings. " exit "${1:-0}" } ## Set default values for variables. set_default(){ last_exit=0 guest_pretty="" get_colors xtrace="" directory_prefix="" set_arg directory_prefix "${HOME}/dist-installer-cli-download" guest="" set_arg guest whonix hypervisor="" set_arg hypervisor virtualbox interface="" set_arg interface xfce oracle_repo="" log_level="" set_arg log_level notice guest_version="" socks_proxy="" onion="" non_interactive="" dev="" noupdate="" dry_run="" getversion="" getopt="" ci="" no_import="" no_boot="" redownload="" import_only="" destroy_existing_guest="" testers="" allow_errors="" mirror="" virtualbox_only="" url_version_domain="" run_background="" background_pid="" debian_derivative_detected="" ubuntu_derivative_detected="" fedora_derivative_detected="" virtualbox_linux_user_group="vboxusers" # shellcheck disable=SC2034 credential_caching_status="" qubes_template_detected="" install_pkg_fasttrack_extra_args_maybe="" user_warned_potential_startup_issue="" } ## Parse script name. parse_name(){ ## if using default file name, ignore the rest test "${me}" = "dist-installer-cli" && return 0 ## check if file name is valid case "${me}" in whonix-xfce-installer-cli | whonix-cli-installer-cli | \ kicksecure-xfce-installer-cli | kicksecure-cli-installer-cli ) log info "Valid script name to set options: '${me}'" ;; virtualbox-installer-cli ) log info "Valid script name to set options: '${me}'" set_arg virtualbox_only 1 set_arg hypervisor virtualbox return 0 ;; *) log error "Invalid script name: '${me}'" log error "If you don't know why this happened, rename this script to" log error " dist-installer-cli and use command-line options instead." return 2 esac ## assign values according to script name set_arg guest "$(echo "${me}" | cut -d "-" -f1)" set_arg interface "$(echo "${me}" | cut -d "-" -f2)" log info "Assigned guest and interface according to script name: '${me}'" return 0 } ## Parse command-line options. parse_opt(){ #test -z "${1:-}" && usage 2 while true; do begin_optparse "${1:-}" "${2:-}" || break # shellcheck disable=SC2034 case "${opt}" in P|directory-prefix) get_arg directory_prefix ;; o|onion) set_arg onion 1 ;; s|socks-proxy) get_arg socks_proxy ;; l|log-level) get_arg log_level ;; g|guest) get_arg guest ;; u|guest-version) get_arg guest_version ;; i|interface) get_arg interface ;; m|hypervisor) get_arg hypervisor ;; oracle-repo) set_arg oracle_repo 1 ;; mirror) get_arg mirror ;; import-only) get_arg import_only ;; allow-errors) set_arg allow_errors 1 ;; redownload) set_arg redownload 1 ;; destroy-existing-guest) set_arg destroy_existing_guest 1 ;; n|non-interactive) set_arg non_interactive 1 ;; k|no-boot) set_arg no_boot 1 ;; no-import) set_arg no_import 1 ;; D|dev) set_arg dev 1 ;; noupdate) set_arg noupdate 1 ;; testers) set_arg testers 1 ;; t|getopt) set_arg getopt 1 ;; ci) set_arg ci 1 ;; d|dry-run) set_arg dry_run 1 ;; virtualbox-only) set_arg virtualbox_only 1 set_arg hypervisor virtualbox set_arg guest none ;; V|version) set_arg getversion 1 ;; h|help) usage 0 ;; *) die 2 "Invalid option: '${opt_orig}'" ;; esac shift "${shift_n:-1}" done ## Put last newline after last argument of arg_saved. arg_saved="$(printf %s"${arg_saved}\n")" ## Test if options are valid range_arg log_level error warn notice info debug if [ "${log_level}" = "debug" ]; then xtrace=1 set -o xtrace fi range_arg guest none whonix kicksecure range_arg interface cli xfce range_arg hypervisor kvm virtualbox range_arg import_only workstation gateway both case "${interface}" in xfce) ## Whonix 17 and above uses Xfce instead of XFCE. interface_name="Xfce" ;; cli) interface_name="CLI" ;; esac if test "${guest}" != "whonix"; then ## Guest is not 'whonix'. I.e. is 'kicksecure' if test -n "${import_only}"; then die 1 "The option '--import-only' option can only be set when the guest is 'whonix'." fi fi test -n "${mirror}" && range_arg mirror 0 1 2 test -n "${socks_proxy}" && is_addr_port "${socks_proxy}" if test -n "${directory_prefix}"; then ## Remove trailing slash from directory. directory_prefix="${directory_prefix%*/}" ## Only accept an absolute path. if [ "${directory_prefix}" = "${directory_prefix#/}" ]; then log error "Invalid directory prefix: '${directory_prefix}'" die 1 "Directory prefix can not be a relative path, must be an absolute path." fi ## Test if parent directory exists. directory_prefix_parent="$(dirname "${directory_prefix}")" if ! test -d "${directory_prefix_parent}"; then die 1 "Directory doesn't exist: '${directory_prefix_parent}'" fi ## Not possible to check if parent dir is writable because if the prefix ## is set to '~/', the parent '/home' is not writable. log info "Creating directory: '${directory_prefix}'" mkdir -p "${directory_prefix}" || die 1 "Failed to created directory: '${directory_prefix}'" test -w "${directory_prefix}" || die 1 "Directory isn't writable: '${directory_prefix}'" test -r "${directory_prefix}" || die 1 "Directory isn't readable: '${directory_prefix}'" log_dir_main="${directory_prefix}/logs" ## Log to incrementing integer to avoid leaking other information such ## as PID or date (even if UTC). if ! test -d "${log_dir_main}/1"; then log_dir_cur="${log_dir_main}/1" else last_run_integer="$(echo "${log_dir_main}"/* | awk '{print NF}')" cur_run_integer=$((last_run_integer+1)) log_dir_cur="${log_dir_main}/${cur_run_integer}" fi log_file_user="${log_dir_cur}/user.log" log_file_debug="${log_dir_cur}/debug.log" ## If the commands below fail, it should have failed earlier for the ## parent directory permissions, not below. mkdir -p "${log_dir_cur}" cp "${0}" "${log_dir_cur}" touch "${log_file_user}" fi # shellcheck disable=SC2194 if test "${getopt}" = "1"; then printf '%s\n' "${arg_saved}" exit 0 fi if test "${getversion}" = "1"; then printf '%s\n' "${me} ${version}" exit 0 fi if test "${dev}" = "1"; then if test -z "${guest_version}"; then log notice "Version Detection: Setting dev software version." guest_version="17.0.3.4" fi fi if test "${dry_run}" = "1"; then if test -z "${guest_version}"; then log notice "Simulation: dry_run set, commands will be printed but not executed." log notice "Version Detection: Using simulated software version because of dry_run." guest_version="17.0.3.4" fi fi if test "${allow_errors}" = "1"; then set +o errexit # shellcheck disable=SC2039,3040 test "${curr_shell}" = "bash" && set +o errtrace fi log info "Option Parsing: 'success'" } ## Logging mechanism. ## Bash supports process substitution and saving xtrace to a file, which is a ## simpler way to log to file and console. log_term_and_file(){ ## Discover if terminal is attached to stdout if ! test -t 1; then log warn "Output is not being sent to the terminal because terminal is not connected to stdout." return 0 fi touch "${log_file_user}" touch "${log_file_debug}" ## By appending '&' at the end, log_file_user would remain empty. true "exec > >(tee -a \"${log_file_user}\") 2> >(tee -a \"${log_file_debug}\" >&2)" exec > >(tee -a "${log_file_user}") 2> >(tee -a "${log_file_debug}" >&2) ## Bash has built-in feature to redirect xtrace to the specified file. # shellcheck disable=SC2039 true "exec 9>>${log_file_debug}" exec 9>>"${log_file_debug}" export BASH_XTRACEFD=9 set -o xtrace xtrace=1 } end_installer() { log notice "Installer Result: ${green}${bold}'SUCCESS'${nobold}${nocolor}" end_exit } ## Wrapper to call all necessary functions in one. run_installer(){ set_globals "${@}" ## Set default values. set_default not_as_root ## Set trap for common signals. set_trap ## Parse script name for wanted values. parse_name ## Parse command-line options. parse_opt "${@}" ## Logging mechanism. log_term_and_file get_utilities get_download_links ## Start main function. main } if [ "$dist_installer_cli_was_sourced" = "true" ]; then true "INFO: Not running the install script because it was sourced by another script." else true "INFO: Running the install script because it was executed." run_installer "${@}" fi