From 6a76352efa9d55c8b2ee3e38a096566bbd481c67 Mon Sep 17 00:00:00 2001 From: elseif Date: Sun, 30 Aug 2026 07:38:00 +0800 Subject: [PATCH] modified: .github/workflows/patch_v7.yml new file: install_routeros.sh new file: npkextract/Makefile new file: npkextract/npkextract new file: npkextract/npkextract.c --- .github/workflows/patch_v7.yml | 71 ++- install_routeros.sh | 1088 ++++++++++++++++++++++++++++++++ npkextract/Makefile | 39 ++ npkextract/npkextract | Bin 0 -> 38420 bytes npkextract/npkextract.c | 426 +++++++++++++ 5 files changed, 1605 insertions(+), 19 deletions(-) create mode 100644 install_routeros.sh create mode 100644 npkextract/Makefile create mode 100644 npkextract/npkextract create mode 100644 npkextract/npkextract.c diff --git a/.github/workflows/patch_v7.yml b/.github/workflows/patch_v7.yml index 16e9674..eef9dab 100644 --- a/.github/workflows/patch_v7.yml +++ b/.github/workflows/patch_v7.yml @@ -379,11 +379,14 @@ jobs: run: wget -nv -O $BUILD_DIR/refind-bin-0.14.2.zip https://downloads.sourceforge.net/project/refind/0.14.2/refind-bin-0.14.2.zip - - name: Build alpine livecd + - name: Build Alpine LiveCD if: matrix.arch == 'x86' run: | set -e - ROOTFS_URL="https://dl-cdn.alpinelinux.org/alpine/v3.24/releases/x86_64/alpine-minirootfs-3.24.1-x86_64.tar.gz" + YAML_URL="https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/latest-releases.yaml" + ROOTFS_FILE=$(curl -sL "$YAML_URL" | grep -m1 'alpine-minirootfs.*\.tar\.gz' | awk '{print $NF}' | tr -d '"') + echo "$ROOTFS_FILE" + ROOTFS_URL="https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/$ROOTFS_FILE" ROOTFS_DIR=$BUILD_DIR/rootfs mkdir -p $ROOTFS_DIR curl -# -L "$ROOTFS_URL" | tar -xzC $ROOTFS_DIR @@ -392,28 +395,50 @@ jobs: sudo mount -vt sysfs sysfs $ROOTFS_DIR/sys sudo mount -vt proc proc $ROOTFS_DIR/proc - echo "nameserver 1.1.1.1" > $ROOTFS_DIR/etc/resolv.conf - cat << EOF > $ROOTFS_DIR/etc/network/interfaces + cat << 'EOF' > $ROOTFS_DIR/etc/network/interfaces auto lo iface lo inet loopback auto eth0 iface eth0 inet dhcp EOF - sed -i 's/^#\(ttyS0.*\)/\1/' $ROOTFS_DIR/etc/inittab - sed -i 's/^root:.*/root:::0:::::/' $ROOTFS_DIR/etc/shadow - echo "" > $ROOTFS_DIR/etc/motd + cat << 'EOF' > $ROOTFS_DIR/etc/inittab + # /etc/inittab + ::sysinit:/sbin/openrc sysinit + ::sysinit:/sbin/openrc boot + ::wait:/sbin/openrc default + tty1::respawn:/sbin/agetty --autologin root tty1 linux + tty2::respawn:/sbin/agetty --autologin root tty2 linux + tty3::respawn:/sbin/agetty --autologin root tty3 linux + tty4::respawn:/sbin/agetty --autologin root tty4 linux + ttyS0::respawn:/sbin/agetty --autologin root ttyS0 linux + ::ctrlaltdel:/sbin/reboot + ::shutdown:/sbin/openrc shutdown + EOF - cat << EOF | sudo chroot $ROOTFS_DIR /usr/bin/env PATH=/usr/bin:/bin:/usr/sbin:/sbin /bin/sh + cp ./install_routeros.sh $ROOTFS_DIR/root/install_routeros.sh + chmod +x $ROOTFS_DIR/root/install_routeros.sh + cat << 'EOF' > $ROOTFS_DIR/root/.zprofile + if [ -x /root/install_routeros.sh ]; then + /root/install_routeros.sh + fi + EOF + cp ./npkextract/npkextract $ROOTFS_DIR/bin/npkextract + chmod +x $ROOTFS_DIR/bin/npkextract + + cat << 'EOF' | sudo chroot $ROOTFS_DIR /usr/bin/env PATH=/usr/bin:/bin:/usr/sbin:/sbin /bin/sh set -e + echo "nameserver 1.1.1.1" > /etc/resolv.conf apk update apk upgrade - apk add alpine-base openrc busybox-mdev-openrc dhcpcd curl iputils ca-certificates linux-lts linux-firmware-none + apk add alpine-base openrc busybox-mdev-openrc dhcpcd curl ca-certificates linux-lts linux-firmware-none + apk add bash dialog sgdisk dosfstools e2fsprogs util-linux parted + apk add zsh zsh-syntax-highlighting zsh-autosuggestions coreutils ncurses iproute2 + rc-update add devfs sysinit rc-update add dmesg sysinit rc-update add mdev sysinit rc-update add hwdrivers sysinit - rc-update add acpid boot rc-update add hostname boot rc-update add hwclock boot @@ -421,24 +446,32 @@ jobs: rc-update add sysctl boot rc-update add bootmisc boot rc-update add dhcpcd boot - rc-update add networking default + curl -o /root/.zshrc https://gitlab.com/kalilinux/packages/kali-defaults/-/raw/kali/master/etc/skel/.zshrc + sed -i 's/^PROMPT_ALTERNATIVE=twoline/PROMPT_ALTERNATIVE=oneline/' /root/.zshrc + sed -i 's/^NEWLINE_BEFORE_PROMPT=yes/NEWLINE_BEFORE_PROMPT=no/' /root/.zshrc + sed -i 's|/usr/share/zsh-syntax-highlighting|/usr/share/zsh/plugins/zsh-syntax-highlighting|g' /root/.zshrc + sed -i 's|/usr/share/zsh-autosuggestions|/usr/share/zsh/plugins/zsh-autosuggestions|g' /root/.zshrc + + sed -i 's|^root:\(.*\):/bin/sh$|root:\1:/bin/zsh|' /etc/passwd + sed -i 's/^root:.*/root:::0:::::/' /etc/shadow + echo "" > /etc/motd + sed -i 's/^features=.*/features="ata base cdrom network scsi nvme ext2 usb virtio"/' /etc/mkinitfs/mkinitfs.conf + KERNEL_VERSION=$(ls /lib/modules | head -1) + mkinitfs -k -t /tmp "$KERNEL_VERSION" + rm -rf /lib/modules + mv /tmp/lib/modules /lib + depmod "$KERNEL_VERSION" + ln -sf sbin/init /init + apk cache clean EOF - sudo sed -i 's/^features=.*/features="ata base cdrom network scsi nvme ext4 usb virtio"/' $ROOTFS_DIR/etc/mkinitfs/mkinitfs.conf - KERNEL_VERSION=$(ls $ROOTFS_DIR/lib/modules | head -1) - sudo chroot $ROOTFS_DIR /usr/bin/env PATH=/usr/bin:/bin:/usr/sbin:/sbin /sbin/mkinitfs -k -t /tmp "$KERNEL_VERSION" - sudo rm -rf $ROOTFS_DIR/lib/modules - sudo mv $ROOTFS_DIR/tmp/lib/modules $ROOTFS_DIR/lib - sudo chroot $ROOTFS_DIR /sbin/depmod -b / "$KERNEL_VERSION" - sudo umount -v $ROOTFS_DIR/dev sudo umount -v $ROOTFS_DIR/sys sudo umount -v $ROOTFS_DIR/proc - sudo ln -sf sbin/init $ROOTFS_DIR/init cp $ROOTFS_DIR/boot/vmlinuz-* $BUILD_DIR/vmlinuz sudo rm -rf $ROOTFS_DIR/tmp/* diff --git a/install_routeros.sh b/install_routeros.sh new file mode 100644 index 0000000..266382d --- /dev/null +++ b/install_routeros.sh @@ -0,0 +1,1088 @@ +#!/bin/zsh +BACKTITLE="MikroTik RouterOS LiveCD Installer" +TITLE="Installation Wizard" + +WORK_DIR="${WORK_DIR:-/tmp/ros_install}" +BUILD_DIR="$WORK_DIR/build" +BACKUP_DIR="$WORK_DIR/rw_backup" +BOOT_MNT="$WORK_DIR/mnt_boot" +ROS_MNT="$WORK_DIR/mnt_ros" +PROBE_MNT="$WORK_DIR/mnt_probe" +LOG_FILE="$WORK_DIR/install.log" + +NPK_DIR="" +MAIN_NPK="" +OPT_NPK="" +typeset -a COMP_NPK_LIST +typeset -a SELECTED_PACKAGES +TARGET_DISK="" +KEEP_CONFIG="no" +EXISTING_ROS_PART="" +LAST_ERROR="" + +# ------------------------------------------------------------------------------ +# Helper functions: Logging & Cleanup +# ------------------------------------------------------------------------------ +log() { + local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*" + echo "$msg" >> "$LOG_FILE" +} + +cleanup_mounts() { + sync + umount "$BOOT_MNT" 2>/dev/null || true + umount "$ROS_MNT" 2>/dev/null || true + if [[ -d "$PROBE_MNT" ]]; then + for m in "$PROBE_MNT"/*(N/); do + umount "$m" 2>/dev/null || true + done + fi +} + +# ------------------------------------------------------------------------------ +# Filter: Detect CD-ROM and USB devices +# ------------------------------------------------------------------------------ +is_cdrom() { + local dev="$1" + [[ -b "$dev" ]] || return 1 + local name="${dev:t}" + + # 1. Standard optical drive names (/dev/sr*, /dev/cdrom*) + [[ "$name" =~ ^(sr|cdrom) ]] && return 0 + + # 2. Optical filesystem signatures (ISO9660, UDF) + local blkinfo + blkinfo=$(blkid "$dev" 2>/dev/null) + if [[ "$blkinfo" =~ (TYPE=\"iso9660\"|TYPE=\"udf\"|iso9660|udf) ]]; then + return 0 + fi + + # 3. SCSI device type 5 (CD-ROM/DVD) + local diskname="$name" + if [[ -f "/sys/class/block/$name/partition" ]]; then + diskname=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -n 1) + [[ -z "$diskname" ]] && diskname="${name%%[0-9]*}" + fi + if [[ -f "/sys/block/$diskname/device/type" ]]; then + local dev_type + dev_type=$(cat "/sys/block/$diskname/device/type" 2>/dev/null) + [[ "$dev_type" == "5" ]] && return 0 + fi + + return 1 +} + +is_usb() { + local dev="$1" + [[ -b "$dev" ]] || return 1 + local name="${dev:t}" + + local diskname="$name" + if [[ -f "/sys/class/block/$name/partition" ]]; then + diskname=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -n 1) + [[ -z "$diskname" ]] && diskname="${name%%[0-9]*}" + fi + + # 1. sysfs device tree path containing /usb + local syspath + syspath=$(readlink -f "/sys/class/block/$diskname" 2>/dev/null || readlink -f "/sys/block/$diskname" 2>/dev/null) + if [[ "$syspath" == *"/usb"* ]]; then + return 0 + fi + + # 2. lsblk transport attribute + local tran + tran=$(lsblk -no TRAN "$dev" 2>/dev/null | head -n 1) + [[ "$tran" == "usb" ]] && return 0 + + # 3. udev property + if type udevadm >/dev/null 2>&1; then + local bus + bus=$(udevadm info -q property -n "$dev" 2>/dev/null | grep -E '^ID_BUS=' | cut -d= -f2) + [[ "$bus" == "usb" ]] && return 0 + fi + + return 1 +} + +is_cdrom_or_usb() { + is_cdrom "$1" || is_usb "$1" +} + +# ------------------------------------------------------------------------------ +# Check if a device or its symlink is already mounted +# ------------------------------------------------------------------------------ +is_device_mounted() { + local dev="$1" + [[ -b "$dev" ]] || return 1 + local realdev + realdev=$(readlink -f "$dev" 2>/dev/null || echo "$dev") + + if awk '{print $1}' /proc/mounts 2>/dev/null | grep -q -x -E "($realdev|$dev)"; then + return 0 + fi + return 1 +} + +# ------------------------------------------------------------------------------ +# Discover and Mount Installation Media +# ------------------------------------------------------------------------------ +discover_and_mount_media() { + mkdir -p /media/cdrom /media/usb "$PROBE_MNT" + + # 1. Check optical drives (/dev/sr0, /dev/cdrom, etc.) + for dev in /dev/sr[0-9]* /dev/cdrom*(N); do + [[ -b "$dev" ]] || continue + + is_device_mounted "$dev" && continue + + local realdev + realdev=$(readlink -f "$dev" 2>/dev/null || echo "$dev") + is_device_mounted "$realdev" && continue + + local target_mnt="$PROBE_MNT/${realdev:t}" + if ! grep -q " /media/cdrom " /proc/mounts 2>/dev/null; then + target_mnt="/media/cdrom" + fi + mkdir -p "$target_mnt" + + mount -t iso9660 -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \ + mount -t udf -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \ + mount -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || true + done + + # 2. Scan block devices from /sys/class/block/ (USB flash drives, etc.) + for b in /sys/class/block/*(N); do + local dev="/dev/${b:t}" + [[ -b "$dev" ]] || continue + + is_cdrom_or_usb "$dev" || continue + + is_device_mounted "$dev" && continue + + local realdev + realdev=$(readlink -f "$dev" 2>/dev/null || echo "$dev") + is_device_mounted "$realdev" && continue + + local target_mnt="$PROBE_MNT/${realdev:t}" + if ! grep -q " /media/usb " /proc/mounts 2>/dev/null; then + target_mnt="/media/usb" + fi + mkdir -p "$target_mnt" + + mount -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \ + mount -t iso9660 -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \ + mount -t udf -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \ + mount -t vfat -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \ + mount -t ext4 -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \ + mount -t ntfs -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || true + done +} + +get_media_dirs() { + local -a dirs + + # Inspect all currently mounted directories from /proc/mounts + while read -r dev mnt rest; do + [[ -d "$mnt" ]] || continue + [[ "$mnt" == "/" || "$mnt" == "/proc"* || "$mnt" == "/sys"* || "$mnt" == "/dev"* ]] && continue + + if is_cdrom_or_usb "$dev" || [[ "$mnt" =~ ^(/media|/cdrom|/mnt|/run/media|"$PROBE_MNT") ]]; then + dirs+=("$mnt") + fi + done < /proc/mounts + + # Deduplicate mount points + local -a unique_dirs + for d in "${dirs[@]}"; do + [[ -d "$d" ]] || continue + if ! (( ${unique_dirs[(Ie)$d]} )); then + unique_dirs+=("$d") + fi + done + + echo "${unique_dirs[@]}" +} + +# ------------------------------------------------------------------------------ +# Step 1: Scan & Mount Media for NPK packages +# ------------------------------------------------------------------------------ +scan_media_npk() { + local fifo="$WORK_DIR/gauge_scan_fifo" + rm -f "$fifo" + mkfifo "$fifo" + + dialog --backtitle "$BACKTITLE" --title "$TITLE" --gauge "Detecting and mounting installation media..." 8 70 0 < "$fifo" & + local gauge_pid=$! + exec 3> "$fifo" + + gauge_update() { + local pct="$1" + local msg="$2" + printf "XXX\n%d\n%s\nXXX\n" "$pct" "$msg" >&3 2>/dev/null || true + } + + gauge_update 10 "Checking and mounting CD-ROM and USB media..." + discover_and_mount_media + sleep 0.3 + + local media_dirs=($(get_media_dirs)) + log "Found media directories to scan: ${media_dirs[*]}" + + if [[ ${#media_dirs[@]} -eq 0 ]]; then + gauge_update 100 "No CD-ROM or USB media detected." + sleep 0.5 + exec 3>&- + kill $gauge_pid 2>/dev/null || true + wait $gauge_pid 2>/dev/null || true + rm -f "$fifo" + return 1 + fi + + gauge_update 30 "Searching for RouterOS packages (*.npk) on media..." + sleep 0.2 + + MAIN_NPK="" + OPT_NPK="" + COMP_NPK_LIST=() + local -a found_files + + local idx=0 + for m in "${media_dirs[@]}"; do + idx=$(( idx + 1 )) + local cur_pct=$(( 30 + idx * 10 )) + [[ $cur_pct -gt 70 ]] && cur_pct=70 + gauge_update "$cur_pct" "Scanning media: $m" + + for f in "$m"/*.npk(N) "$m"/*/*.npk(N) "$m"/packages/*.npk(N); do + [[ -f "$f" ]] || continue + if ! (( ${found_files[(Ie)$f]} )); then + found_files+=("$f") + log "Found NPK: $f" + fi + done + done + + gauge_update 80 "Classifying NPK packages..." + sleep 0.2 + + # Deduplicate component packages by package name tag + typeset -A seen_tags + for f in "${found_files[@]}"; do + local bn=$(basename "$f") + case "$bn" in + routeros-*.npk) + [[ -z "$MAIN_NPK" ]] && MAIN_NPK="$f" + ;; + option-*.npk) + [[ -z "$OPT_NPK" ]] && OPT_NPK="$f" + ;; + *) + local tag="${bn%.npk}" + tag="${tag%-[0-9]*}" + if [[ -z "${seen_tags[$tag]}" ]]; then + seen_tags[$tag]=1 + COMP_NPK_LIST+=("$f") + fi + ;; + esac + done + + if [[ -n "$MAIN_NPK" ]]; then + NPK_DIR="${MAIN_NPK:h}" + gauge_update 100 "Found main package: $(basename "$MAIN_NPK")" + log "Scanned MAIN_NPK: $MAIN_NPK" + [[ -n "$OPT_NPK" ]] && log "Scanned OPT_NPK: $OPT_NPK" + log "Scanned components count: ${#COMP_NPK_LIST[@]}" + else + gauge_update 100 "No routeros-*.npk main package found on CD/USB." + log "No routeros-*.npk found on scanned media" + fi + + sleep 0.4 + exec 3>&- + kill $gauge_pid 2>/dev/null || true + wait $gauge_pid 2>/dev/null || true + rm -f "$fifo" + + [[ -n "$MAIN_NPK" ]] && return 0 + return 1 +} + +step_scan() { + while :; do + if scan_media_npk; then + return 0 + fi + + # Main NPK not found prompt + dialog --backtitle "$BACKTITLE" --title "$TITLE" \ + --yes-label "Retry" \ + --no-label "Exit" \ + --yesno "No RouterOS main package (routeros-*.npk) was found on CD-ROM or USB media.\n\nPlease connect the CD-ROM or USB installation drive.\n\n- Select [Retry] to rescan CD-ROM and USB media\n- Select [Exit] to cancel and return to shell" 13 70 + local rc=$? + if [[ $rc -eq 0 ]]; then + continue + else + return 255 + fi + done +} + +# ------------------------------------------------------------------------------ +# Step 2: Component Package Selection +# ------------------------------------------------------------------------------ +step_packages() { + if [[ ${#COMP_NPK_LIST[@]} -eq 0 ]]; then + dialog --backtitle "$BACKTITLE" --title "$TITLE" \ + --ok-label "Next" \ + --cancel-label "Back" \ + --extra-button --extra-label "Exit" \ + --msgbox "No optional component packages found on media. Only the main system package will be installed.\n\nMain package: $(basename "$MAIN_NPK")" 11 70 + local rc=$? + case $rc in + 0) return 0 ;; # Next + 1) return 1 ;; # Back + 3|255) return 255 ;; # Exit + esac + fi + + local -a check_items + typeset -A added_tags + for f in "${COMP_NPK_LIST[@]}"; do + local bn=$(basename "$f") + local tag="${bn%.npk}" + tag="${tag%-[0-9]*}" + + # Strictly ensure unique tags + [[ -n "${added_tags[$tag]}" ]] && continue + added_tags[$tag]=1 + + local is_on="ON" + if [[ ${#SELECTED_PACKAGES[@]} -gt 0 ]]; then + if (( ${SELECTED_PACKAGES[(Ie)$tag]} )); then + is_on="ON" + else + is_on="OFF" + fi + fi + check_items+=("$tag" "$bn" "$is_on") + done + + local choice + choice=$(dialog --stdout --backtitle "$BACKTITLE" --title "$TITLE" \ + --separate-output \ + --ok-label "Next" \ + --cancel-label "Back" \ + --extra-button --extra-label "Exit" \ + --checklist "Select optional component packages to install (SPACE to toggle, TAB to switch buttons):" 18 72 8 \ + "${check_items[@]}") + local rc=$? + + log "step_packages returned code: $rc, choice: ${choice:-}" + + case $rc in + 0) + if [[ -n "$choice" ]]; then + SELECTED_PACKAGES=(${(f)choice}) + else + SELECTED_PACKAGES=() + fi + log "Selected packages: ${SELECTED_PACKAGES[*]}" + return 0 + ;; + 1) + # Back to step 0 + return 1 + ;; + 3|255) + # Exit wizard + return 255 + ;; + esac +} + +# ------------------------------------------------------------------------------ +# Step 3: Target Disk Selection +# ------------------------------------------------------------------------------ +step_disk() { + local -a menu_items + + # Scan all disk block devices: SATA, SAS, NVMe, VirtIO (vda), Xen (xvda), IDE (hda), eMMC (mmcblk) + while read -r dev size type model; do + [[ "$type" == "disk" ]] || continue + + local devname="${dev:t}" + # Exclude RAM, Loop, ZRAM, and optical CD-ROM devices + [[ "$devname" =~ ^(loop|ram|zram|sr) ]] && continue + + # Exclude USB flash drives and USB installation media + if is_cdrom_or_usb "$dev"; then + continue + fi + + # Construct friendly description + local desc="${size}" + if [[ -n "$model" ]]; then + desc+=" ${model}" + else + if [[ "$devname" =~ ^vd ]]; then + desc+=" VirtIO-Disk" + elif [[ "$devname" =~ ^xvd ]]; then + desc+=" Xen-VDisk" + elif [[ "$devname" =~ ^nvme ]]; then + desc+=" NVMe-SSD" + elif [[ "$devname" =~ ^hd ]]; then + desc+=" IDE/ATA-Disk" + elif [[ "$devname" =~ ^sd ]]; then + desc+=" SATA/SCSI-Disk" + elif [[ "$devname" =~ ^mmcblk ]]; then + desc+=" eMMC-Storage" + else + desc+=" Hard-Disk" + fi + fi + + menu_items+=("$dev" "$desc") + done < <(lsblk -d -p -n -o NAME,SIZE,TYPE,MODEL 2>/dev/null) + + if [[ ${#menu_items[@]} -eq 0 ]]; then + dialog --backtitle "$BACKTITLE" --title "$TITLE" \ + --msgbox "No usable target hard disk found!\n\n(USB drives and CD-ROMs are excluded from target disks)" 11 62 + return 1 + fi + + local choice + choice=$(dialog --stdout --backtitle "$BACKTITLE" --title "$TITLE" \ + --ok-label "Next" \ + --cancel-label "Back" \ + --extra-button --extra-label "Exit" \ + --menu "Select target disk for RouterOS installation (Physical & Virtual Disks):\n\n[WARNING] ALL EXISTING DATA ON THIS DISK WILL BE ERASED!" 18 74 7 \ + "${menu_items[@]}") + local rc=$? + + case $rc in + 0) + TARGET_DISK="$choice" + log "Target disk selected: $TARGET_DISK" + return 0 + ;; + 1) + return 1 + ;; + 3|255) + return 255 + ;; + esac +} + +# ------------------------------------------------------------------------------ +# Step 4: Existing Installation & Config Retention +# ------------------------------------------------------------------------------ +step_existing() { + EXISTING_ROS_PART="" + EXISTING_ROS_PART=$(blkid -L "RouterOS" 2>/dev/null | head -n 1) + + if [[ -z "$EXISTING_ROS_PART" ]]; then + mkdir -p "$ROS_MNT" + for dev in $(lsblk -p -o NAME -n 2>/dev/null | grep -E '^/dev/'); do + if mount -o ro "$dev" "$ROS_MNT" 2>/dev/null; then + if [[ -f "$ROS_MNT/rw/store" || -d "$ROS_MNT/rw/disk" ]]; then + EXISTING_ROS_PART="$dev" + umount "$ROS_MNT" 2>/dev/null || true + break + fi + umount "$ROS_MNT" 2>/dev/null || true + fi + done + fi + + if [[ -n "$EXISTING_ROS_PART" ]]; then + dialog --backtitle "$BACKTITLE" --title "$TITLE" \ + --yes-label "Keep Config" \ + --no-label "Clean Install" \ + --extra-button --extra-label "Back" \ + --yesno "An existing RouterOS installation was detected ($EXISTING_ROS_PART).\n\nDo you want to backup and preserve existing configuration (/rw)?\n\n- [Keep Config]: Backup and restore /rw after installation\n- [Clean Install]: Completely wipe configuration" 15 70 + local rc=$? + case $rc in + 0) + KEEP_CONFIG="yes" + log "Keep config: yes (from $EXISTING_ROS_PART)" + return 0 + ;; + 1) + KEEP_CONFIG="no" + log "Keep config: no" + return 0 + ;; + 3) + # Back to step 2 + return 1 + ;; + 255) + return 255 + ;; + esac + fi + + KEEP_CONFIG="no" + return 0 +} + +# ------------------------------------------------------------------------------ +# Step 5: Installation Summary & Execution +# ------------------------------------------------------------------------------ +step_install() { + local pkgs_str="None (Main system only)" + if [[ ${#SELECTED_PACKAGES[@]} -gt 0 ]]; then + pkgs_str="${(j:, :)SELECTED_PACKAGES}" + fi + + local summary="Please review the installation details:\n\n" + summary+=" • Media Directory : $NPK_DIR\n" + summary+=" • Main Package : $(basename "$MAIN_NPK")\n" + summary+=" • Components : $pkgs_str\n" + summary+=" • Target Disk : $TARGET_DISK\n" + summary+=" • Keep Config : $KEEP_CONFIG\n\n" + summary+="[WARNING] Clicking [Install] will partition and format $TARGET_DISK.\nALL EXISTING DATA WILL BE PERMANENTLY ERASED!" + + dialog --backtitle "$BACKTITLE" --title "$TITLE" \ + --ok-label "Install" \ + --cancel-label "Back" \ + --extra-button --extra-label "Exit" \ + --yesno "$summary" 17 72 + local rc=$? + + case $rc in + 0) ;; # Proceed with installation + 1) return 1 ;; # Back to step 3 + 3|255) return 255 ;; # Exit + esac + + # Start Installation + : > "$LOG_FILE" + LAST_ERROR="" + + local fifo="$WORK_DIR/gauge_install_fifo" + rm -f "$fifo" + mkfifo "$fifo" + + # Expanded height (16 rows) to display rolling progress steps and results + dialog --backtitle "$BACKTITLE" --title "$TITLE" --gauge "Preparing installation environment..." 16 76 0 < "$fifo" & + local gauge_pid=$! + exec 3> "$fifo" + + typeset -a INSTALL_LOG_STEPS + + gauge_install_log() { + local pct="$1" + local step_state="$2" # "run", "done", "fail" + local msg="$3" + + if [[ "$step_state" == "run" ]]; then + INSTALL_LOG_STEPS+=(" [->] $msg") + elif [[ "$step_state" == "done" ]]; then + if [[ ${#INSTALL_LOG_STEPS[@]} -gt 0 && "${INSTALL_LOG_STEPS[-1]}" =~ "^\ \[->\]" ]]; then + INSTALL_LOG_STEPS[-1]=" [OK] $msg" + else + INSTALL_LOG_STEPS+=(" [OK] $msg") + fi + elif [[ "$step_state" == "fail" ]]; then + INSTALL_LOG_STEPS+=(" [ERR] $msg") + fi + + # Keep last 7 lines in the display window + while [[ ${#INSTALL_LOG_STEPS[@]} -gt 7 ]]; do + shift INSTALL_LOG_STEPS + done + + local text="${(F)INSTALL_LOG_STEPS}" + printf "XXX\n%d\n%s\nXXX\n" "$pct" "$text" >&3 2>/dev/null || true + } + + # Execution Routine + do_install_process() { + set -e + trap 'LAST_ERROR="Command failed at line $LINENO: $BASH_COMMAND"' ERR + + # 1. Environment prep & config backup + gauge_install_log 5 "run" "Step 1/8: Initializing environment & kernel modules..." + log "Step 1: Init environment" + cleanup_mounts + mkdir -p "$BUILD_DIR" "$BACKUP_DIR" "$BOOT_MNT" "$ROS_MNT" + rm -rf "$BUILD_DIR/sq" + sleep 0.3 + + if [[ "$KEEP_CONFIG" == "yes" && -n "$EXISTING_ROS_PART" ]]; then + gauge_install_log 10 "run" "Backing up existing configuration (/rw)..." + log "Backing up config from $EXISTING_ROS_PART" + sleep 0.3 + mkdir -p "$ROS_MNT" + if mount -o ro "$EXISTING_ROS_PART" "$ROS_MNT" 2>/dev/null; then + rm -rf "$BACKUP_DIR/rw" + mkdir -p "$BACKUP_DIR/rw" + cp -a "$ROS_MNT/rw/." "$BACKUP_DIR/rw/" 2>/dev/null || true + umount "$ROS_MNT" 2>/dev/null || true + log "Config backup finished" + gauge_install_log 12 "done" "Step 1/8: Environment ready & config backed up" + else + log "Warning: Could not mount $EXISTING_ROS_PART to backup /rw" + gauge_install_log 12 "done" "Step 1/8: Environment ready (config backup skipped)" + sleep 0.3 + fi + else + gauge_install_log 12 "done" "Step 1/8: Environment & modules initialized" + sleep 0.3 + fi + + + # 2. Extract NPK FILE_CONTAINER (bootloader, milo, EFI files) + gauge_install_log 18 "run" "Step 2/8: Extracting NPK FILE_CONTAINER (boot files)..." + log "Step 2: Extracting NPK FILE_CONTAINER from $MAIN_NPK" + sleep 0.3 + local sq="$BUILD_DIR/sq" + rm -rf "$sq" && mkdir -p "$sq" + + local npkextract_bin + npkextract_bin=$(command -v npkextract 2>/dev/null || echo "") + if [[ -z "$npkextract_bin" ]]; then + gauge_install_log 18 "fail" "Step 2/8: npkextract not found in PATH" + LAST_ERROR="npkextract is required but not found. Please ensure it is installed in the LiveCD." + return 1 + fi + + log "Using npkextract: $npkextract_bin" + if ! "$npkextract_bin" "$MAIN_NPK" "$sq" >> "$LOG_FILE" 2>&1; then + gauge_install_log 18 "fail" "Step 2/8: npkextract failed on $MAIN_NPK" + LAST_ERROR="npkextract failed to extract FILE_CONTAINER from $MAIN_NPK" + return 1 + fi + + # Verify key boot files were extracted + if [[ ! -f "$sq/bin/milo" && ! -f "$sq/boot/EFI/BOOT/BOOTX64.EFI" ]]; then + gauge_install_log 18 "fail" "Step 2/8: No boot files found after extraction" + LAST_ERROR="npkextract ran but neither milo nor BOOTX64.EFI was found in the NPK" + return 1 + fi + + log "Extracted: milo=$(test -f $sq/bin/milo && echo yes || echo no), EFI=$(test -f $sq/boot/EFI/BOOT/BOOTX64.EFI && echo yes || echo no)" + gauge_install_log 25 "done" "Step 2/8: Boot files extracted (milo + EFI ready)" + sleep 0.3 + + # Detect boot mode early (needed for partitioning and bootloader steps) + local is_uefi=0 + if [[ -d /sys/firmware/efi ]]; then + is_uefi=1 + log "Boot mode: UEFI (EFI runtime variables found at /sys/firmware/efi)" + else + log "Boot mode: BIOS/Legacy (no EFI runtime variables found)" + fi + + # 3. Disk partitioning + gauge_install_log 32 "run" "Step 3/8: Partitioning $TARGET_DISK ..." + log "Step 3: Partitioning $TARGET_DISK (is_uefi=$is_uefi)" + sleep 0.3 + + dd if=/dev/zero of="$TARGET_DISK" bs=512 count=1 conv=notrunc + sync + # Build common sgdisk arguments + local sgdisk_args=( + --zap-all + --set-alignment=1 + --new=1:34:+32M --typecode=1:8300 --change-name=1:"RouterOS Boot" --attributes=1:set:2 + --new=2:0:-4096 --typecode=2:8300 --change-name=2:"RouterOS" + ) + if ! sgdisk "${sgdisk_args[@]}" --gpttombr=1:2 "$TARGET_DISK" >> "$LOG_FILE" 2>&1; then + gauge_install_log 32 "fail" "Step 3/8: sgdisk (BIOS+gpttombr) failed on $TARGET_DISK" + LAST_ERROR="sgdisk --gpttombr failed on $TARGET_DISK" + return 1 + fi + + # Save the hybrid MBR (first 512 bytes, which now has MBR partition table) + local mbr_tmp="$BUILD_DIR/mbr.bin" + dd if="$TARGET_DISK" of="$mbr_tmp" bs=512 count=1 conv=notrunc >> "$LOG_FILE" 2>&1 || { + gauge_install_log 32 "fail" "Step 3/8: Failed to read MBR from $TARGET_DISK" + LAST_ERROR="dd failed to read MBR from $TARGET_DISK" + return 1 + } + # Byte 446: set to 0x80 (mark first MBR partition as active/bootable) + printf '\x80' | dd of="$mbr_tmp" bs=1 count=1 seek=446 conv=notrunc >> "$LOG_FILE" 2>&1 || true + if [[ $is_uefi -eq 0 ]]; then + # Write MikroTik BIOS bootstrap code into bytes 0-439 of the saved MBR + printf '%s' "FA31C08ED0BC007C89E65007501FFBFCBF0006B90001F2A5EA1D060000BEBE07B304803C807423803C00750983C610FECB75EFCD18BE9B06AC3C00740B56BB0700B40ECD105EEBF0EBFE8B148B4C0289F5BF0500BB007CB8010257CD135F730C31C0CD134F75EDBE7C06EBCCBFFE7D813D55AA75C289EEEA007C00004572726F72206C6F6164696E67206F7065726174696E672073797374656D004D697373696E67206F7065726174696E672073797374656D0000000000" \ + | xxd -r -p \ + | dd of="$mbr_tmp" bs=1 count=440 conv=notrunc >> "$LOG_FILE" 2>&1 || { + gauge_install_log 32 "fail" "Step 3/8: Failed to write MBR bootstrap code" + LAST_ERROR="Failed to write MikroTik BIOS bootstrap into MBR" + return 1 + } + fi + # Second pass: recreate clean GPT (without --gpttombr, so GPT backup is clean) + if ! sgdisk "${sgdisk_args[@]}" "$TARGET_DISK" >> "$LOG_FILE" 2>&1; then + gauge_install_log 32 "fail" "Step 3/8: sgdisk (GPT rebuild) failed on $TARGET_DISK" + LAST_ERROR="sgdisk GPT rebuild failed on $TARGET_DISK" + return 1 + fi + # Restore MBR (bootstrap + hybrid MBR partition table) + dd if="$mbr_tmp" of="$TARGET_DISK" bs=512 count=1 conv=notrunc >> "$LOG_FILE" 2>&1 || { + gauge_install_log 32 "fail" "Step 3/8: Failed to write MBR to $TARGET_DISK" + LAST_ERROR="dd failed to write MBR back to $TARGET_DISK" + return 1 + } + rm -f "$mbr_tmp" + log "BIOS hybrid MBR written to $TARGET_DISK" + + # Force kernel to reread partition table & trigger mdev/udev + partprobe "$TARGET_DISK" >> "$LOG_FILE" 2>&1 || true + blockdev --rereadpt "$TARGET_DISK" 2>/dev/null || true + partx -u "$TARGET_DISK" 2>/dev/null || partx -a "$TARGET_DISK" 2>/dev/null || true + mdev -s 2>/dev/null || true + type udevadm >/dev/null 2>&1 && udevadm settle 2>/dev/null || true + + # Calculate partition paths (nvme0n1p1, mmcblk0p1, vda1, sda1, etc.) + local bootp rosp + if [[ "$TARGET_DISK" =~ [0-9]$ ]]; then + bootp="${TARGET_DISK}p1" + rosp="${TARGET_DISK}p2" + else + bootp="${TARGET_DISK}1" + rosp="${TARGET_DISK}2" + fi + + # Wait and ensure partition device nodes exist in /dev + for p in "$bootp" "$rosp"; do + local wait_count=0 + while [[ ! -b "$p" && $wait_count -lt 15 ]]; do + sleep 0.2 + wait_count=$(( wait_count + 1 )) + mdev -s 2>/dev/null || true + + local pname="${p:t}" + if [[ -f "/sys/class/block/$pname/dev" ]]; then + local maj_min=$(cat "/sys/class/block/$pname/dev") + local maj="${maj_min%%:*}" + local min="${maj_min#*:}" + mknod "$p" b "$maj" "$min" 2>/dev/null || true + fi + done + if [[ ! -b "$p" ]]; then + gauge_install_log 35 "fail" "Step 3/8: Partition device node $p missing" + LAST_ERROR="Partition device node $p was not created by kernel" + return 1 + fi + done + gauge_install_log 45 "done" "Step 3/8: Partitioned $TARGET_DISK (Boot 32M + System)" + sleep 0.3 + + + # 4. Format partitions + if [[ $is_uefi -eq 1 ]]; then + gauge_install_log 48 "run" "Step 4/8: Formatting $bootp (FAT) & $rosp (EXT4)..." + log "Step 4: Formatting $bootp (vfat) and $rosp (ext4)" + if ! mkfs.vfat -n "Boot" "$bootp" >> "$LOG_FILE" 2>&1 && ! mkfs.vfat -n "Boot" "$bootp" >> "$LOG_FILE" 2>&1; then + gauge_install_log 48 "fail" "Step 4/8: Failed to format Boot partition ($bootp)" + LAST_ERROR="mkfs.vfat failed to format Boot partition $bootp" + return 1 + fi + else + gauge_install_log 48 "run" "Step 4/8: Formatting $bootp (EXT2) & $rosp (EXT4)..." + log "Step 4: Formatting $bootp (ext2) and $rosp (ext4)" + if ! mkfs.ext2 -F -m 0 -L "RouterOS Boot" "$bootp" >> "$LOG_FILE" 2>&1 && ! mkfs.ext2 -F -m 0 -L "RouterOS Boot" "$bootp" >> "$LOG_FILE" 2>&1; then + gauge_install_log 48 "fail" "Step 4/8: Failed to format Boot partition ($bootp)" + LAST_ERROR="mkfs.ext2 failed to format Boot partition $bootp" + return 1 + fi + fi + sleep 0.3 + if ! mkfs.ext4 -F -m 0 -L "RouterOS" -m 0 "$rosp" >> "$LOG_FILE" 2>&1; then + gauge_install_log 48 "fail" "Step 4/8: Failed to format RouterOS partition ($rosp)" + LAST_ERROR="mkfs.ext4 failed to format RouterOS partition $rosp" + return 1 + fi + + gauge_install_log 58 "done" "Step 4/8: Formatted $bootp & $rosp" + sleep 0.3 + + # 5. Mount and prepare system dirs + gauge_install_log 62 "run" "Step 5/8: Mounting partitions & creating system layout..." + log "Step 5: Mounting and creating dirs" + sleep 0.3 + mkdir -p "$BOOT_MNT" "$ROS_MNT" + + if [[ $is_uefi -eq 0 ]]; then + if ! mount -t vfat "$bootp" "$BOOT_MNT" >> "$LOG_FILE" 2>&1 && ! mount "$bootp" "$BOOT_MNT" >> "$LOG_FILE" 2>&1; then + gauge_install_log 62 "fail" "Step 5/8: Failed to mount Boot partition ($bootp)" + LAST_ERROR="Failed to mount Boot partition $bootp to $BOOT_MNT" + return 1 + fi + else + if ! mount -t ext2 "$bootp" "$BOOT_MNT" >> "$LOG_FILE" 2>&1 && ! mount "$bootp" "$BOOT_MNT" >> "$LOG_FILE" 2>&1; then + gauge_install_log 62 "fail" "Step 5/8: Failed to mount Boot partition ($bootp)" + LAST_ERROR="Failed to mount Boot partition $bootp to $BOOT_MNT" + return 1 + fi + fi + + if ! mount -t ext4 "$rosp" "$ROS_MNT" >> "$LOG_FILE" 2>&1 && ! mount "$rosp" "$ROS_MNT" >> "$LOG_FILE" 2>&1; then + gauge_install_log 62 "fail" "Step 5/8: Failed to mount RouterOS partition ($rosp)" + LAST_ERROR="Failed to mount RouterOS partition $rosp to $ROS_MNT" + return 1 + fi + + mkdir -p "$ROS_MNT/var/pdb/system" "$ROS_MNT/var/pdb/option" "$ROS_MNT/bin" + gauge_install_log 70 "done" "Step 5/8: Partitions mounted & directories created" + sleep 0.3 + + # 6. Install bootloader (EFI & Milo, using is_uefi detected in step 3) + gauge_install_log 73 "run" "Step 6/8: Installing bootloader..." + log "Step 6: Installing bootloader (is_uefi=$is_uefi)" + sleep 0.3 + # Install milo to $ROS_MNT/bin/milo (used by both BIOS and UEFI) + if [[ -f "$sq/bin/milo" ]]; then + cp "$sq/bin/milo" "$ROS_MNT/bin/milo" 2>> "$LOG_FILE" || { + gauge_install_log 73 "fail" "Step 6/8: Failed to copy milo to $ROS_MNT/bin/" + LAST_ERROR="Failed to copy milo to $ROS_MNT/bin/milo" + return 1 + } + chmod 755 "$ROS_MNT/bin/milo" + log "Installed milo -> $ROS_MNT/bin/milo" + else + log "Warning: milo not found in extracted NPK at $sq/bin/milo" + fi + + # Copy the entire boot/ tree (EFI dir, boot map) to BOOT_MNT + if [[ -d "$sq/boot" ]]; then + cp -a "$sq/boot/." "$BOOT_MNT/" 2>> "$LOG_FILE" || { + gauge_install_log 73 "fail" "Step 6/8: Failed to copy boot/ to $BOOT_MNT" + LAST_ERROR="Failed to copy boot directory to $BOOT_MNT" + return 1 + } + log "Copied boot/ tree to $BOOT_MNT" + else + log "Warning: boot/ directory not found in extracted NPK at $sq/boot" + fi + + if [[ $is_uefi -eq 1 ]]; then + # UEFI boot: EFI files already in place (BOOTX64.EFI was in boot/EFI/BOOT/) + log "UEFI mode: EFI bootloader in place at $BOOT_MNT/EFI/BOOT/BOOTX64.EFI - milo setup via EFI not needed" + gauge_install_log 80 "done" "Step 6/8: UEFI boot - EFI bootloader installed" + else + # BIOS/Legacy boot: run milo to setup legacy boot sector on BOOT_MNT + if [[ -f "$ROS_MNT/bin/milo" ]]; then + log "BIOS mode: running milo $BOOT_MNT to install legacy boot sector" + "$ROS_MNT/bin/milo" "$BOOT_MNT" >> "$LOG_FILE" 2>&1 || { + log "Warning: milo returned non-zero (may be non-fatal on some hardware)" + } + gauge_install_log 80 "done" "Step 6/8: BIOS boot - Milo legacy boot sector installed" + else + log "Warning: milo not available for BIOS boot setup" + gauge_install_log 80 "done" "Step 6/8: Bootloader step complete (milo skipped)" + fi + fi + sleep 0.3 + + # 7. Copy system images and components + gauge_install_log 83 "run" "Step 7/8: Writing $(basename "$MAIN_NPK") and packages..." + log "Step 7: Writing packages" + sleep 0.3 + if ! cp "$MAIN_NPK" "$ROS_MNT/var/pdb/system/image" 2>> "$LOG_FILE" || [[ ! -s "$ROS_MNT/var/pdb/system/image" ]]; then + gauge_install_log 83 "fail" "Step 7/8: Failed to write system image" + LAST_ERROR="Failed to copy main package to $ROS_MNT/var/pdb/system/image" + return 1 + fi + + if [[ -n "$OPT_NPK" && -f "$OPT_NPK" ]]; then + cp "$OPT_NPK" "$ROS_MNT/var/pdb/option/image" 2>> "$LOG_FILE" || true + fi + + for p in "${SELECTED_PACKAGES[@]}"; do + [[ -z "$p" ]] && continue + local src="" + for f in "${COMP_NPK_LIST[@]}"; do + local bn=$(basename "$f") + if [[ "$bn" == ${p}-* || "$bn" == ${p}.npk ]]; then + src="$f" + break + fi + done + if [[ -n "$src" && -f "$src" ]]; then + log "Installing component $p from $src" + mkdir -p "$ROS_MNT/var/pdb/$p" + cp "$src" "$ROS_MNT/var/pdb/$p/image" 2>> "$LOG_FILE" || log "Warning: Failed to copy $p" + else + log "Warning: component $p source not found" + fi + done + gauge_install_log 90 "done" "Step 7/8: System image & selected components written" + sleep 0.3 + + # 8. Restore config & sync + gauge_install_log 93 "run" "Step 8/8: Finalizing configuration & syncing disks..." + log "Step 8: Finalizing & sync" + sleep 0.3 + mkdir -p "$ROS_MNT/rw/disk" + printf '#!/bin/sh\n# before RouterOS loader start\n' > "$ROS_MNT/rw/disk/rc.local" + chmod +x "$ROS_MNT/rw/disk/rc.local" 2>/dev/null || true + + if [[ "$KEEP_CONFIG" == "yes" && -d "$BACKUP_DIR/rw" ]]; then + log "Restoring backup /rw configuration" + rm -rf "$ROS_MNT/rw" + mv "$BACKUP_DIR/rw" "$ROS_MNT/rw" + mkdir -p "$ROS_MNT/rw/disk" + [[ ! -f "$ROS_MNT/rw/disk/rc.local" ]] && printf '#!/bin/sh\n# before RouterOS loader start\n' > "$ROS_MNT/rw/disk/rc.local" + fi + + sync + cleanup_mounts + sleep 0.4 + gauge_install_log 100 "done" "Step 8/8: Installation completed successfully!" + log "Installation succeeded!" + sleep 0.8 + return 0 + } + + local install_err="" + if do_install_process >> "$LOG_FILE" 2>&1; then + exec 3>&- + kill $gauge_pid 2>/dev/null || true + wait $gauge_pid 2>/dev/null || true + rm -f "$fifo" + return 0 + else + install_err="${LAST_ERROR:-An unexpected error occurred during installation. Please check install log.}" + exec 3>&- + kill $gauge_pid 2>/dev/null || true + wait $gauge_pid 2>/dev/null || true + rm -f "$fifo" + cleanup_mounts + LAST_ERROR="$install_err" + return 2 + fi +} + + +main() { + dialog --backtitle "$BACKTITLE" --title "$TITLE" \ + --yes-label "Yes" \ + --no-label "No" \ + --yesno "Welcome to MikroTik RouterOS Installation Wizard.\n\nDo you want to start the installation now?\n\n- Select [Yes] to enter installation wizard\n- Select [No] to exit and return to shell" 12 62 + local start_rc=$? + + if [[ $start_rc -ne 0 ]]; then + echo "Cancelled by user. Exiting wizard, returning to shell." + exit 0 + fi + + mkdir -p "$BUILD_DIR" "$BACKUP_DIR" "$BOOT_MNT" "$ROS_MNT" "$PROBE_MNT" + local step=0 + local install_result=0 + + while :; do + case $step in + 0) + step_scan + local rc=$? + if [[ $rc -eq 0 ]]; then + step=1 + else + echo "Installation wizard cancelled." + exit 0 + fi + ;; + 1) + step_packages + local rc=$? + if [[ $rc -eq 0 ]]; then + step=2 + elif [[ $rc -eq 1 ]]; then + step=0 + else + echo "Installation wizard cancelled." + exit 0 + fi + ;; + 2) + step_disk + local rc=$? + if [[ $rc -eq 0 ]]; then + step=3 + elif [[ $rc -eq 1 ]]; then + step=1 + else + echo "Installation wizard cancelled." + exit 0 + fi + ;; + 3) + step_existing + local rc=$? + if [[ $rc -eq 0 ]]; then + step=4 + elif [[ $rc -eq 1 ]]; then + step=2 + else + echo "Installation wizard cancelled." + exit 0 + fi + ;; + 4) + step_install + local rc=$? + if [[ $rc -eq 0 ]]; then + install_result=0 + break + elif [[ $rc -eq 1 ]]; then + step=3 + elif [[ $rc -eq 255 ]]; then + echo "Installation wizard cancelled." + exit 0 + else + # Error + install_result=1 + break + fi + ;; + esac + done + + # -------------------------------------------------------------------------- + # Finish & Reboot / Shell Prompt + # -------------------------------------------------------------------------- + if [[ $install_result -eq 0 ]]; then + # Installation Success + dialog --backtitle "$BACKTITLE" --title "$TITLE" \ + --yes-label "Reboot" \ + --no-label "Shell" \ + --yesno "RouterOS has been successfully installed to $TARGET_DISK!\n\n[IMPORTANT] Please remove the CD-ROM or USB installation media before rebooting.\n\nDo you want to reboot the system now?\n\n- Select [Reboot] to restart system\n- Select [Shell] to exit to terminal" 14 70 + local post_rc=$? + if [[ $post_rc -eq 0 ]]; then + echo "Rebooting system..." + reboot + else + echo "Installation completed. Returned to shell." + exit 0 + fi + else + # Installation Failure + local err_detail="${LAST_ERROR}" + if [[ -f "$LOG_FILE" ]]; then + local tail_log=$(tail -n 6 "$LOG_FILE" 2>/dev/null) + [[ -n "$tail_log" ]] && err_detail+="\n\nRecent Log Details:\n$tail_log" + fi + + dialog --backtitle "$BACKTITLE" --title "Installation Failed" \ + --yes-label "Reboot" \ + --no-label "Shell" \ + --yesno "RouterOS installation failed!\n\nReason:\n$err_detail\n\nPlease select next action:" 16 72 + local post_rc=$? + if [[ $post_rc -eq 0 ]]; then + echo "Rebooting system..." + reboot + else + echo "Installation failed. Returned to shell. Log file: $LOG_FILE" + exit 1 + fi + fi +} + +main "$@" diff --git a/npkextract/Makefile b/npkextract/Makefile new file mode 100644 index 0000000..25c614e --- /dev/null +++ b/npkextract/Makefile @@ -0,0 +1,39 @@ +TOOLCHAIN_DIR ?= $(HOME)/musl-toolchains/i686-linux-musl-cross +CROSS_COMPILE ?= $(TOOLCHAIN_DIR)/bin/i686-linux-musl- + +CC = $(CROSS_COMPILE)gcc +STRIP = $(CROSS_COMPILE)strip + +TARGET = npkextract +SRC = npkextract.c +OBJ = $(SRC:.c=.o) + +CFLAGS ?= -O2 -Wall -Wextra -D_GNU_SOURCE +CFLAGS += -static -m32 +LDFLAGS += -static -s + + +.PHONY: all clean info + +all: $(TARGET) + +$(TARGET): $(OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ + @$(STRIP) --strip-all $(TARGET) 2>/dev/null || true + @echo "==========================================================" + @echo " Successfully built static binary: $(TARGET)" + @ls -lh $(TARGET) + @file $(TARGET) 2>/dev/null || true + @echo "==========================================================" + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +info: + @echo "Compiler: $(CC)" + @echo "Strip: $(STRIP)" + @echo "CFLAGS: $(CFLAGS)" + @echo "LDFLAGS: $(LDFLAGS)" + +clean: + rm -f $(TARGET) $(OBJ) diff --git a/npkextract/npkextract b/npkextract/npkextract new file mode 100644 index 0000000000000000000000000000000000000000..5c3f0762881decd1787a9f3f1ef01b9bda083744 GIT binary patch literal 38420 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 JE&~;h;9qcacLV?c literal 0 HcmV?d00001 diff --git a/npkextract/npkextract.c b/npkextract/npkextract.c new file mode 100644 index 0000000..9343b5e --- /dev/null +++ b/npkextract/npkextract.c @@ -0,0 +1,426 @@ +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + const unsigned char *source; + size_t source_len; + size_t tag; + unsigned int bitcount; + unsigned char *dest; + size_t dest_len; + size_t dest_cap; +} TINF_DATA; + +typedef struct { + unsigned short table[16]; + unsigned short trans[288]; +} TINF_TREE; + +static const unsigned char tinf_default_length_bits[30] = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 127 +}; + +static const unsigned short tinf_default_length_base[30] = { + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0 +}; + +static const unsigned char tinf_default_dist_bits[30] = { + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 +}; + +static const unsigned short tinf_default_dist_base[30] = { + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 +}; + +static const unsigned char tinf_clcidx[19] = { + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 +}; + +static unsigned int tinf_getbit(TINF_DATA *d) { + if (!d->bitcount--) { + d->tag = d->source_len ? *d->source++ : 0; + if (d->source_len) d->source_len--; + d->bitcount = 7; + } + unsigned int bit = d->tag & 1; + d->tag >>= 1; + return bit; +} + +static unsigned int tinf_read_bits(TINF_DATA *d, int num, int base) { + unsigned int val = 0; + if (num) { + unsigned int limit = 1 << num; + unsigned int mask; + for (mask = 1; mask < limit; mask <<= 1) { + if (tinf_getbit(d)) val += mask; + } + } + return val + base; +} + +static void tinf_build_tree(TINF_TREE *t, const unsigned char *lengths, unsigned int num) { + unsigned short offs[16]; + unsigned int i, sum = 0; + + for (i = 0; i < 16; ++i) t->table[i] = 0; + for (i = 0; i < num; ++i) t->table[lengths[i]]++; + t->table[0] = 0; + + for (i = 0; i < 16; ++i) { + offs[i] = sum; + sum += t->table[i]; + } + + for (i = 0; i < num; ++i) { + if (lengths[i]) t->trans[offs[lengths[i]]++] = i; + } +} + +static int tinf_decode_symbol(TINF_DATA *d, const TINF_TREE *t) { + int sum = 0, cur = 0, len = 0; + do { + cur = 2 * cur + tinf_getbit(d); + len++; + sum += t->table[len]; + cur -= t->table[len]; + } while (cur >= 0); + return t->trans[sum + cur]; +} + +static void tinf_build_fixed_trees(TINF_TREE *lt, TINF_TREE *dt) { + unsigned char lengths[288]; + int i; + for (i = 0; i <= 143; ++i) lengths[i] = 8; + for (; i <= 255; ++i) lengths[i] = 9; + for (; i <= 279; ++i) lengths[i] = 7; + for (; i <= 287; ++i) lengths[i] = 8; + tinf_build_tree(lt, lengths, 288); + for (i = 0; i < 32; ++i) lengths[i] = 5; + tinf_build_tree(dt, lengths, 32); +} + +static int tinf_inflate_block_data(TINF_DATA *d, TINF_TREE *lt, TINF_TREE *dt) { + while (1) { + int sym = tinf_decode_symbol(d, lt); + if (sym == 256) return 0; + + if (sym < 256) { + if (d->dest_len >= d->dest_cap) { + size_t ncap = d->dest_cap ? d->dest_cap * 2 : 1024 * 1024; + unsigned char *nd = realloc(d->dest, ncap); + if (!nd) return -1; + d->dest = nd; + d->dest_cap = ncap; + } + d->dest[d->dest_len++] = (unsigned char)sym; + } else { + sym -= 257; + int length = tinf_read_bits(d, tinf_default_length_bits[sym], tinf_default_length_base[sym]); + int dist_sym = tinf_decode_symbol(d, dt); + int dist = tinf_read_bits(d, tinf_default_dist_bits[dist_sym], tinf_default_dist_base[dist_sym]); + + if (d->dest_len + length > d->dest_cap) { + size_t ncap = (d->dest_len + length) * 2 + 1024 * 1024; + unsigned char *nd = realloc(d->dest, ncap); + if (!nd) return -1; + d->dest = nd; + d->dest_cap = ncap; + } + + unsigned char *src = d->dest + d->dest_len - dist; + while (length--) { + d->dest[d->dest_len++] = *src++; + } + } + } +} + +static int tinf_inflate_uncompressed_block(TINF_DATA *d) { + d->bitcount = 0; + if (d->source_len < 4) return -1; + unsigned int length = d->source[0] | (d->source[1] << 8); + d->source += 4; + d->source_len -= 4; + if (d->source_len < length) return -1; + + if (d->dest_len + length > d->dest_cap) { + size_t ncap = d->dest_len + length + 1024 * 1024; + unsigned char *nd = realloc(d->dest, ncap); + if (!nd) return -1; + d->dest = nd; + d->dest_cap = ncap; + } + + memcpy(d->dest + d->dest_len, d->source, length); + d->dest_len += length; + d->source += length; + d->source_len -= length; + return 0; +} + +static int tinf_inflate_dynamic_block(TINF_DATA *d) { + TINF_TREE lt, dt, ct; + unsigned char lengths[288 + 32]; + unsigned int hlit = tinf_read_bits(d, 5, 257); + unsigned int hdist = tinf_read_bits(d, 5, 1); + unsigned int hclen = tinf_read_bits(d, 4, 4); + unsigned int i, num; + + unsigned char clen[19]; + memset(clen, 0, sizeof(clen)); + for (i = 0; i < hclen; ++i) clen[tinf_clcidx[i]] = tinf_read_bits(d, 3, 0); + tinf_build_tree(&ct, clen, 19); + + for (num = 0; num < hlit + hdist;) { + int sym = tinf_decode_symbol(d, &ct); + if (sym < 16) { + lengths[num++] = sym; + } else if (sym == 16) { + unsigned char prev = lengths[num - 1]; + for (i = tinf_read_bits(d, 2, 3); i; --i) lengths[num++] = prev; + } else if (sym == 17) { + for (i = tinf_read_bits(d, 3, 3); i; --i) lengths[num++] = 0; + } else { + for (i = tinf_read_bits(d, 7, 11); i; --i) lengths[num++] = 0; + } + } + + tinf_build_tree(<, lengths, hlit); + tinf_build_tree(&dt, lengths + hlit, hdist); + return tinf_inflate_block_data(d, <, &dt); +} + +static unsigned char *zlib_decompress(const unsigned char *src, size_t src_len, size_t *out_len) { + if (src_len < 2) return NULL; + size_t offset = 0; + // Check zlib header (RFC 1950) + if ((src[0] * 256 + src[1]) % 31 == 0 && (src[0] & 0x0F) == 8) { + offset = 2; + if (src[1] & 0x20) offset += 4; + } + + TINF_DATA d; + memset(&d, 0, sizeof(d)); + d.source = src + offset; + d.source_len = src_len > offset ? src_len - offset : 0; + d.dest_cap = src_len * 4 + 1024 * 1024; + d.dest = malloc(d.dest_cap); + if (!d.dest) return NULL; + + int bfinal; + do { + bfinal = tinf_getbit(&d); + int btype = tinf_read_bits(&d, 2, 0); + + if (btype == 0) { + if (tinf_inflate_uncompressed_block(&d) < 0) { free(d.dest); return NULL; } + } else if (btype == 1) { + TINF_TREE lt, dt; + tinf_build_fixed_trees(<, &dt); + if (tinf_inflate_block_data(&d, <, &dt) < 0) { free(d.dest); return NULL; } + } else if (btype == 2) { + if (tinf_inflate_dynamic_block(&d) < 0) { free(d.dest); return NULL; } + } else { + free(d.dest); + return NULL; + } + } while (!bfinal); + + *out_len = d.dest_len; + return d.dest; +} + +static void mkdir_p(const char *dir) { + char tmp[1024]; + snprintf(tmp, sizeof(tmp), "%s", dir); + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + mkdir(tmp, 0755); +} + +const char *get_part_name(uint32_t id) { + switch (id) { + case 0x01: return "NAME_INFO"; + case 0x02: return "DESCRIPTION"; + case 0x03: return "DEPENDENCIES"; + case 0x04: return "FILE_CONTAINER"; + case 0x07: return "INSTALL_SCRIPT"; + case 0x08: return "UNINSTALL_SCRIPT"; + case 0x09: return "SIGNATURE"; + case 0x10: return "ARCHITECTURE"; + case 0x11: return "PKG_CONFLICTS"; + case 0x12: return "PKG_INFO"; + case 0x13: return "FEATURES"; + case 0x14: return "PKG_FEATURES"; + case 0x15: return "SQUASHFS"; + case 0x16: return "NULL_BLOCK"; + case 0x17: return "GIT_COMMIT"; + case 0x18: return "CHANNEL"; + case 0x19: return "HEADER"; + default: return "UNKNOWN"; + } +} + +int main(int argc, char *argv[]) { + if (argc < 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + const char *npk_path = argv[1]; + const char *out_dir = argv[2]; + + FILE *fp = fopen(npk_path, "rb"); + if (!fp) { + fprintf(stderr, "Error: Failed to open NPK file '%s'\n", npk_path); + return 1; + } + + fseek(fp, 0, SEEK_END); + long file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + printf("Parsing NPK: '%s' (%ld bytes)\n", npk_path, file_size); + mkdir_p(out_dir); + + int total_extracted = 0; + long offset = 0; + + while (offset + 6 <= file_size) { + fseek(fp, offset, SEEK_SET); + uint16_t id16; + uint32_t size32; + + if (fread(&id16, 2, 1, fp) != 1 || fread(&size32, 4, 1, fp) != 1) { + break; + } + + if (id16 > 0 && id16 <= 0x20 && size32 <= (uint32_t)(file_size - offset - 6)) { + printf(" [Part 0x%02X: %-15s] offset=%ld, size=%u\n", id16, get_part_name(id16), offset, size32); + + if (id16 == 0x04) { // FILE_CONTAINER + unsigned char *zbuf = malloc(size32); + if (zbuf && fread(zbuf, 1, size32, fp) == size32) { + size_t decomp_len = 0; + unsigned char *decomp = zlib_decompress(zbuf, size32, &decomp_len); + if (decomp) { + printf(" -> Decompressed FILE_CONTAINER: %zu bytes\n", decomp_len); + + unsigned char *p = decomp; + unsigned char *end = decomp + decomp_len; + int item_idx = 0; + + while (p + 30 <= end) { + uint32_t fmode = *(uint32_t *)(p + 0); + uint32_t fmtime = *(uint32_t *)(p + 8); + uint32_t fsize = *(uint32_t *)(p + 24); + uint16_t name_len = *(uint16_t *)(p + 28); + p += 30; + + if (p + name_len > end) { + printf(" -> Header overrun: name_len=%u exceeds buffer\n", name_len); + break; + } + + char fname[512] = {0}; + if (name_len >= sizeof(fname)) name_len = sizeof(fname) - 1; + memcpy(fname, p, name_len); + fname[name_len] = '\0'; + p += name_len; + + if (p + fsize > end) { + printf(" -> Data overrun: '%s' size=%u exceeds buffer\n", fname, fsize); + break; + } + + char full_path[1024]; + snprintf(full_path, sizeof(full_path), "%s/%s", out_dir, fname); + + char dir_path[1024]; + snprintf(dir_path, sizeof(dir_path), "%s", full_path); + char *slash = strrchr(dir_path, '/'); + if (slash) { *slash = '\0'; mkdir_p(dir_path); } + + if (S_ISDIR(fmode)) { + mkdir_p(full_path); + printf(" -> [%d] [DIR] %s/\n", ++item_idx, fname); + } else if (S_ISLNK(fmode)) { + char target[1024] = {0}; + size_t tlen = fsize < sizeof(target) ? fsize : sizeof(target) - 1; + memcpy(target, p, tlen); + unlink(full_path); + symlink(target, full_path); + printf(" -> [%d] [LINK] %s -> %s\n", ++item_idx, fname, target); + total_extracted++; + } else { + FILE *out = fopen(full_path, "wb"); + if (out) { + fwrite(p, 1, fsize, out); + fclose(out); + chmod(full_path, fmode ? (fmode & 07777) : 0644); + + if (fmtime) { + struct utimbuf ut; + ut.actime = fmtime; + ut.modtime = fmtime; + utime(full_path, &ut); + } + + printf(" -> [%d] [FILE] %s (%u bytes, mode=%04o)\n", ++item_idx, fname, fsize, fmode & 07777); + total_extracted++; + } else { + fprintf(stderr, "Warning: Failed to create file '%s'\n", full_path); + } + } + + p += fsize; + } + + free(decomp); + } else { + printf(" -> ZLIB decompression failed for FILE_CONTAINER\n"); + } + } + if (zbuf) free(zbuf); + } else if (id16 == 0x15) { + char sq_file[1024]; + snprintf(sq_file, sizeof(sq_file), "%s/image.squashfs", out_dir); + FILE *out = fopen(sq_file, "wb"); + if (out) { + unsigned char buf[65536]; + uint32_t remaining = size32; + while (remaining > 0) { + size_t to_read = remaining > sizeof(buf) ? sizeof(buf) : remaining; + size_t n = fread(buf, 1, to_read, fp); + if (n == 0) break; + fwrite(buf, 1, n, out); + remaining -= n; + } + fclose(out); + printf(" -> Extracted SQUASHFS image: %s (%u bytes)\n", sq_file, size32); + total_extracted++; + } + } + + offset += 6 + size32; + continue; + } + + offset++; + } + + fclose(fp); + printf("\nExtraction complete: %d item(s) extracted from NPK to '%s'.\n", total_extracted, out_dir); + return 0; +}