#!/bin/bash ## vim:sw=2:sts=2:ts=2:et:spell: ## ## Copyright (C) 2023 ENCRYPTED SUPPORT LP ## See the file COPYING for copying conditions. ########################## ## BEGIN DEFAULT VALUES ## ########################## # shellcheck disable=SC2317 set -o errexit set -o nounset 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. " ## Version is commit based: https://github.com/Whonix/usability-misc version="15.9-1" me="${0##*/}" # shellcheck disable=SC2034 all_args="${*}" start_time="$(date +%s)" ## 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" } ## 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-----" virtualbox_version="7.0" virtualbox_linux_user_group="vboxusers" ######################## ## 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." 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 notice su root -s "${cmd}" -- "${@}" ;; sudo) log_run notice sudo -- "${@}" ;; doas) log_run notice 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_detected="" case ${os} in Linux*) if test -f /usr/share/kicksecure/marker; then distro="Kicksecure" distro_version=$(cat /etc/kicksecure_version) elif test -f /usr/share/whonix/marker; then distro="Whonix" distro_version=$(cat /etc/whonix_version) elif 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 ## Debian testing /etc/os-release does not contain VERSION_ID. if echo "$distro" | grep --quiet "/sid" ; then debian_testing_detected=true fi 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 log notice "Detected architecture: '${arch}'" log notice "Detected system: '${os}'" log notice "Detected distribution: '${distro}'" log notice "Detected distribution version: '${distro_version}'" if [ "$debian_testing_detected" = "true" ]; then log notice "Debian testing detection: 'success'" return 0 fi ## 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 "File /etc/os-release contents:" cat /etc/os-release || true else log notice "File /etc/os-release does not exist." 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 if [ "$distro_version" = "n/a" ]; then ## In Debian testing distro_version can be 'n/a'. return 0 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}'" } ############## ## 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}" ;; *) log bug "Unsupported log type: '${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 ;; esac ## reverse importance order is required, excluding 'error' all_log_levels="warn notice info debug" 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 ;; *) 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 exiting now with code '${1}' because allow_errors is set." return 0 fi case "${1}" in 106|107) true ;; *) log error "Aborting installer." ;; 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_without_extrarenous_spaces}" return 0 fi if test "${ci}" != "1"; then ## CI expects no output from root_cmd() which calls log_run(). log "${level}" "Running: $ ${command_without_extrarenous_spaces}" fi "${@}" || return 1 } ## 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 "Elapsed time: $(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: '${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 "Error detected. Installer aborted." log error "No need to panic. Nothing is broken. Just some rare condition has been hit." log error "There is likely a solution for this problem." log error "Try again. If this issue is transient (not happening again) it can be safely ignored." log error "Please see ${guest_pretty} News and ${guest_pretty} User Help Forum." log error "If not already reported, please report this bug!" echo "" log error "Error occurred in 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 "Received signal: '${signal_caught}" fi fi ## Print exit code. log error "Exit code: '${last_exit}'" end_exit } ################# ## END LOGGING ## ################# ########################### ## BEGIN SCRIPT SPECIFIC ## ########################### claim_unsupported_distro(){ status="${1}" distro="${2}" log error "At this moment, your Operating System is unsupported by the ${guest_pretty} Installer." log error "Check if manual installation is supported by visiting:" 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(){ case "${os}" in Linux*) case "${distro}" in "Debian"*|"Tails"*|"Kicksecure"|"Whonix") true "distro: ${distro}" install_package_debian_common install_virtualbox_debian ;; "Kali") log error "Kali Linux support is disabled, reason: legal liability" log error " https://www.kicksecure.com/wiki/Limitations_on_Free_Speech_on_Website_and_Chat#Banned_Subjects" claim_unsupported_distro known ;; "Linux Mint"*|"LinuxMint"*|"mint"*) true "distro: ${distro}" install_package_debian_common install_virtualbox_ubuntu ;; *"buntu"*) true "distro: ${distro}" 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 install_package_debian_common install_virtualbox_ubuntu ;; "Fedora"*|"CentOS"*|"rhel"*|"Redhat"*|"Red hat") true "${distro}" install_package_fedora_common install_virtualbox_fedora ;; "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 } get_independent_host_pkgs(){ ## Platform independent packages if has signify-openbsd; then ## fix Debian unconventional naming signify(){ 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 ## 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. log warn "${underline}Nested Virtualization Test:${nounderline} Program to detect nested virtualization not found." else if test "${dry_run}" = "1"; then log notice "${underline}Nested Virtualization Test:${nounderline} Skipping because dry_run is set." else ## Check if we are a guest of virtualization. if root_cmd "${nested_virt_tool:-}" >/dev/null 2>&1; then nested_virtualization_detected=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 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 log info "${underline}Secure Boot Check Result:${nounderline} 'enabled' (mokutil_output: '$mokutil_output') Ok." else log info "${underline}Secure Boot Check Result:${nounderline} 'disabled' (mokutil_output: '$mokutil_output') Ok." fi } ## Install package only if not installed already. install_pkg(){ pkgs="${*}" pkg_not_installed="" for pkg in ${pkgs}; do ## Test if package exists as a binary or a library, using different tools. # shellcheck disable=SC2086 if ${pkg_mngr_check_installed} "${pkg}" >/dev/null 2>&1; then log info "Package already installed: '${pkg}'" continue fi if has "${pkg}"; then log info "Program already installed: '${pkg}'" continue fi pkg_not_installed="${pkg_not_installed} ${pkg}" done if test -n "${pkg_not_installed}"; then if test "${dry_run}" = "1"; then log notice "Skipping installing the following packages because dry_run is set: '${pkg_not_installed}'" return 0 fi log notice "Installing package(s): '${pkg_not_installed}'" log notice "Updating package list." # shellcheck disable=SC2086 root_cmd ${pkg_mngr_update} || 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: $command_without_extrarenous_spaces The user is advised to attempt to debug this with the following steps: 1. Run above command. 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." log notice "Installing package(s): '${pkg_not_installed}'" # shellcheck disable=SC2086 root_cmd ${pkg_mngr_install} ${pkg_not_installed} || die 1 "${underline}Package Installation:${nounderline} Failed to install package: '${pkg_not_installed}'" ## Test if installation worked. 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 because dry_run is set: '${pkg_not_installed}'" else log error "Failed to locate package(s): '${pkg_not_installed}'" return 1 fi fi } ## 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}" | \ 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_exists_virtualbox(){ if test "${virtualbox_only}" = "1"; then return 0 fi case "${guest}" in whonix) ## Test if machine exists. workstation_exists=0 gateway_exists=0 if vboxmanage showvminfo \ "${guest_pretty}-Gateway-${interface_name}" >/dev/null 2>&1 then gateway_exists=1 fi if vboxmanage showvminfo \ "${guest_pretty}-Workstation-${interface_name}" >/dev/null 2>&1 then workstation_exists=1 fi ## Find discrepancies. if test "${workstation_exists}" = "0" && test "${gateway_exists}" = "1" then log warn "Gateway exists but Workstation doesn't." fi if test "${workstation_exists}" = "1" && test "${gateway_exists}" = "0" then log warn "Workstation exists but Gateway doesn't." fi ## If either one of the Guests exists, proceed. if test "${workstation_exists}" = "1" || test "${gateway_exists}" = "1" then log notice "${underline}Existing VM Check:${nounderline} Virtual Machine(s) have been imported previously." if test "${reimport}" = "1"; then ## Remove Gateway if it exists. if test "${gateway_exists}" = "1"; then check_vm_running_virtualbox "${guest_pretty}-Gateway-${interface_name}" ## Do not remove gateway if import_only is set to workstation if test "${import_only}" = "workstation"; then log info "Although reimport is set, not deleting previously imported gateway because import_only is set to workstation." log info "If you wish to reimport both machines, do not specify import_only." else log warn "Deleting previously imported gateway because reimport is set..." log_run notice vboxmanage unregistervm \ "${guest_pretty}-Gateway-${interface_name}" --delete fi fi ## Remove Workstation if it exists. if test "${workstation_exists}" = "1"; then check_vm_running_virtualbox "${guest_pretty}-Workstation-${interface_name}" ## Do not remove workstation if import_only is set to gateway if test "${import_only}" = "gateway"; then log info "Although reimport is set, not deleting previously imported workstation because import_only is set to gateway." log info "If you wish to reimport both machines, do not specify import_only." else log warn "Deleting previously imported workstation because reimport is set." log_run notice vboxmanage unregistervm \ "${guest_pretty}-Workstation-${interface_name}" --delete fi fi elif test -n "${import_only}"; then ## reimport was not set and the import_only is set to the same ## machine that already exists. if test "${gateway_exists}" = "1" && test "${import_only}" = "gateway" then die 1 "${underline}Check Existing VM Test:${nounderline} import_only was set to 'gateway', but it already exists and reimport was not set." elif test "${workstation_exists}" = "1" && test "${import_only}" = "workstation" then die 1 "${underline}Check Existing VM Test:${nounderline} import_only was set to 'workstation', but it already exists and reimport was not set." fi else ## One of the guests doesn't exist, but neither reimport nor ## import-only was not set. if test "${workstation_exists}" = "0" || test "${gateway_exists}" = "0" then test "${workstation_exists}" = "0" && die 1 "${underline}Check Existing VM Test:${nounderline} Workstation cannot be started because it doesn't exist, try '--import-only=workstation'." test "${gateway_exists}" = "0" && die 1 "${underline}Check Existing VM Test:${nounderline} Gateway cannot be started because it doesn't exist, try '--import-only=gateway'." fi ## VMs already exist, check if user wants to start them. log info "Checking if user wants to start Virtual Machine(s) now." check_guest_boot fi fi ;; kicksecure) if vboxmanage showvminfo \ "${guest_pretty}-${interface_name}" >/dev/null 2>&1 then log notice "${underline}Existing VM Check:${nounderline} Virtual Machine(s) were imported previously." if test "${reimport}" = "1"; then check_vm_running_virtualbox "${guest_pretty}-${interface_name}" ## If VMs exists and reimport is set, remove VMs as they are gonna ## be imported later by main. log warn "Deleting previously imported Virtual Machine(s) because reimport is set." log_run notice vboxmanage unregistervm \ "${guest_pretty}-${interface_name}" --delete else ## VMs already exist, check if user wants to start them. log info "Checking if user wants to start Virtual Machine(s) now." check_guest_boot fi fi ;; esac } ## Check if VM exists using hypervisor tools. check_vm_exists(){ if test "${virtualbox_only}" = "1"; then return 0 fi log info "Checking if Virtual Machine(s) have been already imported." case "${hypervisor}" in virtualbox) check_vm_exists_virtualbox ;; kvm) return 0 ;; esac } ## Check if guest should start or not check_guest_boot(){ if test "${virtualbox_only}" = "1"; then log notice "${underline}VirtualBox Installation Result:${nounderline} ${green}${bold}'SUCCESS'${nobold}${nocolor}" return 0 fi log info "Virtual Machine(s) already exist." if test "${redownload}" != "1"; then log notice "If you would like to redownload the image, read about --redownload (safe)." fi if test "${reimport}" != "1"; then log notice "If you would like to reimport the image, read about --reimport (danger)." fi ## Skip guest boot if test "${no_boot}" = "1"; then log info "no_boot is set." log notice "${underline}VM Startup Check:${nounderline} User declined to start Virtual Machine(s) via command line." log notice "Virtual Machine(s) can be started manually." exit fi ## Default to start guest without interaction if test "${non_interactive}" = "1"; then log notice "${underline}VM Startup Check:${nounderline} VM start agreed by the user by setting non_interactive." start_guest exit fi ## Ask user to start guest or not case "${guest}" in whonix) log notice "Available guest: '${guest_pretty}-Gateway-${interface_name}'" log notice "Available guest: '${guest_pretty}-Workstation-${interface_name}'" ;; kicksecure) log notice "Available guest: '${guest_pretty}-${interface_name}'" ;; esac log notice "${bold}Question:${nobold} Do you want to start the ${guest_pretty} Virtual Machine(s) now? [y/n] (default: yes): " printf '%s' "Your answer: " read -r response log notice "User replied '${response}'" case ${response} in ""|[Yy]|[Yy][Ee][Ss]) log notice "${underline}VM Startup Check:${nounderline} User accepted to start Virtual Machine(s)." start_guest ;; *) 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 ## Last phase, should exit here every time. exit } import_guest(){ if test "${virtualbox_only}" = "1"; then return 0 fi if test "${no_import}" = "1"; then log notice "Not importing guest because no_import is set." exit 0 fi check_vm_exists case "${hypervisor}" in virtualbox) import_virtualbox ;; kvm) import_kvm ;; esac } ## Import VirtualBox images import_virtualbox(){ log notice "Importing Virtual Machine(s)..." ## Check how many systems to import. ## vsys 0: gateway ## vsys 1: workstation vm_purge="purgeme" case "${import_only}" in workstation) vbox_arg="--vsys 0 --eula accept --vmname ${vm_purge} --vsys 1 --eula accept" ;; gateway) vbox_arg="--vsys 0 --eula accept --vsys 1 --eula accept --vmname ${vm_purge}" ;; "") case "${guest}" in whonix) ## if importing whonix, import 2 virtual systems vbox_arg="--vsys 0 --eula accept --vsys 1 --eula accept" ;; kicksecure) vbox_arg="--vsys 0 --eula accept" ;; esac ;; esac ## import VirtualBox image # shellcheck disable=SC2086 log_run notice vboxmanage import \ "${directory_prefix}/${guest_file}.${guest_file_ext}" ${vbox_arg} || 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}"; then log_run notice vboxmanage unregistervm ${vm_purge} --delete || die 1 "${underline}VM Import:${nounderline} Failed to remove extraneous virtual system." 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(){ case "${hypervisor}" in virtualbox) start_virtualbox ;; kvm) start_kvm ;; esac } ## Start the hypervisor with the desired guest start_virtualbox(){ log notice "Starting Virtual Machine(s)." case "${guest}" in whonix) log_run notice vboxmanage startvm \ "${guest_pretty}-Gateway-${interface_name}" || virtualbox_start_failed log_run notice vboxmanage startvm \ "${guest_pretty}-Workstation-${interface_name}" || virtualbox_start_failed ;; kicksecure) log_run notice vboxmanage startvm \ "${guest_pretty}-${interface_name}" || virtualbox_start_failed ;; esac log notice "${underline}Virtual Machine Startup Result:${nounderline} ${green}${bold}'SUCCESS'${nobold}${nocolor}" } virtualbox_start_failed() { if [ "$nested_virtualization_detected" = "true" ] || [ "$virt_detection_success" = "false" ]; then die 106 "${underline}Virtual Machine Startup Result:${nounderline} ${red}${bold}FAIL${nobold}${nocolor} - 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. - This is likely happening due to the virtualization related warnings that have been reported above. - Resolving these issues (as per documentation hyperlinks above) would likely resolve this issue." fi die 107 "${underline}Virtual Machine Startup Result:${nounderline} ${red}${bold}FAIL${nobold}${nocolor} - 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. - Refer to user documentation: ${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" pkg_mngr_update="${pkg_mngr} update --assumeyes" pkg_mngr_check_installed="dnf list installed" if test "${virtualbox_only}" = "1"; then return 0 fi install_pkg torsocks redhat-lsb-core 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" ## '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 log error "Auditing dpkg database returned non-zero exit code." log error "Learn how to fix this issue at:" log error " https://www.kicksecure.com/wiki/Operating_System_Software_and_Updates#Broken_APT" return 1 fi if test "${virtualbox_only}" = "1"; then return 0 fi install_pkg torsocks lsb-release 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 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}'." } ## 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 because dry_run is set." 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 because dry_run is set." else die 1 "${underline}vboxmanage test:${nounderline} Failed to locate 'vboxmanage' program." fi fi add_user_to_vbox_group } install_repositories_for_virtualbox_on_debian(){ distro_codename_real=$(lsb_release --short --codename) distro_codename_use="$distro_codename_real" case "${distro_codename_real}" in trixie) distro_codename_use="bookworm" ;; esac 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_use} contrib" 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_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_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_use} main contrib non-free" apt_torified="" apt_onion="" 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}" "(${backports_clearnet}|${backports_onion})/debian/? ${distro_codename_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 CHECK:${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}" 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}" 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}" fasttrack_domain_debsource="${fasttrack_clearnet}" backports_domain_debsource="${backports_clearnet}" fi oracle_url="${oracle_prefix_debsource} ${protocol_prefix_debsource}${oracle_domain_debsource}${oracle_suffix_debsource}" kicksecure_url="${kicksecure_prefix_debsource} ${protocol_prefix_debsource}${kicksecure_domain_debsource}${kicksecure_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 install_oracle_repository_debian return 0 fi log info "Installing packages required for backports and fasttrack repository." install_pkg apt-transport-https ca-certificates fasttrack-archive-keyring case "${distro_codename_real}" in bullseye) install_backports_and_fasttrack_repository_debian return 0 ;; bookworm) install_kicksecure_repository_debian return 0 ;; trixie) install_kicksecure_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 "Skipped adding Oracle repository because it was already found." else log notice "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 "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 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 fi } install_oracle_repository_debian(){ if test "${oracle_found}" = "1"; then log info "Skipped adding Oracle repository because it was already found." else log notice "Adding Oracle's ${connection_type_debsource} repository to ${oracle_file_debsource}" if test -f /usr/share/keyrings/oracle.asc ; then log info "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 "Key /usr/share/keyrings/derivative.asc already exists." log info "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 "Skipped adding Kicksecure repository because it was already found." else log notice "Adding Kicksecure's ${connection_type_debsource} repository to ${kicksecure_file_debsource}" write_sources_debian "${kicksecure_url}" "${kicksecure_file_debsource}" fi } install_backports_and_fasttrack_repository_debian() { if test "${backports_found}" = "1" && test "${fasttrack_found}" = "1"; then log info "Skipped adding additional APT repositories because backports and fasttrack were already found." return 0 fi if test "${backports_found}" = "1"; then log info "Skipped adding Backports repository because it was already found." else log notice "Adding Backports's ${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 "Skipped adding Fasttrack repository because it was already found." else log notice "Adding Fasttrack's ${connection_type_debsource} repository to ${fasttrack_file_debsource}" write_sources_debian "${fasttrack_url}" "${fasttrack_file_debsource}" fi } ## Install VirtualBox on Fedora install_virtualbox_fedora(){ virtualbox_qt_package_name=VirtualBox-"${virtualbox_version}" has_virtualbox_qt=0 test_pkg "${virtualbox_qt_package_name}" 2>/dev/null && has_virtualbox_qt=1 install_pkg kernel-headers kernel-devel dkms ## Guard against adding extraneous repositories. if test "${has_virtualbox_qt}" != "1"; then log notice "VirtualBox Installation: Preparing to install VirtualBox..." install_oracle_repository_fedora 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(){ if test "${oracle_repo}" = "1"; then virtualbox_qt_package_name=virtualbox-"${virtualbox_version}" ## 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 else virtualbox_qt_package_name=virtualbox-qt fi has_virtualbox_qt=0 linux_headers="linux-headers-$(dpkg --print-architecture)" test_pkg "${virtualbox_qt_package_name}" 2>/dev/null && has_virtualbox_qt=1 install_pkg "${linux_headers}" if test "${has_virtualbox_qt}" != "1"; then log notice "VirtualBox Installation: Preparing to install VirtualBox..." install_repositories_for_virtualbox_on_debian fi install_pkg "$virtualbox_qt_package_name" install_virtualbox_debian_common_end } ## Install VirtualBox on Ubuntu install_virtualbox_ubuntu(){ if has vboxmanage && test_pkg linux-headers-generic; then install_virtualbox_debian_common_end return 0 fi install_pkg virtualbox 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 setting." 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 ## '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 as another user." } case "${sucmd}" in sudo) :;; *) log warn "Using sucmd '$sucmd'. Consider installing 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 info "User system is using credential caching: 'No'" return 0 fi log info "User system is using credential caching: 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 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 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 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:${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_system_stat get_host_pkgs get_independent_host_pkgs nested_virtualization_test secure_boot_test ## 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 dry_run is set." 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="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="$(eval ${cmd_check_proxy} 2>&1 | head -1 | 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 "${actual_response_header}" in *"${expected_response_header}"*) log info "Received header:" log info "'${actual_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' - Received header: '${actual_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 installing 'tor' 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} 'failure' 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 "Detecting guest version..." if test -n "${guest_version:-}"; then log notice "User defined, dry_run or dev version already configured. Autodetection form API not required." return 0 fi log info "Acquiring guest version from API..." log info "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:${nounderline} Invalid guest version: contains unexpected characters." ## block string containing more than 12 chars [ "${#guest_version}" -le 12 ] || die 1 "${underline}Version Check:${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}" log notice "Downloading: '${url:-}'" # shellcheck disable=SC2086 log_run notice ${download_opt_full} || return 1 } ## 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 "Downloads 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}.sha512sums.sig" || return 1 get_file small "${url_guest_file}.sha512sums" || return 1 log notice "Checking if files exists locally." if test "${dry_run}" = "1" || { test -f "${directory_prefix}/${guest_file}.${guest_file_ext}" && test -f "${directory_prefix}/${guest_file}.sha512sums.sig" && test -f "${directory_prefix}/${guest_file}.sha512sums" } then log_time log_run notice touch "${download_flag}" else die 103 "${underline}Download:${nounderline} Failed to download files." fi } ## https://en.wikipedia.org/wiki/X86_virtualization get_virtualization(){ ## 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:=}" in vmx) brand=intel;; svm) brand=amd;; 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 "${underline}Virtualization Support Test:${nounderline} Your CPU supports virtualization: '${brand}': '${virt_flag}'" virt_detection_success=true return 0 ;; "") log warn "${underline}Virtualization Support Test:${nounderline} No virtualization flag found." virt_detection_success=false ;; *) log warn "${underline}Virtualization Support Test:${nounderline} Unknown virtualization flag: '${virt_flag}'" virt_detection_success=false ;; esac if [ "$virt_detection_success" = "false" ]; then 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) 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) 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 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 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}" ;; esac ## ${variable+string} means use string as value if variable is not empty. ## get download links by mirror of choice. case "${mirror}" in "") ## no mirror chosen, use default values. ## Force mirror to be used. site_download_clearnet="mirrors.dotsrc.org/${guest}" site_download_onion="${curl+download.}${site_onion}${rsync+/${guest}}" ;; 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="mirror.koljasagorski.de/${guest}" site_download_onion="" ;; 3) site_download_clearnet="quantum-mirror.hu/mirrors/pub/${guest}" site_download_onion="" ;; 4) ## does not have TLS for Rsync ## When using rsync-ssl, this message appears: ## rsync: did not see server greeting die 1 "${underline}Mirror Selection:${nounderline} Mirror ${mirror} doesn't support TLS for rsync." site_download_clearnet="ftp.icm.edu.pl/pub/Linux/dist/${guest}" site_download_onion="" ;; 5) die 1 "${underline}Mirror Selection:${nounderline} Mirror ${mirror} is currently using an invalid certificate." site_download_clearnet="mirror.gigenet.com/${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 "${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 "Downloading files because redownload is set." return 0 elif test -f "${download_flag:-}"; then ## Do not download if flag exists. log notice "Skipping download. Flag found: '${download_flag}'" 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}.sha512sums.test" cp "${directory_prefix}/${guest_file}.sha512sums" "${directory_prefix}/${guest_file}.sha512sums.test" echo "" | tee "${directory_prefix}/${guest_file}.sha512sums.test" >/dev/null if ! check_signature "${directory_prefix}/${guest_file}.sha512sums.test" 2>/dev/null; then rm -f "${directory_prefix}/${guest_file}.sha512sums.test" log info "Received expected non-zero exit code from unit test." else rm -f "${directory_prefix}/${guest_file}.sha512sums.test" die 104 "${underline}SHA512 Hash Verification (unit test):${nounderline} 'failure' - 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}.sha512sums.test" cp "${directory_prefix}/${guest_file}.sha512sums" "${directory_prefix}/${guest_file}.sha512sums.test" echo "" | tee "${directory_prefix}/${guest_file}.sha512sums.test" >/dev/null if ! check_hash "${directory_prefix}/${guest_file}.sha512sums.test" 2>/dev/null; then rm -f "${directory_prefix}/${guest_file}.sha512sums.test" log info "Received expected non-zero exit code from unit test." else rm -f "${directory_prefix}/${guest_file}.sha512sums.test" die 104 "${underline}SHA512 Hash Verification (unit test):${nounderline} 'failure' - 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 "Skipping integrity checks because dry_run is set." return 0 fi log notice "Performing integrity checks." check_signature "${directory_prefix}/${guest_file}.sha512sums" || die 104 "${underline}Signify Signature Verification:${nounderline} 'failure'" check_hash "${directory_prefix}/${guest_file}.sha512sums" \ || die 104 "${underline}SHA512 Hash Verification:${nounderline} 'failure'" check_signature_test check_hash_test 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 "${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 ## Check if VM were already imported, if true then ask to start if test "${redownload}" != "1"; then check_vm_exists fi ############# ## 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 "Testing internet connection..." 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:${nounderline} Cannot connect to ${url_origin}, perhaps no internet?" log notice "Connection to ${url_origin} succeeded." fi get_version "${url_version}" log notice "Version: '${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 because virtualbox_only." else log notice "Version: '${guest_version}'" fi fi ################## ## END DOWNLOAD ## ################## ########################################## ## BEGIN VERIFICATION, IMPORT AND START ## ########################################## check_integrity 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...] -g, --guest= Set guest. Options: kicksecure, whonix (default) -u, --guest-version= Set guest version, else query from API. -i, --interface= Set interface. Options: cli, xfce (default) -m, --hypervisor= Set virtualization. Options: kvm, virtualbox (default) --oracle-repo Set Oracle's repository for VirtualBox -o, --onion Download files over onion. -s, --socks-proxy= Set TCP SOCKS proxy for onion client connections. (default: TOR_SOCKS_HOST:TOR_SOCKS_PORT, if not set, try TBB (Tor Browser Bundle) proxy at port 9150, else try system Tor (little-t-tor) proxy at port 9050.) -l, --log-level= Set log level. Options: debug, info, notice (default), warn, error. -V, --version Show version and quit. -h, --help Show help for commands and quit. Developer options: --no-show-errors Don't print errors. --allow-errors Don't exit on errors. Dirty mode. --mirror= Set mirror according to number index. Choose closest mirror to you if not using onion for faster downloads. Not that if not choosing mirror, defaults to whonix.org for clearnet and dotsrc.org correspondent for onion. Mirrors index: 0 [DE] download.whonix.org (onion available) 1 [DK] mirrors.dotsrc.org (onion available) 2 [DE] mirror.koljasagorski.de 3 [HU] quantum-mirror.hu 4 [PL] ftp.icm.edu.pl 5 [US] mirrors.gigenet.com --redownload Redo the download of the guest instead of stopping if it was already successfully downloaded. --import-only= Choose the missing VM to import when the guest is whonix. Options: workstation and gateway'. --no-import Don't import guest. Default is to import. --reimport Redo the import of the guest to the hypervisor. Will try to import already existing files if found. This option requires --destroy-existing-guest as pledge. -k, --no-boot Don't boot guest at the end of run. Default is to start. --destroy-existing-guest Required pledge for --reimport. -P, --directory-prefix= Set absolute path to directory prefix to save files to. The directory must already exist and have read and write capabilities for the calling user. If you change this directory, but the previously downloaded files are not changed to the new directory, the download start again. \$HOME/installer-dist-download (default). -n, --non-interactive Set non-interactive mode, license will be accepted. -D, --dev Set development mode. Empty image to download --ci Set CI mode. Test running in CI. --testers Set testers version to be downloaded. -d, --dry-run Simulated run, log commands to info level without executing. --virtualbox-only Only downloads and installs VirtualBox. -t, --getopt Show parsed options and quit. File name: The default file name is installer-dist. Some basic options can be set as the file name if they follow the format 'guest-installer-interface'. Anything different than the default name or the allowed format is rejected. Command line options override the file name definition. " exit "${1:-0}" } ## Set default values for variables. set_default(){ guest_pretty="" get_colors xtrace="" directory_prefix="" set_arg directory_prefix "${HOME}/installer-dist-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="" dry_run="" getversion="" getopt="" ci="" no_import="" no_boot="" redownload="" reimport="" import_only="" destroy_existing_guest="" testers="" allow_errors="" no_show_errors="" mirror="" virtualbox_only="" url_version_domain="" } ## Parse script name. parse_name(){ ## if using default file name, ignore the rest test "${me}" = "installer-dist" && return 0 ## check if file name is valid case "${me}" in whonix-installer-xfce | whonix-installer-cli | \ kicksecure-installer-xfce | kicksecure-installer-cli ) log info "Valid script name to set options: '${me}'" ;; virtualbox-installer ) 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 " installer-dist 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 "-" -f3)" #set_arg hypervisor "$(echo "${me}" | cut -d "-" -f3)" 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 ;; no-show-errors) set_arg no_show_errors 1 ;; redownload) set_arg redownload 1 ;; reimport) set_arg reimport 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 ;; 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 ;; 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 whonix kicksecure range_arg interface cli xfce range_arg hypervisor kvm virtualbox range_arg import_only workstation gateway 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" && test -n "${import_only}"; then die 1 "The option import_only can only be set when the guest is whonix." fi test -n "${mirror}" && range_arg mirror 0 1 2 3 4 5 test -n "${socks_proxy}" && is_addr_port "${socks_proxy}" if test "${reimport}" = "1"; then if test -z "${destroy_existing_guest}"; then log error "The option --reimport requires --destroy-existing-guest." log error "The option --destroy-existing-guest was not set." die 1 "User mistake, read the documentation to understand how to use the options." fi fi 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 "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 "dry_run set, commands will be printed but not executed." log notice "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 ended." } ## 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_bash(){ 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 touch "${log_file_debug}" } ## Logging mechanism. 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 if test "${curr_shell}" = "bash"; then log_term_and_file_bash return 0 fi ## Portable log and console redirection that has the downside of some ## applications doing horrible line wrappings (apt >=2.6.1) if the output is ## not a tty. ## Send fd3 to the terminal true "exec 3>$(tty)" exec 3>>"$(tty)" ## Current problem is that command-line option is necessary to hide stderr ## by sending it to /dev/null, while the desired method would be to avoid ## this workaround and simply be able to redirect with 2>/dev/null. ## Send fd1/stdout to log file. true "exec 1>>${log_file_user}" exec 1>>"${log_file_user}" ## Send fd2/stderr to log file by default. true "exec 2>>${log_file_user}" exec 2>>"${log_file_user}" ## If no_show_errors is set, send fd2/stderr to /dev/null. if test "${no_show_errors}" = "1"; then true "exec 2>/dev/null" exec 2>/dev/null fi ## Copy output of log file to fd3 and send process to the background. if test -f "${log_file_debug}" && test "${log_level}" = "debug"; then true "tail -qf ${log_file_debug} ${log_file_user} >&3 &" tail -qf "${log_file_debug}" "${log_file_user}" >&3 & else true "tail -f ${log_file_user} >&3 &" tail -f "${log_file_user}" >&3 & fi ## Get background job PID. tail_pid="$!" } ## Wrapper to call all necessary functions in one. run_installer(){ ## 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 } ## Run the install script. run_installer "${@}"