关于 routeros-qemu-chr
面向网络工程师和开发者的 RouterOS CHR 虚拟机运行指南,聚焦通过 QEMU 启动 x86_64 与 aarch64 镜像。它比较命令行、脚本和程序化启动方式,并解释启动固件、VirtIO、网络及就绪检测的注意事项。
主要功能
- 01介绍 CHR 镜像架构与启动方式
- 02提供多种 QEMU 启动模式
- 03说明 VirtIO 磁盘与网络配置
- 04讲解用户态 NAT 与二层网络连接
- 05说明加速选择和启动就绪检测
适用场景
- 01在 QEMU 中运行 RouterOS CHR
- 02构建多实例或集成测试环境
- 03连接虚拟机并捕获二层流量
- 04排查固件、设备或启动就绪问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tikoci/ routeros-skills --skill routeros-qemu-chr安装后在 Claude Code 中输入/routeros-qemu-chr确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/chr-licensing.mdgithub-actions-ci.mdknown-issues.mdvirtio-drivers.md详细介绍
使用 QEMU 运行 RouterOS CHR
什么是 CHR
云托管路由器(CHR)是 MikroTik 面向虚拟机设计的 x86_64 和 aarch64 架构 RouterOS 镜像。免费许可证允许无限制使用,但速度限制为 1 Mbps,足以满足开发、测试、API 工作和数据包嗅探调试的需求。免费的 60 天试用版可完全移除速度限制(需要注册免费的 mikrotik.com 账号)。有关许可证等级、试用版激活和过期行为的完整详情,请参阅 CHR 许可证参考。
镜像变体
| 镜像 | 架构 | 启动方式 | 来源 |
|---|---|---|---|
chr-<ver>.img | x86_64 | SeaBIOS(MBR 链式加载) | download.mikrotik.com |
chr-<ver>-arm64.img | aarch64 | UEFI(EDK2 pflash) | download.mikrotik.com |
chr-efi.img(fat-chr) | x86_64 | UEFI(OVMF) | tikoci/fat-chr GitHub |
标准 x86 镜像具有专有启动分区——它在 GPT 中看起来像 EFI 系统分区,但并非 FAT 格式。UEFI 固件(OVMF)无法读取它。只有 SeaBIOS 能通过 MBR 链式加载启动该镜像。
重新打包的 fat-chr 镜像会将其转换为标准 FAT16,并加入 EFI/BOOT/BOOTX64.EFI,从而支持 UEFI 启动。在 X86 macOS 上使用 Apple Virtualization.framework 时必须使用该镜像;其他情况下则非必需。
磁盘布局(两种架构均为 128 MiB):混合 GPT+MBR,分区 1 = 启动分区(约 33 MiB),分区 2 = ext4 根分区(约 94 MiB)。
下载 CHR 镜像
// Resolve current version
const channel = "stable"; // or: long-term, testing, development
const version = await fetch(
`https://upgrade.mikrotik.com/routeros/NEWESTa7.${channel}`
).then(r => r.text()).then(s => s.trim());
// Download x86_64 image
const url = `https://download.mikrotik.com/routeros/${version}/chr-${version}.img.zip`;
// Download aarch64 image
const armUrl = `https://download.mikrotik.com/routeros/${version}/chr-${version}-arm64.img.zip`;
镜像以 .img.zip 格式分发,解压后即可获得原始 .img 磁盘文件。
QEMU 启动方式选项
在 QEMU 下启动 CHR 有几种可行的方法,各有利弊:
方式 A:内联参数(最简单,适合脚本)
所有内容都写在命令行中。便于大模型构建和调试——所有状态一目了然。
qemu-system-x86_64 -M q35 -m 256 -smp 1 \
-drive file=chr.img,format=raw,if=virtio \
-netdev user,id=net0,hostfwd=tcp::9180-:80 \
-device virtio-net-pci,netdev=net0 \
-display none -serial stdio
优点: 单条命令,易于阅读和修改。 缺点: 命令行较长,难以进行版本控制,且不具备持久化能力。
方式 B:包装脚本(适合重复使用)
一个可检测加速方式、处理固件路径并管理 PID 文件的 shell 脚本。
#!/bin/sh
# detect acceleration
if [ "$(uname -s)" = "Linux" ] && [ -w /dev/kvm ]; then
ACCEL="-accel kvm"
elif [ "$(uname -s)" = "Darwin" ] && [ "$(sysctl -n kern.hv_support 2>/dev/null)" = "1" ]; then
ACCEL="-accel hvf"
else
ACCEL="-accel tcg"
fi
qemu-system-x86_64 -M q35 -m 256 -smp 1 \
$ACCEL \
-drive file=chr.img,format=raw,if=virtio \
-netdev user,id=net0,hostfwd=tcp::${PORT:-9180}-:80 \
-device virtio-net-pci,netdev=net0 \
-display none -serial stdio
优点: 可移植,能处理平台差异,可通过参数配置。 缺点: 受 shell 脚本的限制,难以通过 TypeScript 组合使用。
方式 C:通过 Bun/TypeScript 以编程方式启动(适合集成测试)
以子进程方式启动 QEMU,并进行全面控制:
import { $ } from "bun";
const port = 9180;
const accel = await detectAccel();
const proc = Bun.spawn([
"qemu-system-x86_64", "-M", "q35", "-m", "256",
"-accel", accel,
"-drive", `file=chr.img,format=raw,if=virtio`,
"-netdev", `user,id=net0,hostfwd=tcp::${port}-:80`,
"-device", "virtio-net-pci,netdev=net0",
"-display", "none",
"-chardev", `socket,id=serial0,path=/tmp/chr-serial.sock,server=on,wait=off`,
"-serial", "chardev:serial0",
"-monitor", `unix:/tmp/chr-monitor.sock,server,nowait`,
], { stdio: ["ignore", "pipe", "pipe"] });
// Wait for boot
await waitForBoot(`http://127.0.0.1:${port}/`);
优点: 可全面控制生命周期,管理并行实例,原生支持 TypeScript。 缺点: 代码量更多,仍需确保 QEMU 参数正确。
方式 D:配置文件(--readconfig)(声明式,mikropkl 使用)
QEMU 的 --readconfig 会加载 INI 格式的文件来配置设备和机器。mikropkl 项目使用这种方式实现声明式 VM 打包。
权衡: 可将配置与启动分离,但 INI 格式较为生僻,而且并非所有 QEMU 选项都能在其中表达(pflash、-accel、-netdev user,hostfwd 都需要通过命令行参数指定)。最适合以编程方式生成配置的项目。
启动路径
使用 SeaBIOS 启动 x86_64(默认,最快)
无需设置固件——QEMU 内置的 SeaBIOS 可处理 MikroTik 的专有启动扇区:
qemu-system-x86_64 -M q35 -m 256 \
-drive file=chr-7.22.img,format=raw,if=virtio \
-netdev user,id=net0,hostfwd=tcp::9180-:80 \
-device virtio-net-pci,netdev=net0 \
-display none -serial stdio
-M pc(i440fx,传统 PCI)同样适用于 CHR。CHR 不会用到 q35 提供的大多数功能(现代 PCIe 拓扑、基于 ACPI 的热插拔等),因为 RouterOS 内核启动后会绕过 BIOS/ACPI 栈——它实际使用的是 VirtIO PCI 设备,而两种机器类型提供的这些设备完全相同。有关这方面的内核配置证据,请参阅 tikoci/mikrotik-gpl(v7.2 内核配置档案)。
启动时间:约 5 秒(KVM),约 30 秒(TCG)。使用 TCG 时,-accel tcg,tb-size=256 可扩大转换块缓存,在重复运行时明显缩短启动时间。
使用 UEFI(EDK2)启动 aarch64
需要 UEFI pflash 固件文件。两个 pflash 单元的大小必须相同(通常为 64 MiB):
# Copy vars file (writable) — never modify the original
cp /path/to/edk2-arm-vars.fd /tmp/my-vars.fd
qemu-system-aarch64 -M virt -cpu cortex-a710 -m 256 \
-drive if=pflash,format=raw,readonly=on,unit=0,file=/path/to/edk2-aarch64-code.fd \
-drive if=pflash,format=raw,unit=1,file=/tmp/my-vars.fd \
-drive file=chr-arm64.img,format=raw,if=none,id=drive0 \
-device virtio-blk-pci,drive=drive0 \
-netdev user,id=net0,hostfwd=tcp::9180-:80 \
-device virtio-net-pci,netdev=net0 \
-display none -serial stdio
启动时间:约 10 秒(KVM),约 20 秒(TCG 原生),约 20 秒(x86 主机上的 TCG 跨架构运行)。
UEFI 固件位置
| 平台 | 代码 ROM | Vars 文件 |
|---|---|---|
| macOS Homebrew(Apple Silicon) | /opt/homebrew/share/qemu/edk2-aarch64-code.fd | edk2-arm-vars.fd |
| macOS Homebrew(Intel) | /usr/local/share/qemu/edk2-aarch64-code.fd | edk2-arm-vars.fd |
| Ubuntu/Debian | /usr/share/AAVMF/AAVMF_CODE.fd | AAVMF_VARS.fd |
| x86 OVMF(Homebrew) | edk2-x86_64-code.fd | edk2-i386-vars.fd |
| x86 OVMF(Linux) | /usr/share/OVMF/OVMF_CODE.fd | OVMF_VARS.fd |
VirtIO — 关键细节
完整表格请参阅 VirtIO 驱动程序矩阵。
唯一的规则: RouterOS 支持 virtio_pci,但不支持 virtio_mmio。这在 aarch64 上很重要。
if=virtio 陷阱(aarch64)
x86_64 (q35) aarch64 (virt)
if=virtio shorthand → virtio-blk-pci (PCI) ✅ virtio-blk-device (MMIO) ❌
-device virtio-blk-pci → virtio-blk-pci (PCI) ✅ virtio-blk-pci (PCI) ✅
在 x86_64 q35 上,if=virtio 会解析为 PCI,可正常工作。在 aarch64 virt 上,它会解析为 MMIO,导致 RouterOS 内核无声卡死。在 aarch64 上始终使用显式的 -device virtio-blk-pci:
# WRONG on aarch64 — silent boot failure
-drive file=chr.img,format=raw,if=virtio
# CORRECT on aarch64 — explicit PCI device
-drive file=chr.img,format=raw,if=none,id=drive0
-device virtio-blk-pci,drive=drive0
在 x86_64 上,两种方式都可用。显式写法在两种架构上始终安全。
网络 — 通用
所有架构均使用 virtio-net-pci,没有例外:
-netdev user,id=net0,hostfwd=tcp::9180-:80
-device virtio-net-pci,netdev=net0
user(SLIRP)是用户空间 NAT,适合管理用途(通过 hostfwd 转发 REST/SSH/WinBox),
但它会终止第 2 层:广播和未转发的 UDP 数据包都无法到达主机或其他虚拟机。对于任何第 2 层用途(虚拟机间链路、邻居发现、MAC-Telnet),请在 socket netdev 上添加第二块网卡。
第 2 层网络:socket netdev(虚拟机间链路 + 主机抓包)
QEMU 的 socket netdev 通过主机套接字传输原始以太网帧,无需 root 权限,也无需额外设置:
# TCP, point-to-point — one side listens, the other connects
-netdev socket,id=net1,listen=:4001 # VM A (start first)
-netdev socket,id=net1,connect=127.0.0.1:4001 # VM B
# UDP multicast — shared L2 segment for N VMs
-netdev socket,id=net1,mcast=230.0.0.1:4001
⚠
mcast在 macOS 上无法正常工作。 QEMU 的 mcast 套接字只设置了SO_REUSEADDR,但 macOS/BSD 要求所有共享多播端口的套接字都设置SO_REUSEPORT,因此 macOS 上本地套接字之间不会传递任何数据:同一组中的两台虚拟机无法互相发现,主机监听器也收不到任何帧。它在 Linux(以及 CI)上可用。在 macOS 上进行点对点连接,请使用 TCPlisten/connect;目前 macOS 上没有无需 root 权限的多虚拟机第 2 层网段(可使用只需提权一次的socket_vmnet实现)。
在主机上抓取访客第 2 层帧(例如 MNDP)
要让主机进程接收访客的广播(首先要处理的是 MNDP 邻居发现,即 WinBox“Neighbors”选项卡使用的协议,UDP/5678),请让主机成为 TCP socket netdev 的一端:
- 主机在一个环回端口上运行 TCP 服务器。
- CHR 添加第二块网卡:
-netdev socket,id=net1,connect=127.0.0.1:<port>— QEMU 是发起连接的一方,因此必须在 QEMU 启动之前开始监听。 - QEMU 将每个访客帧传送到主机,帧采用长度前缀格式:4 字节大端长度,后接原始以太网帧。
- 主机去除前缀,并解析 Ethernet → IPv4 → UDP/5678 → MNDP TLV。
通过同一连接写回带长度前缀的帧,可将第 2 层数据注入访客(例如发送 MNDP 刷新以触发即时回复——这是 MAC-Telnet 所需的同一基本机制)。仅限环回连接,因此不会泄漏到局域网;在 macOS、Linux 和 Windows 上均可使用。
参考实现和已验证证据:tikoci/quickchr(examples/mndp/、
docs/mndp.md、test/lab/mndp/REPORT.md)。MNDP 线上格式和 TLV 表请参阅
routeros-mndp 技能。
加速检测
import { $ } from "bun";
async function detectAccel(guestArch: string): Promise<string> {
const hostOs = process.platform; // "darwin" | "linux"
const hostArch = process.arch; // "x64" | "arm64"
if (hostOs === "linux") {
// KVM requires host/guest architecture match
const kvm = await Bun.file("/dev/kvm").exists();
const archMatch = (guestArch === "x86_64" && hostArch === "x64")
|| (guestArch === "aarch64" && hostArch === "arm64");
if (kvm && archMatch) return "kvm";
}
if (hostOs === "darwin") {
// HVF may not be available (e.g., GitHub Actions VMs)
const hvOk = await $`sysctl -n kern.hv_support`.text().then(s => s.trim() === "1").catch(() => false);
const archMatch = (guestArch === "aarch64" && hostArch === "arm64")
|| (guestArch === "x86_64" && hostArch === "x64");
if (hvOk && archMatch) return "hvf";
}
return "tcg"; // Software emulation — always available
}
关键规则: KVM 和 HVF 都要求主机与访客架构匹配。跨架构时始终回退到 TCG。不要只检查 /dev/kvm,还要确认架构匹配。
HVF + CPU 型号注意事项(macOS)
使用 -accel hvf 时,QEMU 会直接暴露主机 CPU。在 Apple Silicon(ARMv8.5)上指定 cortex-a710 等 CPU 型号(ARMv9,需要 SVE2)会导致 QEMU 在虚拟机启动前崩溃。使用 HVF 时请指定 -cpu host:
# TCG/KVM — specify exact model
CPU_FLAGS="-cpu cortex-a710"
# HVF — passthrough host CPU
if [ "$ACCEL" = "hvf" ]; then
CPU_FLAGS="-cpu host"
fi
健康检查与等待启动
无需身份验证,RouterOS WebFig 会在端口 80 上响应 HTTP 200,这可用作最简健康检查:
async function waitForBoot(url: string, timeoutMs = 60_000): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const r = await fetch(url, { signal: AbortSignal.timeout(2000) });
if (r.ok) return true;
} catch { /* not ready yet */ }
await Bun.sleep(2000);
}
return false;
}
启动竞态——随后立即调用 REST API 时的陷阱
从客户端的角度来看,RouterOS 的启动分为多个阶段:
- 连接被拒绝——QEMU 仍在启动
ECONNRESET——HTTP 服务器已启动,但 REST 子系统尚未接受请求/rest/*返回 401——身份验证中间件已启动;REST 处理程序可能仍在初始化- 返回 200,但响应正文错误——REST 在路由表稳定之前已初始化;
/system/resource可能会短暂返回一个数组(例如/user列表),随后才返回预期的单个对象 - 返回 200,且响应正文正确——已完全就绪
对根路径 / 执行健康检查只能确认已到达阶段 2,无法判断是否已到达阶段 3–5。对于需要立即调用 REST API 的代码,请对 /rest/system/resource 进行带身份验证的探测,并要求连续两次探测成功且响应正文符合预期结构(包含 board-name 的单个对象)。仅检查身份验证状态(未配置的管理员)时,将 401/403 视为“REST 层已响应”。检查 / 返回 HTTP 200 适用于监控“服务是否启动”,但不适用于判断“是否可以开始调用 REST”。
端口转发
QEMU 用户模式网络(-netdev user,hostfwd=...)可用于常见的 RouterOS 服务:
| 服务 | Guest 端口 | 主机端口示例 | hostfwd |
|---|---|---|---|
| WebFig/REST API | 80 | 9180 | tcp::9180-:80 |
| SSH(RouterOS CLI) | 22 | 9122 | tcp::9122-:22 |
| API 协议 | 8728 | 9728 | tcp::9728-:8728 |
| API-SSL | 8729 | 9729 | tcp::9729-:8729 |
| WinBox | 8291 | 9291 | tcp::9291-:8291 |
在一个 netdev 中配置多个转发:
-netdev user,id=net0,hostfwd=tcp::9180-:80,hostfwd=tcp::9122-:22,hostfwd=tcp::9728-:8728
运行多个 CHR 实例时,每个实例使用唯一的主机端口(9180、9181、9182……)。
已知限制
- QGA(Guest Agent)需要 KVM——RouterOS CHR 的 QGA 守护进程只有在通过 CPUID 检测到 KVM 虚拟机监控程序时才会启动。在 HVF(macOS)或 TCG(软件仿真)下,CPUID 0x40000000 不会返回 KVM 厂商字符串,0x40000001 也不会返回 KVM 功能,因此该守护进程永远不会启动。QEMU 会正确提供 virtio-serial 端口并发送 PORT_OPEN(事件 6)——只是 Guest 始终不会打开该端口(
query-chardev显示frontend-open=false)。这不是 QEMU 的问题。MikroTik 仅在“KVM”部分介绍 QGA。测试 QGA 需要 Linux + KVM(例如 mikropkl lab)。 - 在 aarch64 上,
check-installation在所有 QEMU 环境中都会失败——这是无法解决的固件/DTB 问题(参见已知问题) - 两种架构都无法通过直接
-kernel启动——RouterOS 需要完整的固件启动路径 - 跨架构 TCG:在 aarch64 主机上运行 x86_64 不可行——x86 I/O 端口仿真速度过慢(超时约 300 秒以上)。反过来(在 x86_64 上运行 aarch64)则运行正常(约 20 秒)
- 没有
virtio_mmio驱动——始终显式使用-device virtio-blk-pci,在 aarch64 上绝不要依赖if=virtio - macOS 上的
socket,mcast=虚拟机间网络不可用——QEMU 只设置了SO_REUSEADDR;macOS/BSD 需要SO_REUSEPORT才能共享多播端口,因此本地套接字之间不会传递任何帧(虚拟机无法发现彼此;主机抓包也收不到数据)。点对点连接请使用 TCPsocket(listen/connect),或使用socket_vmnet创建共享网段。在 Linux/CI 上可正常工作。已在tikoci/quickchr中验证(test/lab/mndp/REPORT.md)
其他资源
- VirtIO 驱动矩阵 — 完整的驱动支持表
- 已知问题 — 启动失败、跨架构限制
- GitHub Actions CI 模式 — 在 GitHub 托管的运行器上运行 CHR
- CHR 许可 — 免费套餐(1 Mbps)、60 天试用、付费套餐、过期行为
- quickchr 参考实现 — 通过测试和自动化驱动 CHR(
QuickCHR.start、exec/rest、网络配置示例、用于测试框架的连接接口):参见routeros-quickchr技能 - 启动后使用 RouterOS CLI/REST:参见
routeros-fundamentals技能 - 从 CHR 捕获数据包并通过 TZSP 流式传输:参见
routeros-sniffer技能 - MNDP 邻居发现线路格式 / TLV(通过上述主机捕获配置示例接收):参见
routeros-mndp技能 - /app YAML 容器格式(需要安装 container 软件包的 CHR):参见
routeros-app-yaml技能