#!/bin/bash set -Eeuo pipefail # disable command path hashing as we are going to move fast and break things set +h # config : ${INSTALL_PKG:="true"} : ${TEMP_ROOTFS_DISTRO:="debian"} : ${TEMP_ROOTFS_FLAVOR:="bullseye"} : ${WORKDIR:="/tmp/menhera"} : ${ROOTFS:=} : ${SSHD:="dropbear"} # or "openssh" declare -A ARCH_MAP=( ["x86_64"]="amd64" ) # internal global variables OLDROOT="/" NEWROOT="" MACHINE_TYPE=$(uname -m) ARCH_ID=${ARCH_MAP[$MACHINE_TYPE]:-$MACHINE_TYPE} # fix possible PATH problems export PATH="$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" menhera::reset_sshd_config() { cat > /etc/ssh/sshd_config </dev/null; then echo sshd else echo ssh fi } # environment compatibility menhera::__compat_restart_ssh() { if [ -x "$(command -v systemctl)" ]; then sshd_name=$(menhera::__compat_sshd_name) ! systemctl daemon-reload ! systemctl stop "${sshd_name}" if [ "${SSHD}" = "openssh" ]; then sshd_name=$(menhera::__compat_sshd_name) ! systemctl enable "${sshd_name}" ! systemctl reset-failed "${sshd_name}" if ! { systemctl restart "${sshd_name}" && sleep 2; } || ! systemctl is-active "${sshd_name}" >/dev/null; then >&2 echo "[-] SSH daemon start failed, try resetting config..." menhera::reset_sshd_config systemctl reset-failed "${sshd_name}" if ! { systemctl restart "${sshd_name}" && sleep 2; } || ! systemctl is-active "${sshd_name}" >/dev/null; then >&2 echo "[!] SSH daemon fail to start, dropping you to a shell; please manually launch a forking SSH daemon and exit." sh fi fi elif [ "${SSHD}" = "dropbear" ]; then # Don't rely on systemd anymore; use daemon fork instead. # This is due to newer version of systemd trying to read executables from the old rootfs # thus failing with exit status 203. (Observed on CentOS 8 Stream.) if ! >/var/log/dropbear.log 2>&1 dropbear -E -m -K 10 &2 echo "[!] SSH daemon fail to start, dropping you to a shell; please manually launch a forking SSH daemon and exit." sh fi fi else >&2 echo "[-] ERROR: Cannot restart SSH server, init system not recoginzed" return 1 fi >&2 echo "[+] SSH daemon started" return 0 } menhera::__compat_reload_init() { if [ -x "$(command -v systemctl)" ]; then systemctl daemon-reexec >&2 echo "[+] Reloaded SystemD" elif [ -x "$(command -v telinit)" ]; then telinit u >&2 echo "[+] Reloaded init" else >&2 echo "[-] ERROR: Cannot re-exec init, init system not recognized" return 1 fi return 0 } # fetch URL and output its body to stdout menhera::__compat_download_stdout() { if command -v wget > /dev/null; then wget -qO- --show-progress "$1" elif command -v curl > /dev/null; then curl -L "$1" else >&2 echo "[-] ERROR: No compatible download program is installed, try install curl or wget" return 127 fi return 0 } # fetch URL and put the content to a file menhera::__compat_download_file() { if command -v wget > /dev/null; then wget --continue -q --show-progress -O "$2" "$1" elif command -v curl > /dev/null; then curl -L -C - -o "$2" "$1" else >&2 echo "[-] ERROR: No compatible download program is installed, try install curl or wget" return 127 fi return 0 } # helper functions # https://stackoverflow.com/a/3232082/2646069 menhera::confirm() { # call with a prompt string or use a default read -r -p "${1:-Are you sure? [y/N]} " response case "$response" in [yY][eE][sS]|[yY]) true ;; *) false ;; esac } # jobs menhera::get_rootfs() { if [ -z "${ROOTFS}" ]; then >&2 echo "[*] Getting rootfs URL..." # forgive me for parsing HTML with these shit # and hope it works ROOTFS_TIME=$(menhera::__compat_download_stdout "https://images.linuxcontainers.org/images/${TEMP_ROOTFS_DISTRO}/${TEMP_ROOTFS_FLAVOR}/${ARCH_ID}/default/?C=M;O=D" | grep -oP '(\d{8}_\d{2}:\d{2})' | head -n 1) ROOTFS="https://images.linuxcontainers.org/images/${TEMP_ROOTFS_DISTRO}/${TEMP_ROOTFS_FLAVOR}/${ARCH_ID}/default/${ROOTFS_TIME}/rootfs.squashfs" else >&2 echo "[+] \$ROOTFS is set to '$ROOTFS'" fi return 0 } menhera::sync_filesystem() { >&2 echo "[*] Syncing..." sync sync return 0 } menhera::prepare_environment() { >&2 echo "[*] Loading kernel modules..." modprobe overlay modprobe squashfs sysctl kernel.panic=10 sysctl kernel.sysrq=1 >&2 echo "[*] Creating workspace in '${WORKDIR}'..." # workspace mkdir -p "${WORKDIR}" mount -t tmpfs -o size=100% tmpfs "${WORKDIR}" # new rootfs mkdir -p "${WORKDIR}/newroot" # readonly part of new rootfs mkdir -p "${WORKDIR}/newrootro" # writeable part of new rootfs mkdir -p "${WORKDIR}/newrootrw" # overlayfs workdir mkdir -p "${WORKDIR}/overlayfs_workdir" >&2 echo "[*] Downloading temporary rootfs..." menhera::__compat_download_file "${ROOTFS}" "${WORKDIR}/rootfs.squashfs" return 0 } menhera::mount_new_rootfs() { >&2 echo "[*] Mounting temporary rootfs..." mount -t squashfs "${WORKDIR}/rootfs.squashfs" "${WORKDIR}/newrootro" mount -t overlay overlay -o rw,lowerdir="${WORKDIR}/newrootro",upperdir="${WORKDIR}/newrootrw",workdir="${WORKDIR}/overlayfs_workdir" "${WORKDIR}/newroot" NEWROOT="${WORKDIR}/newroot" return 0 } menhera::install_software() { [ ${INSTALL_PKG} != "true" ] && >&2 echo "[-] New rootfs skip installing packages." && return 0 >&2 echo "[*] Installing SSH Server into new rootfs..." # disable APT cache echo -e 'Dir::Cache "";\nDir::Cache::archives "";' > "${NEWROOT}/etc/apt/apt.conf.d/00_disable-cache-directories" DEBIAN_FRONTEND=noninteractive chroot "${NEWROOT}" apt-get update -y if [ "${SSHD}" = "openssh" ]; then DEBIAN_FRONTEND=noninteractive chroot "${NEWROOT}" apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install -y openssh-server elif [ "${SSHD}" = "dropbear" ]; then DEBIAN_FRONTEND=noninteractive chroot "${NEWROOT}" apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install -y dropbear-bin # convert dropbear key format (if possible; only ed25519 is known to work) mkdir -p "${NEWROOT}/etc/dropbear" ! chroot "${NEWROOT}" dropbearconvert openssh dropbear "/etc/ssh/ssh_host_rsa_key" "/etc/dropbear/dropbear_rsa_host_key" ! chroot "${NEWROOT}" dropbearconvert openssh dropbear "/etc/ssh/ssh_host_dsa_key" "/etc/dropbear/dropbear_dss_host_key" ! chroot "${NEWROOT}" dropbearconvert openssh dropbear "/etc/ssh/ssh_host_ecdsa_key" "/etc/dropbear/dropbear_ecdsa_host_key" ! chroot "${NEWROOT}" dropbearconvert openssh dropbear "/etc/ssh/ssh_host_ed25519_key" "/etc/dropbear/dropbear_ed25519_host_key" fi return 0 } menhera::copy_config() { >&2 echo "[*] Copying important config into new rootfs..." ! cp -axL --remove-destination "${OLDROOT}/etc/resolv.conf" "${NEWROOT}/etc" ! cp -axr "${OLDROOT}/etc/ssh" "${NEWROOT}/etc" ! cp -ax "${OLDROOT}/etc/"{passwd,shadow} "${NEWROOT}/etc" ! cp -axr "${OLDROOT}/root/.ssh" "${NEWROOT}/root" # fix SSH key files permission; otherwise OpenSSH server will refuse to start mkdir -p "${NEWROOT}/etc/ssh" ! chmod 600 -- "${NEWROOT}/etc/ssh/"*_key ! chown -R root:root -- "${NEWROOT}/root/.ssh" ! find "${NEWROOT}/root/.ssh" -type f -exec chmod 600 -- {} + chroot "${NEWROOT}" chsh -s /bin/bash root cat > "${NEWROOT}/etc/motd" < /proc/sysrq-trigger". Your original rootfs is at "/mnt/oldroot". Be careful dealing with it. If it is still occupied, run "fuser -kvm /mnt/oldroot; fuser -kvm -15 /mnt/oldroot" to kill processes using the old rootfs. Have a lot of fun... EOF return 0 } menhera::swap_root() { >&2 echo "[*] Swapping rootfs..." # prepare future mount point for our old rootfs mkdir -p "${WORKDIR}/newroot/mnt/oldroot" mount --make-rprivate / # swap root pivot_root "${WORKDIR}/newroot" "${WORKDIR}/newroot/mnt/oldroot" >&2 echo "[+] Rootfs replaced" OLDROOT="/mnt/oldroot" NEWROOT="/" # move mounts for i in dev proc sys run; do if [ -d "${OLDROOT}/$i" ]; then mount --move "${OLDROOT}/$i" "${NEWROOT}/$i" fi done mount -t tmpfs -o size=100% tmpfs "${NEWROOT}/tmp" mkdir -p "${WORKDIR}" mount --move "${OLDROOT}/${WORKDIR}" "${WORKDIR}" >&2 echo "[*] Restarting SSH daemon..." menhera::__compat_restart_ssh return 0 } menhera::show_hostkey_fingerprint() { command -v ssh-keygen >/dev/null || return 1 if [ -d "${NEWROOT}/etc/dropbear" ]; then >&2 echo "[*] The ssh host key fingerprint in the \${NEWROOT}/etc/dropbear directory is being displayed..." for i in $(ls "${NEWROOT}/etc/dropbear/"*_key); do dropbearkey -y -f $i | ssh-keygen -l -f -; done fi if [ -d "${NEWROOT}/etc/ssh" ]; then >&2 echo "[*] The ssh host key fingerprint in the \${NEWROOT}/etc/ssh directory is being displayed..." for i in $(ls "${NEWROOT}/etc/ssh/"*_key); do ssh-keygen -l -f $i; done fi return 0 } menhera::clear_processes() { >&2 echo "[*] Disabling swap..." swapoff -a >&2 echo "[*] Restarting init process..." menhera::__compat_reload_init # hope 15s is enough sleep 15 >&2 echo "[*] Killing all programs still using the old root... Goodbye! See you on the other side~" fuser -kvm "${OLDROOT}" -15 # in most cases the parent process of this script will be killed, so goodbye return 0 } # main procedure LIBRARY_ONLY=0 while test $# -gt 0 do case "$1" in --lib) LIBRARY_ONLY=1 ;; esac shift done if [[ $LIBRARY_ONLY -eq 1 ]]; then # acting as a library only return 0 fi if [[ $EUID -ne 0 ]]; then >&2 echo "[-] This script must be run as root" exit 1 fi echo -e "We will start a temporary RAM system as your recovery environment." echo -e "Note that this script will kill programs and umount filesystems without prompting." echo -e "Please confirm:" echo -e "\t+ You have closed all programs you can, and backed up all important data" echo -e "\t+ You can SSH into your system as root user" menhera::confirm || exit -1 menhera::get_rootfs menhera::sync_filesystem menhera::prepare_environment menhera::mount_new_rootfs menhera::copy_config menhera::install_software menhera::swap_root menhera::show_hostkey_fingerprint echo -e "If you are connecting from SSH, please create a second session to this host use root and" echo -e "confirm you can get a shell. Host key might change - use \"ssh-keygen -R hostname-or-ip\" to purge the" echo -e "host key cache entry." echo -e "After your confirmation, we are going to kill the old SSH server." if menhera::confirm; then menhera::clear_processes else echo -e "Please manually issue a reboot to recover your old OS. If you believe there is a bug in menhera.sh, " echo -e "raise a ticket at https://github.com/Jamesits/menhera.sh/issues ." echo -e "Force reboot by \"echo b > /proc/sysrq-trigger\"." exit 1 fi