Compare commits

..

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

31 changed files with 996 additions and 3482 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

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

@ -0,0 +1,271 @@
name: Patch Mikrotik RouterOS 6.x
on:
# push:
# branches: [ "main" ]
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
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_LKEY: ${{ secrets.MIKRO_NPK_SIGN_PUBLIC_LKEY }}
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_BuildTime:
runs-on: ubuntu-22.04
outputs:
BUILD_TIME: ${{ steps.set_buildtime.outputs.BUILD_TIME }}
steps:
- name: Set build time
id: set_buildtime
run: echo "BUILD_TIME=$(date +'%s')" >> $GITHUB_OUTPUT
Patch_RouterOS:
needs: Set_BuildTime
runs-on: ubuntu-22.04
strategy:
matrix:
channel: [long-term,stable]
env:
TZ: 'Asia/Shanghai'
LATEST_VERSION: ""
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)
LATEST_VERSION=$(wget -nv -O - https://upgrade.mikrotik.com/routeros/NEWEST6.${{ matrix.channel }} | cut -d ' ' -f1)
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
BUILD_TIME=${{ needs.Set_BuildTime.outputs.BUILD_TIME }}
echo Build Time:$BUILD_TIME
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
- 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
sudo -E python3 patch.py kernel ./new_iso/isolinux/initrd.rgz
NPK_FILES=$(find ./new_iso/*.npk)
for file in $NPK_FILES; do
sudo -E python3 npk.py sign $file $file
done
sudo cp system-$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
cd ./all_packages
sudo zip ../all_packages-x86-$LATEST_VERSION.zip *.npk
cd ..
- name: Create install-image-${{ 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
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 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 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$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: Upload Files
if: steps.get_latest.outputs.has_new_version == 'true'
run: |
mkdir -p ./publish/$LATEST_VERSION
echo $LATEST_VERSION $BUILD_TIME > ./publish/NEWEST6.${{ matrix.channel }}
cp CHANGELOG ./publish/$LATEST_VERSION/
cp ./all_packages/*.npk ./publish/$LATEST_VERSION/
sudo apt-get install -y lftp > /dev/null 2>&1
sudo -E lftp -u ${{ secrets.SSH_USERNAME }},'${{ secrets.SSH_PASSWORD }}' sftp://${{ secrets.SSH_SERVER }}:${{ secrets.SSH_PORT }} <<EOF
set sftp:auto-confirm yes
mirror --reverse --verbose --only-newer ./publish ${{ secrets.SSH_DIRECTORY }}
bye
EOF
- name: Clear Cloudflare cache
if: steps.get_latest.outputs.has_new_version == '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'
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 }}
if: steps.get_latest.outputs.has_new_version == '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
system-${{ env.LATEST_VERSION }}.npk
all_packages-x86-${{ env.LATEST_VERSION }}.zip

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

@ -0,0 +1,433 @@
name: Patch Mikrotik RouterOS 7.x
on:
# push:
# branches: [ "main" ]
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
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_LKEY: ${{ secrets.MIKRO_NPK_SIGN_PUBLIC_LKEY }}
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_BuildTime:
runs-on: ubuntu-22.04
outputs:
BUILD_TIME: ${{ steps.set_buildtime.outputs.BUILD_TIME }}
steps:
- name: Set build time
id: set_buildtime
run: echo "BUILD_TIME=$(date +'%s')" >> $GITHUB_OUTPUT
Patch_RouterOS:
needs: Set_BuildTime
runs-on: ubuntu-22.04
strategy:
matrix:
arch: [x86,arm64]
channel: [stable, testing]
env:
TZ: 'Asia/Shanghai'
LATEST_VERSION: ""
ARCH: ""
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)
LATEST_VERSION=$(wget -nv -O - https://${{ env.MIKRO_UPGRADE_URL }}/routeros/NEWESTa7.${{ matrix.channel }} | cut -d ' ' -f1)
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
BUILD_TIME=${{ needs.Set_BuildTime.outputs.BUILD_TIME }}
echo Build Time:$BUILD_TIME
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
if [ "${{ matrix.arch }}" == "x86" ]; then
ARCH=''
echo "ARCH=$ARCH" >> $GITHUB_ENV
elif [ "${{ matrix.arch }}" == "arm64" ]; then
ARCH='-arm64'
echo "ARCH=$ARCH" >> $GITHUB_ENV
fi
- 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
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-3.11.9.tar.gz -nv https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.11.9+20240713-x86_64-unknown-linux-gnu-install_only.tar.gz
elif [ "${{ matrix.arch }}" == "arm64" ]; then
sudo wget -O cpython-3.11.9.tar.gz -nv https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.11.9+20240713-aarch64-unknown-linux-gnu-install_only.tar.gz
fi
sudo tar -xf cpython-3.11.9.tar.gz
sudo rm cpython-3.11.9.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 NetInstall ${{ env.LATEST_VERSION }}
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86'
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'
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 }}
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86'
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: Cache mikrotik-${{ env.LATEST_VERSION }}${{ env.ARCH }}.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 }}-${{ matrix.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'
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'
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/routeros-$LATEST_VERSION$ARCH.npk ./
sudo -E python3 patch.py npk routeros-$LATEST_VERSION$ARCH.npk
NPK_FILES=$(find ./new_iso/*.npk)
for file in $NPK_FILES; do
sudo -E python3 npk.py sign $file $file
done
sudo cp 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.9"
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 refind
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86'
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'
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 }}.img
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'x86'
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 chr-${{ env.LATEST_VERSION }}${{ env.ARCH }}.img.zip
if: steps.get_latest.outputs.has_new_version == 'true' && matrix.arch == 'arm64'
id: cache-chr-img
uses: actions/cache@v4
with:
path: |
chr.img
key: chr-${{ env.LATEST_VERSION }}-${{ matrix.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 == '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'
run: |
sudo modprobe nbd
sudo apt-get install -y qemu-utils extlinux > /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=mbr.bin 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 mkdir -p ./img/boot/{BOOT,EFI/BOOT}
sudo cp BOOTX64.EFI ./img/boot/EFI/BOOT/BOOTX64.EFI
sudo extlinux --install -H 64 -S 32 ./img/boot/BOOT
echo -e "default system\nlabel system\n\tkernel /EFI/BOOT/BOOTX64.EFI\n\tappend load_ramdisk=1 root=/dev/ram0 quiet" > syslinux.cfg
sudo cp syslinux.cfg ./img/boot/BOOT/
sudo rm syslinux.cfg
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 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: Upload Files
if: steps.get_latest.outputs.has_new_version == 'true'
run: |
mkdir -p ./publish/$LATEST_VERSION
echo $LATEST_VERSION $BUILD_TIME > ./publish/NEWESTa7.${{ matrix.channel }}
cp CHANGELOG ./publish/$LATEST_VERSION/
cp ./all_packages/*.npk ./publish/$LATEST_VERSION/
sudo apt-get install -y lftp > /dev/null 2>&1
sudo -E lftp -u ${{ secrets.SSH_USERNAME }},'${{ secrets.SSH_PASSWORD }}' sftp://${{ secrets.SSH_SERVER }}:${{ secrets.SSH_PORT }} <<EOF
set sftp:auto-confirm yes
mirror --reverse --verbose --only-newer ./publish ${{ secrets.SSH_DIRECTORY }}
bye
EOF
- name: Clear Cloudflare cache
if: steps.get_latest.outputs.has_new_version == '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 }} ${{ matrix.arch }}
if: steps.get_latest.outputs.has_new_version == '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 }} ${{ matrix.arch }}
if: steps.get_latest.outputs.has_new_version == 'true'
uses: softprops/action-gh-release@v2
with:
name: "RouterOS ${{ env.LATEST_VERSION }} ${{ matrix.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
option-${{ env.LATEST_VERSION }}${{ env.ARCH }}.npk
all_packages-*-${{ env.LATEST_VERSION }}.zip
*.EFI

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"

2
.gitignore vendored
View File

@ -2,6 +2,4 @@ __pycache__/
.vscode/
venv/
app/
loader/
test/
test_*.py

View File

@ -1,8 +0,0 @@
# WTF Code of Conduct (WTFCoC)
*"Do What The Fuck You Want, But Don't Be A Dick"*
1. **Do whatever you want** with the code
2. **Don't be a dick** to others
3. **No bureaucracy** - handle problems like adults
4. **You're on your own** - this isn't a safe space
5. **Final rule**: If this bothers you, re-read the WTFPL license name (words 2-4)

13
LICENSE
View File

@ -1,13 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

106
README.md
View File

@ -1,78 +1,50 @@
# MikroTik RouterOS Patch
[![Patch Mikrotik RouterOS](https://github.com/elseif/MikroTikPatch/actions/workflows/main.yml/badge.svg)](https://github.com/elseif/MikroTikPatch/actions/workflows/main.yml)
![Cloud Status](https://img.shields.io/endpoint?url=https://mikrotik.ltd/status/cloud)
[![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)
## ⚠️ 重要声明
**此项目及工具仅供测试用途。使用风险自负。生产环境请使用官方授权版本。**
## 架构x86、arm64
- **主页:** https://mikrotik.ltd/
- **演示:** https://demo.mikrotik.ltd/
- **授权: 安装option.npk后将自动授予最高级别许可证。**
- **源代码:** https://github.com/elseif/MikroTikPatch
- **授权BOT** https://t.me/ROS_Keygen_Bot
- **Docker** `docker run -d --privileged --name chr ghcr.io/elseif/chr:latest`
## 架构arm64, arm, mipsbe, mmips, ppc, smips, x86
- **主页:** https://routeros.ltd/
- **授权: 需要赞助***CHR版本(x86/Arm64)支持在线直接获取授权*
- **功能:** 支持在线自定义制作品牌包
## 支持的云功能
| 功能 | 命令 |
|------|------|
| 在线升级 | `system/package/update/install` |
| DDNS | `ip/cloud/set ddns-enabled=yes` |
| 云备份 | `/system/backup/cloud/upload-file action=create-and-upload password=any` |
## 启用容器模式(无需物理重启)
1. 安装 `option.npk` 包;
2. 打开终端执行:`system/device-mode/update container=yes`
3. 打开新终端执行:`system/shell cmd="reboot -f"`
## 通过终端进入shell
- 安装 `option.npk`
- 打开终端执行:`/system/shell``/sh`
## 通过ssh或telnet进入shell
- 安装 `option.npk`
- ssh或telnet登录用户名为:`devel`,密码与`admin`密码相同。
## x86架构支持CHR和x86模式切换
- 安装 `option.npk`
- 进入shell执行`keygen chr``keygen x86`
## 支持开机自动运行脚本
- 安装 `option.npk`
- 创建或者编辑文件目录下的`rc.local`文件,即:`/rw/disk/rc.local`文件
- 写入脚本内容,例如:
```bash
#!/bin/sh
# This script will be executed *before* RouterOS *loader* start.
# You can put your own initialization stuff in here
echo "Hello, world!"
```
- 开机启动时将看到输出增加:`Starting rc.local...`
- 启动后进入shell执行`cat /ram/startup.catlog | grep 'Hello'` 查看输出。
### 更多关于RouterOS的信息请查看: https://manual.mikrotik.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")
# MikroTik RouterOS Patch [[English](README_EN.md)]
### 国内镜像下载 密码: elseif
[ [123pan](https://www.123pan.com/s/IpxgTd-BYjQh.html) ]
[ [7.15.3](https://elseif.lanzouj.com/b00cre7g7a)]
[ [7.15.2](https://elseif.lanzouj.com/b00crdq4hg)]
[ [7.16beta4](https://elseif.lanzouj.com/b00crdq4fe) ]
[ [7.15.3-arm64](https://elseif.lanzouj.com/b00cre7g8b) ]
[ [7.15.2-arm64](https://elseif.lanzouj.com/b00crdq4ih) ]
[ [7.16beta4-arm64](https://elseif.lanzouj.com/b00crdq4gf) ]
[ [6.49.15](https://elseif.lanzouj.com/b00crdq4ji) ]
[ [6.49.13](https://elseif.lanzouj.com/b00crdq4kj) ]
支持:在线更新、在线授权、云备份、DDNS
![](image/install.png)
![](image/routeros.png)
### x86模式授权许可
![](image/x86.png)
### x86模式在线授权(v6.x)
![](image/renew_v6.png)
### Chr模式在线授权
![](image/renew.png)
### Chr模式授权许可
![](image/chr.png)
## 如何使用Shell
安装 option-{version}.npk 包
telnet到RouterOS,用户名devel,密码与admin的密码相同
## 如何授权许可
进入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/)。

44
README_EN.md Normal file
View File

@ -0,0 +1,44 @@
# MikroTik RouterOS Patch [[中文](README.md)]
### [[Discord](https://discord.gg/keV6MWQFtX)] [[Telegram](https://t.me/+99Mw06p3K7NlMmNl)]
### 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)
## 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
## all patches are applied automatically with [Github Action](https://github.com/elseif/MikroTikPatch/blob/main/.github/workflows/).

BIN
busybox/busybox_aarch64 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

364
chr.sh
View File

@ -1,340 +1,26 @@
#!/bin/bash
set -e
_ask() {
local _redo=0
read resp
case "$resp" in
!) echo "Type 'exit' to return to setup."
sh
_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
echo "$MSG_FILE_DOWNLOAD $(basename "$IMG_URL")"
http_get "$IMG_URL" "/tmp/chr.img.zip"
cd /tmp
extract_zip "chr.img.zip" chr.img
}
create_autorun() {
LOOP=$(losetup -f)
if losetup -P "$LOOP" chr.img; then
sleep 1
MNT=/tmp/chr
mkdir -p $MNT
PARTITION=$([ "$V7" == 1 ] && echo "p2" || echo "p1")
if mount "${LOOP}${PARTITION}" "$MNT"; then
confirm_address
RANDOM_ADMIN_PASS=$(head -c 512 /dev/urandom | tr -dc 'A-HJ-KMNP-Za-hj-kmnp-z2-9' | head -c 16)
ask_until "$MSG_ADMIN_PASSWORD" "$RANDOM_ADMIN_PASS"
RANDOM_ADMIN_PASS=$resp
cat <<EOF > "$MNT/rw/autorun.scr"
/user set admin password="$RANDOM_ADMIN_PASS"
/ip dns set servers=$DNS
/ip address add address=$ADDRESS interface=[/interface ethernet get [find mac-address=$MAC] name]
/ip route add gateway=$GATEWAY
EOF
cat "$MNT/rw/autorun.scr"
echo "$MSG_AUTO_RUN_FILE_CREATED"
umount $MNT
losetup -d "$LOOP"
else
losetup -d "$LOOP"
echo "$MSG_ERROR_MOUNT $PARTITION"
echo "$MSG_AUTO_RUN_FILE_NOT_CREATED"
fi
else
echo "$MSG_ERROR_LOOP"
echo "$MSG_AUTO_RUN_FILE_NOT_CREATED"
fi
}
write_reboot() {
confirm_storage
printf "$MSG_WARNING\n" "$STORAGE"
ask_yesno "$MSG_CONFIRM_CONTINUE"
if [ $? -ne 0 ]; then
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
apt-get install -y gdisk extlinux
dd if=/dev/zero of=/dev/sda bs=1 count=512 conv=notrunc
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 /dev/sda
dd if=/dev/sda of=/tmp/pt.bin bs=1 count=66 skip=446
echo -e "\x80" | dd of=/tmp/pt.bin bs=1 count=1 conv=notrunc
dd if=/dev/zero of=/dev/sda bs=1 count=512 conv=notrunc
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" /dev/sda
wget -O /tmp/mbr.bin https://raw.gitmirror.com/elseif/MikroTikPatch/main/mbr.bin
dd if=/tmp/mbr.bin of=/dev/sda bs=1 count=446 conv=notrunc
dd if=/tmp/pt.bin of=/dev/sda bs=1 count=66 seek=446 conv=notrunc
sync
mkfs.vfat -n "Boot" /dev/sda1
mkfs.ext4 -F -L "RouterOS" -m 0 /dev/sda2
mkdir -p /tmp/{boot,routeros}
mount -o loop,rw /dev/sda1 /tmp/boot
mkdir -p /tmp/boot/{BOOT,EFI/BOOT}
wget -O /tmp/boot/EFI/BOOT/BOOTX64.EFI https://hub.gitmirror.com/https://github.com/elseif/MikroTikPatch/releases/download/7.15.2/BOOTX64.EFI
extlinux --install -H 64 -S 32 /tmp/boot/BOOT
echo -e "default system\nlabel system\n\tkernel /EFI/BOOT/BOOTX64.EFI\n\tappend load_ramdisk=1 root=/dev/ram0 quiet" > /tmp/boot/BOOT/syslinux.cfg
umount /tmp/boot
mount -o loop,rw /dev/sda2 /tmp/routeros
mkdir -p /tmp/routeros/{var/pdb/{system,option},boot,rw}
wget -O /tmp/routeros/var/pdb/option/image https://hub.gitmirror.com/https://github.com/elseif/MikroTikPatch/releases/download/7.15.2/option-7.15.2.npk
wget -O /tmp/routeros/var/pdb/system/image https://hub.gitmirror.com/https://github.com/elseif/MikroTikPatch/releases/download/7.15.2/routeros-7.15.2.npk
umount /tmp/routeros

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/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,770 +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>
</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>
<!-- 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",
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代理加速",
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 === '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");
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";
// 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");
updateAllDownloadLinks();
});
// 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_aarch64 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mbr.bin Normal file

Binary file not shown.

View File

@ -1,9 +1,11 @@
import random
import struct
from sha256 import SHA256
from toyecc import AffineCurvePoint, getcurvebyname, FieldElement,ECPrivateKey,ECPublicKey,Tools
from toyecc.Random import secure_rand_int_between
MIKRO_LICENSE_HEADER = '-----BEGIN MIKROTIK SOFTWARE KEY------------'
MIKRO_LICENSE_FOOTER = '-----END MIKROTIK SOFTWARE KEY--------------'
MIKRO_BASE64_CHARACTER_TABLE = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
SOFTWARE_ID_CHARACTER_TABLE = b'TN0BYX18S5HZ4IA67DGF3LPCJQRUK9MW2VE'
@ -38,8 +40,8 @@ def mikro_softwareid_encode(id:int)->str:
assert(isinstance(id, int))
ret = ''
for i in range(8):
ret += chr(SOFTWARE_ID_CHARACTER_TABLE[id % len(SOFTWARE_ID_CHARACTER_TABLE)])
id //= len(SOFTWARE_ID_CHARACTER_TABLE)
ret += chr(SOFTWARE_ID_CHARACTER_TABLE[id % 0x23])
id //= 0x23
if i == 3:
ret += '-'
return ret
@ -166,7 +168,7 @@ def mikro_kcdsa_sign(data:bytes,private_key:bytes)->bytes:
private_key:ECPrivateKey = ECPrivateKey(Tools.bytestoint_le(private_key), curve)
public_key:ECPublicKey = private_key.pubkey
while True:
nonce_secret = random.SystemRandom().randint(1, curve.n - 1)
nonce_secret = secure_rand_int_between(1, curve.n - 1)
nonce_point = nonce_secret * curve.G
nonce = int(nonce_point.x) % curve.n
nonce_hash = mikro_sha256(Tools.inttobytes_le(nonce,32))
@ -204,4 +206,4 @@ def mikro_kcdsa_verify(data:bytes, signature:bytes, public_key:bytes)->bool:
nonce = int((public_key * signature + curve.G * data_hash).x)
if mikro_sha256(Tools.inttobytes_le(nonce,32))[:len(nonce_hash)] == nonce_hash:
return True
return False
return False

175
npk.py
View File

@ -1,6 +1,6 @@
import struct,zlib
import argparse,os,tempfile
import argparse,os
from datetime import datetime
from dataclasses import dataclass
from enum import IntEnum
@ -51,45 +51,7 @@ class NpkInfo:
def name(self,value:str):
self._name = value[:16].encode().ljust(16,b'\x00')
@staticmethod
def encode_version(version: str) -> 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:
def decode_version(value:bytes):
revision,build,minor,major = struct.unpack_from('4B',value)
if build == 97:
build = 'alpha'
@ -102,10 +64,31 @@ class NpkInfo:
build = 'test'
revision &= 0x7f
else:
build = ''
build = 'final'
else:
build = 'unknown'
return f'{major}.{minor}{build + str(revision) if build !="" else "." + str(revision) if revision != 0 else ""}'
build = 'unknown'
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
def version(self)->str:
return self.decode_version(self._version)
@ -154,7 +137,7 @@ class NpkFileContainer:
self._items= items
def serialize(self)->bytes:
compressed_data = b''
compressor = zlib.compressobj(level=0)
compressor = zlib.compressobj()
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 += item.name + item.data
@ -224,48 +207,7 @@ class NovaPackage(Package):
self._parts.append(NpkPartItem(NpkPartID(part_id),NpkInfo.unserialize_from(part_data)))
else:
self._parts.append(NpkPartItem(NpkPartID(part_id),part_data))
def set_null_block(self):
def rebuild_squashfs(data):
with tempfile.TemporaryDirectory() as tmp_dir:
squashfs_file = os.path.join(tmp_dir, 'squashfs.sfs')
squashfs_root = os.path.join(tmp_dir, 'squashfs-root')
with open(squashfs_file, 'wb') as f:
f.write(data)
os.system(f'unsquashfs -d {squashfs_root} {squashfs_file}')
os.remove(squashfs_file)
os.system(f'mksquashfs {squashfs_root} {squashfs_file} -no-recovery -noappend -exit-on-error -quiet -comp xz -no-xattrs -b 256k -all-root')
with open(squashfs_file, 'rb') as f:
return f.read()
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
for part in package._parts:
if part.id == NpkPartID.SQUASHFS and len(part.data) >= 4 and part.data[:4] in [b'hsqs',b'sqsh']:
part.data = rebuild_squashfs(part.data)
has_squashfs = True
if has_squashfs:
count = offset
for part in package._parts:
count += 6
if part.id == NpkPartID.NULL_BLOCK:
break
count += len(part.data)
count += 6
pad_len = (4096 - (count % 4096)) % 4096
package[NpkPartID.NULL_BLOCK].data = b'\x00' * pad_len
set_null(self,8)
offset = get_size(self,8)
for package in self._packages:
set_null(package,offset)
offset += get_size(package,0)
def get_digest(self,hash_fnc,package:Package=None)->bytes:
parts = package._parts if package else self._parts
for part in parts:
@ -286,33 +228,18 @@ class NovaPackage(Package):
def sign(self,kcdsa_private_key:bytes,eddsa_private_key:bytes):
import hashlib
from mikro import mikro_kcdsa_sign,mikro_eddsa_sign
build_time = os.getenv('BUILD_TIME',None)
self.set_null_block()
build_time = os.environ['BUILD_TIME'] if 'BUILD_TIME' in os.environ else None
if len(self._packages) > 0:
if 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:
if len(package[NpkPartID.SIGNATURE].data) != 20+48+64:
package[NpkPartID.SIGNATURE].data = b'\0'*(20+48+64)
if 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)
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)
package[NpkPartID.SIGNATURE].data = sha1_digest + kcdsa_signature + eddsa_signature
else:
@ -320,16 +247,9 @@ class NovaPackage(Package):
self[NpkPartID.SIGNATURE].data = b'\0'*(20+48+64)
if 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'))
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)
self[NpkPartID.SIGNATURE].data = sha1_digest + kcdsa_signature + eddsa_signature
@ -343,9 +263,9 @@ class NovaPackage(Package):
signature = package[NpkPartID.SIGNATURE].data
if sha1_digest != signature[:20]:
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
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
else:
sha1_digest = self.get_digest(hashlib.new('SHA1'))
@ -353,10 +273,11 @@ class NovaPackage(Package):
signature = self[NpkPartID.SIGNATURE].data
if sha1_digest != signature[:20]:
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
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 True
def save(self,file):
@ -404,10 +325,6 @@ if __name__=='__main__':
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('-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()
kcdsa_private_key = bytes.fromhex(os.environ['CUSTOM_LICENSE_PRIVATE_KEY'])
eddsa_private_key = bytes.fromhex(os.environ['CUSTOM_NPK_SIGN_PRIVATE_KEY'])
@ -438,23 +355,5 @@ if __name__=='__main__':
option_npk.sign(kcdsa_private_key,eddsa_private_key)
option_npk.save(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:
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)

359
patch.py
View File

@ -1,70 +1,7 @@
import subprocess,lzma
import struct,os,re,tempfile
import pefile
from elftools.elf.elffile import ELFFile
import struct,os
from npk import NovaPackage,NpkPartID,NpkFileContainer
def replace_chunks(old_chunks,new_chunks,data,name):
pattern_parts = [re.escape(chunk) + b'(.{0,6})' for chunk in old_chunks[:-1]]
pattern_parts.append(re.escape(old_chunks[-1]))
pattern_bytes = b''.join(pattern_parts)
pattern = re.compile(pattern_bytes, flags=re.DOTALL)
def replace_match(match):
replaced = b''.join([new_chunks[i] + match.group(i+1) for i in range(len(new_chunks) - 1)])
replaced += new_chunks[-1]
print(f'{name} public key patched {b"".join(old_chunks)[:16].hex().upper()}...')
return replaced
return re.sub(pattern, replace_match, data)
def replace_key(old,new,data,name=''):
old_chunks = [old[i:i+4] for i in range(0, len(old), 4)]
new_chunks = [new[i:i+4] for i in range(0, len(new), 4)]
data = replace_chunks(old_chunks, new_chunks, data,name)
key_map = [28,19,25,16,14,3,24,15,22,8,6,17,11,7,9,23,18,13,10,0,26,21,2,5,20,30,31,4,27,29,1,12,]
old_chunks = [bytes([old[i]]) for i in key_map]
new_chunks = [bytes([new[i]]) for i in key_map]
data = replace_chunks(old_chunks, new_chunks, data,name)
arch = os.getenv('ARCH') or 'x86'
arch = arch.replace('-', '')
if arch in ['arm64','arm']:
old_chunks = [old[i:i+4] for i in range(0, len(old), 4)]
new_chunks = [new[i:i+4] for i in range(0, len(new), 4)]
old_bytes = old_chunks[4] + old_chunks[5] + old_chunks[2] + old_chunks[0] + old_chunks[1] + old_chunks[6] + old_chunks[7]
new_bytes = new_chunks[4] + new_chunks[5] + new_chunks[2] + new_chunks[0] + new_chunks[1] + new_chunks[6] + new_chunks[7]
if old_bytes in data:
print(f'{name} public key patched {old[:16].hex().upper()}...')
data = data.replace(old_bytes,new_bytes)
old_codes = [bytes.fromhex('793583E2'),bytes.fromhex('FD3A83E2'),bytes.fromhex('193D83E2')] #0x1e400000+0xfd000+0x640
new_codes = [bytes.fromhex('FF34A0E3'),bytes.fromhex('753C83E2'),bytes.fromhex('FC3083E2')] #0xff0075fc= 0xff000000+0x7500+0xfc
data = replace_chunks(old_codes, new_codes, data,name)
else:
def conver_chunks(data:bytes):
ret = [
(data[2] << 16) | (data[1] << 8) | data[0] | ((data[3] << 24) & 0x03000000),
(data[3] >> 2) | (data[4] << 6) | (data[5] << 14) | ((data[6] << 22) & 0x1C00000),
(data[6] >> 3) | (data[7] << 5) | (data[8] << 13) | ((data[9] << 21) & 0x3E00000),
(data[9] >> 5) | (data[10] << 3) | (data[11] << 11) | ((data[12] << 19) & 0x1F80000),
(data[12] >> 6) | (data[13] << 2) | (data[14] << 10) | (data[15] << 18),
data[16] | (data[17] << 8) | (data[18] << 16) | ((data[19] << 24) & 0x01000000),
(data[19] >> 1) | (data[20] << 7) | (data[21] << 15) | ((data[22] << 23) & 0x03800000),
(data[22] >> 3) | (data[23] << 5) | (data[24] << 13) | ((data[25] << 21) & 0x1E00000),
(data[25] >> 4) | (data[26] << 4) | (data[27] << 12) | ((data[28] << 20) & 0x3F00000),
(data[28] >> 6) | (data[29] << 2) | (data[30] << 10) | (data[31] << 18)
]
return [struct.pack('<I', x ) for x in ret]
old_chunks = conver_chunks(old)
new_chunks = conver_chunks(new)
old_bytes = b''.join([v for i,v in enumerate(old_chunks) if i != 8])
new_bytes = b''.join([v for i,v in enumerate(new_chunks) if i != 8])
if old_bytes in data:
print(f'{name} public key patched {old[:16].hex().upper()}...')
data = data.replace(old_bytes,new_bytes)
old_codes = [bytes.fromhex('713783E2'),bytes.fromhex('223A83E2'),bytes.fromhex('8D3F83E2')] #0x1C40000+0x22000+0x234
new_codes = [bytes.fromhex('973303E3'),bytes.fromhex('DD3883E3'),bytes.fromhex('033483E3')] #0x03DD3397 = 0x3397|0x00DD0000|0x03000000
data = replace_chunks(old_codes, new_codes, data,name)
return data
def patch_bzimage(data:bytes,key_dict:dict):
PE_TEXT_SECTION_OFFSET = 414
HEADER_PAYLOAD_OFFSET = 584
@ -85,7 +22,9 @@ def patch_bzimage(data:bytes,key_dict:dict):
initramfs = initramfs[:cpio_offset2]
new_initramfs = initramfs
for old_public_key,new_public_key in key_dict.items():
new_initramfs = replace_key(old_public_key,new_public_key,new_initramfs,'initramfs')
if old_public_key in new_initramfs:
print(f'initramfs public key patched {old_public_key[:16].hex().upper()}...')
new_initramfs = new_initramfs.replace(old_public_key,new_public_key)
new_vmlinux = vmlinux.replace(initramfs,new_initramfs)
new_vmlinux_xz = lzma.compress(new_vmlinux,check=lzma.CHECK_CRC32,filters=[
{"id": lzma.FILTER_X86},
@ -106,30 +45,47 @@ def patch_bzimage(data:bytes,key_dict:dict):
new_data = new_data.replace(vmlinux_xz,new_vmlinux_xz)
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(',')
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:
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):
try:
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}')
initrd = lzma.decompress(initrd_xz)
new_initrd = initrd
for old_public_key,new_public_key in key_dict.items():
new_initrd = replace_key(old_public_key,new_public_key,new_initrd,'initrd')
preset = 6
new_initrd_xz = lzma.compress(new_initrd,check=lzma.CHECK_CRC32,filters=[{"id": lzma.FILTER_LZMA2, "preset": preset }] )
while len(new_initrd_xz) > len(initrd_xz) and preset < 9:
print(f'preset:{preset}')
print(f'new initrd xz size:{len(new_initrd_xz)}')
print(f'old initrd xz size:{len(initrd_xz)}')
preset += 1
new_initrd_xz = lzma.compress(new_initrd,check=lzma.CHECK_CRC32,filters=[{"id": lzma.FILTER_LZMA2, "preset": preset }] )
if len(new_initrd_xz) > len(initrd_xz):
new_initrd_xz = lzma.compress(new_initrd,check=lzma.CHECK_CRC32,filters=[{"id": lzma.FILTER_LZMA2, "preset": 9 | lzma.PRESET_EXTREME,'dict_size': 32*1024*1024,"lc": 4,"lp": 0, "pb": 0,}] )
if old_public_key in new_initrd:
print(f'initrd public key patched {old_public_key[:16].hex().upper()}...')
new_initrd = new_initrd.replace(old_public_key,new_public_key)
new_initrd_xz = lzma.compress(new_initrd,check=lzma.CHECK_CRC32,filters=[{"id": lzma.FILTER_LZMA2, "preset": 9,}] )
if ljust:
print(f'preset:{preset}')
print(f'new initrd xz size:{len(new_initrd_xz)}')
print(f'old initrd xz size:{len(initrd_xz)}')
print(f'ljust size:{len(initrd_xz)-len(new_initrd_xz)}')
assert len(new_initrd_xz) <= len(initrd_xz),'new initrd xz size is too big'
new_initrd_xz = new_initrd_xz.ljust(len(initrd_xz),b'\0')
return new_initrd_xz
@ -146,7 +102,6 @@ def find_7zXZ_data(data:bytes):
while b'\x00\x00\x00\x00\x01\x59\x5A' in _data:
offset2 = offset2 + _data.index(b'\x00\x00\x00\x00\x01\x59\x5A') + 7
_data = _data[offset2:]
print(f'found 7zXZ data offset:{offset1} size:{offset2-offset1}')
return data[offset1:offset2]
def patch_elf(data: bytes,key_dict:dict):
@ -164,65 +119,16 @@ def patch_pe(data: bytes,key_dict:dict):
new_vmlinux = vmlinux.replace(initrd_xz,new_initrd_xz)
new_vmlinux_xz = lzma.compress(new_vmlinux,check=lzma.CHECK_CRC32,filters=[{"id": lzma.FILTER_LZMA2, "preset": 9,}] )
assert len(new_vmlinux_xz) <= len(vmlinux_xz),'new vmlinux xz size is too big'
print(f'new vmlinux xz size:{len(new_vmlinux_xz)}')
print(f'old vmlinux xz size:{len(vmlinux_xz)}')
print(f'ljust size:{len(vmlinux_xz)-len(new_vmlinux_xz)}')
new_vmlinux_xz = new_vmlinux_xz.ljust(len(vmlinux_xz),b'\0')
new_data = data.replace(vmlinux_xz,new_vmlinux_xz)
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):
netinstall = open(input_file,'rb').read()
if netinstall[:2] == b'MZ':
from package import check_install_package
check_install_package(['pefile'])
import pefile
ROUTEROS_BOOT = {
129:{'arch':'power','name':'Powerboot'},
130:{'arch':'e500','name':'e500_boot'},
@ -244,23 +150,23 @@ def patch_netinstall(key_dict: dict,input_file,output_file=None):
rva = sub_resource.directory.entries[0].data.struct.OffsetToData
size = sub_resource.directory.entries[0].data.struct.Size
data = pe.get_data(rva,size)
_size = struct.unpack('<I',data[:4])[0]
_data = data[4:4+_size]
assert len(data) -4 >= struct.unpack_from('<I',data)[0] ,f'bootloader data size mismathch'
data = data[4:]
try:
if _data[:2] == b'MZ':
new_data = patch_pe(_data,key_dict)
elif _data[:4] == b'\x7FELF':
new_data = patch_elf(_data,key_dict)
if data[:2] == b'MZ':
new_data = patch_pe(data,key_dict)
elif data[:4] == b'\x7FELF':
new_data = patch_elf(data,key_dict)
else:
raise Exception(f'unknown bootloader format {_data[:4].hex().upper()}')
raise Exception(f'unknown bootloader format {data[:4].hex().upper()}')
except Exception as e:
print(f'patch {bootloader["arch"]}({sub_resource.id}) bootloader failed {e}')
new_data = _data
new_data = struct.pack("<I",_size) + new_data.ljust(len(_data),b'\0')
new_data = new_data.ljust(size,b'\0')
new_data = data
new_data = struct.pack("<I",len(new_data)) + new_data.ljust(len(data),b'\0')
pe.set_bytes_at_rva(rva,new_data)
pe.write(output_file or input_file)
elif netinstall[:4] == b'\x7FELF':
import re
# 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
# 81 00 00 00 D3 68 C4 0B 2A 9E AB 08 5C 1B 78 00
@ -321,97 +227,43 @@ def patch_kernel(data:bytes,key_dict):
return patch_elf(data,key_dict)
elif data[:5] == b'\xFD7zXZ':
print('patching initrd')
return patch_initrd_xz(data,key_dict,False)
return patch_initrd_xz(data,key_dict)
else:
raise Exception('unknown kernel format')
def patch_loader(loader_file):
try:
from loader.patch_loader import patch_loader as do_patch_loader
arch = os.getenv('ARCH') or 'x86'
arch = arch.replace('-', '')
do_patch_loader(loader_file,loader_file,arch)
except ImportError as e:
print(e)
print("loader module import failed. cannot run patch_loader.py")
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):
if 'mode' in files and 'keyman' in files:
for file_path in [os.path.join(root, 'mode'),os.path.join(root, 'keyman')]:
with open(file_path, 'rb') as f:
data = f.read()
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 = False
for file in files:
file = os.path.join(root,file)
if os.path.isfile(file):
data = open(file,'rb').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
with open(f'{file_path}_', 'wb') as f:
f.write(data)
if 'loader' in files and os.path.isfile(os.path.join(root, 'loader')):
loader_file = os.path.join(root, 'loader')
patch_loader(loader_file)
if 'BOOTX64.EFI' in files and os.path.isfile(os.path.join(root, 'BOOTX64.EFI')):
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_public_key in data:
print(f'{file} public key patched {old_public_key[:16].hex().upper()}...')
data = data.replace(old_public_key,new_public_key)
open(file,'wb').write(data)
data = open(file,'rb').read()
url_dict = {
os.environ['MIKRO_LICENCE_URL'].encode():os.environ['CUSTOM_LICENCE_URL'].encode(),
os.environ['MIKRO_UPGRADE_URL'].encode():os.environ['CUSTOM_UPGRADE_URL'].encode(),
os.environ['MIKRO_CLOUD_URL'].encode():os.environ['CUSTOM_CLOUD_URL'].encode(),
os.environ['MIKRO_CLOUD_PUBLIC_KEY'].encode():os.environ['CUSTOM_CLOUD_PUBLIC_KEY'].encode(),
}
for old_url,new_url in url_dict.items():
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)
modified = True
if modified:
with open(file_path, 'wb') as f:
f.write(data)
open(file,'wb').write(data)
if os.path.split(file)[1] == 'licupgr':
url_dict = {
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):
process = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -425,21 +277,22 @@ def patch_npk_package(package,key_dict):
print(f'patch {item.name} ...')
item.data = patch_kernel(item.data,key_dict)
package[NpkPartID.FILE_CONTAINER].data = file_container.serialize()
with tempfile.TemporaryDirectory() as tmp_dir:
squashfs_file = os.path.join(tmp_dir, 'squashfs-root.sfs')
extract_dir = os.path.join(tmp_dir, 'squashfs-root')
with open(squashfs_file,'wb') as f:
f.write(package[NpkPartID.SQUASHFS].data)
print(f"extract {squashfs_file} ...")
run_shell_command(f"unsquashfs -d {extract_dir} {squashfs_file}")
patch_squashfs(extract_dir,key_dict)
logo = os.path.join(extract_dir,"nova/lib/console/logo.txt")
run_shell_command(f"sed -i '1d' {logo}")
run_shell_command(f"sed -i '8s#.*# elseif@live.cn https://github.com/elseif/MikroTikPatch#' {logo}")
print(f"pack {extract_dir} ...")
run_shell_command(f"mksquashfs {extract_dir} {squashfs_file} -no-recovery -noappend -exit-on-error -quiet -comp xz -no-xattrs -b 256k -all-root")
with open(squashfs_file,'rb') as f:
package[NpkPartID.SQUASHFS].data = f.read()
squashfs_file = 'squashfs-root.sfs'
extract_dir = 'squashfs-root'
open(squashfs_file,'wb').write(package[NpkPartID.SQUASHFS].data)
print(f"extract {squashfs_file} ...")
run_shell_command(f"unsquashfs -d {extract_dir} {squashfs_file}")
patch_squashfs(extract_dir,key_dict)
logo = os.path.join(extract_dir,"nova/lib/console/logo.txt")
run_shell_command(f"sudo sed -i '1d' {logo}")
run_shell_command(f"sudo sed -i '8s#.*# elseif@live.cn https://github.com/elseif/MikroTikPatch#' {logo}")
print(f"pack {extract_dir} ...")
run_shell_command(f"rm -f {squashfs_file}")
run_shell_command(f"mksquashfs {extract_dir} {squashfs_file} -quiet -comp xz -no-xattrs -b 256k")
print(f"clean ...")
run_shell_command(f"rm -rf {extract_dir}")
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):
npk = NovaPackage.load(input_file)
@ -461,16 +314,16 @@ if __name__ == '__main__':
kernel_parser = subparsers.add_parser('kernel',help='patch kernel file')
kernel_parser.add_argument('input',type=str, help='Input file')
kernel_parser.add_argument('-O','--output',type=str,help='Output file')
buildefi_parser = subparsers.add_parser('buildefi',help='build efi file')
buildefi_parser.add_argument('input',type=str, help='kernel file')
buildefi_parser.add_argument('output',type=str,help='Output to file')
block_parser = subparsers.add_parser('block',help='patch block file')
block_parser.add_argument('dev',type=str, help='block device')
block_parser.add_argument('file',type=str, help='file path')
netinstall_parser = subparsers.add_parser('netinstall',help='patch netinstall file')
netinstall_parser.add_argument('input',type=str, help='Input file')
netinstall_parser.add_argument('-O','--output',type=str,help='Output file')
args = parser.parse_args()
key_dict = {
bytes.fromhex(os.environ['MIKRO_LICENSE_PUBLIC_KEY']):bytes.fromhex(os.environ['CUSTOM_LICENSE_PUBLIC_KEY']),
bytes.fromhex(os.environ['MIKRO_NPK_SIGN_PUBLIC_KEY']):bytes.fromhex(os.environ['CUSTOM_NPK_SIGN_PUBLIC_KEY'])
bytes.fromhex(os.environ['MIKRO_NPK_SIGN_PUBLIC_LKEY']):bytes.fromhex(os.environ['CUSTOM_NPK_SIGN_PUBLIC_KEY'])
}
kcdsa_private_key = bytes.fromhex(os.environ['CUSTOM_LICENSE_PRIVATE_KEY'])
eddsa_private_key = bytes.fromhex(os.environ['CUSTOM_NPK_SIGN_PRIVATE_KEY'])
@ -481,9 +334,9 @@ if __name__ == '__main__':
print(f'patching {args.input} ...')
data = patch_kernel(open(args.input,'rb').read(),key_dict)
open(args.output or args.input,'wb').write(data)
elif args.command == 'buildefi':
print(f'building EFI from {args.input} ...')
build_efi(args.input,args.output)
elif args.command == 'block':
print(f'patching {args.file} in {args.dev} ...')
patch_block(args.dev,args.file,key_dict)
elif args.command == 'netinstall':
print(f'patching {args.input} ...')
patch_netinstall(key_dict,args.input,args.output)

View File

@ -87,7 +87,7 @@ class ShortWeierstrassCurve(EllipticCurve, CurveOpIsomorphism, CurveOpExportSage
@property
def is_koblitz(self):
"""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
Point Multiplication on Elliptic Curves with Efficient Endomorphisms"
by Gallant, Lambert and Vanstone."""