#!/bin/bash
# Make sure the kernel can create WireGuard interfaces.
#
# EL9+ runs a 5.14 kernel and ships wireguard.ko with the kernel packages.
# EL8 runs 4.18, which predates WireGuard's mainline merge (5.6), so it needs
# ELRepo's kmod-wireguard backport - or wireguard-dkms on a custom kernel.
#
# Safe to re-run: every step is a no-op once satisfied, and the script never
# exits non-zero so it cannot break a package transaction or block startup.
set -uo pipefail

STATUS_DIR=/run/wireshield
STATUS_FILE="$STATUS_DIR/wireguard-module.status"
LAST_ERR=""
APT_UPDATED=0
SKIP_INSTALL="${WIRESHIELD_SKIP_PKG_INSTALL:-0}"

# The backend reads this back and shows it in the pairing preflight, so the
# admin sees the real reason instead of a generic "module unavailable".
write_status() {
    mkdir -p "$STATUS_DIR" 2>/dev/null || return 0
    printf '%s\t%s\n' "$1" "$2" >"$STATUS_FILE" 2>/dev/null || true
}

succeed() {
    echo "[SUCCESS] WireGuard kernel module available."
    write_status ok "WireGuard kernel module loaded."
    exit 0
}

give_up() {
    local summary="$1"
    shift
    local detail="$*"
    echo "ERROR: $summary" >&2
    [[ -n "$detail" ]] && echo "       $detail" >&2
    write_status error "${summary}${detail:+ $detail}"
    exit 0
}

have_module() {
    [[ -d /sys/module/wireguard ]] && return 0
    modprobe wireguard >/dev/null 2>&1
}

# Bounded so a hung mirror cannot stall service startup - this also runs from
# the unit's ExecStartPre.
pm() { timeout 300 "$@"; }

rpm_installed() { rpm -q "$1" >/dev/null 2>&1; }
deb_installed() { dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q 'ok installed'; }

# A transaction already holds the package lock - almost always the one installing
# WireShield, since %post starts the unit and ExecStartPre lands back here. dnf
# and apt would block on that lock rather than fail, so don't even start.
pkg_manager_busy() {
    local p
    for p in dnf dnf5 yum rpm apt apt-get dpkg; do
        pgrep -x "$p" >/dev/null 2>&1 && return 0
    done
    return 1
}

remember_err() {
    LAST_ERR=$(printf '%s' "$1" | grep -iE 'error|no match|nothing provides|unable to|not found' | tail -n 1)
    LAST_ERR=${LAST_ERR:-$2}
}

install_rpm() {
    local out
    if out=$(pm dnf install -y "$@" 2>&1); then
        return 0
    fi
    remember_err "$out" "dnf install $* failed"
    return 1
}

install_deb() {
    local out
    if [[ $APT_UPDATED -eq 0 ]]; then
        pm apt-get update -qq >/dev/null 2>&1
        APT_UPDATED=1
    fi
    if out=$(DEBIAN_FRONTEND=noninteractive pm apt-get install -y "$@" 2>&1); then
        return 0
    fi
    remember_err "$out" "apt-get install $* failed"
    return 1
}

os_major=0
if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    os_major=${VERSION_ID:-0}
    os_major=${os_major%%.*}
fi

if command -v dnf >/dev/null 2>&1; then
    pkg_mgr=dnf
elif command -v apt-get >/dev/null 2>&1; then
    pkg_mgr=apt
else
    pkg_mgr=""
fi

# wg-quick lives in wireguard-tools, which is a package dependency, but a
# reinstall or a minimised image can still leave it missing.
ensure_tools() {
    command -v wg-quick >/dev/null 2>&1 && return 0
    [[ "$SKIP_INSTALL" == "1" || -z "$pkg_mgr" ]] && return 1
    pkg_manager_busy && return 1
    case "$pkg_mgr" in
        dnf) install_rpm wireguard-tools ;;
        apt) install_deb wireguard-tools ;;
    esac
}

install_module_el() {
    if [[ ${os_major:-0} -ge 9 ]]; then
        # 5.14 ships wireguard.ko, so a miss here means the running kernel has no
        # matching modules rather than anything being uninstalled.
        give_up "The WireGuard kernel module is missing from the running kernel ($(uname -r))." \
            "EL${os_major} ships it with the kernel: reboot into the newest kernel, or reinstall the matching kernel-core package."
    fi

    rpm_installed elrepo-release || install_rpm "https://www.elrepo.org/elrepo-release-${os_major}.el${os_major}.elrepo.noarch.rpm" ||
        give_up "Could not add the ELRepo repository, which provides the WireGuard module on EL${os_major}." "$LAST_ERR"

    if install_rpm kmod-wireguard && have_module; then
        return 0
    fi

    # The prebuilt kmod only matches stock kernel ABIs, so fall back to a DKMS
    # build (needs EPEL for dkms itself plus headers for the running kernel).
    echo "[*] Prebuilt module unusable on kernel $(uname -r); building via DKMS..."
    rpm_installed epel-release || install_rpm "https://dl.fedoraproject.org/pub/epel/epel-release-latest-${os_major}.noarch.rpm" || true
    install_rpm "kernel-devel-$(uname -r)" || install_rpm kernel-devel || true
    install_rpm wireguard-dkms ||
        give_up "Could not install the WireGuard kernel module on EL${os_major}." "$LAST_ERR"
}

install_module_deb() {
    install_deb wireguard ||
        give_up "Could not install the wireguard package." "$LAST_ERR"
    have_module && return 0
    install_deb "linux-headers-$(uname -r)" || true
    install_deb wireguard-dkms || true
}

ensure_tools || true
have_module && succeed

if [[ "$SKIP_INSTALL" == "1" ]] || pkg_manager_busy; then
    echo "WARNING: the WireGuard kernel module is not available on this host." >&2
    echo "         A package transaction is in progress; install runs once it finishes." >&2
    echo "         To do it now: /usr/libexec/wireshield/ensure-wireguard-module" >&2
    write_status pending "WireGuard kernel module not installed yet; installation runs automatically once the current package transaction finishes."
    exit 0
fi

case "$pkg_mgr" in
    dnf) install_module_el ;;
    apt) install_module_deb ;;
    *) give_up "No supported package manager found to install the WireGuard kernel module." ;;
esac

have_module && succeed

give_up "Installed the WireGuard module packages, but the kernel still cannot load wireguard on $(uname -r)." \
    "Reboot this server to finish loading it. If it still fails after a reboot, the running kernel has no matching module build."
