Compare commits

..

No commits in common. "main" and "7.20beta9" have entirely different histories.

31 changed files with 1587 additions and 3352 deletions

View File

@ -1,246 +0,0 @@
name: Build CHR Docker
on:
workflow_dispatch:
inputs:
version:
description: "Specify version (e.g., 7.19.2), empty for latest"
required: false
default: ''
type: string
permissions:
packages: write
jobs:
Prepare:
runs-on: ubuntu-24.04
outputs:
MATRIX: ${{ steps.get_versions.outputs.MATRIX }}
steps:
- name: Get versions and generate matrix
id: get_versions
run: |
VERSION="${{ inputs.version }}"
if [ -n "$VERSION" ]; then
MATRIX=$(jq -c -n '
[
{ "version": $VERSION, "channel": ""}
]
' --arg VERSION "$VERSION")
else
TIMESTAMP=$(date +%s)
LONG_TERM=$(wget -nv -O - "https://upgrade.mikrotik.ltd/routeros/NEWESTa7.long-term?t=$TIMESTAMP" | cut -d ' ' -f1)
STABLE=$(wget -nv -O - "https://upgrade.mikrotik.ltd/routeros/NEWESTa7.stable?t=$TIMESTAMP" | cut -d ' ' -f1)
echo "Long-term version: $LONG_TERM"
echo "Stable version: $STABLE"
if [ -z "$LONG_TERM" ]; then
echo "Failed to get long-term version"
exit 1
fi
if [ -z "$STABLE" ]; then
echo "Failed to get stable version"
exit 1
fi
MATRIX=$(jq -c -n '
[
{version: $LONG_TERM, channel: "long-term"},
{version: $STABLE, channel: "stable"}
]
' --arg LONG_TERM "$LONG_TERM" --arg STABLE "$STABLE")
fi
echo "Generated matrix: $MATRIX"
echo "MATRIX=$MATRIX" >> $GITHUB_OUTPUT
Build:
runs-on: ubuntu-24.04
needs: Prepare
strategy:
max-parallel: 1
matrix:
include: ${{ fromJson(needs.Prepare.outputs.MATRIX) }}
env:
TZ: 'Asia/Shanghai'
VERSION: ${{ matrix.version }}
CHANNEL: ${{ matrix.channel }}
IMAGE: "ghcr.io/${{ github.repository_owner }}/chr"
DOCKER_BUILD_SUMMARY: 'false'
DOCKER_BUILD_RECORD_UPLOAD: 'false'
steps:
- name: Show build info
run: |
echo "Version: ${{ env.VERSION }}"
echo "Channel: ${{ env.CHANNEL }}"
- name: Download CHR image
run: |
# x86 架构
mkdir -p "amd64"
wget -nv -O "amd64/chr-$VERSION.img.zip" \
"https://github.com/elseif/MikroTikPatch/releases/download/$VERSION/chr-$VERSION.img.zip"
unzip "amd64/chr-$VERSION.img.zip" -d "amd64/"
rm "amd64/chr-$VERSION.img.zip"
# arm64 架构
mkdir -p "arm64"
wget -nv -O "arm64/chr-$VERSION-arm64.img.zip" \
"https://github.com/elseif/MikroTikPatch/releases/download/$VERSION-arm64/chr-$VERSION-arm64.img.zip"
unzip "arm64/chr-$VERSION-arm64.img.zip" -d "arm64/"
rm "arm64/chr-$VERSION-arm64.img.zip"
ls -la amd64/ arm64/
- name: Create start.sh
run: |
cat <<'EOF' > start.sh
#!/bin/sh
set -e
MEM="${MEM:=512m}"
DISK="${DISK:=128M}"
CPUS="${CPUS:=1}"
bridge_index=0
get_mac_address() {
ifname="$1"
ip link show "$ifname" 2>/dev/null | awk '/link\/ether/ {print $2}'
}
increment_mac() {
mac=$1
mac_no_colons=$(echo "$mac" | tr -d ':' | tr '[:lower:]' '[:upper:]')
mac_dec=$((16#${mac_no_colons}))
mac_dec=$((mac_dec + 1))
mac_hex=$(printf "%012X" "$mac_dec")
echo "$mac_hex" | sed 's/.\{2\}/&:/g;s/:$//'
}
echo "" > /etc/qemu/bridge.conf
qemu="qemu-system-x86_64"
if [[ `uname -m` == "aarch64" ]]; then
qemu="qemu-system-aarch64 -pflash /usr/share/qemu/edk2-aarch64-code.fd -M virt"
fi
if [ -e /dev/kvm ]; then
qemu="$qemu -enable-kvm -cpu host"
else
if [[ `uname -m` == "aarch64" ]]; then
qemu="$qemu -cpu cortex-a57"
fi
fi
img=`ls /diskimage/*.img`
qemu="$qemu -nographic -m $MEM -smp cpus=$CPUS -drive file=$img,format=raw"
truncate -s $DISK $img
while read -r line; do
iface=$(echo "$line" | sed "s/ *\([a-z0-9]*\):.*/\1/")
if [[ "$iface" =~ " " ]]; then
continue
fi
if [[ "$iface" =~ ^br ]]; then
continue
fi
if [[ "$iface" == "lo" ]]; then
continue
fi
if [[ "$iface" == "" ]]; then
continue
fi
echo "iface: $iface"
bridge="br$bridge_index"
ip link add name "$bridge" type bridge
ip link set dev "$bridge" up
ip link set "$iface" master "$bridge"
echo "allow $bridge" >> /etc/qemu/bridge.conf
veth_mac=$(get_mac_address "$iface")
tap_mac=$(increment_mac "$veth_mac")
bridge_index=$((bridge_index + 1))
qemu="$qemu -netdev bridge,id=$bridge,br=$bridge -device virtio-net,netdev=$bridge,mac=$tap_mac "
done < /proc/net/dev
echo "qemu: $qemu"
exec $qemu
EOF
chmod +x start.sh
- name: Create Dockerfile
run: |
cat > Dockerfile << 'EOF'
FROM alpine:latest
ARG TARGETARCH
ARG VERSION
RUN case "$TARGETARCH" in \
amd64) apk add --no-cache qemu-system-x86_64 ;; \
arm64) apk add --no-cache qemu-system-aarch64 ;; \
esac \
&& rm -rf /var/cache/apk/*
RUN mkdir -p /diskimage
COPY ${TARGETARCH}/*.img /diskimage/
COPY start.sh /start.sh
ENTRYPOINT ["/start.sh"]
EOF
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build and Push
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.IMAGE }}:${{ env.VERSION }}
build-args: VERSION=${{ env.VERSION }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
sbom: false
outputs: >
type=image,
annotation-index.org.opencontainers.image.title=MikroTik CHR RouterOS,
annotation-index.org.opencontainers.image.description=MikroTik CHR (Cloud Hosted Router) running on QEMU with Docker,
annotation-index.org.opencontainers.image.version=${{ env.VERSION }},
annotation-index.org.opencontainers.image.url=https://mikrotik.ltd
- name: Update channel tags
if: env.CHANNEL != ''
run: |
TAGS="-t ${{ env.IMAGE }}:${{ env.CHANNEL }}"
if [ "${{ env.CHANNEL }}" = "stable" ]; then
TAGS="$TAGS -t ${{ env.IMAGE }}:latest"
fi
echo "Creating tags: $TAGS"
docker buildx imagetools create $TAGS ${{ env.IMAGE }}:${{ env.VERSION }}
- name: Save Docker Image as tar
run: |
mkdir -p artifacts
echo "Exporting amd64 tar..."
docker pull --platform linux/amd64 ${{ env.IMAGE }}:${{ env.VERSION }}
docker save ${{ env.IMAGE }}:${{ env.VERSION }} -o artifacts/chr-${{ env.VERSION }}-amd64.tar
echo "Exporting arm64 tar..."
docker pull --platform linux/arm64 ${{ env.IMAGE }}:${{ env.VERSION }}
docker save ${{ env.IMAGE }}:${{ env.VERSION }} -o artifacts/chr-${{ env.VERSION }}-arm64.tar
ls -lh artifacts/
- name: Upload tar artifact
uses: actions/upload-artifact@v7
with:
name: chr-${{ env.VERSION }}-docker-images
path: artifacts/
retention-days: 30

View File

@ -1,76 +0,0 @@
name: Patch Mikrotik RouterOS
on:
schedule:
- cron: "00 10 * * *" # At UTC 10:00 (Beijing 18:00) on every day
workflow_dispatch:
inputs:
version:
description: 'Select Version (v6/v7/all)'
required: true
default: 'all'
type: choice
options:
- all
- v6
- v7
jobs:
Patch_Stable_V7:
if: |
(github.event_name == 'schedule') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.version == 'all' || github.event.inputs.version == 'v7'))
uses: ./.github/workflows/patch_v7.yml
with:
channel: stable
release: true
secrets: inherit
Patch_LongTerm_V7:
if: |
(github.event_name == 'schedule') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.version == 'all' || github.event.inputs.version == 'v7'))
uses: ./.github/workflows/patch_v7.yml
with:
channel: long-term
release: true
secrets: inherit
Patch_Testing_V7:
if: |
(github.event_name == 'schedule') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.version == 'all' || github.event.inputs.version == 'v7'))
uses: ./.github/workflows/patch_v7.yml
with:
channel: testing
release: true
secrets: inherit
Patch_Development_V7:
if: |
(github.event_name == 'schedule') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.version == 'all' || github.event.inputs.version == 'v7'))
uses: ./.github/workflows/patch_v7.yml
with:
channel: development
release: true
secrets: inherit
Patch_Stable_V6:
if: |
(github.event_name == 'schedule') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.version == 'all' || github.event.inputs.version == 'v6'))
uses: ./.github/workflows/patch_v6.yml
with:
channel: stable
release: true
secrets: inherit
Patch_LongTerm_V6:
if: |
(github.event_name == 'schedule') ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.version == 'all' || github.event.inputs.version == 'v6'))
uses: ./.github/workflows/patch_v6.yml
with:
channel: long-term
release: true
secrets: inherit

408
.github/workflows/mikrotik_patch_6.yml vendored Normal file
View File

@ -0,0 +1,408 @@
name: Patch Mikrotik RouterOS 6.x
on:
# push:
# branches: [ "main" ]
# schedule:
# - cron: "0 0 * * *"
workflow_dispatch:
inputs:
channel:
description: 'Channel (stable, long-term)'
required: true
default: 'stable'
type: choice
options:
- stable
- long-term
version:
description: "Specify version (e.g., 6.49.19), empty for latest"
required: false
default: ''
type: string
buildtime:
description: "Custom build time, leave empty for now,when set specify version"
required: false
default: ''
type: string
release:
description: "Release to GitHub & Upload "
required: false
default: false
type: boolean
permissions:
contents: write
env:
CUSTOM_LICENSE_PRIVATE_KEY: ${{ secrets.CUSTOM_LICENSE_PRIVATE_KEY }}
CUSTOM_LICENSE_PUBLIC_KEY: ${{ secrets.CUSTOM_LICENSE_PUBLIC_KEY }}
CUSTOM_NPK_SIGN_PRIVATE_KEY: ${{ secrets.CUSTOM_NPK_SIGN_PRIVATE_KEY }}
CUSTOM_NPK_SIGN_PUBLIC_KEY: ${{ secrets.CUSTOM_NPK_SIGN_PUBLIC_KEY }}
CUSTOM_CLOUD_PUBLIC_KEY: ${{ secrets.CUSTOM_CLOUD_PUBLIC_KEY }}
MIKRO_LICENSE_PUBLIC_KEY: ${{ secrets.MIKRO_LICENSE_PUBLIC_KEY }}
MIKRO_NPK_SIGN_PUBLIC_KEY: ${{ secrets.MIKRO_NPK_SIGN_PUBLIC_KEY }}
MIKRO_CLOUD_PUBLIC_KEY: ${{ secrets.MIKRO_CLOUD_PUBLIC_KEY }}
MIKRO_LICENCE_URL: ${{ secrets.MIKRO_LICENCE_URL }}
CUSTOM_LICENCE_URL: ${{ secrets.CUSTOM_LICENCE_URL }}
MIKRO_UPGRADE_URL: ${{ secrets.MIKRO_UPGRADE_URL }}
CUSTOM_UPGRADE_URL: ${{ secrets.CUSTOM_UPGRADE_URL }}
MIKRO_RENEW_URL: ${{ secrets.MIKRO_RENEW_URL }}
CUSTOM_RENEW_URL: ${{ secrets.CUSTOM_RENEW_URL }}
MIKRO_CLOUD_URL: ${{ secrets.MIKRO_CLOUD_URL }}
CUSTOM_CLOUD_URL: ${{ secrets.CUSTOM_CLOUD_URL }}
jobs:
Set_Variables:
runs-on: ubuntu-22.04
outputs:
BUILD_TIME: ${{ steps.set_vars.outputs.BUILD_TIME }}
RELEASE: ${{ steps.set_vars.outputs.RELEASE }}
MATRIX_JSON: ${{ steps.set_vars.outputs.MATRIX_JSON }}
steps:
- name: Set variables
id: set_vars
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
BUILD_TIME="${{ github.event.inputs.buildtime }}"
RELEASE="${{ github.event.inputs.release }}"
if [ -z "$BUILD_TIME" ]; then
BUILD_TIME=$(date +'%s')
fi
if [ -z "$RELEASE" ]; then
RELEASE=false
fi
ARCH="${{ github.event.inputs.arch }}"
CHANNEL="${{ github.event.inputs.channel }}"
MATRIX_JSON=$(jq -nc --arg arch "$ARCH" --arg channel "$CHANNEL" '[{arch: $arch, channel: $channel}]')
else
BUILD_TIME=$(date +'%s')
RELEASE=true
MATRIX_JSON='[{"arch":"x86","channel":"stable"},{"arch":"x86","channel":"long-term"}]'
fi
echo "BUILD_TIME=$BUILD_TIME" >> $GITHUB_OUTPUT
echo "RELEASE=$RELEASE" >> $GITHUB_OUTPUT
echo "MATRIX_JSON=$MATRIX_JSON" >> $GITHUB_OUTPUT
echo "BUILD_TIME: $BUILD_TIME"
echo "RELEASE: $RELEASE"
echo "MATRIX_JSON: $MATRIX_JSON"
Patch_RouterOS:
needs: Set_Variables
runs-on: ubuntu-22.04
strategy:
matrix:
include: ${{ fromJSON(needs.Set_Variables.outputs.MATRIX_JSON) }}
env:
TZ: 'Asia/Shanghai'
BUILD_TIME: ${{ needs.Set_Variables.outputs.BUILD_TIME }}
RELEASE: ${{ needs.Set_Variables.outputs.RELEASE }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Get latest routeros version
id: get_latest
run: |
echo $(uname -a)
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
if [ -n "${{ github.event.inputs.version }}" ]; then
LATEST_VERSION="${{ github.event.inputs.version }}"
else
read LATEST_VERSION BUILD_TIME <<< "$(wget -nv -O - "https://${{ env.MIKRO_UPGRADE_URL }}/routeros/NEWEST6.${{ matrix.channel }}")"
fi
else
read LATEST_VERSION BUILD_TIME <<< "$(wget -nv -O - "https://${{ env.MIKRO_UPGRADE_URL }}/routeros/NEWEST6.${{ matrix.channel }}")"
fi
echo Build Time:$BUILD_TIME
echo Latest Version:$LATEST_VERSION
if [ "${{ github.event_name }}" == "schedule" ]; then
_LATEST_VERSION=$(wget -nv -O - https://${{ env.CUSTOM_UPGRADE_URL }}/routeros/NEWEST6.${{ matrix.channel }} | cut -d ' ' -f1)
if [ "$_LATEST_VERSION" == "$LATEST_VERSION" ]; then
echo "No new version found"
echo "has_new_version=false" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "has_new_version=true" >> $GITHUB_OUTPUT
wget -nv -O CHANGELOG https://${{ env.MIKRO_UPGRADE_URL }}/routeros/$LATEST_VERSION/CHANGELOG
cat CHANGELOG
echo "LATEST_VERSION=${LATEST_VERSION}" >> $GITHUB_ENV
echo "BUILD_TIME=${BUILD_TIME}" >> $GITHUB_ENV
sudo apt-get update > /dev/null
- name: Get loader patch files
if: steps.get_latest.outputs.has_new_version == 'true'
run: |
wget -nv -O loader.7z https://${{ env.CUSTOM_UPGRADE_URL }}/routeros/loader.7z
sudo apt-get install -y p7zip-full > /dev/null
7z x -p"${{ secrets.LOADER_7Z_PASSWORD }}" loader.7z >> /dev/null
rm loader.7z
- name: Cache Squashfs
if: steps.get_latest.outputs.has_new_version == 'true'
id: cache-squashfs
uses: actions/cache@v4
with:
path: |
python3.sfs
option.sfs
key: busybox-python3-squashfs-${{ matrix.arch }}
- name: Create Squashfs for option and python3
if: steps.get_latest.outputs.has_new_version == 'true' && steps.cache-squashfs.outputs.cache-hit != 'true'
run: |
sudo mkdir -p ./option-root/bin/
sudo cp busybox/busybox_x86 ./option-root/bin/busybox
sudo chmod +x ./option-root/bin/busybox
sudo cp keygen/keygen_x86 ./option-root/bin/keygen
sudo chmod +x ./option-root/bin/keygen
sudo chmod +x ./busybox/busybox_x86
COMMANDS=$(./busybox/busybox_x86 --list)
for cmd in $COMMANDS; do
sudo ln -sf /pckg/option/bin/busybox ./option-root/bin/$cmd
done
sudo mksquashfs option-root option.sfs -quiet -comp xz -no-xattrs -b 256k
sudo rm -rf option-root
sudo wget -O cpython.tar.gz -nv https://github.com/indygreg/python-build-standalone/releases/download/20250708/cpython-3.11.13+20250708-x86_64-unknown-linux-musl-install_only_stripped.tar.gz
sudo tar -xf cpython.tar.gz
sudo rm cpython.tar.gz
sudo rm -rf ./python/include
sudo rm -rf ./python/share
sudo mksquashfs python python3.sfs -quiet -comp xz -no-xattrs -b 256k
sudo rm -rf ./python
- name: Cache mikrotik-${{ env.LATEST_VERSION }}.iso
if: steps.get_latest.outputs.has_new_version == 'true'
id: cache-mikrotik
uses: actions/cache@v4
with:
path: |
mikrotik.iso
key: mikrotik-${{ env.LATEST_VERSION }}
- name: Get mikrotik-${{ env.LATEST_VERSION }}.iso
if: steps.get_latest.outputs.has_new_version == 'true' && steps.cache-mikrotik.outputs.cache-hit != 'true'
run: |
sudo wget -nv -O mikrotik.iso https://download.mikrotik.com/routeros/$LATEST_VERSION/mikrotik-$LATEST_VERSION.iso
- name: Patch mikrotik-${{ env.LATEST_VERSION }}.iso
if: steps.get_latest.outputs.has_new_version == 'true'
run: |
sudo apt-get install -y mkisofs xorriso > /dev/null
sudo mkdir ./iso
sudo mount -o loop,ro mikrotik.iso ./iso
sudo mkdir ./new_iso
sudo cp -r ./iso/* ./new_iso/
sudo rsync -a ./iso/ ./new_iso/
sudo umount ./iso
sudo rm -rf ./iso
sudo mv ./new_iso/system-$LATEST_VERSION.npk ./
sudo -E python3 patch.py npk system-$LATEST_VERSION.npk
NPK_FILES=$(find ./new_iso -type f -name "*.npk")
if [ -z "$NPK_FILES" ]; then
echo "No .npk files found in iso, downloading..."
URL="https://download.mikrotik.com/routeros/$LATEST_VERSION/all_packages-${{ matrix.arch }}-$LATEST_VERSION.zip"
TMP_ZIP="./all_packages.zip"
sudo wget -nv -O "$TMP_ZIP" "$URL"
sudo unzip -o "$TMP_ZIP" -d ./new_iso
sudo rm -f "$TMP_ZIP"
NPK_FILES=$(find ./new_iso -type f -name "*.npk")
fi
if [ -n "$NPK_FILES" ]; then
for file in $NPK_FILES; do
sudo -E python3 npk.py sign "$file" "$file"
done
fi
sudo cp -f system-$LATEST_VERSION.npk ./new_iso/
sudo -E python3 patch.py kernel ./new_iso/isolinux/initrd.rgz
#sudo -E python3 npk.py create ./new_iso/gps-$LATEST_VERSION.npk ./option-$LATEST_VERSION.npk option ./option.sfs -desc="busybox"
#sudo cp option-$LATEST_VERSION.npk ./new_iso/
sudo mkisofs -o mikrotik-$LATEST_VERSION.iso \
-V "MikroTik $LATEST_VERSION" \
-sysid "" -preparer "MiKroTiK" \
-publisher "" -A "MiKroTiK RouterOS" \
-input-charset utf-8 \
-b isolinux/isolinux.bin \
-c isolinux/boot.cat \
-no-emul-boot \
-boot-load-size 4 \
-boot-info-table \
-R -J \
./new_iso
sudo mkdir ./all_packages
sudo cp ./new_iso/*.npk ./all_packages/
sudo rm -rf ./new_iso
- name: Create install-image-${{ env.LATEST_VERSION }}.img
if: steps.get_latest.outputs.has_new_version == 'true'
run: |
sudo modprobe nbd
sudo apt-get update
sudo apt-get install -y qemu-utils extlinux > /dev/null
truncate --size 64M install-image-$LATEST_VERSION.img
sudo qemu-nbd -c /dev/nbd0 -f raw install-image-$LATEST_VERSION.img
sudo mkfs.vfat -n "Install" /dev/nbd0
sudo mkdir ./install
sudo mount /dev/nbd0 ./install
sudo extlinux --install -H 64 -S 32 ./install/
echo -e 'default system\nLABEL system\n\tKERNEL linux\n\tAPPEND load_ramdisk=1 initrd=initrd.rgz -hdd-install' \
> syslinux.cfg
sudo cp syslinux.cfg ./install/
sudo rm syslinux.cfg
NPK_FILES=($(find ./all_packages/*.npk))
for ((i=1; i<=${#NPK_FILES[@]}; i++))
do
echo "${NPK_FILES[$i-1]}=>$i.npk"
sudo cp ${NPK_FILES[$i-1]} ./install/$i.npk
done
sudo touch ./install/CHOOSE
sudo touch ./install/autorun.scr
sudo umount /dev/nbd0
sudo qemu-nbd -d /dev/nbd0
sudo rm -rf ./install
sudo zip install-image-$LATEST_VERSION.zip ./install-image-$LATEST_VERSION.img
sudo rm ./install-image-$LATEST_VERSION.img
- name: Cache chr-${{ env.LATEST_VERSION }}.img.zip
if: steps.get_latest.outputs.has_new_version == 'true'
id: cache-chr-img
uses: actions/cache@v4
with:
path: |
chr.img
key: chr-${{ env.LATEST_VERSION }}.img
- name: Get chr-${{ env.LATEST_VERSION }}.img
if: steps.get_latest.outputs.has_new_version == 'true' && steps.cache-chr-img.outputs.cache-hit != 'true'
run: |
sudo wget -nv -O chr.img.zip https://download.mikrotik.com/routeros/$LATEST_VERSION/chr-$LATEST_VERSION.img.zip
sudo unzip chr.img.zip
sudo rm chr.img.zip
sudo mv chr-$LATEST_VERSION.img chr.img
- name: Create chr-${{ env.LATEST_VERSION }}.img
if: steps.get_latest.outputs.has_new_version == 'true'
run: |
sudo modprobe nbd
sudo apt-get install -y qemu-utils extlinux > /dev/null
sudo mkdir ./chr
sudo cp chr.img chr-$LATEST_VERSION.img
sudo qemu-nbd -c /dev/nbd0 -f raw chr-$LATEST_VERSION.img
sudo -E python3 patch.py block /dev/nbd0p1 boot/initrd.rgz
sudo mount /dev/nbd0p1 ./chr
sudo cp ./all_packages/dude-$LATEST_VERSION.npk ./chr/var/pdb/dude/image
sudo -E python3 patch.py npk ./chr/var/pdb/routeros-x86/image
sudo cp ./chr/var/pdb/routeros-x86/image ./all_packages/routeros-x86-$LATEST_VERSION.npk
#sudo mkdir -p ./chr/var/pdb/option
#sudo cp ./all_packages/option-$LATEST_VERSION.npk ./chr/var/pdb/option/image
sudo umount ./chr
sudo qemu-nbd -d /dev/nbd0
sudo rm -rf ./chr
sudo qemu-img convert -f raw -O qcow2 chr-$LATEST_VERSION.img chr-$LATEST_VERSION.qcow2
sudo qemu-img convert -f raw -O vmdk chr-$LATEST_VERSION.img chr-$LATEST_VERSION.vmdk
sudo qemu-img convert -f raw -O vpc chr-$LATEST_VERSION.img chr-$LATEST_VERSION.vhd
sudo qemu-img convert -f raw -O vhdx chr-$LATEST_VERSION.img chr-$LATEST_VERSION.vhdx
sudo qemu-img convert -f raw -O vdi chr-$LATEST_VERSION.img chr-$LATEST_VERSION.vdi
sudo zip chr-$LATEST_VERSION.qcow2.zip chr-$LATEST_VERSION.qcow2
sudo zip chr-$LATEST_VERSION.vmdk.zip chr-$LATEST_VERSION.vmdk
sudo zip chr-$LATEST_VERSION.vhd.zip chr-$LATEST_VERSION.vhd
sudo zip chr-$LATEST_VERSION.vhdx.zip chr-$LATEST_VERSION.vhdx
sudo zip chr-$LATEST_VERSION.vdi.zip chr-$LATEST_VERSION.vdi
sudo zip chr-$LATEST_VERSION.img.zip chr-$LATEST_VERSION.img
sudo rm chr-$LATEST_VERSION.qcow2
sudo rm chr-$LATEST_VERSION.vmdk
sudo rm chr-$LATEST_VERSION.vhd
sudo rm chr-$LATEST_VERSION.vhdx
sudo rm chr-$LATEST_VERSION.vdi
sudo rm chr-$LATEST_VERSION.img
cd ./all_packages
sudo zip ../all_packages-x86-$LATEST_VERSION.zip *.npk
cd ..
- name: Upload Files
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'true'
run: |
sudo mkdir -p ./publish/$LATEST_VERSION
sudo cp CHANGELOG ./publish/$LATEST_VERSION/
sudo cp ./all_packages/*.npk ./publish/$LATEST_VERSION/
sudo chown -R root:root ./publish/
LOCAL_PATH=./publish/$LATEST_VERSION
REMOTE_PATH=${{ secrets.SSH_DIRECTORY }}
SERVER=${{ secrets.SSH_SERVER }}
USER=${{ secrets.SSH_USERNAME }}
PASS=${{ secrets.SSH_PASSWORD }}
PORT=${{ secrets.SSH_PORT }}
CHANNEL=${{ matrix.channel }}
sudo apt-get install -y lftp ssh sshpass > /dev/null 2>&1
sudo -E lftp -u "$USER","$PASS" sftp://$SERVER:$PORT <<EOF
set sftp:auto-confirm yes
mirror --reverse --verbose --only-newer ./publish "$REMOTE_PATH"
bye
EOF
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no -p $PORT $USER@$SERVER \
"echo $LATEST_VERSION $BUILD_TIME | tee /rw/disk/$REMOTE_PATH/NEWEST6.$CHANNEL; chown -R 32768:32768 /rw/disk/$REMOTE_PATH/"
- name: Clear Cloudflare cache
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'true'
run: |
curl --request POST --url "https://api.cloudflare.com/client/v4/zones/fe6831ed6dc9e6235e69ef2a31f2e7fe/purge_cache" \
--header "Authorization: Bearer 9GDQkzU51QXaqzp1qMjyFKpyeJyOdnNoG9GZQaGP" \
--header "Content-Type:application/json" \
--data '{"purge_everything": true}'
- name: Delete Release tag ${{ env.LATEST_VERSION }}
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'true'
run: |
HEADER="Authorization: token ${{ secrets.GITHUB_TOKEN }}"
RELEASE_INFO=$(curl -s -H $HEADER https://api.github.com/repos/${{ github.repository }}/releases/tags/$LATEST_VERSION)
RELEASE_ID=$(echo $RELEASE_INFO | jq -r '.id')
echo "Release ID: $RELEASE_ID"
if [ "$RELEASE_ID" != "null" ]; then
curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/git/refs/tags/$LATEST_VERSION
echo "Tag $LATEST_VERSION deleted successfully."
curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID
echo "Release with tag $LATEST_VERSION deleted successfully."
else
echo "Release not found for tag: $LATEST_VERSION)"
fi
- name: Create Release tag ${{ env.LATEST_VERSION }}
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'true'
uses: softprops/action-gh-release@v2
with:
name: "RouterOS ${{ env.LATEST_VERSION }}"
body_path: "CHANGELOG"
tag_name: ${{ env.LATEST_VERSION }}
make_latest: false
prerelease: ${{ matrix.channel == 'testing' }}
files: |
mikrotik-${{ env.LATEST_VERSION }}.iso
chr-${{ env.LATEST_VERSION }}*.zip
install-image-${{ env.LATEST_VERSION }}.zip
routeros-x86-${{ env.LATEST_VERSION }}.npk
all_packages-x86-${{ env.LATEST_VERSION }}.zip
- name: Upload Files as Artifact (No Release)
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'false'
uses: actions/upload-artifact@v4
with:
name: mikrotik-${{ env.LATEST_VERSION }}
path: |
all_packages-x86-${{ env.LATEST_VERSION }}.zip
mikrotik-${{ env.LATEST_VERSION }}.iso
chr-${{ env.LATEST_VERSION }}*.zip

762
.github/workflows/mikrotik_patch_7.yml vendored Normal file
View File

@ -0,0 +1,762 @@
name: Patch Mikrotik RouterOS 7.x
on:
# push:
# branches: [ "main" ]
schedule:
- cron: "0 0 * * *" # At UTC 00:00 on every day
workflow_dispatch:
inputs:
arch:
description: 'Architecture (x86, arm64, arm)'
required: true
default: 'x86'
type: choice
options:
- x86
- arm64
- arm
channel:
description: 'Channel (stable, testing)'
required: true
default: 'stable'
type: choice
options:
- stable
- testing
version:
description: "Specify version (e.g., 7.19.2), empty for latest"
required: false
default: ''
type: string
buildtime:
description: "Custom build time, leave empty for now"
required: false
default: ''
type: string
release:
description: "Release to GitHub & Upload "
required: false
default: false
type: boolean
permissions:
contents: write
env:
CUSTOM_LICENSE_PRIVATE_KEY: ${{ secrets.CUSTOM_LICENSE_PRIVATE_KEY }}
CUSTOM_LICENSE_PUBLIC_KEY: ${{ secrets.CUSTOM_LICENSE_PUBLIC_KEY }}
CUSTOM_NPK_SIGN_PRIVATE_KEY: ${{ secrets.CUSTOM_NPK_SIGN_PRIVATE_KEY }}
CUSTOM_NPK_SIGN_PUBLIC_KEY: ${{ secrets.CUSTOM_NPK_SIGN_PUBLIC_KEY }}
CUSTOM_CLOUD_PUBLIC_KEY: ${{ secrets.CUSTOM_CLOUD_PUBLIC_KEY }}
MIKRO_LICENSE_PUBLIC_KEY: ${{ secrets.MIKRO_LICENSE_PUBLIC_KEY }}
MIKRO_NPK_SIGN_PUBLIC_KEY: ${{ secrets.MIKRO_NPK_SIGN_PUBLIC_KEY }}
MIKRO_CLOUD_PUBLIC_KEY: ${{ secrets.MIKRO_CLOUD_PUBLIC_KEY }}
MIKRO_LICENCE_URL: ${{ secrets.MIKRO_LICENCE_URL }}
CUSTOM_LICENCE_URL: ${{ secrets.CUSTOM_LICENCE_URL }}
MIKRO_UPGRADE_URL: ${{ secrets.MIKRO_UPGRADE_URL }}
CUSTOM_UPGRADE_URL: ${{ secrets.CUSTOM_UPGRADE_URL }}
MIKRO_RENEW_URL: ${{ secrets.MIKRO_RENEW_URL }}
CUSTOM_RENEW_URL: ${{ secrets.CUSTOM_RENEW_URL }}
MIKRO_CLOUD_URL: ${{ secrets.MIKRO_CLOUD_URL }}
CUSTOM_CLOUD_URL: ${{ secrets.CUSTOM_CLOUD_URL }}
jobs:
Set_Variables:
runs-on: ubuntu-22.04
outputs:
BUILD_TIME: ${{ steps.set_vars.outputs.BUILD_TIME }}
RELEASE: ${{ steps.set_vars.outputs.RELEASE }}
MATRIX_JSON: ${{ steps.set_vars.outputs.MATRIX_JSON }}
steps:
- name: Set variables
id: set_vars
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
BUILD_TIME="${{ github.event.inputs.buildtime }}"
RELEASE="${{ github.event.inputs.release }}"
if [ -z "$BUILD_TIME" ]; then
BUILD_TIME=$(date +'%s')
fi
if [ -z "$RELEASE" ]; then
RELEASE=false
fi
ARCH="${{ github.event.inputs.arch }}"
CHANNEL="${{ github.event.inputs.channel }}"
MATRIX_JSON=$(jq -nc --arg arch "$ARCH" --arg channel "$CHANNEL" '[{arch: $arch, channel: $channel}]')
else
BUILD_TIME=$(date +'%s')
RELEASE=true
MATRIX_JSON='[{"arch":"x86","channel":"stable"},{"arch":"arm64","channel":"stable"},{"arch":"arm","channel":"stable"},{"arch":"x86","channel":"testing"},{"arch":"arm64","channel":"testing"},{"arch":"arm","channel":"testing"}]'
MATRIX_JSON='[{"arch":"x86","channel":"stable"},{"arch":"arm64","channel":"stable"},{"arch":"x86","channel":"testing"},{"arch":"arm64","channel":"testing"}]'
fi
echo "BUILD_TIME=$BUILD_TIME" >> $GITHUB_OUTPUT
echo "RELEASE=$RELEASE" >> $GITHUB_OUTPUT
echo "MATRIX_JSON=$MATRIX_JSON" >> $GITHUB_OUTPUT
echo "BUILD_TIME: $BUILD_TIME"
echo "RELEASE: $RELEASE"
echo "MATRIX_JSON: $MATRIX_JSON"
Patch_RouterOS:
needs: Set_Variables
runs-on: ubuntu-22.04
strategy:
matrix:
include: ${{ fromJSON(needs.Set_Variables.outputs.MATRIX_JSON) }}
env:
TZ: 'Asia/Shanghai'
BUILD_TIME: ${{ needs.Set_Variables.outputs.BUILD_TIME }}
RELEASE: ${{ needs.Set_Variables.outputs.RELEASE }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Get latest routeros version
id: get_latest
run: |
echo $(uname -a)
echo Build Time:$BUILD_TIME
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
if [ -n "${{ github.event.inputs.version }}" ]; then
LATEST_VERSION="${{ github.event.inputs.version }}"
else
LATEST_VERSION=$(wget -nv -O - https://${{ env.MIKRO_UPGRADE_URL }}/routeros/NEWESTa7.${{ matrix.channel }} | cut -d ' ' -f1)
fi
else
LATEST_VERSION=$(wget -nv -O - https://${{ env.MIKRO_UPGRADE_URL }}/routeros/NEWESTa7.${{ matrix.channel }} | cut -d ' ' -f1)
fi
echo Latest Version:$LATEST_VERSION
if [ "${{ github.event_name }}" == "schedule" ]; then
_LATEST_VERSION=$(wget -nv -O - https://${{ env.CUSTOM_UPGRADE_URL }}/routeros/NEWESTa7.${{ matrix.channel }} | cut -d ' ' -f1)
if [ "$_LATEST_VERSION" == "$LATEST_VERSION" ]; then
echo "No new version found"
echo "has_new_version=false" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "has_new_version=true" >> $GITHUB_OUTPUT
wget -nv -O CHANGELOG https://${{ env.MIKRO_UPGRADE_URL }}/routeros/$LATEST_VERSION/CHANGELOG
cat CHANGELOG
if [[ "${{ matrix.arch }}" == "arm64" ]]; then
echo "ARCH=-arm64" >> $GITHUB_ENV
elif [[ "${{ matrix.arch }}" == "arm" ]]; then
echo "ARCH=-arm" >> $GITHUB_ENV
else
echo "ARCH=" >> $GITHUB_ENV
fi
echo "LATEST_VERSION=${LATEST_VERSION}" >> $GITHUB_ENV
sudo apt-get update > /dev/null
- name: Get loader patch files
if: steps.get_latest.outputs.has_new_version == 'true'
run: |
wget -nv -O loader.7z https://${{ env.CUSTOM_UPGRADE_URL }}/routeros/loader.7z
sudo apt-get install -y p7zip-full > /dev/null
7z x -p"${{ secrets.LOADER_7Z_PASSWORD }}" loader.7z >> /dev/null
rm loader.7z
- name: Cache Squashfs
if: steps.get_latest.outputs.has_new_version == 'true'
id: cache-squashfs
uses: actions/cache@v4
with:
path: |
python3.sfs
option.sfs
key: busybox-python3-squashfs-${{ matrix.arch }}
- name: Create Squashfs for option and python3
if: steps.get_latest.outputs.has_new_version == 'true' && steps.cache-squashfs.outputs.cache-hit != 'true'
run: |
sudo mkdir -p ./option-root/bin/
if [ "${{ matrix.arch }}" == "x86" ]; then
sudo cp busybox/busybox_x86 ./option-root/bin/busybox
sudo chmod +x ./option-root/bin/busybox
sudo cp keygen/keygen_x86 ./option-root/bin/keygen
sudo chmod +x ./option-root/bin/keygen
elif [ "${{ matrix.arch }}" == "arm64" ]; then
sudo cp busybox/busybox_aarch64 ./option-root/bin/busybox
sudo chmod +x ./option-root/bin/busybox
sudo cp keygen/keygen_aarch64 ./option-root/bin/keygen
sudo chmod +x ./option-root/bin/keygen
elif [ "${{ matrix.arch }}" == "arm" ]; then
sudo cp busybox/busybox_arm ./option-root/bin/busybox
sudo chmod +x ./option-root/bin/busybox
sudo cp keygen/keygen_arm ./option-root/bin/keygen
sudo chmod +x ./option-root/bin/keygen
else
echo "Unsupported architecture: ${{ matrix.arch }}"
exit 1
fi
sudo chmod +x ./busybox/busybox_x86
COMMANDS=$(./busybox/busybox_x86 --list)
for cmd in $COMMANDS; do
sudo ln -sf /pckg/option/bin/busybox ./option-root/bin/$cmd
done
sudo mksquashfs option-root option.sfs -quiet -comp xz -no-xattrs -b 256k
sudo rm -rf option-root
if [ "${{ matrix.arch }}" == "x86" ]; then
sudo wget -O cpython.tar.gz -nv https://github.com/indygreg/python-build-standalone/releases/download/20250708/cpython-3.11.13+20250708-x86_64-unknown-linux-musl-install_only_stripped.tar.gz
elif [ "${{ matrix.arch }}" == "arm64" ]; then
sudo wget -O cpython.tar.gz -nv https://github.com/indygreg/python-build-standalone/releases/download/20250708/cpython-3.11.13+20250708-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz
elif [ "${{ matrix.arch }}" == "arm" ]; then
sudo wget -O cpython.tar.gz -nv https://github.com/indygreg/python-build-standalone/releases/download/20250708/cpython-3.11.13+20250708-armv7-unknown-linux-gnueabi-install_only_stripped.tar.gz
fi
sudo tar -xf cpython.tar.gz
sudo rm cpython.tar.gz
sudo rm -rf ./python/include
sudo rm -rf ./python/share
sudo mksquashfs python python3.sfs -quiet -comp xz -no-xattrs -b 256k
sudo rm -rf ./python
- name: Cache mikrotik-${{ env.LATEST_VERSION }}${{ env.ARCH }}.iso
if: steps.get_latest.outputs.has_new_version == 'true' && (matrix.arch == 'x86' || matrix.arch == 'arm64')
id: cache-mikrotik
uses: actions/cache@v4
with:
path: |
mikrotik.iso
key: mikrotik-${{ env.LATEST_VERSION }}${{ env.ARCH }}
- name: Get mikrotik-${{ env.LATEST_VERSION }}${{ env.ARCH }}.iso
if: steps.get_latest.outputs.has_new_version == 'true' && steps.cache-mikrotik.outputs.cache-hit != 'true' && (matrix.arch == 'x86' || matrix.arch == 'arm64')
run: |
sudo wget -nv -O mikrotik.iso https://download.mikrotik.com/routeros/$LATEST_VERSION/mikrotik-$LATEST_VERSION$ARCH.iso
- name: Patch mikrotik-${{ env.LATEST_VERSION }}${{ env.ARCH }}.iso
if: steps.get_latest.outputs.has_new_version == 'true' && (matrix.arch == 'x86' || matrix.arch == 'arm64')
run: |
sudo apt-get install -y mkisofs xorriso > /dev/null
sudo mkdir ./iso
sudo mount -o loop,ro mikrotik.iso ./iso
sudo mkdir ./new_iso
sudo rsync -a ./iso/ ./new_iso/
sudo umount ./iso
sudo rm -rf ./iso
sudo mv ./new_iso/routeros-$LATEST_VERSION$ARCH.npk ./
sudo -E python3 patch.py npk routeros-$LATEST_VERSION$ARCH.npk
NPK_FILES=$(find ./new_iso -type f -name "*.npk")
if [ -z "$NPK_FILES" ]; then
echo "No .npk files found in iso, downloading..."
URL="https://download.mikrotik.com/routeros/$LATEST_VERSION/all_packages-${{ matrix.arch }}-$LATEST_VERSION.zip"
TMP_ZIP="./all_packages.zip"
sudo wget -nv -O "$TMP_ZIP" "$URL"
sudo unzip -o "$TMP_ZIP" -d ./new_iso
sudo rm -f "$TMP_ZIP"
NPK_FILES=$(find ./new_iso -type f -name "*.npk")
fi
if [ -n "$NPK_FILES" ]; then
for file in $NPK_FILES; do
sudo -E python3 npk.py sign "$file" "$file"
done
fi
sudo cp -f routeros-$LATEST_VERSION$ARCH.npk ./new_iso/
sudo -E python3 npk.py create ./new_iso/gps-$LATEST_VERSION$ARCH.npk ./option-$LATEST_VERSION$ARCH.npk option ./option.sfs -desc="busybox"
sudo cp option-$LATEST_VERSION$ARCH.npk ./new_iso/
sudo -E python3 npk.py create ./new_iso/gps-$LATEST_VERSION$ARCH.npk ./python3-$LATEST_VERSION$ARCH.npk python3 ./python3.sfs -desc="python 3.11.13"
sudo cp python3-$LATEST_VERSION$ARCH.npk ./new_iso/
sudo mkdir ./efiboot
sudo mount -o loop ./new_iso/efiboot.img ./efiboot
if [ "${{ matrix.arch }}" == "x86" ]; then
sudo -E python3 patch.py kernel ./efiboot/linux.x86_64
sudo cp ./efiboot/linux.x86_64 ./BOOTX64.EFI
sudo cp ./BOOTX64.EFI ./new_iso/isolinux/linux
sudo umount ./efiboot
sudo mkisofs -o mikrotik-$LATEST_VERSION$ARCH.iso \
-V "MikroTik $LATEST_VERSION ${{ matrix.arch }}" \
-sysid "" -preparer "MiKroTiK" \
-publisher "" -A "MiKroTiK RouterOS" \
-input-charset utf-8 \
-b isolinux/isolinux.bin \
-c isolinux/boot.cat \
-no-emul-boot \
-boot-load-size 4 \
-boot-info-table \
-eltorito-alt-boot \
-e efiboot.img \
-no-emul-boot \
-R -J \
./new_iso
elif [ "${{ matrix.arch }}" == "arm64" ]; then
sudo -E python3 patch.py kernel ./efiboot/EFI/BOOT/BOOTAA64.EFI
sudo umount ./efiboot
sudo xorriso -as mkisofs -o mikrotik-$LATEST_VERSION$ARCH.iso \
-V "MikroTik $LATEST_VERSION ${{ matrix.arch }}" \
-sysid "" -preparer "MiKroTiK" \
-publisher "" -A "MiKroTiK RouterOS" \
-input-charset utf-8 \
-b efiboot.img \
-no-emul-boot \
-R -J \
./new_iso
fi
sudo rm -rf ./efiboot
sudo mkdir ./all_packages
sudo cp ./new_iso/*.npk ./all_packages/
sudo rm -rf ./new_iso
cd ./all_packages
sudo zip ../all_packages-${{ matrix.arch }}-$LATEST_VERSION.zip *.npk
cd ..
- name: Cache chr-${{ env.LATEST_VERSION }}${{ env.ARCH }}.img.zip
if: steps.get_latest.outputs.has_new_version == 'true' && (matrix.arch == 'x86' || matrix.arch == 'arm64')
id: cache-chr-img
uses: actions/cache@v4
with:
path: |
chr.img
key: chr-${{ env.LATEST_VERSION }}${{ env.ARCH }}.img
- name: Get chr-${{ env.LATEST_VERSION }}${{ env.ARCH }}.img
if: steps.get_latest.outputs.has_new_version == 'true' && steps.cache-chr-img.outputs.cache-hit != 'true' && (matrix.arch == 'x86' || matrix.arch == 'arm64')
run: |
sudo wget -nv -O chr.img.zip https://download.mikrotik.com/routeros/$LATEST_VERSION/chr-$LATEST_VERSION$ARCH.img.zip
sudo unzip chr.img.zip
sudo rm chr.img.zip
sudo mv chr-$LATEST_VERSION$ARCH.img chr.img
- name: Create chr-${{ env.LATEST_VERSION }}${{ env.ARCH }}.img
if: steps.get_latest.outputs.has_new_version == 'true' && (matrix.arch == 'x86' || matrix.arch == 'arm64')
run: |
OVF_TEMPLATE='<?xml version="1.0" encoding="UTF-8"?>
<Envelope vmw:buildId="build-18663434" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<References>
<File ovf:href="{VMDK_FILE}" ovf:id="file1" ovf:size="{FILE_SIZE}"/>
</References>
<DiskSection>
<Info>Virtual disk information</Info>
<Disk ovf:capacity="{CAPACITY_MB}" ovf:capacityAllocationUnits="byte * 2^20" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="{POPULATED_SIZE}"/>
</DiskSection>
<NetworkSection>
<Info>The list of logical networks</Info>
<Network ovf:name="VM Network">
<Description>The VM Network network</Description>
</Network>
</NetworkSection>
<VirtualSystem ovf:id="vm">
<Info>A virtual machine</Info>
<Name>MikroTik_RouterOS_CHR</Name>
<OperatingSystemSection ovf:id="100" vmw:osType="other3xLinux64Guest">
<Info>The kind of installed guest operating system</Info>
</OperatingSystemSection>
<VirtualHardwareSection>
<Info>Virtual hardware requirements</Info>
<System>
<vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
<vssd:InstanceID>0</vssd:InstanceID>
<vssd:VirtualSystemIdentifier>MikroTik_RouterOS_CHR</vssd:VirtualSystemIdentifier>
<vssd:VirtualSystemType>vmx-10</vssd:VirtualSystemType>
</System>
<Item>
<rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
<rasd:Description>Number of Virtual CPUs</rasd:Description>
<rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
<rasd:InstanceID>1</rasd:InstanceID>
<rasd:ResourceType>3</rasd:ResourceType>
<rasd:VirtualQuantity>1</rasd:VirtualQuantity>
</Item>
<Item>
<rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
<rasd:Description>Memory Size</rasd:Description>
<rasd:ElementName>1024MB of memory</rasd:ElementName>
<rasd:InstanceID>2</rasd:InstanceID>
<rasd:ResourceType>4</rasd:ResourceType>
<rasd:VirtualQuantity>1024</rasd:VirtualQuantity>
</Item>
<Item>
<rasd:Address>0</rasd:Address>
<rasd:Description>IDE Controller</rasd:Description>
<rasd:ElementName>ideController0</rasd:ElementName>
<rasd:InstanceID>3</rasd:InstanceID>
<rasd:ResourceType>5</rasd:ResourceType>
</Item>
<Item>
<rasd:AddressOnParent>0</rasd:AddressOnParent>
<rasd:ElementName>disk0</rasd:ElementName>
<rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
<rasd:InstanceID>4</rasd:InstanceID>
<rasd:Parent>3</rasd:Parent>
<rasd:ResourceType>17</rasd:ResourceType>
</Item>
<Item>
<rasd:AddressOnParent>1</rasd:AddressOnParent>
<rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
<rasd:Connection>VM Network</rasd:Connection>
<rasd:Description>VmxNet3 ethernet adapter on &quot;VM Network&quot;</rasd:Description>
<rasd:ElementName>ethernet0</rasd:ElementName>
<rasd:InstanceID>5</rasd:InstanceID>
<rasd:ResourceSubType>VmxNet3</rasd:ResourceSubType>
<rasd:ResourceType>10</rasd:ResourceType>
<vmw:Config ovf:required="false" vmw:key="slotInfo.pciSlotNumber" vmw:value="160"/>
<vmw:Config ovf:required="false" vmw:key="connectable.allowGuestControl" vmw:value="true"/>
</Item>
<Item ovf:required="false">
<rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
<rasd:ElementName>video</rasd:ElementName>
<rasd:InstanceID>6</rasd:InstanceID>
<rasd:ResourceType>24</rasd:ResourceType>
</Item>
<Item ovf:required="false">
<rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
<rasd:ElementName>vmci</rasd:ElementName>
<rasd:InstanceID>7</rasd:InstanceID>
<rasd:ResourceSubType>vmware.vmci</rasd:ResourceSubType>
<rasd:ResourceType>1</rasd:ResourceType>
</Item>
<vmw:Config ovf:required="false" vmw:key="firmware" vmw:value="{FIRMWARE_TYPE}"/>
</VirtualHardwareSection>
<AnnotationSection ovf:required="false">
<Info>A human-readable annotation</Info>
<Annotation>MikroTik RouterOS CHR v7.19.4</Annotation>
</AnnotationSection>
</VirtualSystem>
</Envelope>'
sudo modprobe nbd
sudo apt-get install -y qemu-utils > /dev/null
truncate --size 128M chr-$LATEST_VERSION$ARCH.img
sgdisk --clear --set-alignment=2 \
--new=1::+32M --typecode=1:8300 --change-name=1:"RouterOS Boot" --attributes=1:set:2 \
--new=2::-0 --typecode=2:8300 --change-name=2:"RouterOS" \
--gpttombr=1:2 \
chr-$LATEST_VERSION$ARCH.img
dd if=chr-$LATEST_VERSION$ARCH.img of=pt.bin bs=1 count=66 skip=446
echo -e "\x80" | dd of=pt.bin bs=1 count=1 conv=notrunc
sgdisk --mbrtogpt --clear --set-alignment=2 \
--new=1::+32M --typecode=1:8300 --change-name=1:"RouterOS Boot" --attributes=1:set:2 \
--new=2::-0 --typecode=2:8300 --change-name=2:"RouterOS" \
chr-$LATEST_VERSION$ARCH.img
dd if=chr.img of=chr-$LATEST_VERSION$ARCH.img bs=1 count=446 conv=notrunc
dd if=pt.bin of=chr-$LATEST_VERSION$ARCH.img bs=1 count=66 seek=446 conv=notrunc
sudo qemu-nbd -c /dev/nbd0 -f raw chr-$LATEST_VERSION$ARCH.img
sudo mkfs.vfat -n "Boot" /dev/nbd0p1
sudo mkfs.ext4 -F -L "RouterOS" -m 0 /dev/nbd0p2
sudo mkdir -p ./img/{boot,routeros}
sudo mount /dev/nbd0p1 ./img/boot/
if [ "${{ matrix.arch }}" == "x86" ]; then
sudo cp chr.img chr-$LATEST_VERSION$ARCH-legacy-bios.img
sudo qemu-nbd -c /dev/nbd1 -f raw chr-$LATEST_VERSION$ARCH-legacy-bios.img
sudo -E python3 patch.py block /dev/nbd1p1 EFI/BOOT/BOOTX64.EFI
sudo mkdir -p ./chr/{boot,routeros}
sudo mount /dev/nbd1p1 ./chr/boot/
sudo mkdir -p ./img/boot/EFI/BOOT
sudo cp ./chr/boot/EFI/BOOT/BOOTX64.EFI ./img/boot/EFI/BOOT/BOOTX64.EFI
sudo umount /dev/nbd1p1
sudo shred -v -n 1 -z /dev/nbd1p2
sudo mkfs.ext4 -F -L "RouterOS" -m 0 /dev/nbd1p2
sudo mount /dev/nbd1p2 ./chr/routeros/
sudo mkdir -p ./chr/routeros/{var/pdb/{system,option},boot,rw/disk}
sudo cp ./all_packages/option-$LATEST_VERSION$ARCH.npk ./chr/routeros/var/pdb/option/image
sudo cp ./all_packages/routeros-$LATEST_VERSION$ARCH.npk ./chr/routeros/var/pdb/system/image
echo -e '#!/bin/sh\n# This script will be executed *before* RouterOS *loader* start.\n# You can put your own initialization stuff in here\n' | sudo tee ./chr/routeros/rw/disk/rc.local
sudo umount /dev/nbd1p2
sudo qemu-nbd -d /dev/nbd1
sudo rm -rf ./chr
sudo qemu-img convert -f raw -O qcow2 chr-$LATEST_VERSION$ARCH-legacy-bios.img chr-$LATEST_VERSION$ARCH-legacy-bios.qcow2
sudo qemu-img convert -f raw -O vmdk chr-$LATEST_VERSION$ARCH-legacy-bios.img chr-$LATEST_VERSION$ARCH-legacy-bios.vmdk
sudo qemu-img convert -f raw -O vpc chr-$LATEST_VERSION$ARCH-legacy-bios.img chr-$LATEST_VERSION$ARCH-legacy-bios.vhd
sudo qemu-img convert -f raw -O vhdx chr-$LATEST_VERSION$ARCH-legacy-bios.img chr-$LATEST_VERSION$ARCH-legacy-bios.vhdx
sudo qemu-img convert -f raw -O vdi chr-$LATEST_VERSION$ARCH-legacy-bios.img chr-$LATEST_VERSION$ARCH-legacy-bios.vdi
sudo qemu-img convert -f raw -O vmdk -o subformat=streamOptimized chr-$LATEST_VERSION$ARCH-legacy-bios.img chr-$LATEST_VERSION$ARCH-legacy-bios-disk1.vmdk
VMDK=chr-$LATEST_VERSION$ARCH-legacy-bios-disk1.vmdk
OVF=chr-$LATEST_VERSION$ARCH-legacy-bios.ovf
OVA=chr-$LATEST_VERSION$ARCH-legacy-bios.ova
FILE_SIZE=$(stat -c %s "$VMDK")
VIRTUAL_SIZE=$(qemu-img info --output=json "$VMDK" | jq '.["virtual-size"]' | head -n 1)
CAPACITY_MB=$((VIRTUAL_SIZE / 1024 / 1024))
POPULATED_SIZE=$FILE_SIZE
FIRMWARE_TYPE="bios"
echo "$OVF_TEMPLATE" | sudo sed -e "s|{VMDK_FILE}|$VMDK|g" \
-e "s|{FILE_SIZE}|$FILE_SIZE|g" \
-e "s|{CAPACITY_MB}|$CAPACITY_MB|g" \
-e "s|{POPULATED_SIZE}|$POPULATED_SIZE|g" \
-e "s|{FIRMWARE_TYPE}|$FIRMWARE_TYPE|g" |sudo tee $OVF
sudo tar -cvf "$OVA" "$OVF" "$VMDK"
sudo rm $VMDK
sudo rm $OVF
sudo zip $OVA.zip $OVA
sudo rm $OVA
sudo zip chr-$LATEST_VERSION$ARCH-legacy-bios.qcow2.zip chr-$LATEST_VERSION$ARCH-legacy-bios.qcow2
sudo zip chr-$LATEST_VERSION$ARCH-legacy-bios.vmdk.zip chr-$LATEST_VERSION$ARCH-legacy-bios.vmdk
sudo zip chr-$LATEST_VERSION$ARCH-legacy-bios.vhd.zip chr-$LATEST_VERSION$ARCH-legacy-bios.vhd
sudo zip chr-$LATEST_VERSION$ARCH-legacy-bios.vhdx.zip chr-$LATEST_VERSION$ARCH-legacy-bios.vhdx
sudo zip chr-$LATEST_VERSION$ARCH-legacy-bios.vdi.zip chr-$LATEST_VERSION$ARCH-legacy-bios.vdi
sudo zip chr-$LATEST_VERSION$ARCH-legacy-bios.img.zip chr-$LATEST_VERSION$ARCH-legacy-bios.img
sudo rm chr-$LATEST_VERSION$ARCH-legacy-bios.qcow2
sudo rm chr-$LATEST_VERSION$ARCH-legacy-bios.vmdk
sudo rm chr-$LATEST_VERSION$ARCH-legacy-bios.vhd
sudo rm chr-$LATEST_VERSION$ARCH-legacy-bios.vhdx
sudo rm chr-$LATEST_VERSION$ARCH-legacy-bios.vdi
sudo rm chr-$LATEST_VERSION$ARCH-legacy-bios.img
elif [ "${{ matrix.arch }}" == "arm64" ]; then
sudo qemu-nbd -c /dev/nbd1 -f raw chr.img
sudo mkdir -p ./chr/boot
sudo mount /dev/nbd1p1 ./chr/boot/
sudo -E python3 patch.py kernel ./chr/boot/EFI/BOOT/BOOTAA64.EFI -O ./BOOTAA64.EFI
sudo mkdir -p ./img/boot/EFI/BOOT
sudo cp ./BOOTAA64.EFI ./img/boot/EFI/BOOT/BOOTAA64.EFI
sudo umount /dev/nbd1p1
sudo rm -rf ./chr
sudo qemu-nbd -d /dev/nbd1
fi
sudo umount /dev/nbd0p1
sudo mount /dev/nbd0p2 ./img/routeros/
sudo mkdir -p ./img/routeros/{var/pdb/{system,option},boot,rw}
sudo cp ./all_packages/option-$LATEST_VERSION$ARCH.npk ./img/routeros/var/pdb/option/image
sudo cp ./all_packages/routeros-$LATEST_VERSION$ARCH.npk ./img/routeros/var/pdb/system/image
sudo umount /dev/nbd0p2
sudo rm -rf ./img
sudo qemu-nbd -d /dev/nbd0
sudo qemu-img convert -f raw -O qcow2 chr-$LATEST_VERSION$ARCH.img chr-$LATEST_VERSION$ARCH.qcow2
sudo qemu-img convert -f raw -O vmdk chr-$LATEST_VERSION$ARCH.img chr-$LATEST_VERSION$ARCH.vmdk
sudo qemu-img convert -f raw -O vpc chr-$LATEST_VERSION$ARCH.img chr-$LATEST_VERSION$ARCH.vhd
sudo qemu-img convert -f raw -O vhdx chr-$LATEST_VERSION$ARCH.img chr-$LATEST_VERSION$ARCH.vhdx
sudo qemu-img convert -f raw -O vdi chr-$LATEST_VERSION$ARCH.img chr-$LATEST_VERSION$ARCH.vdi
sudo qemu-img convert -f raw -O vmdk -o subformat=streamOptimized chr-$LATEST_VERSION$ARCH.img chr-$LATEST_VERSION$ARCH-disk1.vmdk
VMDK=chr-$LATEST_VERSION$ARCH-disk1.vmdk
OVF=chr-$LATEST_VERSION$ARCH.ovf
OVA=chr-$LATEST_VERSION$ARCH.ova
FILE_SIZE=$(stat -c %s "$VMDK")
VIRTUAL_SIZE=$(qemu-img info --output=json "$VMDK" | jq '.["virtual-size"]' | head -n 1)
CAPACITY_MB=$((VIRTUAL_SIZE / 1024 / 1024))
POPULATED_SIZE=$FILE_SIZE
FIRMWARE_TYPE="efi"
echo "$OVF_TEMPLATE" | sudo sed -e "s|{VMDK_FILE}|$VMDK|g" \
-e "s|{FILE_SIZE}|$FILE_SIZE|g" \
-e "s|{CAPACITY_MB}|$CAPACITY_MB|g" \
-e "s|{POPULATED_SIZE}|$POPULATED_SIZE|g" \
-e "s|{FIRMWARE_TYPE}|$FIRMWARE_TYPE|g" |sudo tee $OVF
sudo tar -cvf "$OVA" "$OVF" "$VMDK"
sudo rm $VMDK
sudo rm $OVF
sudo zip $OVA.zip $OVA
sudo rm $OVA
sudo zip chr-$LATEST_VERSION$ARCH.qcow2.zip chr-$LATEST_VERSION$ARCH.qcow2
sudo zip chr-$LATEST_VERSION$ARCH.vmdk.zip chr-$LATEST_VERSION$ARCH.vmdk
sudo zip chr-$LATEST_VERSION$ARCH.vhd.zip chr-$LATEST_VERSION$ARCH.vhd
sudo zip chr-$LATEST_VERSION$ARCH.vhdx.zip chr-$LATEST_VERSION$ARCH.vhdx
sudo zip chr-$LATEST_VERSION$ARCH.vdi.zip chr-$LATEST_VERSION$ARCH.vdi
sudo zip chr-$LATEST_VERSION$ARCH.img.zip chr-$LATEST_VERSION$ARCH.img
sudo rm chr-$LATEST_VERSION$ARCH.qcow2
sudo rm chr-$LATEST_VERSION$ARCH.vmdk
sudo rm chr-$LATEST_VERSION$ARCH.vhd
sudo rm chr-$LATEST_VERSION$ARCH.vhdx
sudo rm chr-$LATEST_VERSION$ARCH.vdi
sudo rm chr-$LATEST_VERSION$ARCH.img
- name: Cache all_packages${{ env.ARCH }}-${{ env.LATEST_VERSION }}.zip
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch != 'x86' && matrix.arch != 'arm64'
id: cache-all-packages
uses: actions/cache@v4
with:
path: all_packages.zip
key: all_packages${{ env.ARCH }}-${{ env.LATEST_VERSION }}.zip
- name: Get all_packages{{ env.ARCH }}-${{ env.LATEST_VERSION }}.zip
if: steps.get_latest.outputs.has_new_version == 'true' && steps.cache-all-packages.outputs.cache-hit != 'true' && matrix.arch != 'x86' && matrix.arch != 'arm64'
run: |
sudo wget -nv -O all_packages.zip https://download.mikrotik.com/routeros/$LATEST_VERSION/all_packages-${{ matrix.arch }}-$LATEST_VERSION.zip
- name: Cache Main package routeros-${{ env.LATEST_VERSION }}${{ env.ARCH }}.npk
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch != 'x86' && matrix.arch != 'arm64'
id: cache-main-package
uses: actions/cache@v4
with:
path: routeros.npk
key: routeros-${{ env.LATEST_VERSION }}${{ env.ARCH }}.npk
- name: Get Main package routeros-${{ env.LATEST_VERSION }}${{ env.ARCH }}.npk
if: steps.get_latest.outputs.has_new_version == 'true' && steps.cache-main-package.outputs.cache-hit != 'true' && matrix.arch != 'x86' && matrix.arch != 'arm64'
run: |
sudo wget -nv -O routeros.npk https://download.mikrotik.com/routeros/$LATEST_VERSION/routeros-$LATEST_VERSION$ARCH.npk
- name: Patch all_packages{{ env.ARCH }}-${{ env.LATEST_VERSION }}.zip
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch != 'x86' && matrix.arch != 'arm64'
run: |
sudo unzip all_packages.zip -d ./all_packages
sudo cp routeros.npk routeros-$LATEST_VERSION$ARCH.npk
sudo -E python3 patch.py npk routeros-$LATEST_VERSION$ARCH.npk
NPK_FILES=$(find ./all_packages/*.npk)
for file in $NPK_FILES; do
sudo -E python3 npk.py sign $file $file
done
sudo cp -f routeros-$LATEST_VERSION$ARCH.npk ./all_packages/routeros-$LATEST_VERSION$ARCH.npk
sudo -E python3 npk.py create ./all_packages/gps-$LATEST_VERSION$ARCH.npk ./all_packages/option-$LATEST_VERSION$ARCH.npk option ./option.sfs -desc="busybox"
sudo -E python3 npk.py create ./all_packages/gps-$LATEST_VERSION$ARCH.npk ./all_packages/python3-$LATEST_VERSION$ARCH.npk python3 ./python3.sfs -desc="python 3.11.13"
cd ./all_packages
sudo zip ../all_packages-${{ matrix.arch }}-$LATEST_VERSION.zip *.npk
cd ..
- name: Cache refind
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86' && env.RELEASE == 'true'
id: cache-refind
uses: actions/cache@v4
with:
path: refind-bin-0.14.2.zip
key: refind
- name: Get refind
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86' && steps.cache-refind.outputs.cache-hit != 'true' && env.RELEASE == 'true'
run: sudo wget -nv -O refind-bin-0.14.2.zip https://nchc.dl.sourceforge.net/project/refind/0.14.2/refind-bin-0.14.2.zip
- name: Create install-image-${{ env.LATEST_VERSION }}${{ env.ARCH }}.img
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86' && env.RELEASE == 'true'
run: |
sudo modprobe nbd
sudo apt-get install -y qemu-utils extlinux > /dev/null
truncate --size 128M install-image-$LATEST_VERSION.img
sudo qemu-nbd -c /dev/nbd0 -f raw install-image-$LATEST_VERSION.img
sudo mkfs.vfat -n "Install" /dev/nbd0
sudo mkdir ./install
sudo mount /dev/nbd0 ./install
sudo mkdir -p ./install/EFI/BOOT
sudo unzip refind-bin-0.14.2.zip refind-bin-0.14.2/refind/refind_x64.efi
sudo cp refind-bin-0.14.2/refind/refind_x64.efi ./install/EFI/BOOT/BOOTX64.EFI
sudo rm -rf refind-bin-0.14.2
echo -e 'timeout 0\ntextonly\ntextmode 0\nshowtools shutdown, reboot, exit\nmenuentry "Install RouterOS" {\n\tloader /linux\n\toptions "load_ramdisk=1 root=/dev/ram0 -install -hdd"\n}\ndefault_selection /EFI/BOOT/BOOTX64.EFI' \
> refind.conf
sudo cp refind.conf ./install/EFI/BOOT/
sudo rm refind.conf
sudo extlinux --install -H 64 -S 32 ./install/
echo -e 'default system\nLABEL system\n\tKERNEL linux\n\tAPPEND load_ramdisk=1 -install -hdd' \
> syslinux.cfg
sudo cp syslinux.cfg ./install/
sudo rm syslinux.cfg
sudo cp ./BOOTX64.EFI ./install/linux
NPK_FILES=($(find ./all_packages/*.npk))
for ((i=1; i<=${#NPK_FILES[@]}; i++))
do
echo "${NPK_FILES[$i-1]}=>$i.npk"
sudo cp ${NPK_FILES[$i-1]} ./install/$i.npk
done
sudo touch ./install/CHOOSE
sudo touch ./install/autorun.scr
sudo umount /dev/nbd0
sudo qemu-nbd -d /dev/nbd0
sudo rm -rf ./install
sudo zip install-image-$LATEST_VERSION.zip ./install-image-$LATEST_VERSION.img
sudo rm ./install-image-$LATEST_VERSION.img
- name: Cache NetInstall ${{ env.LATEST_VERSION }}${{ env.ARCH }}
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86' && env.RELEASE == 'true'
id: cache-netinstall
uses: actions/cache@v4
with:
path: |
netinstall.zip
netinstall.tar.gz
key: netinstall-${{ env.LATEST_VERSION }}
- name: Get netinstall ${{ env.LATEST_VERSION }}
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86' && steps.cache-netinstall.outputs.cache-hit != 'true' && env.RELEASE == 'true'
run: |
sudo wget -nv -O netinstall.zip https://download.mikrotik.com/routeros/$LATEST_VERSION/netinstall-$LATEST_VERSION.zip
sudo wget -nv -O netinstall.tar.gz https://download.mikrotik.com/routeros/$LATEST_VERSION/netinstall-$LATEST_VERSION.tar.gz
- name: Patch netinstall ${{ env.LATEST_VERSION }}${{ env.ARCH }}
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86' && env.RELEASE == 'true'
run: |
sudo unzip netinstall.zip
sudo -E python3 patch.py netinstall netinstall.exe
sudo zip netinstall-$LATEST_VERSION.zip ./netinstall.exe
sudo tar -xvf netinstall.tar.gz
sudo -E python3 patch.py netinstall netinstall-cli
sudo tar -czvf netinstall-$LATEST_VERSION.tar.gz ./netinstall-cli
- name: Upload Files
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'true'
run: |
sudo mkdir -p ./publish/$LATEST_VERSION
sudo cp CHANGELOG ./publish/$LATEST_VERSION/
sudo cp ./all_packages/*.npk ./publish/$LATEST_VERSION/
sudo chown -R root:root ./publish/
LOCAL_PATH=./publish/$LATEST_VERSION
REMOTE_PATH=${{ secrets.SSH_DIRECTORY }}
SERVER=${{ secrets.SSH_SERVER }}
USER=${{ secrets.SSH_USERNAME }}
PASS=${{ secrets.SSH_PASSWORD }}
PORT=${{ secrets.SSH_PORT }}
CHANNEL=${{ matrix.channel }}
sudo apt-get install -y lftp ssh sshpass > /dev/null 2>&1
sudo -E lftp -u "$USER","$PASS" sftp://$SERVER:$PORT <<EOF
set sftp:auto-confirm yes
mirror --reverse --verbose --only-newer ./publish "$REMOTE_PATH"
bye
EOF
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no -p $PORT $USER@$SERVER \
"echo $LATEST_VERSION $BUILD_TIME | tee /rw/disk/$REMOTE_PATH/NEWESTa7.$CHANNEL; bash /rw/disk/$REMOTE_PATH/packages.sh /rw/disk/$REMOTE_PATH/$LATEST_VERSION"
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no -p $PORT $USER@$SERVER \
"chown -R 32768:32768 /rw/disk/$REMOTE_PATH/"
- name: Clear Cloudflare cache
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'true'
run: |
curl --request POST --url "https://api.cloudflare.com/client/v4/zones/fe6831ed6dc9e6235e69ef2a31f2e7fe/purge_cache" \
--header "Authorization: Bearer 9GDQkzU51QXaqzp1qMjyFKpyeJyOdnNoG9GZQaGP" \
--header "Content-Type:application/json" \
--data '{"purge_everything": true}'
- name: Delete Release tag ${{ env.LATEST_VERSION }} ${{ env.ARCH }}
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'true'
run: |
HEADER="Authorization: token ${{ secrets.GITHUB_TOKEN }}"
RELEASE_INFO=$(curl -s -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/releases/tags/$LATEST_VERSION$ARCH)
RELEASE_ID=$(echo $RELEASE_INFO | jq -r '.id')
echo "Release ID: $RELEASE_ID"
if [ "$RELEASE_ID" != "null" ]; then
curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/git/refs/tags/$LATEST_VERSION$ARCH
echo "Tag $LATEST_VERSION$ARCH deleted successfully."
curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID
echo "Release with tag $LATEST_VERSION$ARCH deleted successfully."
else
echo "Release not found for tag: $LATEST_VERSION)"
fi
- name: Create Release tag ${{ env.LATEST_VERSION }} ${{ env.ARCH }}
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'true'
uses: softprops/action-gh-release@v2
with:
name: "RouterOS ${{ env.LATEST_VERSION }} ${{ env.ARCH }}"
body_path: "CHANGELOG"
tag_name: ${{ env.LATEST_VERSION }}${{ env.ARCH }}
make_latest: ${{ matrix.channel == 'stable'}} && ${{ matrix.arch == 'x86'}}
prerelease: ${{ matrix.channel == 'testing' }}
files: |
mikrotik-${{ env.LATEST_VERSION }}${{ env.ARCH }}.iso
chr-${{ env.LATEST_VERSION }}*.zip
netinstall-${{ env.LATEST_VERSION }}.*
install-image-${{ env.LATEST_VERSION }}.zip
routeros-${{ env.LATEST_VERSION }}${{ env.ARCH }}.npk
all_packages-*-${{ env.LATEST_VERSION }}.zip
- name: Upload Files as Artifact (No Release)
if: steps.get_latest.outputs.has_new_version == 'true' && env.RELEASE == 'false'
uses: actions/upload-artifact@v4
with:
name: mikrotik-${{ env.LATEST_VERSION }}${{ env.ARCH }}
path: |
all_packages-*-${{ env.LATEST_VERSION }}.zip
mikrotik-${{ env.LATEST_VERSION }}${{ env.ARCH }}.iso
chr-${{ env.LATEST_VERSION }}*.zip

View File

@ -1,570 +0,0 @@
name: Patch RouterOS v6
on:
workflow_dispatch:
inputs:
channel:
description: 'Channel (stable, long-term)'
required: true
default: 'stable'
type: choice
options:
- stable
- long-term
version:
description: "Specify version (e.g., 6.49.19), empty for latest"
required: false
default: ''
type: string
buildtime:
description: "Custom build time, leave empty for original"
required: false
default: ''
type: string
release:
description: "Release to Upload "
required: false
default: false
type: boolean
workflow_call:
inputs:
channel:
required: true
type: string
version:
required: false
type: string
default: ''
buildtime:
required: false
type: string
default: ''
release:
required: false
type: boolean
default: false
env:
CUSTOM_LICENSE_PRIVATE_KEY: ${{ secrets.CUSTOM_LICENSE_PRIVATE_KEY }}
CUSTOM_LICENSE_PUBLIC_KEY: ${{ secrets.CUSTOM_LICENSE_PUBLIC_KEY }}
CUSTOM_NPK_SIGN_PRIVATE_KEY: ${{ secrets.CUSTOM_NPK_SIGN_PRIVATE_KEY }}
CUSTOM_NPK_SIGN_PUBLIC_KEY: ${{ secrets.CUSTOM_NPK_SIGN_PUBLIC_KEY }}
CUSTOM_CLOUD_PUBLIC_KEY: ${{ secrets.CUSTOM_CLOUD_PUBLIC_KEY }}
MIKRO_LICENSE_PUBLIC_KEY: ${{ secrets.MIKRO_LICENSE_PUBLIC_KEY }}
MIKRO_NPK_SIGN_PUBLIC_KEY: ${{ secrets.MIKRO_NPK_SIGN_PUBLIC_KEY }}
MIKRO_CLOUD_PUBLIC_KEY: ${{ secrets.MIKRO_CLOUD_PUBLIC_KEY }}
MIKRO_LICENCE_URL: ${{ secrets.MIKRO_LICENCE_URL }}
CUSTOM_LICENCE_URL: ${{ secrets.CUSTOM_LICENCE_URL }}
MIKRO_UPGRADE_URL: ${{ secrets.MIKRO_UPGRADE_URL }}
CUSTOM_UPGRADE_URL: ${{ secrets.CUSTOM_UPGRADE_URL }}
MIKRO_RENEW_URL: ${{ secrets.MIKRO_RENEW_URL }}
CUSTOM_RENEW_URL: ${{ secrets.CUSTOM_RENEW_URL }}
MIKRO_CLOUD_URL: ${{ secrets.MIKRO_CLOUD_URL }}
CUSTOM_CLOUD_URL: ${{ secrets.CUSTOM_CLOUD_URL }}
jobs:
Prepare_Patch:
runs-on: ubuntu-24.04
outputs:
VERSION: ${{ steps.prepare.outputs.VERSION }}
BUILD_TIME: ${{ steps.prepare.outputs.BUILD_TIME }}
ARCHS: ${{ steps.prepare.outputs.ARCHS }}
RELEASE: ${{ steps.prepare.outputs.RELEASE }}
SKIP_BUILD: ${{ steps.prepare.outputs.SKIP_BUILD }}
steps:
- name: Prepare Patch for ${{ inputs.channel }}
id: prepare
run: |
CHANNEL="${{ inputs.channel }}"
VERSION="${{ inputs.version }}"
RELEASE="${{ inputs.release }}"
BUILD_TIME="${{ inputs.buildtime }}"
ARCHS='["x86"]'
SKIP_BUILD=false
if [ -z "$VERSION" ]; then
echo "Checking latest version from $CHANNEL channel..."
TIMESTAMP=$(date +%s)
NEWEST=$(wget -nv -O - "https://${{ env.MIKRO_UPGRADE_URL }}/routeros/NEWEST6.$CHANNEL?t=$TIMESTAMP")
VERSION=$(echo "$NEWEST" | cut -d ' ' -f1)
echo "Official version: $VERSION"
if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then
PATCH_NEWEST=$(wget -nv -O - "https://${{ env.CUSTOM_UPGRADE_URL }}/routeros/NEWEST6.$CHANNEL?t=$TIMESTAMP" 2>/dev/null || echo "")
PATCH_VERSION=$(echo "$PATCH_NEWEST" | cut -d ' ' -f1)
echo "Patch version: ${PATCH_VERSION:-none}"
if [ "$PATCH_VERSION" == "$VERSION" ]; then
echo "No new version for $CHANNEL, skipping..."
SKIP_BUILD=true
else
echo "New version $VERSION available, building..."
fi
fi
fi
echo "SKIP_BUILD=$SKIP_BUILD" >> $GITHUB_OUTPUT
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "BUILD_TIME=$BUILD_TIME" >> $GITHUB_OUTPUT
echo "ARCHS=$ARCHS" >> $GITHUB_OUTPUT
echo "RELEASE=$RELEASE" >> $GITHUB_OUTPUT
echo "Version: $VERSION, Archs: $ARCHS"
Patch_RouterOS:
needs: Prepare_Patch
if: needs.Prepare_Patch.outputs.SKIP_BUILD == 'false'
runs-on: ubuntu-24.04
strategy:
matrix:
arch: ${{ fromJson(needs.Prepare_Patch.outputs.ARCHS) }}
fail-fast: false
env:
TZ: 'Asia/Shanghai'
CHANNEL: ${{ inputs.channel }}
ARCH: ${{ matrix.arch }}
VERSION: ${{ needs.Prepare_Patch.outputs.VERSION }}
BUILD_TIME: ${{ needs.Prepare_Patch.outputs.BUILD_TIME }}
BUILD_DIR: "/tmp/build/${{ needs.Prepare_Patch.outputs.VERSION }}"
PUBLISH_DIR: "${{ github.workspace }}/publish/${{ needs.Prepare_Patch.outputs.VERSION }}"
ARCH_EXT: ""
PACKAGES: ""
steps:
- uses: actions/checkout@v6
- name: Setup environment
run: |
sudo apt-get update > /dev/null
sudo apt-get install -y zip unzip squashfs-tools jq > /dev/null
sudo apt-get install -y python3-crcmod python3-pefile python3-pyelftools > /dev/null
if [ "$ARCH" == "x86" ]; then
ARCH_EXT=""
else
ARCH_EXT="-$ARCH"
fi
echo "ARCH_EXT=$ARCH_EXT" >> $GITHUB_ENV
echo "ARCH_EXT: $ARCH_EXT"
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
echo "BUILD_DIR: $BUILD_DIR"
rm -rf "$PUBLISH_DIR"
mkdir -p "$PUBLISH_DIR"
echo "PUBLISH_DIR: $PUBLISH_DIR"
wget -nv -O $PUBLISH_DIR/CHANGELOG "https://${{ env.MIKRO_UPGRADE_URL }}/routeros/$VERSION/CHANGELOG"
cat $PUBLISH_DIR/CHANGELOG
- name: Create Squashfs for Option Package
run: |
echo "Creating Option Package Squashfs for channel: $CHANNEL, arch: $ARCH"
mkdir -p $BUILD_DIR/option-root/bin/
cp ./busybox/busybox_$ARCH $BUILD_DIR/option-root/bin/busybox
chmod +x $BUILD_DIR/option-root/bin/busybox
cp ./keygen/keygen_$ARCH $BUILD_DIR/option-root/bin/keygen
chmod +x $BUILD_DIR/option-root/bin/keygen
chmod +x ./busybox/busybox_x86
COMMANDS=$(./busybox/busybox_x86 --list)
for cmd in $COMMANDS; do
ln -sf /pckg/option/bin/busybox $BUILD_DIR/option-root/bin/$cmd
done
mkdir -p $BUILD_DIR/option-root/home/web/webfig/
BUSYBOX=$(./busybox/busybox_x86 | head -1 | cut -d' ' -f2)
tee $BUILD_DIR/option.jg > /dev/null << EOF
[
{
name:'Patch About',
title:'Patch About',
icon: 30,
prio: 2000,
c:[
{
title:'Patch About',
type:'item',
path:[24, 2],
autorefresh: 1000,
ro: 1,
c: [
{type:'gridcell'},
{name:'Patch',type:'tab',inline: 1},
{name:'Author',type:'string',def:'elseif@live.cn'},
{name:'GitHub',type:'string',def:'https://github.com/elseif/MikroTikPatch', url:1},
{name:'Telegram',type:'string',def:'https://telegram.me/mikrotikpatch', url:1},
{name:'Homepage',type:'string',def:'https://mikrotik.ltd', url:1},
{name:'Busybox',type:'string',def:'$BUSYBOX'},
{name:'System',type:'tab',inline: 1},
{name:'Uptime',type:'interval',id:'u1'},
{name:'Architecture Name',type:'string',id:'s1a'},
{name:'Board Name',type:'string',id:'s2c'},
{name:'Version',type:'version',id:'s16'},
{name:'Build Time',type:'string',id:'s27'},
{type:'separator'},
{type:'toolbar',
c: [
{name:'License',type:'contextmenu',autostart: 1,group:'System',link: [],open:'License',tab:'License'},
{name:'Check For Updates',type:'contextmenu',autostart: 1,group:'System',link: [],open:'Packages',tab:'Check For Updates'}
]
}
]
}
]
}
]
EOF
HASH=$(python3 -E <<EOF
import hashlib, crcmod, sys
with open('$BUILD_DIR/option.jg','rb') as f:
data = f.read()
sha1 = hashlib.sha1(data).hexdigest()[:12]
crc = crcmod.mkCrcFun(0x104C11DB7, initCrc=0xFFFFFFFF, rev=False, xorOut=0xFFFFFFFF)(data)
print(f"{sha1} {crc}")
EOF
)
SHA1_PREFIX=$(echo "$HASH" | awk '{print $1}')
CRC=$(echo "$HASH" | awk '{print $2}')
mv $BUILD_DIR/option.jg $BUILD_DIR/option-$SHA1_PREFIX.jg
python3 -E << EOF
import gzip
gzip.open('$BUILD_DIR/option-$SHA1_PREFIX.jg.gz', 'wb', compresslevel=0).write(open('$BUILD_DIR/option-$SHA1_PREFIX.jg', 'rb').read())
EOF
SIZE=$(stat -c %s $BUILD_DIR/option-$SHA1_PREFIX.jg.gz)
mv $BUILD_DIR/option-$SHA1_PREFIX.jg.gz $BUILD_DIR/option-root/home/web/webfig/
tee $BUILD_DIR/option-root/home/web/webfig/option.info > /dev/null << EOF
{ crc: $CRC, size: $SIZE, name: "option.jg", unique: "option-$SHA1_PREFIX.jg",version: "$VERSION" },
EOF
ln -sf option-$SHA1_PREFIX.jg.gz $BUILD_DIR/option-root/home/web/webfig/option.jg.gz
mksquashfs $BUILD_DIR/option-root $BUILD_DIR/option.sfs -no-recovery -noappend -exit-on-error -quiet -comp xz -no-xattrs -b 256k -all-root
rm -rf $BUILD_DIR/option-root
echo "✅ Option Package Squashfs created: $BUILD_DIR/option.sfs"
ls -la "$BUILD_DIR/option.sfs"
- name: Cache mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}.iso
id: cache-mikrotik-iso
uses: actions/cache@v5
with:
path: ${{ env.BUILD_DIR }}/mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}.iso
key: mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}.iso
- name: Get mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}.iso
if: steps.cache-mikrotik-iso.outputs.cache-hit != 'true'
run: |
wget -nv -O $BUILD_DIR/mikrotik-$VERSION$ARCH_EXT.iso \
https://download.mikrotik.com/routeros/$VERSION/mikrotik-$VERSION$ARCH_EXT.iso
- name: Unpack ISO
run: |
mkdir -p $BUILD_DIR/all_packages
mkdir -p $BUILD_DIR/iso
mkdir -p $BUILD_DIR/new_iso
sudo mount -o loop,ro $BUILD_DIR/mikrotik-$VERSION$ARCH_EXT.iso $BUILD_DIR/iso
cp -r $BUILD_DIR/iso/* $BUILD_DIR/new_iso/
chmod -R +w $BUILD_DIR/new_iso/
mv $BUILD_DIR/new_iso/*.npk $BUILD_DIR/all_packages/
mv $BUILD_DIR/all_packages/system-$VERSION$ARCH_EXT.npk $BUILD_DIR/
sudo umount $BUILD_DIR/iso
rm -rf $BUILD_DIR/iso
- name: Cache routeros-${{ env.ARCH }}-${{ env.VERSION }}.npk
id: cache-routeros-npk
uses: actions/cache@v5
with:
path: ${{ env.BUILD_DIR }}/routeros-${{ env.ARCH }}-${{ env.VERSION }}.npk
key: routeros-${{ env.ARCH }}-${{ env.VERSION }}.npk
- name: Get routeros-${{ env.ARCH }}-${{ env.VERSION }}.npk
if: steps.cache-routeros-npk.outputs.cache-hit != 'true'
run: |
wget -nv -O $BUILD_DIR/routeros-$ARCH-$VERSION.npk \
https://download.mikrotik.com/routeros/$VERSION/routeros-$ARCH-$VERSION.npk
- name: Get loader patch files
run: |
wget -nv -O loader.7z https://${{ env.CUSTOM_UPGRADE_URL }}/routeros/loader.7z
sudo apt-get install -y p7zip-full > /dev/null
7z x -p"${{ secrets.LOADER_7Z_PASSWORD }}" loader.7z >> /dev/null
rm loader.7z
- name: Patch NPK files
run: |
sudo apt-get install build-essential -y > /dev/null
chmod +w $BUILD_DIR/all_packages/*.npk
NPK_FILES=$(find $BUILD_DIR/all_packages/*.npk)
for file in $NPK_FILES; do
python3 npk.py sign $file $file
done
OPTION_FROM_NPK=gps-$VERSION$ARCH_EXT.npk
OPTION_NPK=option-$VERSION$ARCH_EXT.npk
python3 npk.py create $BUILD_DIR/all_packages/$OPTION_FROM_NPK $BUILD_DIR/all_packages/$OPTION_NPK option $BUILD_DIR/option.sfs -desc="busybox"
rm -rf $BUILD_DIR/option.sfs
python3 patch.py npk $BUILD_DIR/system-$VERSION$ARCH_EXT.npk -O $BUILD_DIR/all_packages/system-$VERSION$ARCH_EXT.npk
(cd $BUILD_DIR/all_packages && zip -r $PUBLISH_DIR/all_packages-$ARCH-$VERSION.zip .)
mv $BUILD_DIR/all_packages/*.npk $PUBLISH_DIR/
python3 patch.py npk $BUILD_DIR/routeros-$ARCH-$VERSION.npk -O $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk
echo "✅ all package npk files are signed and patched: "
ls -la $PUBLISH_DIR/*.npk
- name: Create ISO & INSTALL image files
run: |
set -e
echo "Creating ISO..."
sudo modprobe nbd
sudo apt-get install syslinux syslinux-common xorriso extlinux qemu-utils -y > /dev/null
unzip $PUBLISH_DIR/all_packages-$ARCH-$VERSION.zip -d $BUILD_DIR/new_iso/
mv $BUILD_DIR/new_iso/isolinux/initrd.rgz $BUILD_DIR/initrd.rgz
python3 patch.py kernel $BUILD_DIR/initrd.rgz -O $BUILD_DIR/new_iso/isolinux/initrd.rgz
rm -f $BUILD_DIR/initrd.rgz
xorriso -as mkisofs -o $PUBLISH_DIR/mikrotik-$VERSION$ARCH_EXT.iso \
-V "MikroTik $VERSION" \
-sysid "" -preparer "MiKroTiK" \
-publisher "" -A "MiKroTiK RouterOS" \
-input-charset utf-8 \
-b isolinux/isolinux.bin \
-c isolinux/boot.cat \
-no-emul-boot \
-boot-load-size 4 \
-boot-info-table \
-R -J \
$BUILD_DIR/new_iso
echo "✅ ISO created: "
ls -la $PUBLISH_DIR/mikrotik-$VERSION$ARCH_EXT.iso
echo "Creating install image..."
truncate --size 128M $BUILD_DIR/install-image-$VERSION.img
mkfs.vfat -I -F 32 -n "Install" $BUILD_DIR/install-image-$VERSION.img
LOOP=$(sudo losetup -f --show $BUILD_DIR/install-image-$VERSION.img)
sudo syslinux --install $LOOP
mkdir -p $BUILD_DIR/install
sudo mount -o loop,uid=$(id -u),gid=$(id -g),umask=000 $LOOP $BUILD_DIR/install
cp -vf $BUILD_DIR/new_iso/isolinux/linux $BUILD_DIR/install/
cp -vf /usr/lib/syslinux/modules/bios/ldlinux.c32 $BUILD_DIR/install/
cp -vf $BUILD_DIR/new_iso/isolinux/initrd.rgz $BUILD_DIR/install/initrd.rgz
tee $BUILD_DIR/install/syslinux.cfg > /dev/null << 'EOF'
default system
LABEL system
KERNEL linux
APPEND load_ramdisk=1 initrd=initrd.rgz -hdd-install
EOF
NPK_FILES=($(find $BUILD_DIR/new_iso/*.npk))
for ((i=1; i<=${#NPK_FILES[@]}; i++))
do
echo "${NPK_FILES[$i-1]}=>$i.npk"
cp ${NPK_FILES[$i-1]} $BUILD_DIR/install/$i.npk
done
touch $BUILD_DIR/install/CHOOSE
sync
sudo umount $BUILD_DIR/install
sudo losetup -d $LOOP
rm -rf $BUILD_DIR/install
echo "✅ install imgage created: "
ls -la $BUILD_DIR/install-image-$VERSION.img
(cd $BUILD_DIR && zip $PUBLISH_DIR/install-image-$VERSION.zip install-image-$VERSION.img)
rm $BUILD_DIR/install-image-$VERSION.img
rm -rf $BUILD_DIR/new_iso
- name: Create CHR image files
run: |
set -e
echo "Creating CHR image..."
mkdir -p $BUILD_DIR/chr
truncate --size 128M $BUILD_DIR/chr-$VERSION$ARCH_EXT.img
sfdisk $BUILD_DIR/chr-$VERSION$ARCH_EXT.img <<EOF
label: dos
unit: sectors
start=1, size=, type=83, bootable
EOF
dd if=$BUILD_DIR/chr-$VERSION$ARCH_EXT.img of=$BUILD_DIR/mbr.bin bs=1 count=512 conv=notrunc
echo "FA31C08ED0BC007C89E65007501FFBFCBF0006B90001F2A5EA1D060000BEBE07B304803C807423803C00750983C610FECB75EFCD18BE9B06AC3C00740B56BB0700B40ECD105EEBF0EBFE8B148B4C0289F5BF0500BB007CB8010257CD135F730C31C0CD134F75EDBE7C06EBCCBFFE7D813D55AA75C289EEEA007C00004572726F72206C6F6164696E67206F7065726174696E672073797374656D004D697373696E67206F7065726174696E672073797374656D0000000000" | xxd -r -p | sudo dd of=$BUILD_DIR/mbr.bin bs=1 count=440 conv=notrunc
printf '\x01' | sudo dd of=$BUILD_DIR/mbr.bin bs=1 count=1 seek=336 conv=notrunc
dd if=$BUILD_DIR/mbr.bin of=$BUILD_DIR/chr-$VERSION$ARCH_EXT.img bs=1 count=512 conv=notrunc
sudo qemu-nbd -d /dev/nbd0
sudo qemu-nbd -c /dev/nbd0 -f raw $BUILD_DIR/chr-$VERSION$ARCH_EXT.img
sudo partprobe /dev/nbd0
sudo mkfs.ext3 -F -L "system" -m 0 /dev/nbd0p1
sudo mount /dev/nbd0p1 $BUILD_DIR/chr
sudo mkdir -p $BUILD_DIR/chr/{boot,bin,rw/disk,var/pdb/{routeros-x86,option,dude}}
mkdir -p $BUILD_DIR/{boot,bin}
python3 npk.py extract $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk bin/milo $BUILD_DIR/bin/milo
chmod +x $BUILD_DIR/bin/milo
python3 npk.py extract $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk boot/vmlinuz-64 $BUILD_DIR/boot/vmlinuz-64
python3 npk.py extract $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk boot/initrd.rgz $BUILD_DIR/boot/initrd.rgz
python3 npk.py extract $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk boot/map $BUILD_DIR/boot/map
python3 npk.py extract $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk boot/milo.conf $BUILD_DIR/boot/milo.conf
python3 npk.py extract $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk boot/vmlinuz $BUILD_DIR/boot/vmlinuz
python3 npk.py extract $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk boot/vmlinuz-smp $BUILD_DIR/boot/vmlinuz-smp
sudo mv $BUILD_DIR/boot $BUILD_DIR/chr/
sudo mv $BUILD_DIR/bin $BUILD_DIR/chr/
sudo cp $PUBLISH_DIR/routeros-$ARCH-$VERSION.npk $BUILD_DIR/chr/var/pdb/routeros-x86/image
sudo cp $PUBLISH_DIR/option-$VERSION$ARCH_EXT.npk $BUILD_DIR/chr/var/pdb/option/image
sudo cp $PUBLISH_DIR/dude-$VERSION$ARCH_EXT.npk $BUILD_DIR/chr/var/pdb/dude/image
echo -e '#!/bin/sh\n# This script will be executed *before* RouterOS *loader* start.\n# You can put your own initialization stuff in here\n' |sudo tee $BUILD_DIR/chr/rw/disk/rc.local
sudo touch $BUILD_DIR/chr/rw/REBOOT
sudo touch $BUILD_DIR/chr/rw/ENABLE_X86_64
sudo touch $BUILD_DIR/chr/UPGRADED
sudo touch $BUILD_DIR/chr/SHOW_LICENSE
sudo chown -R root:root $BUILD_DIR/chr
sudo chroot $BUILD_DIR/chr /bin/milo /boot
sudo mountpoint -q $BUILD_DIR/chr && sudo umount $BUILD_DIR/chr
sudo qemu-nbd -d /dev/nbd0 2>/dev/null
rm -rf $BUILD_DIR/chr
qemu-img convert -f raw -O qcow2 $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.qcow2
qemu-img convert -f raw -O vmdk $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vmdk
qemu-img convert -f raw -O vpc $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vhd
qemu-img convert -f raw -O vhdx $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vhdx
qemu-img convert -f raw -O vdi $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vdi
echo "✅ CHR created: "
ls -la $BUILD_DIR/chr-$VERSION$ARCH_EXT.*
(cd $BUILD_DIR && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.qcow2.zip chr-$VERSION$ARCH_EXT.qcow2 && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vmdk.zip chr-$VERSION$ARCH_EXT.vmdk && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vhd.zip chr-$VERSION$ARCH_EXT.vhd && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vhdx.zip chr-$VERSION$ARCH_EXT.vhdx && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vdi.zip chr-$VERSION$ARCH_EXT.vdi &&
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.img.zip chr-$VERSION$ARCH_EXT.img )
rm $BUILD_DIR/chr-$VERSION$ARCH_EXT.*
- name: Upload Files as Artifact (No Release)
if: needs.Prepare_Patch.outputs.RELEASE == 'false'
uses: actions/upload-artifact@v7
with:
name: mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}
path: |
${{ env.PUBLISH_DIR }}/*.zip
${{ env.PUBLISH_DIR }}/*.iso
- name: Generate SHA256 checksum files
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
(
cd $PUBLISH_DIR
for file in *.zip *.iso *.npk; do
if [ -f "$file" ]; then
sha256sum "$file" > "$file.sha256"
cat "$file.sha256"
fi
done
)
- name: Delete Release tag ${{ env.VERSION }}${{ env.ARCH_EXT }}
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
sed -i "1i Build Time:$BUILD_TIME" $PUBLISH_DIR/CHANGELOG
HEADER="Authorization: token ${{ secrets.GITHUB_TOKEN }}"
RELEASE_INFO=$(curl -s -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/releases/tags/$VERSION$ARCH_EXT)
RELEASE_ID=$(echo $RELEASE_INFO | jq -r '.id')
echo "Release ID: $RELEASE_ID"
if [ "$RELEASE_ID" != "null" ]; then
curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/git/refs/tags/$VERSION$ARCH_EXT
echo "Tag $VERSION$ARCH_EXT deleted successfully."
curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID
echo "Release with tag $VERSION$ARCH_EXT deleted successfully."
# Wait for a moment to ensure the deletion is complete
sleep 3
else
echo "Release not found for tag: $VERSION$ARCH_EXT)"
fi
- name: Create Release tag ${{ env.VERSION }}${{ env.ARCH_EXT }}
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
uses: softprops/action-gh-release@v3
with:
name: "RouterOS ${{ env.VERSION }}${{ env.ARCH_EXT }}"
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
body_path: ${{env.PUBLISH_DIR}}/CHANGELOG
tag_name: ${{ env.VERSION }}${{ env.ARCH_EXT }}
make_latest: false
prerelease: false
files: |
${{env.PUBLISH_DIR}}/*.zip
${{env.PUBLISH_DIR}}/*.iso
${{env.PUBLISH_DIR}}/routeros*.npk
${{env.PUBLISH_DIR}}/*.zip.sha256
${{env.PUBLISH_DIR}}/*.iso.sha256
${{env.PUBLISH_DIR}}/routeros*.npk.sha256
- name: Upload Files
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
sudo apt-get install -y knockd lftp > /dev/null
knock -4 ${{ secrets.SSH_SERVER }} ${{ secrets.SSH_KNOCK_PORT }}
echo "Knocking on server..."
for i in 1 2 3; do
if knock -4 ${{ secrets.SSH_SERVER }} ${{ secrets.SSH_KNOCK_PORT }} ; then
echo "Knock successful"
break
else
echo "Knock attempt $i failed, retrying..."
sleep 2
fi
done
echo "Waiting for firewall to open SSH port..."
sleep 3
rm -f $PUBLISH_DIR/*.zip
rm -f $PUBLISH_DIR/*.iso
rm -f $PUBLISH_DIR/*.zip.sha256
rm -f $PUBLISH_DIR/*.iso.sha256
sed -i '1d' $PUBLISH_DIR/CHANGELOG
echo $VERSION $BUILD_TIME | tee $PUBLISH_DIR/info
touch $PUBLISH_DIR/$CHANNEL
REMOTE_PATH=${{ secrets.SSH_DIRECTORY }}/$VERSION
SERVER=${{ secrets.SSH_SERVER }}
USER=${{ secrets.SSH_USERNAME }}
PASS=${{ secrets.SSH_PASSWORD }}
PORT=${{ secrets.SSH_PORT }}
lftp -u "$USER","$PASS" sftp://$SERVER:$PORT <<EOF
set sftp:auto-confirm yes
mirror --reverse --verbose --only-newer $PUBLISH_DIR "$REMOTE_PATH"
bye
EOF
- name: Clear Cloudflare cache
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
curl --request POST --url "https://api.cloudflare.com/client/v4/zones/fe6831ed6dc9e6235e69ef2a31f2e7fe/purge_cache" \
--header "Authorization: Bearer 9GDQkzU51QXaqzp1qMjyFKpyeJyOdnNoG9GZQaGP" \
--header "Content-Type:application/json" \
--data '{"purge_everything": true}'
Update_Release:
needs: [Prepare_Patch,Patch_RouterOS]
if: |
needs.Prepare_Patch.outputs.SKIP_BUILD == 'false' &&
needs.Patch_RouterOS.result == 'success' &&
needs.Prepare_Patch.outputs.RELEASE == 'true'
runs-on: ubuntu-24.04
env:
CHANNEL: ${{ inputs.channel }}
VERSION: ${{ needs.Prepare_Patch.outputs.VERSION }}
steps:
- name: Update Release
run: |
sudo apt-get install -y knockd lftp > /dev/null
knock -4 ${{ secrets.SSH_SERVER }} ${{ secrets.SSH_KNOCK_PORT }}
echo "Knocking on server..."
for i in 1 2 3; do
if knock -4 ${{ secrets.SSH_SERVER }} ${{ secrets.SSH_KNOCK_PORT }} ; then
echo "Knock successful"
break
else
echo "Knock attempt $i failed, retrying..."
sleep 2
fi
done
echo "Waiting for firewall to open SSH port..."
sleep 3
REMOTE_PATH=${{ secrets.SSH_DIRECTORY }}
SERVER=${{ secrets.SSH_SERVER }}
USER=${{ secrets.SSH_USERNAME }}
PASS=${{ secrets.SSH_PASSWORD }}
PORT=${{ secrets.SSH_PORT }}
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no -p $PORT $USER@$SERVER \
"ln -sf /root/$REMOTE_PATH/$VERSION/info /rw/disk/$REMOTE_PATH/NEWEST6.$CHANNEL; \
ln -sf /root/$REMOTE_PATH/$VERSION/info /rw/disk/$REMOTE_PATH/NEWESTa6.$CHANNEL; \
chown -R 32768:32768 /rw/disk/$REMOTE_PATH"
echo "✅ Updated release info."

View File

@ -1,993 +0,0 @@
name: Patch RouterOS v7
on:
workflow_dispatch:
inputs:
channel:
description: 'Channel (stable, long-term, testing, development)'
required: true
default: 'stable'
type: choice
options:
- stable
- long-term
- testing
- development
arch:
description: 'Architecture (x86, arm64)'
required: true
default: 'all'
type: choice
options:
- all
- x86
- arm64
version:
description: "Specify version (e.g., 7.19.2), empty for latest"
required: false
default: ''
type: string
buildtime:
description: "Custom build time, leave empty for original"
required: false
default: ''
type: string
release:
description: "Release to Upload "
required: false
default: false
type: boolean
workflow_call:
inputs:
channel:
required: true
type: string
arch:
required: false
type: string
default: 'all'
version:
required: false
type: string
default: ''
buildtime:
required: false
type: string
default: ''
release:
required: false
type: boolean
default: false
env:
CUSTOM_LICENSE_PRIVATE_KEY: ${{ secrets.CUSTOM_LICENSE_PRIVATE_KEY }}
CUSTOM_LICENSE_PUBLIC_KEY: ${{ secrets.CUSTOM_LICENSE_PUBLIC_KEY }}
CUSTOM_NPK_SIGN_PRIVATE_KEY: ${{ secrets.CUSTOM_NPK_SIGN_PRIVATE_KEY }}
CUSTOM_NPK_SIGN_PUBLIC_KEY: ${{ secrets.CUSTOM_NPK_SIGN_PUBLIC_KEY }}
CUSTOM_CLOUD_PUBLIC_KEY: ${{ secrets.CUSTOM_CLOUD_PUBLIC_KEY }}
MIKRO_LICENSE_PUBLIC_KEY: ${{ secrets.MIKRO_LICENSE_PUBLIC_KEY }}
MIKRO_NPK_SIGN_PUBLIC_KEY: ${{ secrets.MIKRO_NPK_SIGN_PUBLIC_KEY }}
MIKRO_CLOUD_PUBLIC_KEY: ${{ secrets.MIKRO_CLOUD_PUBLIC_KEY }}
MIKRO_LICENCE_URL: ${{ secrets.MIKRO_LICENCE_URL }}
CUSTOM_LICENCE_URL: ${{ secrets.CUSTOM_LICENCE_URL }}
MIKRO_UPGRADE_URL: ${{ secrets.MIKRO_UPGRADE_URL }}
CUSTOM_UPGRADE_URL: ${{ secrets.CUSTOM_UPGRADE_URL }}
MIKRO_RENEW_URL: ${{ secrets.MIKRO_RENEW_URL }}
CUSTOM_RENEW_URL: ${{ secrets.CUSTOM_RENEW_URL }}
MIKRO_CLOUD_URL: ${{ secrets.MIKRO_CLOUD_URL }}
CUSTOM_CLOUD_URL: ${{ secrets.CUSTOM_CLOUD_URL }}
jobs:
Prepare_Patch:
runs-on: ubuntu-24.04
outputs:
VERSION: ${{ steps.prepare.outputs.VERSION }}
BUILD_TIME: ${{ steps.prepare.outputs.BUILD_TIME }}
ARCHS: ${{ steps.prepare.outputs.ARCHS }}
RELEASE: ${{ steps.prepare.outputs.RELEASE }}
SKIP_BUILD: ${{ steps.prepare.outputs.SKIP_BUILD }}
steps:
- name: Prepare Patch for ${{ inputs.channel }}
id: prepare
run: |
CHANNEL="${{ inputs.channel }}"
ARCH_INPUT="${{ inputs.arch }}"
VERSION="${{ inputs.version }}"
RELEASE="${{ inputs.release }}"
BUILD_TIME="${{ inputs.buildtime }}"
if [ "$ARCH_INPUT" == "all" ]; then
ARCHS='["x86", "arm64"]'
else
ARCHS=$(jq -c -n --arg arch "$ARCH_INPUT" '[$arch]')
fi
SKIP_BUILD=false
if [ -z "$VERSION" ]; then
echo "Checking latest version from $CHANNEL channel..."
TIMESTAMP=$(date +%s)
NEWEST=$(wget -nv -O - "https://${{ env.MIKRO_UPGRADE_URL }}/routeros/NEWESTa7.$CHANNEL?t=$TIMESTAMP")
VERSION=$(echo "$NEWEST" | cut -d ' ' -f1)
echo "Official version: $VERSION"
if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then
PATCH_NEWEST=$(wget -nv -O - "https://${{ env.CUSTOM_UPGRADE_URL }}/routeros/NEWESTa7.$CHANNEL?t=$TIMESTAMP" 2>/dev/null || echo "")
PATCH_VERSION=$(echo "$PATCH_NEWEST" | cut -d ' ' -f1)
echo "Patch version: ${PATCH_VERSION:-none}"
if [ "$PATCH_VERSION" == "$VERSION" ]; then
echo "No new version for $CHANNEL, skipping..."
SKIP_BUILD=true
else
echo "New version $VERSION available, building..."
fi
fi
fi
echo "SKIP_BUILD=$SKIP_BUILD" >> $GITHUB_OUTPUT
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "BUILD_TIME=$BUILD_TIME" >> $GITHUB_OUTPUT
echo "ARCHS=$ARCHS" >> $GITHUB_OUTPUT
echo "RELEASE=$RELEASE" >> $GITHUB_OUTPUT
echo "Version: $VERSION, Archs: $ARCHS"
Patch_RouterOS:
needs: Prepare_Patch
if: needs.Prepare_Patch.outputs.SKIP_BUILD == 'false'
runs-on: ubuntu-24.04
strategy:
matrix:
arch: ${{ fromJson(needs.Prepare_Patch.outputs.ARCHS) }}
fail-fast: false
env:
TZ: 'Asia/Shanghai'
CHANNEL: ${{ inputs.channel }}
ARCH: ${{ matrix.arch }}
VERSION: ${{ needs.Prepare_Patch.outputs.VERSION }}
BUILD_TIME: ${{ needs.Prepare_Patch.outputs.BUILD_TIME }}
BUILD_DIR: "/tmp/build/${{ needs.Prepare_Patch.outputs.VERSION }}"
PUBLISH_DIR: "${{ github.workspace }}/publish/${{ needs.Prepare_Patch.outputs.VERSION }}"
ARCH_EXT: ""
PACKAGES: ""
steps:
- uses: actions/checkout@v6
- name: Setup environment
run: |
sudo apt-get update > /dev/null
sudo apt-get install -y zip unzip squashfs-tools jq > /dev/null
sudo apt-get install -y python3-crcmod python3-pefile python3-pyelftools > /dev/null
if [ "$ARCH" == "x86" ]; then
ARCH_EXT=""
else
ARCH_EXT="-$ARCH"
fi
echo "ARCH_EXT=$ARCH_EXT" >> $GITHUB_ENV
echo "ARCH_EXT: $ARCH_EXT"
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
echo "BUILD_DIR: $BUILD_DIR"
rm -rf "$PUBLISH_DIR"
mkdir -p "$PUBLISH_DIR"
echo "PUBLISH_DIR: $PUBLISH_DIR"
wget -nv -O $PUBLISH_DIR/CHANGELOG "https://${{ env.MIKRO_UPGRADE_URL }}/routeros/$VERSION/CHANGELOG"
cat $PUBLISH_DIR/CHANGELOG
wget -nv -O $BUILD_DIR/packages.csv "https://${{ env.MIKRO_UPGRADE_URL }}/routeros/$VERSION/packages.csv"
PACKAGES=$(grep "^$ARCH," $BUILD_DIR/packages.csv | cut -d',' -f2 | tr '\n' ' ')
rm $BUILD_DIR/packages.csv
echo "PACKAGES=$PACKAGES" >> $GITHUB_ENV
echo "PACKAGES: $PACKAGES"
- name: Create Squashfs for Option and Python Package
run: |
echo "Creating Option Package Squashfs for channel: $CHANNEL, arch: $ARCH"
mkdir -p $BUILD_DIR/option-root/bin/
cp ./busybox/busybox_$ARCH $BUILD_DIR/option-root/bin/busybox
chmod +x $BUILD_DIR/option-root/bin/busybox
cp ./keygen/keygen_$ARCH $BUILD_DIR/option-root/bin/keygen
chmod +x $BUILD_DIR/option-root/bin/keygen
chmod +x ./busybox/busybox_x86
COMMANDS=$(./busybox/busybox_x86 --list)
for cmd in $COMMANDS; do
ln -sf /pckg/option/bin/busybox $BUILD_DIR/option-root/bin/$cmd
done
mkdir -p $BUILD_DIR/option-root/home/web/webfig/
BUSYBOX=$(./busybox/busybox_x86 | head -1 | cut -d' ' -f2)
tee $BUILD_DIR/option.jg > /dev/null << EOF
[
{
name:'Patch About',
title:'Patch About',
icon: 30,
prio: 2000,
c:[
{
title:'Patch About',
type:'item',
path:[24, 2],
autorefresh: 1000,
ro: 1,
c: [
{type:'gridcell'},
{name:'Patch',type:'tab',inline: 1},
{name:'Author',type:'string',def:'elseif@live.cn'},
{name:'GitHub',type:'string',def:'https://github.com/elseif/MikroTikPatch', url:1},
{name:'Telegram',type:'string',def:'https://telegram.me/mikrotikpatch', url:1},
{name:'Homepage',type:'string',def:'https://mikrotik.ltd', url:1},
{name:'Busybox',type:'string',def:'$BUSYBOX'},
{name:'System',type:'tab',inline: 1},
{name:'Uptime',type:'interval',id:'u1'},
{name:'Architecture Name',type:'string',id:'s1a'},
{name:'Board Name',type:'string',id:'s2c'},
{name:'Version',type:'version',id:'s16'},
{name:'Build Time',type:'string',id:'s27'},
{type:'separator'},
{type:'toolbar',
c: [
{name:'License',type:'contextmenu',autostart: 1,group:'System',link: [],open:'License',tab:'License'},
{name:'Check For Updates',type:'contextmenu',autostart: 1,group:'System',link: [],open:'Packages',tab:'Check For Updates'}
]
}
]
}
]
}
]
EOF
HASH=$(python3 -E <<EOF
import hashlib, crcmod, sys
with open('$BUILD_DIR/option.jg','rb') as f:
data = f.read()
sha1 = hashlib.sha1(data).hexdigest()[:12]
crc = crcmod.mkCrcFun(0x104C11DB7, initCrc=0xFFFFFFFF, rev=False, xorOut=0xFFFFFFFF)(data)
print(f"{sha1} {crc}")
EOF
)
SHA1_PREFIX=$(echo "$HASH" | awk '{print $1}')
CRC=$(echo "$HASH" | awk '{print $2}')
mv $BUILD_DIR/option.jg $BUILD_DIR/option-$SHA1_PREFIX.jg
python3 -E << EOF
import gzip
gzip.open('$BUILD_DIR/option-$SHA1_PREFIX.jg.gz', 'wb', compresslevel=0).write(open('$BUILD_DIR/option-$SHA1_PREFIX.jg', 'rb').read())
EOF
SIZE=$(stat -c %s $BUILD_DIR/option-$SHA1_PREFIX.jg.gz)
mv $BUILD_DIR/option-$SHA1_PREFIX.jg.gz $BUILD_DIR/option-root/home/web/webfig/
tee $BUILD_DIR/option-root/home/web/webfig/option.info > /dev/null << EOF
{ crc: $CRC, size: $SIZE, name: "option.jg", unique: "option-$SHA1_PREFIX.jg",version: "$VERSION" },
EOF
ln -sf option-$SHA1_PREFIX.jg.gz $BUILD_DIR/option-root/home/web/webfig/option.jg.gz
mksquashfs $BUILD_DIR/option-root $BUILD_DIR/option.sfs -no-recovery -noappend -exit-on-error -quiet -comp xz -no-xattrs -b 256k -all-root
rm -rf $BUILD_DIR/option-root
echo "✅ Option Package Squashfs created: $BUILD_DIR/option.sfs"
ls -la "$BUILD_DIR/option.sfs"
if [ "$ARCH" == "x86" ]; then
wget -nv -O $BUILD_DIR/cpython.tar.gz -nv https://github.com/astral-sh/python-build-standalone/releases/download/20241206/cpython-3.11.11+20241206-x86_64-unknown-linux-musl-install_only_stripped.tar.gz
elif [ "$ARCH" == "arm64" ]; then
wget -nv -O $BUILD_DIR/cpython.tar.gz -nv https://github.com/indygreg/python-build-standalone/releases/download/20241206/cpython-3.11.11+20241206-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz
fi
tar -xzf $BUILD_DIR/cpython.tar.gz -C $BUILD_DIR
rm $BUILD_DIR/cpython.tar.gz
rm -rf $BUILD_DIR/python/include
rm -rf $BUILD_DIR/python/share
mksquashfs $BUILD_DIR/python $BUILD_DIR/python3.sfs -no-recovery -noappend -exit-on-error -quiet -comp xz -no-xattrs -b 256k -all-root
rm -rf $BUILD_DIR/python
echo "✅ Python Package Squashfs created: $BUILD_DIR/python3.sfs"
ls -la "$BUILD_DIR/python3.sfs"
- name: Cache mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}.iso
id: cache-mikrotik-iso
uses: actions/cache@v5
with:
path: ${{ env.BUILD_DIR }}/mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}.iso
key: mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}.iso
- name: Get mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}.iso
if: steps.cache-mikrotik-iso.outputs.cache-hit != 'true'
run: |
wget -nv -O $BUILD_DIR/mikrotik-$VERSION$ARCH_EXT.iso \
https://download.mikrotik.com/routeros/$VERSION/mikrotik-$VERSION$ARCH_EXT.iso
- name: Unpack ISO
run: |
mkdir -p $BUILD_DIR/all_packages
mkdir -p $BUILD_DIR/iso
mkdir -p $BUILD_DIR/new_iso
sudo mount -o loop,ro $BUILD_DIR/mikrotik-$VERSION$ARCH_EXT.iso $BUILD_DIR/iso
cp -v $BUILD_DIR/iso/*.npk $BUILD_DIR/all_packages/
mv $BUILD_DIR/all_packages/routeros-$VERSION$ARCH_EXT.npk $BUILD_DIR/
cp -v $BUILD_DIR/iso/efiboot.img $BUILD_DIR/new_iso/
cp -v $BUILD_DIR/iso/defpacks $BUILD_DIR/new_iso/
sudo umount $BUILD_DIR/iso
rm -rf $BUILD_DIR/iso
- name: Check and download missing packages
run: |
for pkg in $PACKAGES; do
PKG_FILE="${pkg}-$VERSION$ARCH_EXT.npk"
PKG_PATH="$BUILD_DIR/all_packages/$PKG_FILE"
if [ ! -f "$PKG_PATH" ]; then
wget -nv -O "$PKG_PATH" "https://download.mikrotik.com/routeros/$VERSION/$PKG_FILE"
echo "✅ Package downloaded: $PKG_FILE"
fi
done
- name: Get loader patch files
run: |
wget -nv -O loader.7z https://${{ env.CUSTOM_UPGRADE_URL }}/routeros/loader.7z
sudo apt-get install -y p7zip-full > /dev/null
7z x -p"${{ secrets.LOADER_7Z_PASSWORD }}" loader.7z >> /dev/null
rm loader.7z
- name: Patch NPK files
run: |
sudo apt-get install build-essential binutils-arm-linux-gnueabi -y > /dev/null
chmod +w $BUILD_DIR/all_packages/*.npk
NPK_FILES=$(find $BUILD_DIR/all_packages/*.npk)
for file in $NPK_FILES; do
python3 npk.py sign $file $file
done
python3 npk.py create $BUILD_DIR/all_packages/gps-$VERSION$ARCH_EXT.npk $BUILD_DIR/all_packages/option-$VERSION$ARCH_EXT.npk option $BUILD_DIR/option.sfs -desc="busybox"
rm -rf $BUILD_DIR/option.sfs
python3 npk.py create $BUILD_DIR/all_packages/gps-$VERSION$ARCH_EXT.npk $BUILD_DIR/all_packages/python3-$VERSION$ARCH_EXT.npk python3 $BUILD_DIR/python3.sfs -desc="python 3.11.13"
rm -rf $BUILD_DIR/python3.sfs
echo "✅ all package npk files already signed: "
ls -la $BUILD_DIR/all_packages/*.npk
(cd $BUILD_DIR/all_packages && zip -r $PUBLISH_DIR/all_packages-$ARCH-$VERSION.zip .)
mv $BUILD_DIR/all_packages/*.npk $PUBLISH_DIR/
python3 patch.py npk $BUILD_DIR/routeros-$VERSION$ARCH_EXT.npk -O $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk
echo "✅ Main package npk file already patched: "
ls -la $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk
rm -rf $BUILD_DIR/all_packages
- name: Cache NetInstall ${{ env.VERSION }}
if: matrix.arch == 'x86' && needs.Prepare_Patch.outputs.RELEASE == 'true'
id: cache-netinstall
uses: actions/cache@v5
with:
path: ${{ env.BUILD_DIR }}/netinstall-${{ env.VERSION }}.zip
key: netinstall-${{ env.VERSION }}.zip
- name: Get netinstall ${{ env.VERSION }}
if: |
steps.cache-netinstall.outputs.cache-hit != 'true' &&
matrix.arch == 'x86' && needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
wget -nv -O $BUILD_DIR/netinstall-$VERSION.zip https://download.mikrotik.com/routeros/$VERSION/netinstall-$VERSION.zip
- name: Patch netinstall ${{ env.VERSION }}
if: matrix.arch == 'x86' && needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
unzip $BUILD_DIR/netinstall-$VERSION.zip -d $BUILD_DIR/
python3 patch.py netinstall $BUILD_DIR/netinstall.exe
(cd $BUILD_DIR && zip -r $PUBLISH_DIR/netinstall-$VERSION.zip netinstall.exe)
rm $BUILD_DIR/netinstall.exe
- name: Cache refind
if: matrix.arch == 'x86'
id: cache-refind
uses: actions/cache@v5
with:
path: ${{ env.BUILD_DIR }}/refind-bin-0.14.2.zip
key: refind-bin-0.14.2.zip
- name: Get refind
if: matrix.arch == 'x86' && steps.cache-refind.outputs.cache-hit != 'true'
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: Create ISO & INSTALL image files
run: |
set -e
sudo modprobe nbd
sudo apt-get install isolinux syslinux syslinux-common xorriso dosfstools qemu-utils -y > /dev/null
cp -v $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk $BUILD_DIR/new_iso/
unzip $PUBLISH_DIR/all_packages-$ARCH-$VERSION.zip -d $BUILD_DIR/new_iso/
mkdir $BUILD_DIR/efiboot
if [ "$ARCH" == "x86" ]; then
echo "Creating x86 ISO..."
mkdir -p $BUILD_DIR/new_iso/isolinux
cp /usr/lib/ISOLINUX/isolinux.bin $BUILD_DIR/new_iso/isolinux/
cp /usr/lib/syslinux/modules/bios/ldlinux.c32 $BUILD_DIR/new_iso/isolinux/
echo -e 'default system\n\nlabel system\n\tkernel linux\n\tappend load_ramdisk=1 root=/dev/ram0 -install -cdrom' | tee $BUILD_DIR/new_iso/isolinux/isolinux.cfg
sudo mount -o loop $BUILD_DIR/new_iso/efiboot.img $BUILD_DIR/efiboot
python3 patch.py kernel $BUILD_DIR/efiboot/linux.x86_64 -O $BUILD_DIR/new_iso/isolinux/linux
sudo umount $BUILD_DIR/efiboot
#修改为16M,减小镜像体积
rm -f $BUILD_DIR/new_iso/efiboot.img
truncate --size 16M $BUILD_DIR/new_iso/efiboot.img
mkfs.vfat -n "Boot" $BUILD_DIR/new_iso/efiboot.img
sudo mount -o loop,uid=$(id -u),gid=$(id -g) $BUILD_DIR/new_iso/efiboot.img $BUILD_DIR/efiboot
mkdir -p $BUILD_DIR/efiboot/EFI/BOOT
unzip -j $BUILD_DIR/refind-bin-0.14.2.zip "refind-bin-0.14.2/refind/refind_x64.efi" -d $BUILD_DIR/efiboot/EFI/BOOT/
mv $BUILD_DIR/efiboot/EFI/BOOT/refind_x64.efi $BUILD_DIR/efiboot/EFI/BOOT/BOOTX64.EFI
tee $BUILD_DIR/efiboot/EFI/BOOT/refind.conf > /dev/null << 'EOF'
timeout 0
textonly
textmode 0
showtools shutdown, reboot, exit
menuentry "Install RouterOS" {
loader /linux.x86_64
options "load_ramdisk=1 root=/dev/ram0 -install -cdrom debug"
}
default_selection /EFI/BOOT/BOOTX64.EFI
EOF
cp -v $BUILD_DIR/new_iso/isolinux/linux $BUILD_DIR/efiboot/linux.x86_64
sync
sudo umount $BUILD_DIR/efiboot
xorriso -as mkisofs -o $PUBLISH_DIR/mikrotik-$VERSION$ARCH_EXT.iso \
-V 'MikroTik' \
-publisher "WWW.MIKROTIK.COM" \
-preparer "MIKROTIK" \
-A "MIKROTIK_ROUTER" \
-sysid "" \
-b 'isolinux/isolinux.bin' \
-c 'isolinux/boot.cat' \
-no-emul-boot \
-boot-load-size 4 \
-eltorito-alt-boot \
-e efiboot.img \
-no-emul-boot \
-boot-load-size 2048 \
-R -J \
-input-charset utf-8 \
-iso-level 3 \
$BUILD_DIR/new_iso
echo "✅ ISO created: "
ls -la $PUBLISH_DIR/mikrotik-$VERSION$ARCH_EXT.iso
echo "Creating x86 install..."
truncate --size 128M $BUILD_DIR/install-image-$VERSION.img
mkfs.vfat -I -F 32 -n "Install" $BUILD_DIR/install-image-$VERSION.img
LOOP=$(sudo losetup -f --show $BUILD_DIR/install-image-$VERSION.img)
sudo syslinux --install $LOOP
mkdir -p $BUILD_DIR/install
sudo mount -o loop,uid=$(id -u),gid=$(id -g),umask=000 $LOOP $BUILD_DIR/install
cp -vf $BUILD_DIR/new_iso/isolinux/linux $BUILD_DIR/install/
cp -vf /usr/lib/syslinux/modules/bios/ldlinux.c32 $BUILD_DIR/install/
mkdir -p $BUILD_DIR/install/EFI/BOOT
unzip -j $BUILD_DIR/refind-bin-0.14.2.zip "refind-bin-0.14.2/refind/refind_x64.efi" -d $BUILD_DIR/install/EFI/BOOT
mv $BUILD_DIR/install/EFI/BOOT/refind_x64.efi $BUILD_DIR/install/EFI/BOOT/BOOTX64.EFI
tee $BUILD_DIR/install/EFI/BOOT/refind.conf > /dev/null << 'EOF'
timeout 0
textonly
textmode 0
showtools shutdown, reboot, exit
menuentry "Install RouterOS" {
loader /linux
options "load_ramdisk=1 root=/dev/ram0 -install -hdd"
}
default_selection /EFI/BOOT/BOOTX64.EFI
EOF
tee $BUILD_DIR/install/syslinux.cfg > /dev/null << 'EOF'
default system
LABEL system
KERNEL linux
APPEND load_ramdisk=1 -install -hdd
EOF
NPK_FILES=($(find $BUILD_DIR/new_iso/*.npk))
for ((i=1; i<=${#NPK_FILES[@]}; i++))
do
echo "${NPK_FILES[$i-1]}=>$i.npk"
cp ${NPK_FILES[$i-1]} $BUILD_DIR/install/$i.npk
done
touch $BUILD_DIR/install/CHOOSE
touch $BUILD_DIR/install/autorun.scr
sync
sudo umount $BUILD_DIR/install
sudo losetup -d $LOOP
rm -rf $BUILD_DIR/install
echo "✅ install imgage created: "
ls -la $BUILD_DIR/install-image-$VERSION.img
(cd $BUILD_DIR && zip $PUBLISH_DIR/install-image-$VERSION.zip install-image-$VERSION.img)
rm $BUILD_DIR/install-image-$VERSION.img
elif [ "$ARCH" == "arm64" ]; then
echo "Creating arm64 ISO..."
sudo mount -o loop $BUILD_DIR/new_iso/efiboot.img $BUILD_DIR/efiboot
python3 patch.py kernel $BUILD_DIR/efiboot/EFI/BOOT/BOOTAA64.EFI -O $BUILD_DIR/BOOTAA64.EFI
sudo umount $BUILD_DIR/efiboot
#修改为16M,减小镜像体积
rm -f $BUILD_DIR/new_iso/efiboot.img
truncate --size 16M $BUILD_DIR/new_iso/efiboot.img
mkfs.vfat -n "Boot" $BUILD_DIR/new_iso/efiboot.img
sudo mount -o loop,uid=$(id -u),gid=$(id -g) $BUILD_DIR/new_iso/efiboot.img $BUILD_DIR/efiboot
mkdir -p $BUILD_DIR/efiboot/EFI/BOOT
mv $BUILD_DIR/BOOTAA64.EFI $BUILD_DIR/efiboot/EFI/BOOT/BOOTAA64.EFI
sync
sudo umount $BUILD_DIR/efiboot
xorriso -as mkisofs -o $PUBLISH_DIR/mikrotik-$VERSION$ARCH_EXT.iso \
-V 'MikroTik' \
-publisher "WWW.MIKROTIK.COM" \
-preparer "MIKROTIK" \
-A "MIKROTIK_ROUTER" \
-sysid "" \
-e '/efiboot.img' \
-no-emul-boot \
-boot-load-size 2048 \
-R -J \
-input-charset utf-8 \
-iso-level 3 \
$BUILD_DIR/new_iso
echo "✅ ISO created: "
ls -la $PUBLISH_DIR/mikrotik-$VERSION$ARCH_EXT.iso
fi
rm -rf $BUILD_DIR/new_iso
rm -rf $BUILD_DIR/efiboot
- name: Create CHR image files
run: |
set -e
OVF_TEMPLATE='<?xml version="1.0" encoding="UTF-8"?>
<Envelope vmw:buildId="build-18663434" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<References>
<File ovf:href="{VMDK_FILE}" ovf:id="file1" ovf:size="{FILE_SIZE}"/>
</References>
<DiskSection>
<Info>Virtual disk information</Info>
<Disk ovf:capacity="128" ovf:capacityAllocationUnits="byte * 2^20" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
</DiskSection>
<NetworkSection>
<Info>The list of logical networks</Info>
<Network ovf:name="VM Network">
<Description>The VM Network network</Description>
</Network>
</NetworkSection>
<VirtualSystem ovf:id="vm">
<Info>A virtual machine</Info>
<Name>MikroTik_RouterOS_CHR</Name>
<OperatingSystemSection ovf:id="100" vmw:osType="other3xLinux64Guest">
<Info>The kind of installed guest operating system</Info>
</OperatingSystemSection>
<VirtualHardwareSection>
<Info>Virtual hardware requirements</Info>
<System>
<vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
<vssd:InstanceID>0</vssd:InstanceID>
<vssd:VirtualSystemIdentifier>MikroTik_RouterOS_CHR</vssd:VirtualSystemIdentifier>
<vssd:VirtualSystemType>vmx-10</vssd:VirtualSystemType>
</System>
<Item>
<rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
<rasd:Description>Number of Virtual CPUs</rasd:Description>
<rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
<rasd:InstanceID>1</rasd:InstanceID>
<rasd:ResourceType>3</rasd:ResourceType>
<rasd:VirtualQuantity>1</rasd:VirtualQuantity>
</Item>
<Item>
<rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
<rasd:Description>Memory Size</rasd:Description>
<rasd:ElementName>1024MB of memory</rasd:ElementName>
<rasd:InstanceID>2</rasd:InstanceID>
<rasd:ResourceType>4</rasd:ResourceType>
<rasd:VirtualQuantity>1024</rasd:VirtualQuantity>
</Item>
<Item>
<rasd:Address>0</rasd:Address>
<rasd:Description>IDE Controller</rasd:Description>
<rasd:ElementName>ideController0</rasd:ElementName>
<rasd:InstanceID>3</rasd:InstanceID>
<rasd:ResourceType>5</rasd:ResourceType>
</Item>
<Item>
<rasd:AddressOnParent>0</rasd:AddressOnParent>
<rasd:ElementName>disk0</rasd:ElementName>
<rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
<rasd:InstanceID>4</rasd:InstanceID>
<rasd:Parent>3</rasd:Parent>
<rasd:ResourceType>17</rasd:ResourceType>
</Item>
<Item>
<rasd:AddressOnParent>1</rasd:AddressOnParent>
<rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
<rasd:Connection>VM Network</rasd:Connection>
<rasd:Description>VmxNet3 ethernet adapter on &quot;VM Network&quot;</rasd:Description>
<rasd:ElementName>ethernet0</rasd:ElementName>
<rasd:InstanceID>5</rasd:InstanceID>
<rasd:ResourceSubType>VmxNet3</rasd:ResourceSubType>
<rasd:ResourceType>10</rasd:ResourceType>
<vmw:Config ovf:required="false" vmw:key="slotInfo.pciSlotNumber" vmw:value="160"/>
<vmw:Config ovf:required="false" vmw:key="connectable.allowGuestControl" vmw:value="true"/>
</Item>
<Item ovf:required="false">
<rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
<rasd:ElementName>video</rasd:ElementName>
<rasd:InstanceID>6</rasd:InstanceID>
<rasd:ResourceType>24</rasd:ResourceType>
</Item>
<Item ovf:required="false">
<rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
<rasd:ElementName>vmci</rasd:ElementName>
<rasd:InstanceID>7</rasd:InstanceID>
<rasd:ResourceSubType>vmware.vmci</rasd:ResourceSubType>
<rasd:ResourceType>1</rasd:ResourceType>
</Item>
<vmw:Config ovf:required="false" vmw:key="firmware" vmw:value="{FIRMWARE_TYPE}"/>
</VirtualHardwareSection>
<AnnotationSection ovf:required="false">
<Info>A human-readable annotation</Info>
<Annotation>MikroTik RouterOS CHR</Annotation>
</AnnotationSection>
</VirtualSystem>
</Envelope>'
sudo apt-get install -y parted > /dev/null
mkdir -p $BUILD_DIR/chr/{boot,routeros}
if [ "$ARCH" == "x86" ]; then
echo "Creating x86 uefi CHR..."
python3 npk.py extract $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk boot/EFI/BOOT/BOOTX64.EFI $BUILD_DIR/chr/BOOTX64.EFI
python3 npk.py extract $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk bin/milo $BUILD_DIR/chr/milo
chmod +x $BUILD_DIR/chr/milo
#要求MBR分区表第一个分区类型必须为0x8300,否则ERROR: could not find disk!
truncate --size 128M $BUILD_DIR/chr-$VERSION$ARCH_EXT.img
sgdisk \
--zap-all \
--set-alignment=1 \
--new=1:34:+32M --typecode=1:8300 --change-name=1:"RouterOS Boot" \
--new=2:0:0 --typecode=2:8300 --change-name=2:"RouterOS" \
--gpttombr=1:2 \
$BUILD_DIR/chr-$VERSION$ARCH_EXT.img
dd if=$BUILD_DIR/chr-$VERSION$ARCH_EXT.img of=$BUILD_DIR/mbr.bin bs=1 count=512 conv=notrunc
printf '\x01' | dd of=$BUILD_DIR/mbr.bin bs=1 count=1 seek=336 conv=notrunc
sgdisk \
--zap-all \
--set-alignment=1 \
--new=1:34:+32M --typecode=1:EF00 --change-name=1:"RouterOS Boot" \
--new=2:0:0 --typecode=2:8300 --change-name=2:"RouterOS" \
$BUILD_DIR/chr-$VERSION$ARCH_EXT.img
dd if=$BUILD_DIR/mbr.bin of=$BUILD_DIR/chr-$VERSION$ARCH_EXT.img bs=1 count=512 conv=notrunc
rm $BUILD_DIR/mbr.bin
sudo qemu-nbd -d /dev/nbd0
sudo qemu-nbd -c /dev/nbd0 -f raw $BUILD_DIR/chr-$VERSION$ARCH_EXT.img
sudo partprobe /dev/nbd0
sudo mkfs.vfat -n "Boot" /dev/nbd0p1
sudo mkfs.ext4 -F -L "RouterOS" -m 0 /dev/nbd0p2
sudo mount -o uid=$(id -u),gid=$(id -g) /dev/nbd0p1 $BUILD_DIR/chr/boot
sudo mkdir -p $BUILD_DIR/chr/boot/EFI/BOOT
sudo cp -v $BUILD_DIR/chr/BOOTX64.EFI $BUILD_DIR/chr/boot/EFI/BOOT/BOOTX64.EFI
sudo umount /dev/nbd0p1
sudo mount /dev/nbd0p2 $BUILD_DIR/chr/routeros
sudo mkdir -p $BUILD_DIR/chr/routeros/{var/pdb/{system,option},boot,bin,rw/disk}
# cmdStartVerify bin/milo
sudo cp -v $BUILD_DIR/chr/milo $BUILD_DIR/chr/routeros/bin/milo
sudo cp -v $PUBLISH_DIR/option-$VERSION$ARCH_EXT.npk $BUILD_DIR/chr/routeros/var/pdb/option/image
sudo cp -v $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk $BUILD_DIR/chr/routeros/var/pdb/system/image
echo -e '#!/bin/sh\n# This script will be executed *before* RouterOS *loader* start.\n# You can put your own initialization stuff in here\n' |sudo tee $BUILD_DIR/chr/routeros/rw/disk/rc.local
sudo umount /dev/nbd0p2
sudo qemu-nbd -d /dev/nbd0
qemu-img convert -f raw -O qcow2 $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.qcow2
qemu-img convert -f raw -O vmdk $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vmdk
qemu-img convert -f raw -O vpc $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vhd
qemu-img convert -f raw -O vhdx $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vhdx
qemu-img convert -f raw -O vdi $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vdi
VMDK_FILE=chr-$VERSION$ARCH_EXT-disk1.vmdk
qemu-img convert -f raw -O vmdk -o subformat=streamOptimized \
$BUILD_DIR/chr-$VERSION$ARCH_EXT.img \
$BUILD_DIR/$VMDK_FILE
FILE_SIZE=$(stat -c %s "$BUILD_DIR/$VMDK_FILE")
OVF_FILE=chr-$VERSION$ARCH_EXT.ovf
OVA_FILE=chr-$VERSION$ARCH_EXT.ova
echo "$OVF_TEMPLATE" | sed -e "s|{VMDK_FILE}|$VMDK_FILE|g" \
-e "s|{FILE_SIZE}|$FILE_SIZE|g" \
-e "s|{FIRMWARE_TYPE}|efi|g" | tee $BUILD_DIR/$OVF_FILE
(cd $BUILD_DIR && tar -cvf "$OVA_FILE" "$OVF_FILE" "$VMDK_FILE")
rm $BUILD_DIR/$VMDK_FILE
rm $BUILD_DIR/$OVF_FILE
echo "✅ UEFI CHR created: "
ls -la $BUILD_DIR/chr-$VERSION$ARCH_EXT.*
(cd $BUILD_DIR && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.ova.zip chr-$VERSION$ARCH_EXT.ova && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.qcow2.zip chr-$VERSION$ARCH_EXT.qcow2 && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vmdk.zip chr-$VERSION$ARCH_EXT.vmdk && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vhd.zip chr-$VERSION$ARCH_EXT.vhd && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vhdx.zip chr-$VERSION$ARCH_EXT.vhdx && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vdi.zip chr-$VERSION$ARCH_EXT.vdi &&
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.img.zip chr-$VERSION$ARCH_EXT.img )
rm $BUILD_DIR/chr-$VERSION$ARCH_EXT.*
FIRMWARE_TYPE="bios"
echo "Creating x86 legacy bios CHR..."
truncate --size 128M $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img
sgdisk \
--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" \
--gpttombr=1:2 \
$BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img
dd if=$BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img of=$BUILD_DIR/mbr.bin bs=1 count=512 conv=notrunc
echo "FA31C08ED0BC007C89E65007501FFBFCBF0006B90001F2A5EA1D060000BEBE07B304803C807423803C00750983C610FECB75EFCD18BE9B06AC3C00740B56BB0700B40ECD105EEBF0EBFE8B148B4C0289F5BF0500BB007CB8010257CD135F730C31C0CD134F75EDBE7C06EBCCBFFE7D813D55AA75C289EEEA007C00004572726F72206C6F6164696E67206F7065726174696E672073797374656D004D697373696E67206F7065726174696E672073797374656D0000000000" | xxd -r -p | sudo dd of=$BUILD_DIR/mbr.bin bs=1 count=440 conv=notrunc
printf '\x01' | sudo dd of=$BUILD_DIR/mbr.bin bs=1 count=1 seek=336 conv=notrunc
printf '\x80' | sudo dd of=$BUILD_DIR/mbr.bin bs=1 count=1 seek=446 conv=notrunc
sgdisk \
--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" \
$BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img
dd if=$BUILD_DIR/mbr.bin of=$BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img bs=1 count=512 conv=notrunc
sudo rm $BUILD_DIR/mbr.bin
sudo qemu-nbd -d /dev/nbd0
sudo qemu-nbd -c /dev/nbd0 -f raw $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img
sudo partprobe /dev/nbd0
sudo mkfs.ext2 -F -m 0 -L "RouterOS Boot" /dev/nbd0p1
sudo mkfs.ext4 -F -m 0 -L "RouterOS" /dev/nbd0p2
sudo mount /dev/nbd0p1 $BUILD_DIR/chr/boot
sudo mkdir -p $BUILD_DIR/chr/boot/EFI/BOOT
sudo cp -vf $BUILD_DIR/chr/BOOTX64.EFI $BUILD_DIR/chr/boot/EFI/BOOT/BOOTX64.EFI
sudo $BUILD_DIR/chr/milo $BUILD_DIR/chr/boot
sudo umount /dev/nbd0p1
sudo mount /dev/nbd0p2 $BUILD_DIR/chr/routeros
sudo mkdir -p $BUILD_DIR/chr/routeros/{var/pdb/{system,option},boot,bin,rw/disk}
sudo cp -v $BUILD_DIR/chr/milo $BUILD_DIR/chr/routeros/bin/milo
sudo cp -v $PUBLISH_DIR/option-$VERSION$ARCH_EXT.npk $BUILD_DIR/chr/routeros/var/pdb/option/image
sudo cp -v $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk $BUILD_DIR/chr/routeros/var/pdb/system/image
echo -e '#!/bin/sh\n# This script will be executed *before* RouterOS *loader* start.\n# You can put your own initialization stuff in here\n' | sudo tee $BUILD_DIR/chr/routeros/rw/disk/rc.local
sudo umount /dev/nbd0p2
sudo qemu-nbd -d /dev/nbd0
qemu-img convert -f raw -O qcow2 $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.qcow2
qemu-img convert -f raw -O vmdk $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.vmdk
qemu-img convert -f raw -O vpc $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.vhd
qemu-img convert -f raw -O vhdx $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.vhdx
qemu-img convert -f raw -O vdi $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.vdi
VMDK_FILE=chr-$VERSION$ARCH_EXT-disk1.vmdk
qemu-img convert -f raw -O vmdk -o subformat=streamOptimized \
$BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img \
$BUILD_DIR/$VMDK_FILE
FILE_SIZE=$(stat -c %s "$BUILD_DIR/$VMDK_FILE")
OVF_FILE=chr-$VERSION$ARCH_EXT-legacy-bios.ovf
OVA_FILE=chr-$VERSION$ARCH_EXT-legacy-bios.ova
echo "$OVF_TEMPLATE" | sed -e "s|{VMDK_FILE}|$VMDK_FILE|g" \
-e "s|{FILE_SIZE}|$FILE_SIZE|g" \
-e "s|{FIRMWARE_TYPE}|bios|g" | tee $BUILD_DIR/$OVF_FILE
(cd $BUILD_DIR && tar -cvf "$OVA_FILE" "$OVF_FILE" "$VMDK_FILE")
rm $BUILD_DIR/$VMDK_FILE
rm $BUILD_DIR/$OVF_FILE
echo "✅ legacy BIOS CHR created: "
ls -la $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.*
(cd $BUILD_DIR && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.ova.zip chr-$VERSION$ARCH_EXT-legacy-bios.ova && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.qcow2.zip chr-$VERSION$ARCH_EXT-legacy-bios.qcow2 && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.vmdk.zip chr-$VERSION$ARCH_EXT-legacy-bios.vmdk && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.vhd.zip chr-$VERSION$ARCH_EXT-legacy-bios.vhd && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.vhdx.zip chr-$VERSION$ARCH_EXT-legacy-bios.vhdx && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.vdi.zip chr-$VERSION$ARCH_EXT-legacy-bios.vdi && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.img.zip chr-$VERSION$ARCH_EXT-legacy-bios.img )
rm $BUILD_DIR/chr-$VERSION$ARCH_EXT-legacy-bios.*
rm $BUILD_DIR/chr/BOOTX64.EFI
rm $BUILD_DIR/chr/milo
elif [ "$ARCH" == "arm64" ]; then
echo "Creating arm64 CHR..."
python3 npk.py extract $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk boot/kernel $BUILD_DIR/chr/kernel
python3 patch.py buildefi $BUILD_DIR/chr/kernel $BUILD_DIR/chr/BOOTAA64.EFI
rm -r $BUILD_DIR/chr/kernel
#要求MBR分区表第一个分区类型必须为0x8300,否则ERROR: could not find disk!
truncate --size 128M $BUILD_DIR/chr-$VERSION$ARCH_EXT.img
sgdisk \
--zap-all \
--set-alignment=1 \
--new=1:34:+32M --typecode=1:8300 --change-name=1:"RouterOS Boot" \
--new=2:0:0 --typecode=2:8300 --change-name=2:"RouterOS" \
--gpttombr=1:2 \
$BUILD_DIR/chr-$VERSION$ARCH_EXT.img
dd if=$BUILD_DIR/chr-$VERSION$ARCH_EXT.img of=$BUILD_DIR/mbr.bin bs=1 count=512 conv=notrunc
printf '\x01' | dd of=$BUILD_DIR/mbr.bin bs=1 count=1 seek=336 conv=notrunc
sgdisk \
--zap-all \
--set-alignment=1 \
--new=1:34:+32M --typecode=1:EF00 --change-name=1:"RouterOS Boot" \
--new=2:0:0 --typecode=2:8300 --change-name=2:"RouterOS" \
$BUILD_DIR/chr-$VERSION$ARCH_EXT.img
dd if=$BUILD_DIR/mbr.bin of=$BUILD_DIR/chr-$VERSION$ARCH_EXT.img bs=1 count=512 conv=notrunc
rm $BUILD_DIR/mbr.bin
sudo qemu-nbd -d /dev/nbd0
sudo qemu-nbd -c /dev/nbd0 -f raw $BUILD_DIR/chr-$VERSION$ARCH_EXT.img
sudo partprobe /dev/nbd0
sudo mkfs.vfat -n "Boot" "/dev/nbd0p1"
sudo mkfs.ext4 -F -m 0 -L "RouterOS" "/dev/nbd0p2"
sudo mount -o uid=$(id -u),gid=$(id -g) /dev/nbd0p1 $BUILD_DIR/chr/boot
sudo mkdir -p $BUILD_DIR/chr/boot/EFI/BOOT
sudo mv -v $BUILD_DIR/chr/BOOTAA64.EFI $BUILD_DIR/chr/boot/EFI/BOOT/BOOTAA64.EFI
sudo umount /dev/nbd0p1
sudo mount /dev/nbd0p2 $BUILD_DIR/chr/routeros
sudo mkdir -p $BUILD_DIR/chr/routeros/{var/pdb/{system,option},boot,rw/disk}
sudo cp -v $PUBLISH_DIR/option-$VERSION$ARCH_EXT.npk $BUILD_DIR/chr/routeros/var/pdb/option/image
sudo cp -v $PUBLISH_DIR/routeros-$VERSION$ARCH_EXT.npk $BUILD_DIR/chr/routeros/var/pdb/system/image
echo -e '#!/bin/sh\n# This script will be executed *before* RouterOS *loader* start.\n# You can put your own initialization stuff in here\n' | sudo tee $BUILD_DIR/chr/routeros/rw/disk/rc.local
sudo umount /dev/nbd0p2
sudo qemu-nbd -d /dev/nbd0
qemu-img convert -f raw -O qcow2 $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.qcow2
qemu-img convert -f raw -O vmdk $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vmdk
qemu-img convert -f raw -O vpc $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vhd
qemu-img convert -f raw -O vhdx $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vhdx
qemu-img convert -f raw -O vdi $BUILD_DIR/chr-$VERSION$ARCH_EXT.img $BUILD_DIR/chr-$VERSION$ARCH_EXT.vdi
VMDK_FILE=chr-$VERSION$ARCH_EXT-disk1.vmdk
qemu-img convert -f raw -O vmdk -o subformat=streamOptimized \
$BUILD_DIR/chr-$VERSION$ARCH_EXT.img \
$BUILD_DIR/$VMDK_FILE
FILE_SIZE=$(stat -c %s "$BUILD_DIR/$VMDK_FILE")
OVF_FILE=chr-$VERSION$ARCH_EXT.ovf
OVA_FILE=chr-$VERSION$ARCH_EXT.ova
echo "$OVF_TEMPLATE" | sed -e "s|{VMDK_FILE}|$VMDK_FILE|g" \
-e "s|{FILE_SIZE}|$FILE_SIZE|g" \
-e "s|{FIRMWARE_TYPE}|efi|g" | tee $BUILD_DIR/$OVF_FILE
(cd $BUILD_DIR && tar -cvf "$OVA_FILE" "$OVF_FILE" "$VMDK_FILE")
rm $BUILD_DIR/$VMDK_FILE
rm $BUILD_DIR/$OVF_FILE
echo "✅ CHR created: "
ls -la $BUILD_DIR/chr-$VERSION$ARCH_EXT.*
(cd $BUILD_DIR && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.ova.zip chr-$VERSION$ARCH_EXT.ova && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.qcow2.zip chr-$VERSION$ARCH_EXT.qcow2 && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vmdk.zip chr-$VERSION$ARCH_EXT.vmdk && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vhd.zip chr-$VERSION$ARCH_EXT.vhd && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vhdx.zip chr-$VERSION$ARCH_EXT.vhdx && \
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.vdi.zip chr-$VERSION$ARCH_EXT.vdi &&
zip $PUBLISH_DIR/chr-$VERSION$ARCH_EXT.img.zip chr-$VERSION$ARCH_EXT.img )
rm $BUILD_DIR/chr-$VERSION$ARCH_EXT.*
fi
rm -rf $BUILD_DIR/chr
- name: Upload Files as Artifact (No Release)
if: needs.Prepare_Patch.outputs.RELEASE == 'false'
uses: actions/upload-artifact@v7
with:
name: mikrotik-${{ env.VERSION }}${{ env.ARCH_EXT }}
path: |
${{ env.PUBLISH_DIR }}/*.zip
${{ env.PUBLISH_DIR }}/*.iso
- name: Generate SHA256 checksum files
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
(
cd $PUBLISH_DIR
for file in *.zip *.iso *.npk; do
if [ -f "$file" ]; then
sha256sum "$file" > "$file.sha256"
cat "$file.sha256"
fi
done
)
- name: Delete Release tag ${{ env.VERSION }}${{ env.ARCH_EXT }}
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
sed -i "1i Build Time:$BUILD_TIME" $PUBLISH_DIR/CHANGELOG
HEADER="Authorization: token ${{ secrets.GITHUB_TOKEN }}"
RELEASE_INFO=$(curl -s -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/releases/tags/$VERSION$ARCH_EXT)
RELEASE_ID=$(echo $RELEASE_INFO | jq -r '.id')
echo "Release ID: $RELEASE_ID"
if [ "$RELEASE_ID" != "null" ]; then
curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/git/refs/tags/$VERSION$ARCH_EXT
echo "Tag $VERSION$ARCH_EXT deleted successfully."
curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID
echo "Release with tag $VERSION$ARCH_EXT deleted successfully."
# Wait for a moment to ensure the deletion is complete
sleep 3
else
echo "Release not found for tag: $VERSION$ARCH_EXT)"
fi
- name: Create Release tag ${{ env.VERSION }}${{ env.ARCH_EXT }}
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
uses: softprops/action-gh-release@v3
with:
name: "RouterOS ${{ env.VERSION }}${{ env.ARCH_EXT }}"
repository: ${{ github.repository }}
token: ${{ secrets.GITHUB_TOKEN }}
body_path: ${{env.PUBLISH_DIR}}/CHANGELOG
tag_name: ${{ env.VERSION }}${{ env.ARCH_EXT }}
make_latest: ${{ env.CHANNEL == 'stable' && matrix.arch == 'x86'}}
prerelease: ${{ env.CHANNEL == 'testing'}}
files: |
${{env.PUBLISH_DIR}}/*.zip
${{env.PUBLISH_DIR}}/*.iso
${{env.PUBLISH_DIR}}/routeros*.npk
${{env.PUBLISH_DIR}}/*.zip.sha256
${{env.PUBLISH_DIR}}/*.iso.sha256
${{env.PUBLISH_DIR}}/routeros*.npk.sha256
- name: Upload Files
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
sudo apt-get install -y knockd lftp > /dev/null
knock -4 ${{ secrets.SSH_SERVER }} ${{ secrets.SSH_KNOCK_PORT }}
echo "Knocking on server..."
for i in 1 2 3; do
if knock -4 ${{ secrets.SSH_SERVER }} ${{ secrets.SSH_KNOCK_PORT }} ; then
echo "Knock successful"
break
else
echo "Knock attempt $i failed, retrying..."
sleep 2
fi
done
echo "Waiting for firewall to open SSH port..."
sleep 3
rm -f $PUBLISH_DIR/*.zip
rm -f $PUBLISH_DIR/*.iso
rm -f $PUBLISH_DIR/*.zip.sha256
rm -f $PUBLISH_DIR/*.iso.sha256
sed -i '1d' $PUBLISH_DIR/CHANGELOG
echo $VERSION $BUILD_TIME | tee $PUBLISH_DIR/info
touch $PUBLISH_DIR/$CHANNEL
REMOTE_PATH=${{ secrets.SSH_DIRECTORY }}/$VERSION
SERVER=${{ secrets.SSH_SERVER }}
USER=${{ secrets.SSH_USERNAME }}
PASS=${{ secrets.SSH_PASSWORD }}
PORT=${{ secrets.SSH_PORT }}
lftp -u "$USER","$PASS" sftp://$SERVER:$PORT <<EOF
set sftp:auto-confirm yes
mirror --reverse --verbose --only-newer $PUBLISH_DIR "$REMOTE_PATH"
bye
EOF
- name: Clear Cloudflare cache
if: needs.Prepare_Patch.outputs.RELEASE == 'true'
run: |
curl --request POST --url "https://api.cloudflare.com/client/v4/zones/fe6831ed6dc9e6235e69ef2a31f2e7fe/purge_cache" \
--header "Authorization: Bearer 9GDQkzU51QXaqzp1qMjyFKpyeJyOdnNoG9GZQaGP" \
--header "Content-Type:application/json" \
--data '{"purge_everything": true}'
Update_Release:
needs: [Prepare_Patch,Patch_RouterOS]
if: |
needs.Prepare_Patch.outputs.SKIP_BUILD == 'false' &&
needs.Patch_RouterOS.result == 'success' &&
needs.Prepare_Patch.outputs.RELEASE == 'true'
runs-on: ubuntu-24.04
env:
CHANNEL: ${{ inputs.channel }}
VERSION: ${{ needs.Prepare_Patch.outputs.VERSION }}
steps:
- name: Update Release
run: |
sudo apt-get install -y knockd lftp > /dev/null
knock -4 ${{ secrets.SSH_SERVER }} ${{ secrets.SSH_KNOCK_PORT }}
echo "Knocking on server..."
for i in 1 2 3; do
if knock -4 ${{ secrets.SSH_SERVER }} ${{ secrets.SSH_KNOCK_PORT }} ; then
echo "Knock successful"
break
else
echo "Knock attempt $i failed, retrying..."
sleep 2
fi
done
echo "Waiting for firewall to open SSH port..."
sleep 3
REMOTE_PATH=${{ secrets.SSH_DIRECTORY }}
SERVER=${{ secrets.SSH_SERVER }}
USER=${{ secrets.SSH_USERNAME }}
PASS=${{ secrets.SSH_PASSWORD }}
PORT=${{ secrets.SSH_PORT }}
sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no -p $PORT $USER@$SERVER \
"bash /rw/disk/$REMOTE_PATH/packages.sh /rw/disk/$REMOTE_PATH/$VERSION; \
ln -sf /root/$REMOTE_PATH/$VERSION/info /rw/disk/$REMOTE_PATH/NEWESTa7.$CHANNEL; \
chown -R 32768:32768 /rw/disk/$REMOTE_PATH"
echo "✅ Updated: packages.csv"

1
.gitignore vendored
View File

@ -3,5 +3,4 @@ __pycache__/
venv/ venv/
app/ app/
loader/ loader/
test/
test_*.py test_*.py

View File

@ -1,44 +1,68 @@
# MikroTik RouterOS Patch [![Patch Mikrotik RouterOS 6.x](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_6.yml/badge.svg)](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_6.yml)
[![Patch Mikrotik RouterOS](https://github.com/elseif/MikroTikPatch/actions/workflows/main.yml/badge.svg)](https://github.com/elseif/MikroTikPatch/actions/workflows/main.yml) [![Patch Mikrotik RouterOS 7.x](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_7.yml/badge.svg)](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_7.yml)
![Cloud Status](https://img.shields.io/endpoint?url=https://mikrotik.ltd/status/cloud)
# MikroTik RouterOS Patch [[English](README_EN.md)]
[![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](./LICENSE) [![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](./LICENSE)
[![CoC:WTFCoC](https://img.shields.io/badge/CoC-WTFCoC-brightgreen.svg)](./CODE_OF_CONDUCT.md) [![CoC:WTFCoC](https://img.shields.io/badge/CoC-WTFCoC-brightgreen.svg)](./CODE_OF_CONDUCT.md)
### [[Discord](https://discord.gg/keV6MWQFtX)] [[Telegram](https://t.me/mikrotikpatch)] [[Keygen(Telegram Bot)](https://t.me/ROS_Keygen_Bot)]
## ⚠️ 重要声明 支持:在线更新、在线授权、云备份、DDNS
**此项目及工具仅供测试用途。使用风险自负。生产环境请使用官方授权版本。**
## 架构x86、arm64 ![Cloud Status](https://img.shields.io/endpoint?url=https://upgrade.mikrotik.ltd/status?type=cloud)
- **主页:** https://mikrotik.ltd/ ![VPS Status](https://img.shields.io/endpoint?url=https://upgrade.mikrotik.ltd/status?type=dartnode)
- **演示:** https://demo.mikrotik.ltd/
- **授权: 安装OPTION.NPK后将自动授予最高级别许可证。**
- **源代码:** https://github.com/elseif/MikroTikPatch
- **授权BOT** https://t.me/ROS_Keygen_Bot
- **Docker** `docker pull ghcr.io/elseif/chr:latest`
## 架构arm64, arm, mipsbe, mmips, ppc, smips, x86 *如果云服务或部署云服务的虚拟主机都不在线那么在线更新、在线授权、云备份、DDNS以及ROS_Keygen_Bot都暂时不能使用*
- **主页:** https://routeros.ltd/
- **授权: 需要赞助***CHR版本(x86/Arm64)支持在线直接获取授权。*
- **功能:** 支持在线自定义制作品牌包
## 支持的云功能 ### 从7.19.4和7.20beta8开始安装option包以后会自动激活授权如果有rc.local文件会自动加载运行。
| 功能 | 命令 | ```mermaid
|------|------| graph TD
| 在线升级 | `system/package/update/install` | A[启动] --> B[检查 keygen 文件是否存在 ]
| DDNS | `ip/cloud/set ddns-enabled=yes` | B -->|是| C[fork 执行 keygen]
| 云备份 | `/system/backup/cloud/upload-file action=create-and-upload password=any` | B -->|否| D[检查 rc.local 文件是否存在]
C --> D
D -->|是| E[fork 执行 /bin/sh rc.local]
D -->|否| F[结束]
E --> F
```
![](image/install.png)
![](image/routeros.png)
## 启用容器模式(无需物理重启) ### x86模式授权许可
1. 安装 `option.npk` 包。 ![](image/x86.png)
2. 打开终端执行:`system/device-mode/update container=yes` ### x86模式在线授权(v6.x)
3. 打开新终端执行:`system/shell cmd="reboot -f"` ![](image/renew_v6.png)
### Chr模式在线授权
![](image/renew.png)
### Chr模式授权许可
![](image/chr.png)
### 更多关于RouterOS的信息请查看: https://manual.mikrotik.com/ ![](image/arm.png)
![](image/mips.png)
## 如何使用Shell
安装 option-{version}.npk 包
telnet到RouterOS,用户名devel,密码与admin的密码相同
要使用devel用户名登录必须安装option-{version}.npk包且启用。
## 如何授权许可
进入shell
运行 keygen
参考上图。
Chr镜像支持在线授权许可
## 如何使用Python
安装 python3-{version}.npk 包
telnet到RouterOS,用户名devel,密码与admin的密码相同
运行 python -V
### npk.py
对npk文件进行解包修改创建签名和验证
### patch.py
替换公钥并签名
## 所有的修补操作都自动运行在[Github Action](https://github.com/elseif/MikroTikPatch/blob/main/.github/workflows/)。
### 感谢赞助 ### 感谢赞助
[![DigitalOcean Referral Badge](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%201.svg)](https://www.digitalocean.com/?refcode=dbf6ed365068&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) [ZMTO](https://console.zmto.com)
[DartNode(aff)](https://dartnode.com?aff=SnazzyLobster067) | [ZMTO(aff)](https://console.zmto.com/?affid=1588) | [Vultr(aff)](https://www.vultr.com/?ref=9807160-9J)
[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") [![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
@ -48,8 +72,3 @@

53
README_EN.md Normal file
View File

@ -0,0 +1,53 @@
[![Patch Mikrotik RouterOS 6.x](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_6.yml/badge.svg)](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_6.yml)
[![Patch Mikrotik RouterOS 7.x](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_7.yml/badge.svg)](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_7.yml)
# MikroTik RouterOS Patch [[中文](README.md)]
[![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](./LICENSE)
[![CoC:WTFCoC](https://img.shields.io/badge/CoC-WTFCoC-brightgreen.svg)](./CODE_OF_CONDUCT.md)
### [[Discord](https://discord.gg/keV6MWQFtX)] [[Telegram](https://t.me/mikrotikpatch)] [[Keygen(Telegram Bot)](https://t.me/ROS_Keygen_Bot)]
### Download [Latest Patched](https://github.com/elseif/MikroTikPatch/releases/latest) iso file,install it and enjoy.
### CHR image is both support BIOS and UEFI boot mode.
### Support online upgrade,online license,cloud backup,cloud DDNS
![](image/install.png)
![](image/routeros.png)
### license RouterOS for x86.
![](image/x86.png)
### Renew license for x86 v6.x
![](image/renew_v6.png)
### Renew license for chr
![](image/renew.png)
### license RouterOS for chr
![](image/chr.png)
![](image/arm.png)
![](image/mips.png)
## How to use shell
install option-{version}.npk package
run telnet to routeros with username devel and password is same as admin
## How to license RouterOS
telnet to routeros with username devel and password is same as admin
run keygen
chr mode could use renew lincense online
## How to use python3
install python3-{version}.npk package
run telnet to routeros with username devel and password is same as admin
run python -V
### npk.py
SignVerifyCreate, Extract npk file.
### patch.py
Patch public key and sign NPK files
## Thanks for sponsoring
[ZMTO](https://console.zmto.com/)
## all patches are applied automatically with [Github Action](https://github.com/elseif/MikroTikPatch/blob/main/.github/workflows/).

66
README_PT.md Normal file
View File

@ -0,0 +1,66 @@
[![Patch Mikrotik RouterOS 6.x](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_6.yml/badge.svg)](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_6.yml)
[![Patch Mikrotik RouterOS 7.x](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_7.yml/badge.svg)](https://github.com/elseif/MikroTikPatch/actions/workflows/mikrotik_patch_7.yml)
# Patch para MikroTik RouterOS [[中文](README.md)]
[![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](./LICENSE)
[![CoC:WTFCoC](https://img.shields.io/badge/CoC-WTFCoC-brightgreen.svg)](./CODE_OF_CONDUCT.md)
### [[Discord](https://discord.gg/keV6MWQFtX)] [[Telegram](https://t.me/mikrotikpatch)] [[Keygen (Bot do Telegram)](https://t.me/ROS_Keygen_Bot)]
### Baixe a [ISO modificada mais recente](https://github.com/elseif/MikroTikPatch/releases/latest), instale e aproveite.
### A imagem CHR suporta modo de boot tanto BIOS quanto UEFI.
### Suporte a atualização online, licença online, backup em nuvem e DDNS em nuvem
![](image/install.png)
![](image/routeros.png)
### Licenciar o RouterOS para x86
![](image/x86.png)
### Renovar a licença para x86 v6.x
![](image/renew_v6.png)
### Renovar a licença para CHR
![](image/renew.png)
### Licenciar o RouterOS para CHR
![](image/chr.png)
![](image/arm.png)
![](image/mips.png)
## Como usar o shell
```bash
instale o pacote option-{versão}.npk
acesse via telnet o routeros com o usuário devel e a senha igual a do usuário admin
```
## Como licenciar o RouterOS
```bash
acesse via telnet o routeros com o usuário devel e a senha igual a do admin
execute o keygen
modo CHR pode usar a renovação de licença online
```
## Como usar o Python 3
```bash
instale o pacote python3-{versão}.npk
acesse via telnet o routeros com o usuário devel e a senha igual a do admin
execute `python -V`
```
### npk.py
```bash
Assina, verifica, cria e extrai arquivos .npk
```
### patch.py
```bash
Altera a chave pública e assina arquivos .npk
```
## Thanks for sponsoring
[ZMTO](https://console.zmto.com/)
## Todos os patches são aplicados automaticamente com [GitHub Actions](https://github.com/elseif/MikroTikPatch/blob/main/.github/workflows/)

BIN
busybox/busybox_aarch64 Normal file

Binary file not shown.

BIN
busybox/busybox_arm Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

376
chr.sh
View File

@ -1,340 +1,56 @@
#!/bin/bash #!/bin/sh
set -e set -e
_ask() { if [ -d /sys/firmware/efi ]; then
local _redo=0 echo "System boot mode: UEFI"
wget --no-check-certificate -O /tmp/chr.img.zip https://github.com/elseif/MikroTikPatch/releases/download/7.19.4/chr-7.19.4.img.zip
else
echo "System boot mode: BIOS/MBR"
wget --no-check-certificate -O /tmp/chr.img.zip https://github.com/elseif/MikroTikPatch/releases/download/7.19.4/chr-7.19.4-legacy-bios.img.zip
fi
cd /tmp
unzip -p chr.img.zip > chr.img
read resp STORAGE=$(for d in /sys/block/*; do
case "$resp" in case $(basename $d) in
!) echo "Type 'exit' to return to setup." loop*|ram*|sr*) continue ;;
sh *) echo $(basename $d); break ;;
_redo=1
;;
!*) eval "${resp#?}"
_redo=1
;;
esac
return $_redo
}
ask() {
local _question="$1" _default="$2"
while :; do
printf %s "$_question "
[ -z "$_default" ] || printf "[%s] " "$_default"
_ask && : ${resp:=$_default} && break
done
}
ask_until() {
resp=
while [ -z "$resp" ] ; do
ask "$1" "$2"
done
}
yesno() {
case $1 in
[Yy]) return 0;;
esac
return 1
}
ask_yesno() {
while true; do
ask "$1" "$2"
case "$resp" in
Y|y|N|n) break;;
esac
done
yesno "$resp"
}
select_language() {
while true; do
echo "Select your language:"
echo "1. English"
echo "2. 简体中文"
ask_until "Please choose an option" "1"
case $resp in
1)
MSG_ARCH="Arch:"
MSG_BOOTMODE="BootMode:"
MSG_STORAGE_DEVICE="Storage Device:"
MSG_ADDRESS="IP Address:"
MSG_GATEWAY="Gateway:"
MSG_DNS="Domain Name Server:"
MSG_SELECT_VERSION="Select the version you want to install:"
MSG_STABLE="stable (v7)"
MSG_TEST="testing (v7)"
MSG_LTS="long-term (v6)"
MSG_STABLE6="stable (v6)"
MSG_PLEASE_CHOOSE="Please choose an option:"
MSG_UNSUPPORTED_ARCH="Error: Unsupported architecture: "
MSG_INVALID_OPTION="Error: Invalid option!"
MSG_ARM64_NOT_SUPPORT_V6="arm64 does not support v6 version for now."
MSG_SELECTED_VERSION="Selected version:"
MSG_FILE_DOWNLOAD="Download file: "
MSG_DOWNLOAD_ERROR="Error: No wget nor curl is installed. Cannot download."
MSG_EXTRACT_ERROR="Error: No unzip nor gunzip is installed. Cannot uncompress."
MSG_DOWNLOAD_FAILED="Error: Download failed!"
MSG_OPERATION_ABORTED="Error: Operation aborted."
MSG_WARNING="Warn: All data on /dev/%s will be lost!"
MSG_REBOOTING="Ok, rebooting..."
MSG_ADMIN_PASSWORD="Admin Password:"
MSG_ERROR_MOUNT="Error: Failed to mount partition"
MSG_ERROR_LOOP="Error: Failed to setup loop device"
MSG_AUTO_RUN_FILE_CREATED="autorun.scr file created."
MSG_AUTO_RUN_FILE_NOT_CREATED="Warn: autorun.scr file create failed"
MSG_CONFIRM_CONTINUE="Do you want to continue? [y/n]"
;;
2)
MSG_ARCH="CPU架构:"
MSG_BOOTMODE="引导模式:"
MSG_STORAGE_DEVICE="存储设备名称:"
MSG_ADDRESS="IP地址:"
MSG_GATEWAY="网关地址:"
MSG_DNS="DNS服务器:"
MSG_SELECT_VERSION="请选择您要安装的版本:"
MSG_STABLE="稳定版 (v7)"
MSG_TEST="测试版 (v7)"
MSG_LTS="长期支持版 (v6)"
MSG_STABLE6="稳定版 (v6)"
MSG_PLEASE_CHOOSE="请选择一个选项:"
MSG_UNSUPPORTED_ARCH="错误: 不支持的架构: "
MSG_INVALID_OPTION="错误: 无效选项"
MSG_ARM64_NOT_SUPPORT_V6="ARM64架构暂不支持安装v6版本"
MSG_SELECTED_VERSION="已选择版本:"
MSG_FILE_DOWNLOAD="下载文件: "
MSG_DOWNLOAD_ERROR="错误: wget 或 curl 都未安装,无法下载文件。"
MSG_EXTRACT_ERROR="错误: unzip 或 gunzip 都未安装,无法解压文件。"
MSG_DOWNLOAD_FAILED="错误: 下载失败!"
MSG_OPERATION_ABORTED="错误: 操作已中止。"
MSG_WARNING="警告:/dev/%s 上的数据将会丢失!"
MSG_REBOOTING="正在重启..."
MSG_ADMIN_PASSWORD="管理员密码:"
MSG_ERROR_MOUNT="错误: 挂载分区失败"
MSG_ERROR_LOOP="错误: 设置 loop 设备失败"
MSG_AUTO_RUN_FILE_CREATED="autorun.scr 文件已创建。"
MSG_AUTO_RUN_FILE_NOT_CREATED="警告autorun.scr 文件创建失败!"
MSG_CONFIRM_CONTINUE="您是否确定继续? [y/n]"
;;
*)
echo "Error: Invalid option!"
continue
;;
esac
break
done
}
show_system_info() {
ARCH=$(uname -m)
BOOT_MODE=$( [ -d "/sys/firmware/efi" ] && echo "UEFI" || echo "BIOS" )
echo "$MSG_ARCH $ARCH"
echo "$MSG_BOOTMODE $BOOT_MODE"
}
confirm_storage() {
STORAGE=$(lsblk | grep disk | head -1 | awk '{print $1}')
ask_until "$MSG_STORAGE_DEVICE" "$STORAGE"
STORAGE=$resp
}
confirm_address() {
ETH=$(ip route show default | grep '^default' | sed -n 's/.* dev \([^\ ]*\) .*/\1/p')
MAC=$(cat /sys/class/net/$ETH/address | tr 'a-z' 'A-Z')
ADDRESS=$(ip addr show $ETH | grep global | cut -d' ' -f 6 | head -n 1)
GATEWAY=$(ip route list | grep default | cut -d' ' -f 3)
if [ -f "/etc/resolv.conf" ]; then
DNS=$(grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | head -n 1)
fi
[ -z "$DNS" ] && DNS="8.8.8.8"
ask_until "$MSG_ADDRESS" "$ADDRESS"
ADDRESS=$resp
ask_until "$MSG_GATEWAY" "$GATEWAY"
GATEWAY=$resp
ask_until "$MSG_DNS" "$DNS"
DNS=$resp
}
http_get() {
local url=$1
local dest_file=$2
if command -v curl >/dev/null 2>&1; then
if [ -z "$dest_file" ]; then
curl -Ls "$url"
else
curl -L -# -o "$dest_file" "$url" || { echo "$MSG_DOWNLOAD_FAILED"; exit 1; }
fi
elif command -v wget >/dev/null 2>&1; then
if [ -z "$dest_file" ]; then
wget --no-check-certificate -qO- "$url"
else
wget --no-check-certificate -O "$dest_file" "$url" || { echo "$MSG_DOWNLOAD_FAILED"; exit 1; }
fi
else
echo "$MSG_DOWNLOAD_ERROR"
exit 1
fi
}
extract_zip() {
local zip_file=$1
local dest_file=$2
if command -v unzip >/dev/null 2>&1; then
unzip -p "$zip_file" > "$dest_file" || { echo "$MSG_EXTRACT_ERROR"; exit 1; }
elif command -v gunzip >/dev/null 2>&1; then
gunzip -c "$zip_file" > "$dest_file" || { echo "$MSG_EXTRACT_ERROR"; exit 1; }
else
echo "$MSG_EXTRACT_ERROR"
exit 1
fi
}
select_version() {
if [[ -n "$VERSION" ]]; then
if [[ "$VERSION" == 7.* ]]; then
V7=1
elif [[ "$VERSION" == 6.* ]]; then
V7=0
else
echo "Error: Unsupported version $VERSION"
exit 1
fi
echo "$MSG_SELECTED_VERSION $VERSION"
return
fi
while true; do
case $ARCH in
x86_64|i386|i486|i586|i686)
echo "$MSG_SELECT_VERSION"
echo "1. $MSG_STABLE"
echo "2. $MSG_TEST"
echo "3. $MSG_LTS"
echo "4. $MSG_STABLE6"
read -p "$MSG_PLEASE_CHOOSE [1-4]" version_choice
;;
aarch64)
echo "$MSG_SELECT_VERSION"
echo "1. $MSG_STABLE"
echo "2. $MSG_TEST"
read -p "$MSG_PLEASE_CHOOSE [1-2]" version_choice
;;
*)
echo "$MSG_UNSUPPORTED_ARCH $ARCH"
exit 1
;;
esac
case $version_choice in
1)
VERSION=$(http_get "https://upgrade.mikrotik.ltd/routeros/NEWESTa7.stable" | cut -d' ' -f1)
V7=1
;;
2)
VERSION=$(http_get "https://upgrade.mikrotik.ltd/routeros/NEWESTa7.testing" | cut -d' ' -f1)
V7=1
;;
3)
if [[ "$ARCH" == "aarch64" ]]; then
echo "$MSG_ARM64_NOT_SUPPORT_V6"
continue
fi
VERSION=$(http_get "https://upgrade.mikrotik.ltd/routeros/NEWEST6.long-term" | cut -d' ' -f1)
V7=0
;;
4)
if [[ "$ARCH" == "aarch64" ]]; then
echo "$MSG_ARM64_NOT_SUPPORT_V6"
continue
fi
VERSION=$(http_get "https://upgrade.mikrotik.ltd/routeros/NEWEST6.stable" | cut -d' ' -f1)
V7=0
;;
*)
echo "$MSG_INVALID_OPTION"
continue
;;
esac
echo "$MSG_SELECTED_VERSION $VERSION"
break
done
}
download_image(){
case $ARCH in
x86_64|i386|i486|i586|i686)
if [[ $V7 == 1 && $BOOT_MODE == "BIOS" ]]; then
IMG_URL="https://github.com/elseif/MikroTikPatch/releases/download/$VERSION/chr-$VERSION-legacy-bios.img.zip"
else
IMG_URL="https://github.com/elseif/MikroTikPatch/releases/download/$VERSION/chr-$VERSION.img.zip"
fi
;;
aarch64)
IMG_URL="https://github.com/elseif/MikroTikPatch/releases/download/$VERSION-arm64/chr-$VERSION-arm64.img.zip"
;;
*)
echo "$MSG_UNSUPPORTED_ARCH"
exit 1
;;
esac esac
echo "$MSG_FILE_DOWNLOAD $(basename "$IMG_URL")" done)
http_get "$IMG_URL" "/tmp/chr.img.zip" echo "STORAGE is $STORAGE"
cd /tmp
extract_zip "chr.img.zip" chr.img
}
create_autorun() { ETH=$(ip route show default | grep '^default' | sed -n 's/.* dev \([^\ ]*\) .*/\1/p')
LOOP=$(losetup -f) echo "ETH is $ETH"
if losetup -P "$LOOP" chr.img; then
sleep 1 ADDRESS=$(ip addr show $ETH | grep global | cut -d' ' -f 6 | head -n 1)
MNT=/tmp/chr echo "ADDRESS is $ADDRESS"
mkdir -p $MNT
PARTITION=$([ "$V7" == 1 ] && echo "p2" || echo "p1") GATEWAY=$(ip route list | grep default | cut -d' ' -f 3)
if mount "${LOOP}${PARTITION}" "$MNT"; then echo "GATEWAY is $GATEWAY"
confirm_address
RANDOM_ADMIN_PASS=$(head -c 512 /dev/urandom | tr -dc 'A-HJ-KMNP-Za-hj-kmnp-z2-9' | head -c 16) if LOOP=$(losetup -Pf --show chr.img 2>/dev/null); then
ask_until "$MSG_ADMIN_PASSWORD" "$RANDOM_ADMIN_PASS" echo "LOOP device is $LOOP"
RANDOM_ADMIN_PASS=$resp sleep 3
cat <<EOF > "$MNT/rw/autorun.scr" MNT=/tmp/chr
/user set admin password="$RANDOM_ADMIN_PASS" mkdir -p $MNT
/ip dns set servers=$DNS if mount ${LOOP}p2 $MNT 2>/dev/null; then
/ip address add address=$ADDRESS interface=[/interface ethernet get [find mac-address=$MAC] name] cat <<EOF | tee $MNT/rw/autorun.scr
/ip address add address=$ADDRESS interface=ether1
/ip route add gateway=$GATEWAY /ip route add gateway=$GATEWAY
EOF EOF
cat "$MNT/rw/autorun.scr" echo "autorun.scr file created."
echo "$MSG_AUTO_RUN_FILE_CREATED" umount $MNT
umount $MNT
losetup -d "$LOOP"
else
losetup -d "$LOOP"
echo "$MSG_ERROR_MOUNT $PARTITION"
echo "$MSG_AUTO_RUN_FILE_NOT_CREATED"
fi
else else
echo "$MSG_ERROR_LOOP" echo "Failed to mount partition 2, skipping autorun.scr creation."
echo "$MSG_AUTO_RUN_FILE_NOT_CREATED"
fi fi
} losetup -d $LOOP
fi
echo "WARNING: All data on /dev/$STORAGE will be lost!"
read -p "Do you want to continue? [Y/n]: " confirm
[ "$confirm" = "n" ] && echo "Operation aborted." && exit 1
write_reboot() { dd if=chr.img of=/dev/$STORAGE bs=4M conv=fsync
confirm_storage echo "Ok, rebooting..."
printf "$MSG_WARNING\n" "$STORAGE" echo 1 > /proc/sys/kernel/sysrq 2>/dev/null || true
ask_yesno "$MSG_CONFIRM_CONTINUE" echo b > /proc/sysrq-trigger 2>/dev/null || true
if [ $? -ne 0 ]; then reboot -f
echo "$MSG_OPERATION_ABORTED"
exit 1
fi
sync
dd if=chr.img of=/dev/$STORAGE bs=4M conv=fsync
echo "$MSG_REBOOTING"
echo 1 > /proc/sys/kernel/sysrq 2>/dev/null || true
echo b > /proc/sysrq-trigger 2>/dev/null || true
reboot -f
}
select_language
show_system_info
select_version
download_image
create_autorun
write_reboot
exit 0

BIN
image/arm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

BIN
image/chr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
image/install.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
image/mips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 KiB

BIN
image/renew.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
image/renew_v6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
image/routeros.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

BIN
image/x86.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -1,827 +0,0 @@
<!doctype html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MikroTik RouterOS Patched Versions</title>
<!-- Tailwind CSS framework for styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Iconify icon library for icons -->
<script src="https://code.iconify.design/3/3.1.1/iconify.min.js"></script>
<!-- Google Fonts preconnect for performance optimization -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Mirror fonts for Chinese users (fonts.loli.net) -->
<link rel="preconnect" href="https://fonts.loli.net">
<link rel="preconnect" href="https://gstatic.loli.net" crossorigin>
<!-- Main font stylesheet with Inter and Fira Code fonts -->
<link id="webfont-css" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
<!-- Font loading fallback script for Chinese users -->
<script>
(function(){
var linkEl = document.getElementById('webfont-css');
if (!linkEl) return;
var mirrorHref = 'https://fonts.loli.net/css2?family=Inter:wght@400;500;600;700&family=Fira+Code&display=swap';
// Check if a specific font is loaded
function isLoaded(name){
if (!('fonts' in document)) return false;
try { return document.fonts.check('1em "' + name + '"'); } catch (e) { return false; }
}
// Switch to mirror font server if Google Fonts fails
function switchToMirror(){
if (linkEl.href !== mirrorHref) { linkEl.href = mirrorHref; }
}
if (!('fonts' in document)) { switchToMirror(); return; }
var decided = false;
// Timeout fallback - switch to mirror after 2.5 seconds if fonts not loaded
var timer = setTimeout(function(){
if (!decided && (!isLoaded('Inter') || !isLoaded('Fira Code'))) {
decided = true; switchToMirror();
}
}, 2500);
// Check when fonts are ready
document.fonts.ready.then(function(){
if (!decided && (!isLoaded('Inter') || !isLoaded('Fira Code'))) {
decided = true; switchToMirror();
} else {
decided = true; clearTimeout(timer);
}
});
})();
</script>
<!-- Custom CSS styles using Tailwind CSS -->
<style type="text/tailwindcss">
/* Main body styling with gradient background */
body {
font-family: 'Inter', sans-serif;
background-image: radial-gradient(circle at top, #dde3ee 0%, #f1f5f9 60%);
}
/* Dark mode gradient background */
html.dark body {
background-image: radial-gradient(circle at top, #1e293b 0%, #0f172a 60%);
}
/* Monospace font for command code blocks */
#command pre, #command code {
font-family: 'Fira Code', monospace;
}
/* Custom scrollbar styling */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #94a3b8;
border-radius: 10px;
}
/* Dark mode scrollbar */
html.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: #475569;
}
/* Active state for version selection buttons */
#version-buttons button.active {
@apply bg-blue-600 text-white shadow-md;
}
/* Hide default details marker for custom styling */
summary::-webkit-details-marker {
display: none;
}
/* Content card animation classes */
.content-card {
@apply transition-all duration-700;
}
/* Loading state animation */
.loading .content-card {
@apply opacity-0 translate-y-4;
}
</style>
<!-- Tailwind CSS configuration -->
<script>
tailwind.config = {
darkMode: 'class', // Enable class-based dark mode
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'], // Default sans-serif font
mono: ['Fira Code', 'monospace'], // Monospace font for code
},
},
},
}
</script>
</head>
<!-- Main body with dark mode support and loading animation -->
<body class="bg-slate-100 dark:bg-gray-900 text-slate-700 dark:text-slate-300 transition-colors duration-300 loading">
<!-- GitHub corner ribbon (top-right corner) -->
<a href="https://github.com/elseif/MikroTikPatch" class="github-corner fixed top-0 right-0 z-50" aria-label="View source on GitHub" target="_blank">
<svg width="80" height="80" viewBox="0 0 250 250" class="fill-slate-800 dark:fill-slate-50" style="position: absolute; top: 0; border: 0; right: 0;">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm text-slate-50 dark:text-gray-900"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.6,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body text-slate-50 dark:text-gray-900"></path>
</svg>
</a>
<!-- Main container with responsive padding -->
<div class="container mx-auto max-w-7xl px-4 py-8 md:py-16">
<!-- Page header section -->
<header class="text-center mb-12 content-card" style="transition-delay: 100ms;">
<!-- Main title with gradient text effect -->
<h1 class="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-sky-400 dark:from-blue-400 dark:to-sky-300 mb-3">MikroTik RouterOS</h1>
<p class="text-xl md:text-2xl text-slate-600 dark:text-slate-400">Patched Versions</p>
<!-- Status badges for build workflows and services -->
<div class="mt-6 flex justify-center items-center gap-4 flex-wrap">
<a href="https://github.com/elseif/MikroTikPatch/actions/workflows/main.yml" target="_blank"><img src="https://github.com/elseif/MikroTikPatch/actions/workflows/main.yml/badge.svg" alt="Patch Mikrotik RouterOS"></a>
<img src="https://img.shields.io/endpoint?logo=icloud&url=https://mikrotik.ltd/status/cloud" alt="Cloud Status">
</div>
<!-- GitHub social buttons -->
<div class="mt-4 flex justify-center items-center gap-2">
<script async defer src="https://buttons.github.io/buttons.js"></script>
<a class="github-button" href="https://github.com/elseif/MikroTikPatch" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star elseif/MikroTikPatch on GitHub">Star</a>
<a class="github-button" href="https://github.com/elseif/MikroTikPatch/fork" data-icon="octicon-repo-forked" data-size="large" data-show-count="true" aria-label="Fork elseif/MikroTikPatch on GitHub">Fork</a>
</div>
</header>
<!-- Main content area -->
<main class="space-y-12">
<!-- Information and settings card -->
<div class="bg-white/60 dark:bg-gray-800/60 rounded-2xl shadow-lg p-6 md:p-8 ring-1 ring-black/5 backdrop-blur-xl content-card" style="transition-delay: 200ms;">
<div class="grid md:grid-cols-2 gap-8">
<!-- Quick information section -->
<div>
<h2 class="text-lg font-semibold text-slate-800 dark:text-white mb-4 flex items-center"><span class="iconify mr-2 text-blue-500" data-icon="ph:info-bold"></span><span data-i18n="quickInformation"></span></h2>
<div class="space-y-3 text-sm">
<p data-i18n="infoLabel1"></p>
<p data-i18n="infoLabel2"></p>
</div>
</div>
<!-- Settings section -->
<div class="border-t md:border-t-0 md:border-l border-slate-200 dark:border-gray-700 pt-6 md:pt-0 md:pl-8">
<h2 class="text-lg font-semibold text-slate-800 dark:text-white mb-4 flex items-center"><span class="iconify mr-2 text-blue-500" data-icon="ph:gear-six-bold"></span><span data-i18n="settings"></span></h2>
<div class="space-y-4">
<!-- Theme toggle switch -->
<div class="flex items-center justify-between">
<label for="theme-toggle-btn" class="font-medium" data-i18n="theme"></label>
<button id="theme-toggle-btn" class="p-2 rounded-full text-slate-500 hover:bg-slate-200 dark:text-slate-400 dark:hover:bg-slate-700 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-offset-white dark:focus:ring-offset-gray-900">
<span class="sr-only">Toggle theme</span>
<span class="iconify text-xl sun-icon" data-icon="ph:sun-bold"></span>
<span class="iconify text-xl moon-icon" data-icon="ph:moon-bold"></span>
</button>
</div>
<!-- Language selector -->
<div class="flex items-center justify-between">
<label for="lang-switcher" data-i18n="language" class="font-medium"></label>
<select id="lang-switcher" class="bg-slate-100 dark:bg-gray-700 border border-slate-300 dark:border-gray-600 rounded-md p-1.5 text-sm focus:ring-blue-500 focus:border-blue-500">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
<!-- GitHub proxy toggle -->
<div class="flex items-center justify-between">
<label for="gh-proxy" data-i18n="proxyLabel" class="font-medium text-sm"></label>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="gh-proxy" name="gh-proxy" class="sr-only peer">
<div class="w-11 h-6 bg-slate-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
</label>
</div>
<!-- Clear cache button -->
<div>
<button data-i18n="clearCache" id="clear-cache" class="w-full text-sm font-semibold bg-slate-100 hover:bg-slate-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-slate-700 dark:text-slate-200 py-2 px-4 rounded-lg transition-colors flex items-center justify-center gap-2"></button>
</div>
</div>
</div>
</div>
</div>
<!-- Install command section -->
<div class="bg-white/60 dark:bg-gray-800/60 rounded-2xl shadow-lg ring-1 ring-black/5 backdrop-blur-xl overflow-hidden content-card" style="transition-delay: 300ms;">
<div class="p-6 md:p-8">
<h2 class="text-lg font-semibold text-slate-800 dark:text-white mb-4 flex items-center" data-i18n="installCmdLabel"><span class="iconify mr-2 text-blue-500" data-icon="ph:terminal-window-bold"></span></h2>
<!-- Version and tool selection controls -->
<div class="flex flex-wrap items-center gap-2 mb-4">
<!-- Version selection buttons (populated by JavaScript) -->
<div id="version-buttons" class="flex flex-wrap items-center gap-2 p-1 bg-slate-100 dark:bg-gray-700/50 rounded-lg">
</div>
<!-- Tool selection (curl/wget) -->
<div class="flex items-center gap-2 p-1 bg-slate-100 dark:bg-gray-700/50 rounded-lg">
<label class="flex items-center px-2 cursor-pointer">
<input type="radio" name="tool" value="curl" checked class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<span class="ml-2 text-sm font-medium">curl</span>
</label>
<label class="flex items-center px-2 cursor-pointer">
<input type="radio" name="tool" value="wget" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<span class="ml-2 text-sm font-medium">wget</span>
</label>
</div>
</div>
<!-- Command display area with copy button -->
<div id="command-container" class="relative bg-slate-900 dark:bg-black/50 p-4 text-sm">
<button id="copy-btn" class="absolute top-3 right-3 p-1.5 bg-slate-700 hover:bg-slate-600 rounded-md text-slate-300 hover:text-white transition-colors">
<span class="sr-only">Copy command</span>
<span class="iconify text-lg copy-icon" data-icon="ph:copy-bold"></span>
<span class="iconify text-lg check-icon hidden" data-icon="ph:check-bold"></span>
</button>
<div id="command"></div>
</div>
</div>
</div>
<!-- Download sections container (populated by JavaScript) -->
<div id="download-rows" class="space-y-8 content-card" style="transition-delay: 400ms;">
<!-- Loading overlay shown while fetching version data -->
<div class="loading-overlay fixed inset-0 bg-slate-100/80 dark:bg-gray-900/80 flex flex-col items-center justify-center z-50" id="loading">
<span class="iconify text-4xl text-blue-600 animate-spin" data-icon="ph:spinner-gap-bold"></span>
<span data-i18n="loading" class="mt-4 text-lg font-medium"></span>
</div>
</div>
</main>
<!-- Footer with banner-style social links -->
<footer class="mt-16 pt-8 border-t border-slate-200 dark:border-gray-700/50">
<div class="text-center">
<!-- Social links without container background -->
<div class="flex justify-center items-center gap-4">
<!-- Telegram link -->
<a href="https://t.me/mikrotikpatch" target="_blank"
class="group flex items-center gap-3 px-6 py-3 rounded-xl bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white shadow-md hover:shadow-lg transition-all duration-300 hover:scale-105">
<span class="iconify text-xl transition-transform duration-300 group-hover:rotate-12" data-icon="ph:telegram-logo-bold"></span>
<span class="font-semibold text-sm">Telegram</span>
</a>
<!-- GitHub link -->
<a href="https://github.com/elseif/MikroTikPatch" target="_blank"
class="group flex items-center gap-3 px-6 py-3 rounded-xl bg-gradient-to-r from-gray-800 to-gray-900 hover:from-gray-700 hover:to-gray-800 dark:from-slate-600 dark:to-slate-700 dark:hover:from-slate-500 dark:hover:to-slate-600 text-white shadow-md hover:shadow-lg transition-all duration-300 hover:scale-105">
<span class="iconify text-xl transition-transform duration-300 group-hover:rotate-12" data-icon="ph:github-logo-bold"></span>
<span class="font-semibold text-sm">GitHub</span>
</a>
<!-- Telegram Keyget Bot link -->
<a href="https://t.me/ROS_Keygen_Bot" target="_blank"
class="group flex items-center gap-3 px-6 py-3 rounded-xl bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white shadow-md hover:shadow-lg transition-all duration-300 hover:scale-105">
<span class="iconify text-xl transition-transform duration-300 group-hover:rotate-12" data-icon="ph:telegram-logo-bold"></span>
<span class="font-semibold text-sm">Keygen Bot</span>
</a>
</div>
</div>
</footer>
</div>
<!-- URLs modal for cache clearing -->
<div id="urls-pannel" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden" tabindex="-1">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col transform transition-all opacity-0 scale-95" id="modal-content">
<header class="p-4 border-b border-slate-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-slate-800 dark:text-white" data-i18n="copyUrlsClearCache"></h3>
<button id="urls-close" class="p-1 rounded-full text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-700">
<span class="sr-only">Close modal</span>
<span class="iconify text-2xl" data-icon="ph:x-bold"></span>
</button>
</header>
<!-- URLs list display area -->
<div id="urls-list" class="p-6 text-sm text-slate-600 dark:text-slate-400 overflow-y-auto custom-scrollbar flex-grow bg-slate-50 dark:bg-gray-900/50 rounded-b-xl whitespace-pre-wrap break-all"></div>
<footer class="p-4 border-t border-slate-200 dark:border-gray-700">
<button id="copy-to-clear-cache" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-offset-white dark:focus:ring-offset-gray-800 flex items-center justify-center gap-2" data-i18n="copyUrlsClearCache" href="#"></button>
</footer>
</div>
</div>
<!-- Changelog popup modal -->
<div id="changelog-modal" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden" tabindex="-1">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col transform transition-all opacity-0 scale-95" id="changelog-modal-content">
<header class="p-4 border-b border-slate-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-slate-800 dark:text-white flex items-center gap-2">
<span class="iconify text-blue-500" data-icon="ph:newspaper-clipping-bold"></span>
<span data-i18n="changelog"></span>
</h3>
<button id="changelog-close" class="p-1 rounded-full text-slate-400 hover:bg-slate-100 dark:hover:bg-gray-700">
<span class="sr-only">Close modal</span>
<span class="iconify text-2xl" data-icon="ph:x-bold"></span>
</button>
</header>
<!-- Changelog content display area -->
<div id="changelog-content" class="p-6 text-sm text-slate-600 dark:text-slate-400 overflow-y-auto custom-scrollbar flex-grow bg-slate-50 dark:bg-gray-900/50 rounded-b-xl">
<!-- Loading state for changelog -->
<div class="flex items-center justify-center py-8">
<span class="iconify text-4xl text-blue-600 animate-spin" data-icon="ph:spinner-gap-bold"></span>
<span class="ml-4 text-lg font-medium" data-i18n="loadingChangelog"></span>
</div>
</div>
</div>
</div>
<!-- Main JavaScript functionality -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
// Icon definitions for consistent icon usage
const ICONS = {
download: 'ph:download-simple-bold',
changelog: 'ph:newspaper-clipping-bold',
trash: 'ph:trash-bold',
copyAndOpen: 'ph:copy-simple-bold'
};
// Function to fetch latest version numbers with timeout fallback
async function fetch_latest_versions(url, defaultValue, timeout = 3000) {
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const res = await fetch(url, { signal: controller.signal });
clearTimeout(id);
if (res.ok) {
const text = await res.text();
return text.split(" ")[0]; // Extract version number from response
}
} catch (e) {
console.warn("fetch failed or timeout:", e);
}
return defaultValue; // Return fallback version if fetch fails
}
// Internationalization (i18n) translations for English and Chinese
const i18n = {
en: {
language: "Language",
infoLabel1:`If you are already running <b>patched</b> RouterOS, upgrading to the latest version can be done by clicking on <b>"Check For Updates"</b> in <b>QuickSet or System > Packages</b> menu in WebFig or WinBox.`,
infoLabel2:`For more information about <b>install and upgrade</b> see the <a href="https://help.mikrotik.com/docs/" target="_blank" class="text-blue-500 dark:text-blue-400 hover:underline">Documentation</a>. For more information about <b>patch</b> see the <a href="https://github.com/elseif/MikroTikPatch" target="_blank" class="text-blue-500 dark:text-blue-400 hover:underline">MikroTikPatch</a>.`,
installCmdLabel:"Install Command",
proxyLabel:"Enable GitHub Proxy Accelerator",
clearCache:"Clear gh-proxy Cache",
copyUrlsClearCache:"Copy URLs & Open Clear Cache Page",
loading:"Loading latest versions...",
quickInformation:"Quick Information",
settings:"Settings",
theme:"Theme",
changelog:"Changelog",
loadingChangelog:"Loading changelog...",
changelogError:"Failed to load changelog. Please try again later.",
},
zh: {
language: "语言",
infoLabel1:`如果你已经在运行<b>Patch</b>过的RouterOS可以通过WebFig或WinBox中的<b>QuickSet或System > Packages</b>菜单点击<b>"Check For Updates"</b>来升级到最新版本。`,
infoLabel2:`有关<b>安装和升级</b>的更多信息,请参阅<a href="https://help.mikrotik.com/docs/" target="_blank" class="text-blue-500 dark:text-blue-400 hover:underline">文档</a><br>有关<b>Patch</b>的更多信息,请参阅<a href="https://github.com/elseif/MikroTikPatch" target="_blank" class="text-blue-500 dark:text-blue-400 hover:underline">MikroTikPatch</a>`,
installCmdLabel:"安装命令",
proxyLabel:"启用GitHub代理加速",
clearCache:"清除gh-proxy缓存",
copyUrlsClearCache:"复制链接并打开缓存清除页面",
loading:"正在加载最新版本...",
quickInformation:"快速信息",
settings:"设置",
theme:"主题",
changelog:"更新日志",
loadingChangelog:"正在加载更新日志...",
changelogError:"加载更新日志失败,请稍后重试。",
}
};
// Function to set language and update all i18n elements
function setLanguage(lang) {
document.querySelectorAll("[data-i18n]").forEach(el => {
const key = el.getAttribute("data-i18n");
if (i18n[lang] && i18n[lang][key]) {
// Special handling for buttons that need icons
if (key === 'clearCache') {
el.innerHTML = `<span class="iconify" data-icon="${ICONS.trash}"></span> ${i18n[lang][key]}`;
} else if (key === 'copyUrlsClearCache' && el.id === 'copy-to-clear-cache') {
el.innerHTML = `<span class="iconify" data-icon="${ICONS.copyAndOpen}"></span> ${i18n[lang][key]}`;
} else if (key === 'installCmdLabel') {
el.innerHTML = `<span class="iconify mr-2 text-blue-500" data-icon="ph:terminal-window-bold"></span>${i18n[lang][key]}`;
}
else {
el.innerHTML = i18n[lang][key];
}
}
});
localStorage.setItem("lang", lang); // Save language preference
}
// Initialize language based on saved preference or browser language
function initLanguage() {
let savedLang = localStorage.getItem("lang");
if (!savedLang) {
// Auto-detect Chinese language, default to English
savedLang = navigator.language.startsWith("zh") ? "zh" : "en";
}
document.getElementById("lang-switcher").value = savedLang;
setLanguage(savedLang);
}
// Language switcher event listener
document.getElementById("lang-switcher").addEventListener("change", e => {
setLanguage(e.target.value);
});
// Theme toggle functionality
const themeBtn = document.getElementById("theme-toggle-btn");
const htmlEl = document.documentElement;
const sunIcon = themeBtn.querySelector('.sun-icon');
const moonIcon = themeBtn.querySelector('.moon-icon');
// Update theme UI icons based on current theme
function updateThemeUI() {
if (htmlEl.classList.contains('dark')) {
sunIcon.classList.remove('hidden');
moonIcon.classList.add('hidden');
} else {
sunIcon.classList.add('hidden');
moonIcon.classList.remove('hidden');
}
}
// Initialize theme based on saved preference or system preference
function initializeTheme() {
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
htmlEl.classList.toggle("dark", savedTheme === "dark");
} else {
// Use system preference if no saved theme
htmlEl.classList.toggle("dark", window.matchMedia('(prefers-color-scheme: dark)').matches);
}
updateThemeUI();
}
// Theme toggle button event listener
themeBtn.addEventListener("click", () => {
htmlEl.classList.toggle("dark");
localStorage.setItem("theme", htmlEl.classList.contains("dark") ? "dark" : "light");
updateThemeUI();
});
// Initialize theme and language
initializeTheme();
initLanguage();
// Fetch latest version numbers from MikroTik servers
const loadingOverlay = document.getElementById("loading");
const [routeros7_stable, routeros7_testing, routeros6_longterm, routeros6_stable] = await Promise.all([
fetch_latest_versions(`https://upgrade.mikrotik.ltd/routeros/NEWESTa7.stable?ts=${Date.now()}`, "7.19.4"),
fetch_latest_versions(`https://upgrade.mikrotik.ltd/routeros/NEWESTa7.testing?ts=${Date.now()}`, "7.20rc1"),
fetch_latest_versions(`https://upgrade.mikrotik.ltd/routeros/NEWEST6.long-term?ts=${Date.now()}`, "6.49.18"),
fetch_latest_versions(`https://upgrade.mikrotik.ltd/routeros/NEWEST6.stable?ts=${Date.now()}`, "6.49.19"),
]);
// Hide loading overlay and remove loading class
loadingOverlay.style.display = "none";
document.body.classList.remove('loading');
// Download sections configuration with versions and file types
const downloads =[
{
title:"RouterOS v7",
versions:[ { title:"Stable", version:routeros7_stable }, { title:"Testing", version:routeros7_testing } ],
groups: [
{ title:"ARM64 / AMPERE", arch:"arm64", downloads:[ { title:"Main package", type:"main" }, { title:"Extra packages", type:"extra" }, { title:"ISO image for AMPERE", type:"iso" } ] },
{ title:"X86", arch:"x86", downloads:[ { title:"Main package", type:"main" }, { title:"Extra packages", type:"extra" }, { title:"CD Image", type:"iso" }, { title:"Install image", type:"install" } ] },
{ title:"GENERAL", arch:"general", downloads:[ { title:"Netinstall (Windows)", type:"netinstall_windows" }, { title:"Netinstall (CLI Linux)", type:"netinstall_linux_cli" }, { title:"Changelog", type:"changlog" } ] }
]
},
{
title:"RouterOS v6",
versions:[ { title:"Long-Term", version:routeros6_longterm }, { title:"Stable", version:routeros6_stable } ],
groups: [
{ title:"X86", arch:"x86", downloads:[ { title:"Main package", type:"main" },{ title:"Extra packages", type:"extra" }, { title:"CD Image", type:"iso" }, { title:"Install image", type:"install" }, { title:"Changelog", type:"changlog" } ] }
]
},
{
title:"Cloud Hosted Router",
versions:[ { title:"Long-Term", version:routeros6_longterm }, { title:"Stable", version:routeros6_stable }, { title:"Stable", version:routeros7_stable }, { title:"Testing", version:routeros7_testing } ],
groups:[
{ title:"X86", arch:"x86", downloads:[ { title:"Main package", type:"main" }, { title:"Extra packages", type:"extra" }, { title:"VHDX image", type:"vhdx" }, { title:"VMDK image", type:"vmdk" }, { title:"VDI image", type:"vdi" }, { title:"VirtualPC image", type:"vhd" }, { title:"Raw disk image", type:"img" }, { title:"OVA template", type:"ova" } ] },
{ title:"ARM64 / AMPERE", arch:"arm64", downloads:[ { title:"Main package", type:"main" }, { title:"Extra packages", type:"extra" }, { title:"VHDX image", type:"vhdx" }, { title:"VMDK image", type:"vmdk" }, { title:"VDI image", type:"vdi" }, { title:"VirtualPC image", type:"vhd" }, { title:"Raw disk image", type:"img" } ] }
]
},
];
// Function to generate file names based on version, architecture, and type
const fileNames = (version, arch, type) => {
const files = [];
// RouterOS v6 only supports x86 architecture
if (version.charAt(0) === "6" && arch !== "x86") return files;
// Main package for RouterOS v7
if (type === "main" && version.charAt(0) === "7") files.push(`routeros-${version}${arch === "x86" ? "" : "-" + arch}.npk`);
// Main package for RouterOS v6
if (type === "main" && version.charAt(0) === "6") files.push(`routeros-x86-${version}.npk`);
// ISO images
if (type === "iso") files.push(`mikrotik-${version}${arch === "x86" ? "" : "-" + arch}.iso`);
// Extra packages
if (type === "extra") files.push(`all_packages-${arch}-${version}.zip`);
// Install images
if (type === "install") files.push(`install-image-${version}${arch === "x86" ? "" : "-" + arch}.zip`);
// Virtual machine images
if (type in { vhdx: 1, vmdk: 1, vdi: 1, vhd: 1, img: 1 }) {
files.push(`chr-${version}${arch === "x86" ? "" : "-" + arch}.${type}.zip`);
// Add legacy BIOS version for x86 RouterOS v7
if (arch === "x86" && version.charAt(0) !== "6") files.push(`chr-${version}-legacy-bios.${type}.zip`);
}
// OVA templates (RouterOS v7 only)
if (type === "ova" && version.charAt(0) !== "6") {
files.push(`chr-${version}${arch === "x86" ? "" : "-" + arch}.${type}.zip`);
if (arch === "x86") files.push(`chr-${version}-legacy-bios.${type}.zip`);
}
// Netinstall packages
if (type === "netinstall_windows") files.push(`netinstall-${version}.zip`);
if (type === "netinstall_linux_cli") files.push(`netinstall-${version}.tar.gz`);
return files;
};
// Generate base URL for downloads with optional GitHub proxy
const baseUrl = (version, arch) => `${localStorage.getItem("gh-proxy-enabled") === "true" ? "https://gh-proxy.com/" : ""}https://github.com/elseif/MikroTikPatch/releases/download/${version}${arch === "x86" || arch === "general" ? "" : "-" + arch}/`;
// Clear and populate download sections
const container = document.getElementById("download-rows");
container.innerHTML = '';
// Generate download sections dynamically
downloads.forEach((download, index) => {
const accordion = document.createElement("details");
accordion.className = "bg-white/60 dark:bg-gray-800/60 rounded-2xl shadow-lg ring-1 ring-black/5 backdrop-blur-xl overflow-hidden group";
accordion.open = index === 0; // Open first section by default
const summary = document.createElement("summary");
summary.className = "p-6 cursor-pointer text-xl font-bold text-slate-800 dark:text-white flex justify-between items-center";
summary.innerHTML = `<span>${download.title}</span><span class="iconify text-2xl transition-transform duration-300 group-open:rotate-180" data-icon="ph:caret-down-bold"></span>`;
const content = document.createElement("div");
content.className = "px-6 pb-6";
const table = document.createElement("div");
table.className = "overflow-x-auto";
table.innerHTML = `
<table class="w-full min-w-max text-sm text-left text-slate-500 dark:text-slate-400">
<thead class="text-xs text-slate-700 dark:text-slate-200 uppercase bg-slate-50/80 dark:bg-gray-700/80">
<tr>
<th scope="col" class="px-6 py-3 rounded-l-lg">Package / Architecture</th>
${download.versions.map(v => `<th scope="col" class="px-6 py-3 text-center">${v.version}<br><span class="font-normal normal-case">${v.title}</span></th>`).join("")}
<th class="rounded-r-lg"></th>
</tr>
</thead>
<tbody>
${download.groups.map(g => `
<tr class="bg-slate-100/80 dark:bg-gray-700/50">
<th colspan="${download.versions.length + 2}" class="px-6 py-2 text-base font-semibold text-slate-800 dark:text-white">${g.title}</th>
</tr>
${g.downloads.map(d => `
<tr class="bg-transparent border-b last:border-b-0 border-slate-200 dark:border-gray-700 hover:bg-slate-50/50 dark:hover:bg-gray-800/50 transition-colors">
<td class="px-6 py-4 font-medium text-slate-900 dark:text-white whitespace-nowrap">${d.title}</td>
${download.versions.map(v => {
if (d.type === "changlog") {
return `<td class="px-6 py-4 text-center"><button onclick="showChangelog('${v.version}')" class="inline-flex items-center gap-1.5 text-blue-500 dark:text-blue-400 hover:underline cursor-pointer" title="Changelog"><span class="iconify" data-icon="${ICONS.changelog}"></span> View</button></td>`;
}
const files = fileNames(v.version, g.arch, d.type);
if (!files || files.length === 0) return `<td class="px-6 py-4"></td>`;
const links = files.map(file => {
const url = `${baseUrl(v.version, g.arch)}${file}`;
let label = "Download";
if (download.title === "Cloud Hosted Router" && file.includes('chr')) {
if (v.version.charAt(0) === "6" ){
label = "Legacy";
} else {
label = file.includes("legacy-bios") ? "Legacy" : "UEFI";
}
}
return `<a href="${url}" class="inline-flex items-center gap-1.5 text-blue-500 dark:text-blue-400 hover:underline" title="${file}" target="_blank"><span class="iconify" data-icon="${ICONS.download}"></span> ${label}</a>`;
}).join("");
return `<td class="px-6 py-4 text-center space-x-2">${links}</td>`;
}).join("")}
<td></td>
</tr>
`).join("")}
`).join("")}
</tbody>
</table>
`;
content.appendChild(table);
accordion.appendChild(summary);
accordion.appendChild(content);
container.appendChild(accordion);
});
// GitHub proxy functionality
const ghProxyCheckbox = document.getElementById("gh-proxy");
const clearCacheBtn = document.getElementById("clear-cache");
if (localStorage.getItem("gh-proxy-enabled") === null && navigator.language.startsWith("zh")) {
// Enable GitHub proxy by default for Chinese users
localStorage.setItem("gh-proxy-enabled", "true");
}
ghProxyCheckbox.checked = localStorage.getItem("gh-proxy-enabled") === "true";
clearCacheBtn.style.display = ghProxyCheckbox.checked ? "inline-flex" : "none";
// Update all download links with/without GitHub proxy
function updateAllDownloadLinks() {
// Only apply proxy to download links (releases/download), not to other GitHub links
document.querySelectorAll('a[href*="github.com/elseif/MikroTikPatch/releases/download"]').forEach(link => {
const isProxyEnabled = localStorage.getItem("gh-proxy-enabled") === "true";
const hasProxy = link.href.includes('gh-proxy.com');
if (isProxyEnabled && !hasProxy) {
link.href = link.href.replace("https://github.com/", "https://gh-proxy.com/https://github.com/");
} else if (!isProxyEnabled && hasProxy) {
link.href = link.href.replace("https://gh-proxy.com/", "");
}
});
// Update command display if a version is selected
const activeBtn = document.querySelector("#version-buttons button.active");
activeBtn ? updateCommand(activeBtn.dataset.version) : updateCommand();
}
// GitHub proxy toggle event listener
ghProxyCheckbox.addEventListener("change", () => {
localStorage.setItem("gh-proxy-enabled", ghProxyCheckbox.checked ? "true" : "false");
clearCacheBtn.style.display = ghProxyCheckbox.checked ? "inline-flex" : "none";
updateAllDownloadLinks();
});
// Cache clearing modal functionality
const modal = document.getElementById("urls-pannel");
const modalContent = document.getElementById("modal-content");
clearCacheBtn.addEventListener("click", () => {
const links = document.querySelectorAll('a[href^="https://gh-proxy.com/https://github.com/elseif/MikroTikPatch/"]');
document.getElementById("urls-list").innerText = Array.from(links).map(a => a.href).filter(href => !href.includes(".yml")).join("\n");
modal.classList.remove('hidden');
setTimeout(() => { modalContent.classList.add('opacity-100', 'scale-100'); modalContent.classList.remove('opacity-0', 'scale-95'); }, 10);
});
// Modal close functionality
document.getElementById("urls-close").addEventListener("click", () => {
modalContent.classList.remove('opacity-100', 'scale-100');
modalContent.classList.add('opacity-0', 'scale-95');
setTimeout(() => modal.classList.add('hidden'), 200);
});
// Copy URLs and open cache clearing page
document.getElementById("copy-to-clear-cache").addEventListener("click", () => {
navigator.clipboard.writeText(document.getElementById("urls-list").innerText);
window.open("https://cache.gh-proxy.com/", "_blank");
document.getElementById("urls-close").click();
});
// Changelog modal functionality
const changelogModal = document.getElementById("changelog-modal");
const changelogModalContent = document.getElementById("changelog-modal-content");
const changelogContent = document.getElementById("changelog-content");
const changelogClose = document.getElementById("changelog-close");
// Fetch changelog content from MikroTik servers
async function fetchChangelog(version) {
try {
const response = await fetch(`https://upgrade.mikrotik.ltd/routeros/${version}/CHANGELOG`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
return text;
} catch (error) {
console.error("Failed to fetch changelog:", error);
return null;
}
}
// Global function to show changelog modal
window.showChangelog = function(version) {
changelogModal.classList.remove('hidden');
changelogContent.innerHTML = `
<div class="flex items-center justify-center py-8">
<span class="iconify text-4xl text-blue-600 animate-spin" data-icon="ph:spinner-gap-bold"></span>
<span class="ml-4 text-lg font-medium" data-i18n="loadingChangelog"></span>
</div>
`;
setLanguage(document.getElementById("lang-switcher").value);
setTimeout(() => {
changelogModalContent.classList.add('opacity-100', 'scale-100');
changelogModalContent.classList.remove('opacity-0', 'scale-95');
}, 10);
// Fetch and display changelog content
fetchChangelog(version).then(content => {
if (content) {
changelogContent.innerHTML = `
<div class="prose prose-slate dark:prose-invert max-w-none">
<pre class="whitespace-pre-wrap text-sm leading-relaxed">${content}</pre>
</div>
`;
} else {
// Show error state with retry button
changelogContent.innerHTML = `
<div class="flex flex-col items-center justify-center py-8 text-center">
<span class="iconify text-6xl text-red-500 mb-4" data-icon="ph:warning-circle-bold"></span>
<p class="text-lg font-medium text-slate-700 dark:text-slate-300 mb-2" data-i18n="changelogError"></p>
<button onclick="showChangelog('${version}')" class="mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
<span class="iconify mr-2" data-icon="ph:arrow-clockwise-bold"></span>
Retry
</button>
</div>
`;
setLanguage(document.getElementById("lang-switcher").value);
}
});
};
// Hide changelog modal with animation
function hideChangelog() {
changelogModalContent.classList.remove('opacity-100', 'scale-100');
changelogModalContent.classList.add('opacity-0', 'scale-95');
setTimeout(() => changelogModal.classList.add('hidden'), 200);
}
changelogClose.addEventListener("click", hideChangelog);
// Close modal when clicking outside
changelogModal.addEventListener("click", (e) => {
if (e.target === changelogModal) {
hideChangelog();
}
});
// Close modal with Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !changelogModal.classList.contains("hidden")) {
hideChangelog();
}
});
// Command generation and version selection
const versionBtnsContainer = document.getElementById("version-buttons");
const commandDiv = document.getElementById("command");
const copyBtn = document.getElementById("copy-btn");
// Generate version selection buttons
versionBtnsContainer.innerHTML = `
<button data-version="${routeros7_stable}" class="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-blue-200 dark:hover:bg-gray-600 transition-all">v7 Stable</button>
<button data-version="${routeros7_testing}" class="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-blue-200 dark:hover:bg-gray-600 transition-all">v7 Testing</button>
<button data-version="${routeros6_longterm}" class="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-blue-200 dark:hover:bg-gray-600 transition-all">v6 Long-Term</button>
<button data-version="${routeros6_stable}" class="px-3 py-1.5 text-sm font-medium rounded-md hover:bg-blue-200 dark:hover:bg-gray-600 transition-all">v6 Stable</button>
`;
// Update command display based on selected tool and version
function updateCommand(version) {
const tool = document.querySelector('input[name="tool"]:checked').value;
const _version = version===undefined?"": `<span class="text-green-500 font-semibold">VERSION</span>=<span class="text-yellow-400">${version}</span> `
const cmd = tool === "curl"
? `<pre class="text-slate-300"><code>${_version}<span class="text-blue-400 font-bold">bash</span> <span class="text-pink-400">&lt;(</span><span class="text-blue-400 font-bold">curl</span> <span class="text-teal-400">https://mikrotik.ltd/chr.sh</span><span class="text-pink-400">)</span></code></pre>`
: `<pre class="text-slate-300"><code>${_version}<span class="text-blue-400 font-bold">bash</span> <span class="text-pink-400">&lt;(</span><span class="text-blue-400 font-bold">wget -O - </span><span class="text-teal-400">https://mikrotik.ltd/chr.sh</span><span class="text-pink-400">)</span></code></pre>`;
commandDiv.innerHTML = cmd;
}
// Tool selection event listeners
document.querySelectorAll('input[name="tool"]').forEach(radio => {
radio.addEventListener("change", () => {
const activeBtn = document.querySelector("#version-buttons button.active");
activeBtn ? updateCommand(activeBtn.dataset.version) : updateCommand();
});
});
// Version button event listeners
const buttons = document.querySelectorAll("#version-buttons button");
buttons.forEach(btn => {
btn.addEventListener("click", (e) => {
e.preventDefault();
if (btn.classList.contains("active")) {
updateCommand();
btn.classList.remove("active");
}else{
updateCommand(btn.dataset.version);
buttons.forEach(b => b.classList.remove("active"));
btn.classList.add("active");
}
});
});
updateCommand();
// Copy command to clipboard functionality
copyBtn.addEventListener("click", () => {
const code = commandDiv.querySelector("code").innerText;
navigator.clipboard.writeText(code).then(() => {
copyBtn.querySelector('.copy-icon').classList.add('hidden');
copyBtn.querySelector('.check-icon').classList.remove('hidden');
setTimeout(() => {
copyBtn.querySelector('.copy-icon').classList.remove('hidden');
copyBtn.querySelector('.check-icon').classList.add('hidden');
}, 2000);
}).catch(err => console.error("Copy failed:", err));
});
});
</script>
</body>
</html>

BIN
keygen/keygen_arm Normal file

Binary file not shown.

Binary file not shown.

194
npk.py
View File

@ -1,6 +1,6 @@
import struct,zlib import struct,zlib
import argparse,os,tempfile import argparse,os
from datetime import datetime from datetime import datetime
from dataclasses import dataclass from dataclasses import dataclass
from enum import IntEnum from enum import IntEnum
@ -51,45 +51,7 @@ class NpkInfo:
def name(self,value:str): def name(self,value:str):
self._name = value[:16].encode().ljust(16,b'\x00') self._name = value[:16].encode().ljust(16,b'\x00')
@staticmethod @staticmethod
def encode_version(version: str) -> bytes: def decode_version(value:bytes):
numbers = []
build = None
i = 0
n = len(version)
while i < n:
if version[i].isdigit():
num_str = ''
while i < n and version[i].isdigit():
num_str += version[i]
i += 1
numbers.append(int(num_str))
elif version[i] == '.':
i += 1
elif version[i].isalpha():
build = ''
while i < n and version[i].isalpha():
build += version[i]
i += 1
else:
i += 1
major = numbers[0] if len(numbers) > 0 else 0
minor = numbers[1] if len(numbers) > 1 else 0
revision = numbers[2] if len(numbers) > 2 else 0
if build == 'alpha':
build = 97
elif build == 'beta':
build = 98
elif build == 'rc':
build = 99
elif build == 'test':
build = 102
revision |= 0x80
else:# 'final'
build = 102
revision &= 0x7f
return struct.pack('4B',revision,build,minor,major)
@staticmethod
def decode_version(value:bytes)->str:
revision,build,minor,major = struct.unpack_from('4B',value) revision,build,minor,major = struct.unpack_from('4B',value)
if build == 97: if build == 97:
build = 'alpha' build = 'alpha'
@ -102,10 +64,31 @@ class NpkInfo:
build = 'test' build = 'test'
revision &= 0x7f revision &= 0x7f
else: else:
build = '' build = 'final'
else: else:
build = 'unknown' build = 'unknown'
return f'{major}.{minor}{build + str(revision) if build !="" else "." + str(revision) if revision != 0 else ""}' return f'{major}.{minor}.{revision}.{build}'
@staticmethod
def encode_version(value:str):
s = value.split('.')
if 4 != len(s) and s[3] in [ 'alpha', 'beta', 'rc','final', 'test']:
raise ValueError('Invalid version string')
major = int(s[0])
minor = int(s[1])
revision = int(s[2])
if s[3] == 'alpha':
build = 97
elif s[3] == 'beta':
build = 98
elif s[3] == 'rc':
build = 99
elif s[3] == 'final':
build = 102
revision &= 0x7f
else: #'test'
build = 102
revision |= 0x80
return struct.pack('4B',revision,build,minor,major)
@property @property
def version(self)->str: def version(self)->str:
return self.decode_version(self._version) return self.decode_version(self._version)
@ -154,7 +137,7 @@ class NpkFileContainer:
self._items= items self._items= items
def serialize(self)->bytes: def serialize(self)->bytes:
compressed_data = b'' compressed_data = b''
compressor = zlib.compressobj(level=0) compressor = zlib.compressobj()
for item in self._items: for item in self._items:
data = struct.pack(self._format, item.perm,item.type,item.usr_or_grp, item.modify_time,item.revision,item.rc,item.minor,item.major,item.create_time,item.unknow,len(item.data),len(item.name)) data = struct.pack(self._format, item.perm,item.type,item.usr_or_grp, item.modify_time,item.revision,item.rc,item.minor,item.major,item.create_time,item.unknow,len(item.data),len(item.name))
data += item.name + item.data data += item.name + item.data
@ -224,33 +207,29 @@ class NovaPackage(Package):
self._parts.append(NpkPartItem(NpkPartID(part_id),NpkInfo.unserialize_from(part_data))) self._parts.append(NpkPartItem(NpkPartID(part_id),NpkInfo.unserialize_from(part_data)))
else: else:
self._parts.append(NpkPartItem(NpkPartID(part_id),part_data)) self._parts.append(NpkPartItem(NpkPartID(part_id),part_data))
def set_null_block(self): def set_null_block(self):
def rebuild_squashfs(data): def rebuild_squashfs(data):
with tempfile.TemporaryDirectory() as tmp_dir: with open('squashfs.sfs', 'wb') as f:
squashfs_file = os.path.join(tmp_dir, 'squashfs.sfs') f.write(data)
squashfs_root = os.path.join(tmp_dir, 'squashfs-root') os.system('unsquashfs -d squashfs-root squashfs.sfs')
with open(squashfs_file, 'wb') as f: os.system('rm squashfs.sfs')
f.write(data) os.system('mksquashfs squashfs-root squashfs.sfs -comp xz -no-xattrs -b 256k')
os.system(f'unsquashfs -d {squashfs_root} {squashfs_file}') os.system('rm -rf squashfs-root')
os.remove(squashfs_file) with open('squashfs.sfs', 'rb') as f:
os.system(f'mksquashfs {squashfs_root} {squashfs_file} -no-recovery -noappend -exit-on-error -quiet -comp xz -no-xattrs -b 256k -all-root') return f.read()
with open(squashfs_file, 'rb') as f:
return f.read() if len(self._packages) > 0:
def get_size(package,size):
for part in package._parts:
size += 6
size += len(part.data)
return size
def set_null(package,offset):
has_squashfs = False has_squashfs = False
for part in package._parts: for package in self._packages:
if part.id == NpkPartID.SQUASHFS and len(part.data) >= 4 and part.data[:4] in [b'hsqs',b'sqsh']: for part in package._parts:
part.data = rebuild_squashfs(part.data) if part.id == NpkPartID.SQUASHFS:
has_squashfs = True part.data = rebuild_squashfs(part.data)
if has_squashfs: has_squashfs = True
count = offset
if not has_squashfs:
return
for package in self._packages:
count = 8
for part in package._parts: for part in package._parts:
count += 6 count += 6
if part.id == NpkPartID.NULL_BLOCK: if part.id == NpkPartID.NULL_BLOCK:
@ -260,12 +239,23 @@ class NovaPackage(Package):
pad_len = (4096 - (count % 4096)) % 4096 pad_len = (4096 - (count % 4096)) % 4096
package[NpkPartID.NULL_BLOCK].data = b'\x00' * pad_len package[NpkPartID.NULL_BLOCK].data = b'\x00' * pad_len
set_null(self,8) else:
offset = get_size(self,8) has_squashfs = False
for package in self._packages: for part in self._parts:
set_null(package,offset) if part.id == NpkPartID.SQUASHFS:
offset += get_size(package,0) part.data = rebuild_squashfs(part.data)
has_squashfs = True
if not has_squashfs:
return
count = 8
for part in self._parts:
count += 6
if part.id == NpkPartID.NULL_BLOCK:
break
count += len(part.data)
count += 6
pad_len = (4096 - (count % 4096)) % 4096
self[NpkPartID.NULL_BLOCK].data = b'\x00' * pad_len
def get_digest(self,hash_fnc,package:Package=None)->bytes: def get_digest(self,hash_fnc,package:Package=None)->bytes:
parts = package._parts if package else self._parts parts = package._parts if package else self._parts
for part in parts: for part in parts:
@ -291,28 +281,14 @@ class NovaPackage(Package):
if len(self._packages) > 0: if len(self._packages) > 0:
if build_time: if build_time:
self[NpkPartID.PKG_INFO].data._build_time = int(build_time) self[NpkPartID.PKG_INFO].data._build_time = int(build_time)
else:
build_time = self[NpkPartID.PKG_INFO].data._build_time
os.environ['BUILD_TIME'] = str(build_time)
github_env = os.getenv('GITHUB_ENV')
if github_env:
with open(github_env, 'a') as f:
f.write(f"BUILD_TIME={build_time}\n")
for package in self._packages: for package in self._packages:
if len(package[NpkPartID.SIGNATURE].data) != 20+48+64: if len(package[NpkPartID.SIGNATURE].data) != 20+48+64:
package[NpkPartID.SIGNATURE].data = b'\0'*(20+48+64) package[NpkPartID.SIGNATURE].data = b'\0'*(20+48+64)
if build_time: if build_time:
package[NpkPartID.NAME_INFO].data._build_time = int(build_time) package[NpkPartID.NAME_INFO].data._build_time = int(build_time)
else:
build_time = self[NpkPartID.NAME_INFO].data._build_time
os.environ['BUILD_TIME'] = str(build_time)
github_env = os.getenv('GITHUB_ENV')
if github_env:
with open(github_env, 'a') as f:
f.write(f"BUILD_TIME={build_time}\n")
sha1_digest = self.get_digest(hashlib.new('SHA1'),package) sha1_digest = self.get_digest(hashlib.new('SHA1'),package)
sha256_digest = self.get_digest(hashlib.new('SHA256'),package) sha256_digest = self.get_digest(hashlib.new('SHA256'),package)
kcdsa_signature = mikro_kcdsa_sign(sha1_digest,kcdsa_private_key) kcdsa_signature = mikro_kcdsa_sign(sha256_digest[:20],kcdsa_private_key)
eddsa_signature = mikro_eddsa_sign(sha256_digest,eddsa_private_key) eddsa_signature = mikro_eddsa_sign(sha256_digest,eddsa_private_key)
package[NpkPartID.SIGNATURE].data = sha1_digest + kcdsa_signature + eddsa_signature package[NpkPartID.SIGNATURE].data = sha1_digest + kcdsa_signature + eddsa_signature
else: else:
@ -320,16 +296,9 @@ class NovaPackage(Package):
self[NpkPartID.SIGNATURE].data = b'\0'*(20+48+64) self[NpkPartID.SIGNATURE].data = b'\0'*(20+48+64)
if build_time: if build_time:
self[NpkPartID.NAME_INFO].data._build_time = int(build_time) self[NpkPartID.NAME_INFO].data._build_time = int(build_time)
else:
build_time = self[NpkPartID.NAME_INFO].data._build_time
os.environ['BUILD_TIME'] = str(build_time)
github_env = os.getenv('GITHUB_ENV')
if github_env:
with open(github_env, 'a') as f:
f.write(f"BUILD_TIME={build_time}\n")
sha1_digest = self.get_digest(hashlib.new('SHA1')) sha1_digest = self.get_digest(hashlib.new('SHA1'))
sha256_digest = self.get_digest(hashlib.new('SHA256')) sha256_digest = self.get_digest(hashlib.new('SHA256'))
kcdsa_signature = mikro_kcdsa_sign(sha1_digest,kcdsa_private_key) kcdsa_signature = mikro_kcdsa_sign(sha256_digest[:20],kcdsa_private_key)
eddsa_signature = mikro_eddsa_sign(sha256_digest,eddsa_private_key) eddsa_signature = mikro_eddsa_sign(sha256_digest,eddsa_private_key)
self[NpkPartID.SIGNATURE].data = sha1_digest + kcdsa_signature + eddsa_signature self[NpkPartID.SIGNATURE].data = sha1_digest + kcdsa_signature + eddsa_signature
@ -343,9 +312,9 @@ class NovaPackage(Package):
signature = package[NpkPartID.SIGNATURE].data signature = package[NpkPartID.SIGNATURE].data
if sha1_digest != signature[:20]: if sha1_digest != signature[:20]:
return False return False
if not mikro_kcdsa_verify(sha1_digest,signature[20:68],kcdsa_public_key): if not mikro_kcdsa_verify(sha256_digest[:20],signature[20:68],kcdsa_public_key):
return False return False
if len(signature) == 132 and not mikro_eddsa_verify(sha256_digest,signature[68:132],eddsa_public_key): if not mikro_eddsa_verify(sha256_digest,signature[68:132],eddsa_public_key):
return False return False
else: else:
sha1_digest = self.get_digest(hashlib.new('SHA1')) sha1_digest = self.get_digest(hashlib.new('SHA1'))
@ -353,10 +322,11 @@ class NovaPackage(Package):
signature = self[NpkPartID.SIGNATURE].data signature = self[NpkPartID.SIGNATURE].data
if sha1_digest != signature[:20]: if sha1_digest != signature[:20]:
return False return False
if not mikro_kcdsa_verify(sha1_digest,signature[20:68],kcdsa_public_key): if not mikro_kcdsa_verify(sha256_digest[:20],signature[20:68],kcdsa_public_key):
return False return False
if len(signature) == 132 and not mikro_eddsa_verify(sha256_digest,signature[68:132],eddsa_public_key): if not mikro_eddsa_verify(sha256_digest,signature[68:132],eddsa_public_key):
return False return False
return True return True
def save(self,file): def save(self,file):
@ -404,10 +374,6 @@ if __name__=='__main__':
create_option_parser.add_argument('name',type=str,help='NPK name') create_option_parser.add_argument('name',type=str,help='NPK name')
create_option_parser.add_argument('squashfs',type=str,help='NPK squashfs file') create_option_parser.add_argument('squashfs',type=str,help='NPK squashfs file')
create_option_parser.add_argument('-desc','--description',type=str,help='NPK description') create_option_parser.add_argument('-desc','--description',type=str,help='NPK description')
extract_parser = subparsers.add_parser('extract',help='Extract npk file')
extract_parser.add_argument('input',type=str, help='Input file')
extract_parser.add_argument('name',type=str,help='File to extract')
extract_parser.add_argument('output',type=str,help='Output to file')
args = parser.parse_args() args = parser.parse_args()
kcdsa_private_key = bytes.fromhex(os.environ['CUSTOM_LICENSE_PRIVATE_KEY']) kcdsa_private_key = bytes.fromhex(os.environ['CUSTOM_LICENSE_PRIVATE_KEY'])
eddsa_private_key = bytes.fromhex(os.environ['CUSTOM_NPK_SIGN_PRIVATE_KEY']) eddsa_private_key = bytes.fromhex(os.environ['CUSTOM_NPK_SIGN_PRIVATE_KEY'])
@ -438,23 +404,5 @@ if __name__=='__main__':
option_npk.sign(kcdsa_private_key,eddsa_private_key) option_npk.sign(kcdsa_private_key,eddsa_private_key)
option_npk.save(args.output) option_npk.save(args.output)
print(f'Created {args.output}') print(f'Created {args.output}')
elif args.command =='extract':
print(f'Extracting {args.name} from {args.input}')
npk = NovaPackage.load(args.input)
file_container = None
if len(npk._packages) > 0:
for package in npk._packages:
if package[NpkPartID.NAME_INFO].data.name == 'system':
file_container = NpkFileContainer.unserialize_from(package[NpkPartID.FILE_CONTAINER].data)
else:
if npk[NpkPartID.NAME_INFO].data.name == 'system':
file_container = NpkFileContainer.unserialize_from(npk[NpkPartID.FILE_CONTAINER].data)
if file_container:
for item in file_container:
if item.name == args.name.encode():
with open(args.output,'wb') as f:
f.write(item.data)
print(f'Extracted {args.name} to {args.output}')
break
else: else:
parser.print_help() parser.print_help()

32
package.py Normal file
View File

@ -0,0 +1,32 @@
def install_package(package, version="upgrade", index_url='https://mirrors.aliyun.com/pypi/simple/'):
from sys import executable
from subprocess import check_call
result = False
try:
if version.lower() == "upgrade":
result = check_call([executable, "-m", "pip", "install", package, "--upgrade", "-i", index_url])
else:
from pkg_resources import get_distribution
current_package_version = None
try:
current_package_version = get_distribution(package)
except Exception:
pass
if current_package_version is None or current_package_version != version:
installation_sign = "==" if ">=" not in version else ""
result = check_call([executable, "-m", "pip", "install", package + installation_sign + version, "-i", index_url])
except Exception as e:
print(e)
result = -1
return result
def check_package(package):
from importlib import import_module
try:
import_module(package)
return True
except ImportError:
return False
def check_install_package(packages):
for package in packages:
if not check_package(package):
install_package(package)

246
patch.py
View File

@ -1,7 +1,5 @@
import subprocess,lzma import subprocess,lzma
import struct,os,re,tempfile import struct,os,re
import pefile
from elftools.elf.elffile import ELFFile
from npk import NovaPackage,NpkPartID,NpkFileContainer from npk import NovaPackage,NpkPartID,NpkFileContainer
def replace_chunks(old_chunks,new_chunks,data,name): def replace_chunks(old_chunks,new_chunks,data,name):
@ -106,12 +104,42 @@ def patch_bzimage(data:bytes,key_dict:dict):
new_data = new_data.replace(vmlinux_xz,new_vmlinux_xz) new_data = new_data.replace(vmlinux_xz,new_vmlinux_xz)
return new_data return new_data
def patch_block(dev:str,file:str,key_dict):
BLOCK_SIZE = 4096
#sudo debugfs /dev/nbd0p1 -R 'stats' | grep "Block size" | sed -n '1p' | cut -d ':' -f 2
#sudo debugfs /dev/nbd0p1 -R 'stat boot/initrd.rgz' 2> /dev/null | sed -n '11p'
stdout,_ = run_shell_command(f"debugfs {dev} -R 'stat {file}' 2> /dev/null | sed -n '11p' ")
#(0-11):1592-1603, (IND):1173, (12-15):1604-1607, (16-26):1424-1434
blocks_info = stdout.decode().strip().split(',')
print(f'blocks_info : {blocks_info}')
blocks = []
ind_block_id = None
for block_info in blocks_info:
_tmp = block_info.strip().split(':')
if _tmp[0].strip() == '(IND)':
ind_block_id = int(_tmp[1])
else:
print(f'block_info : {block_info}')
id_range = _tmp[0].strip().replace('(','').replace(')','').split('-')
block_range = _tmp[1].strip().replace('(','').replace(')','').split('-')
blocks += [id for id in range(int(block_range[0]),int(block_range[1])+1)]
print(f' blocks : {len(blocks)} ind_block_id : {ind_block_id}')
#sudo debugfs /dev/nbd0p1 -R 'cat boot/initrd.rgz' > data
data,stderr = run_shell_command(f"debugfs {dev} -R 'cat {file}' 2> /dev/null")
new_data = patch_kernel(data,key_dict)
print(f'write block {len(blocks)} : [',end="")
with open(dev,'wb') as f:
for index,block_id in enumerate(blocks):
print('#',end="")
f.seek(block_id*BLOCK_SIZE)
f.write(new_data[index*BLOCK_SIZE:(index+1)*BLOCK_SIZE])
f.flush()
print(']')
def patch_initrd_xz(initrd_xz:bytes,key_dict:dict,ljust=True): def patch_initrd_xz(initrd_xz:bytes,key_dict:dict,ljust=True):
try: initrd = lzma.decompress(initrd_xz)
initrd = lzma.decompress(initrd_xz)
except Exception as e:
print(f'size:{len(initrd_xz)},header:{initrd_xz[:20].hex().upper()},footer:{initrd_xz[-20:].hex().upper()}\n')
raise Exception(f'failed to decompress initrd_xz: {e}')
new_initrd = initrd new_initrd = initrd
for old_public_key,new_public_key in key_dict.items(): for old_public_key,new_public_key in key_dict.items():
new_initrd = replace_key(old_public_key,new_public_key,new_initrd,'initrd') new_initrd = replace_key(old_public_key,new_public_key,new_initrd,'initrd')
@ -171,58 +199,12 @@ def patch_pe(data: bytes,key_dict:dict):
new_data = data.replace(vmlinux_xz,new_vmlinux_xz) new_data = data.replace(vmlinux_xz,new_vmlinux_xz)
return new_data return new_data
def build_efi(input_file, output_file):
def find_xz_streams(data:bytes):
streams = []
XZ_HEADER_MAGIC = b'\xFD7zXZ\x00\x00\x01'
XZ_FOOTER_MAGIC = b'\x00\x00\x00\x00\x01\x59\x5A'
i = 0
while True:
start = data.find(XZ_HEADER_MAGIC, i)
if start == -1:
break
end = data.find(XZ_FOOTER_MAGIC, start)
assert end != -1, 'XZ footer not found'
end += len(XZ_FOOTER_MAGIC)
streams.append((start, end))
i = end
return streams
with open(input_file, 'rb') as f:
elf = ELFFile(f)
initrd_section =elf.get_section_by_name('initrd')
assert initrd_section is not None,'initrd section not found'
initrd_data = initrd_section.data()
xz_streams = find_xz_streams(initrd_data)
assert len(xz_streams) == 2,'only support 2 xz streams'
efi_xz = initrd_data[xz_streams[0][0]:xz_streams[0][1]]
cpio_xz = initrd_data[xz_streams[1][0]:xz_streams[1][1]]
try:
efi = lzma.decompress(efi_xz)
except Exception as e:
print(f'size:{len(efi_xz)},header:{efi_xz[:20].hex().upper()},footer:{efi_xz[-20:].hex().upper()}\n')
raise Exception(f'failed to decompress efi: {e}')
with pefile.PE(data = efi) as pe:
data_section =[section for section in pe.sections if section.Name == b'.data\x00\x00\x00'][0]
rva = data_section.VirtualAddress
addr = data_section.PointerToRawData
data = data_section.get_data()
size = len(data)
alignment = ((rva + size) + 4096 - 1) & ~(4096 - 1) #4096对齐
alignment = alignment - (rva+size)
new_data = data + b'\x00'*alignment
new_data += struct.pack('<I',len(cpio_xz))
new_data += cpio_xz
data_section.SizeOfRawData = len(new_data)
new_file_data = pe.write()
new_file_data = new_file_data[:addr]
new_file_data += new_data
with open(output_file, 'wb') as f:
f.write(new_file_data)
def patch_netinstall(key_dict: dict,input_file,output_file=None): def patch_netinstall(key_dict: dict,input_file,output_file=None):
netinstall = open(input_file,'rb').read() netinstall = open(input_file,'rb').read()
if netinstall[:2] == b'MZ': if netinstall[:2] == b'MZ':
from package import check_install_package
check_install_package(['pefile'])
import pefile
ROUTEROS_BOOT = { ROUTEROS_BOOT = {
129:{'arch':'power','name':'Powerboot'}, 129:{'arch':'power','name':'Powerboot'},
130:{'arch':'e500','name':'e500_boot'}, 130:{'arch':'e500','name':'e500_boot'},
@ -261,6 +243,7 @@ def patch_netinstall(key_dict: dict,input_file,output_file=None):
pe.set_bytes_at_rva(rva,new_data) pe.set_bytes_at_rva(rva,new_data)
pe.write(output_file or input_file) pe.write(output_file or input_file)
elif netinstall[:4] == b'\x7FELF': elif netinstall[:4] == b'\x7FELF':
import re
# 83 00 00 00 C4 68 C4 0B 5A C2 04 08 10 9E 52 00 # 83 00 00 00 C4 68 C4 0B 5A C2 04 08 10 9E 52 00
# 8A 00 00 00 C3 68 C4 0B 6A 60 57 08 C0 3D 54 00 # 8A 00 00 00 C3 68 C4 0B 6A 60 57 08 C0 3D 54 00
# 81 00 00 00 D3 68 C4 0B 2A 9E AB 08 5C 1B 78 00 # 81 00 00 00 D3 68 C4 0B 2A 9E AB 08 5C 1B 78 00
@ -321,12 +304,14 @@ def patch_kernel(data:bytes,key_dict):
return patch_elf(data,key_dict) return patch_elf(data,key_dict)
elif data[:5] == b'\xFD7zXZ': elif data[:5] == b'\xFD7zXZ':
print('patching initrd') print('patching initrd')
return patch_initrd_xz(data,key_dict,False) return patch_initrd_xz(data,key_dict)
else: else:
raise Exception('unknown kernel format') raise Exception('unknown kernel format')
def patch_loader(loader_file): def patch_loader(loader_file):
try: try:
from package import check_install_package
check_install_package(['pyelftools'])
from loader.patch_loader import patch_loader as do_patch_loader from loader.patch_loader import patch_loader as do_patch_loader
arch = os.getenv('ARCH') or 'x86' arch = os.getenv('ARCH') or 'x86'
arch = arch.replace('-', '') arch = arch.replace('-', '')
@ -336,82 +321,40 @@ def patch_loader(loader_file):
print("loader module import failed. cannot run patch_loader.py") print("loader module import failed. cannot run patch_loader.py")
def patch_squashfs(path,key_dict): def patch_squashfs(path,key_dict):
url_replacements = {
os.environ.get('MIKRO_LICENCE_URL', '').encode(): os.environ.get('CUSTOM_LICENCE_URL', '').encode(),
os.environ.get('MIKRO_UPGRADE_URL', '').encode(): os.environ.get('CUSTOM_UPGRADE_URL', '').encode(),
os.environ.get('MIKRO_CLOUD_URL', '').encode(): os.environ.get('CUSTOM_CLOUD_URL', '').encode(),
os.environ.get('MIKRO_CLOUD_PUBLIC_KEY', '').encode(): os.environ.get('CUSTOM_CLOUD_PUBLIC_KEY', '').encode(),
}
url_replacements = {k: v for k, v in url_replacements.items() if k and v}
renew_replacements = {
os.environ.get('MIKRO_RENEW_URL', '').encode(): os.environ.get('CUSTOM_RENEW_URL', '').encode(),
}
renew_replacements = {k: v for k, v in renew_replacements.items() if k and v}
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
if 'mode' in files and 'keyman' in files: for _file in files:
for file_path in [os.path.join(root, 'mode'),os.path.join(root, 'keyman')]: file = os.path.join(root,_file)
with open(file_path, 'rb') as f: if os.path.isfile(file):
data = f.read() if _file =='loader':
for old_url,new_url in url_replacements.items(): patch_loader(file)
if old_url in data: continue
print(f'{file_path} url patched {old_url.decode()[:7]}...') data = open(file,'rb').read()
data = data.replace(old_url,new_url)
modified = False
for old_public_key,new_public_key in key_dict.items(): for old_public_key,new_public_key in key_dict.items():
new_data = replace_key(old_public_key,new_public_key,data,file_path) _data = replace_key(old_public_key,new_public_key,data,file)
if new_data != data: if _data != data:
data = new_data open(file,'wb').write(_data)
modified = True url_dict = {
with open(f'{file_path}_', 'wb') as f: os.environ['MIKRO_LICENCE_URL'].encode():os.environ['CUSTOM_LICENCE_URL'].encode(),
f.write(data) os.environ['MIKRO_UPGRADE_URL'].encode():os.environ['CUSTOM_UPGRADE_URL'].encode(),
if 'loader' in files and os.path.isfile(os.path.join(root, 'loader')): os.environ['MIKRO_CLOUD_URL'].encode():os.environ['CUSTOM_CLOUD_URL'].encode(),
loader_file = os.path.join(root, 'loader') os.environ['MIKRO_CLOUD_PUBLIC_KEY'].encode():os.environ['CUSTOM_CLOUD_PUBLIC_KEY'].encode(),
patch_loader(loader_file) }
data = open(file,'rb').read()
if 'BOOTX64.EFI' in files and os.path.isfile(os.path.join(root, 'BOOTX64.EFI')): for old_url,new_url in url_dict.items():
efi_file = os.path.join(root, 'BOOTX64.EFI')
with open(efi_file, 'rb') as f:
data = f.read()
new_data = patch_kernel(data,key_dict)
assert new_data != data, f'{file_path} key not patched'
with open(efi_file, 'wb') as f:
f.write(new_data)
for filename in files:
if filename in ['mode','keyman','loader','BOOTX64.EFI']:
continue
file_path = os.path.join(root,filename)
if not os.path.isfile(file_path):
continue
modified = False
with open(file_path, 'rb') as f:
data = f.read()
for old_public_key,new_public_key in key_dict.items():
new_data = replace_key(old_public_key,new_public_key,data,file_path)
if new_data != data:
data = new_data
modified = True
for old_url,new_url in url_replacements.items():
if old_url in data:
print(f'{file_path} url patched {old_url.decode()[:7]}...')
data = data.replace(old_url,new_url)
modified = True
if filename == 'licupgr':
for old_url,new_url in renew_replacements.items():
if old_url in data: if old_url in data:
print(f'{file_path} url patched {old_url.decode()[:7]}...') print(f'{file} url patched {old_url.decode()[:7]}...')
data = data.replace(old_url,new_url) data = data.replace(old_url,new_url)
modified = True open(file,'wb').write(data)
if modified: if os.path.split(file)[1] == 'licupgr':
with open(file_path, 'wb') as f: url_dict = {
f.write(data) os.environ['MIKRO_RENEW_URL'].encode():os.environ['CUSTOM_RENEW_URL'].encode(),
}
for old_url,new_url in url_dict.items():
if old_url in data:
print(f'{file} url patched {old_url.decode()[:7]}...')
data = data.replace(old_url,new_url)
open(file,'wb').write(data)
def run_shell_command(command): def run_shell_command(command):
process = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) process = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -425,21 +368,22 @@ def patch_npk_package(package,key_dict):
print(f'patch {item.name} ...') print(f'patch {item.name} ...')
item.data = patch_kernel(item.data,key_dict) item.data = patch_kernel(item.data,key_dict)
package[NpkPartID.FILE_CONTAINER].data = file_container.serialize() package[NpkPartID.FILE_CONTAINER].data = file_container.serialize()
with tempfile.TemporaryDirectory() as tmp_dir: squashfs_file = 'squashfs-root.sfs'
squashfs_file = os.path.join(tmp_dir, 'squashfs-root.sfs') extract_dir = 'squashfs-root'
extract_dir = os.path.join(tmp_dir, 'squashfs-root') open(squashfs_file,'wb').write(package[NpkPartID.SQUASHFS].data)
with open(squashfs_file,'wb') as f: print(f"extract {squashfs_file} ...")
f.write(package[NpkPartID.SQUASHFS].data) run_shell_command(f"unsquashfs -d {extract_dir} {squashfs_file}")
print(f"extract {squashfs_file} ...") patch_squashfs(extract_dir,key_dict)
run_shell_command(f"unsquashfs -d {extract_dir} {squashfs_file}") logo = os.path.join(extract_dir,"nova/lib/console/logo.txt")
patch_squashfs(extract_dir,key_dict) run_shell_command(f"sudo sed -i '1d' {logo}")
logo = os.path.join(extract_dir,"nova/lib/console/logo.txt") run_shell_command(f"sudo sed -i '8s#.*# elseif@live.cn https://github.com/elseif/MikroTikPatch#' {logo}")
run_shell_command(f"sed -i '1d' {logo}") print(f"pack {extract_dir} ...")
run_shell_command(f"sed -i '8s#.*# elseif@live.cn https://github.com/elseif/MikroTikPatch#' {logo}") run_shell_command(f"rm -f {squashfs_file}")
print(f"pack {extract_dir} ...") run_shell_command(f"mksquashfs {extract_dir} {squashfs_file} -quiet -comp xz -no-xattrs -b 256k")
run_shell_command(f"mksquashfs {extract_dir} {squashfs_file} -no-recovery -noappend -exit-on-error -quiet -comp xz -no-xattrs -b 256k -all-root") print(f"clean ...")
with open(squashfs_file,'rb') as f: run_shell_command(f"rm -rf {extract_dir}")
package[NpkPartID.SQUASHFS].data = f.read() package[NpkPartID.SQUASHFS].data = open(squashfs_file,'rb').read()
run_shell_command(f"rm -f {squashfs_file}")
def patch_npk_file(key_dict,kcdsa_private_key,eddsa_private_key,input_file,output_file=None): def patch_npk_file(key_dict,kcdsa_private_key,eddsa_private_key,input_file,output_file=None):
npk = NovaPackage.load(input_file) npk = NovaPackage.load(input_file)
@ -461,9 +405,9 @@ if __name__ == '__main__':
kernel_parser = subparsers.add_parser('kernel',help='patch kernel file') kernel_parser = subparsers.add_parser('kernel',help='patch kernel file')
kernel_parser.add_argument('input',type=str, help='Input file') kernel_parser.add_argument('input',type=str, help='Input file')
kernel_parser.add_argument('-O','--output',type=str,help='Output file') kernel_parser.add_argument('-O','--output',type=str,help='Output file')
buildefi_parser = subparsers.add_parser('buildefi',help='build efi file') block_parser = subparsers.add_parser('block',help='patch block file')
buildefi_parser.add_argument('input',type=str, help='kernel file') block_parser.add_argument('dev',type=str, help='block device')
buildefi_parser.add_argument('output',type=str,help='Output to file') block_parser.add_argument('file',type=str, help='file path')
netinstall_parser = subparsers.add_parser('netinstall',help='patch netinstall file') netinstall_parser = subparsers.add_parser('netinstall',help='patch netinstall file')
netinstall_parser.add_argument('input',type=str, help='Input file') netinstall_parser.add_argument('input',type=str, help='Input file')
netinstall_parser.add_argument('-O','--output',type=str,help='Output file') netinstall_parser.add_argument('-O','--output',type=str,help='Output file')
@ -481,9 +425,9 @@ if __name__ == '__main__':
print(f'patching {args.input} ...') print(f'patching {args.input} ...')
data = patch_kernel(open(args.input,'rb').read(),key_dict) data = patch_kernel(open(args.input,'rb').read(),key_dict)
open(args.output or args.input,'wb').write(data) open(args.output or args.input,'wb').write(data)
elif args.command == 'buildefi': elif args.command == 'block':
print(f'building EFI from {args.input} ...') print(f'patching {args.file} in {args.dev} ...')
build_efi(args.input,args.output) patch_block(args.dev,args.file,key_dict)
elif args.command == 'netinstall': elif args.command == 'netinstall':
print(f'patching {args.input} ...') print(f'patching {args.input} ...')
patch_netinstall(key_dict,args.input,args.output) patch_netinstall(key_dict,args.input,args.output)

View File

@ -87,7 +87,7 @@ class ShortWeierstrassCurve(EllipticCurve, CurveOpIsomorphism, CurveOpExportSage
@property @property
def is_koblitz(self): def is_koblitz(self):
"""Returns whether the curve allows for efficient computation of a map """Returns whether the curve allows for efficient computation of a map
phi in the field (i.e. that the curve is commonly known as a 'Koblitz \phi in the field (i.e. that the curve is commonly known as a 'Koblitz
Curve'). This corresponds to examples 3 and 4 of the paper "Faster Curve'). This corresponds to examples 3 and 4 of the paper "Faster
Point Multiplication on Elliptic Curves with Efficient Endomorphisms" Point Multiplication on Elliptic Curves with Efficient Endomorphisms"
by Gallant, Lambert and Vanstone.""" by Gallant, Lambert and Vanstone."""