mcpField
R
routeros-qemu-chr

routeros-qemu-chr

github.com/tikoci云与 DevOps8343 次安装5 天前更新
代码仓库

关于 routeros-qemu-chr

面向网络工程师和开发者的 RouterOS CHR 虚拟机运行指南,聚焦通过 QEMU 启动 x86_64 与 aarch64 镜像。它比较命令行、脚本和程序化启动方式,并解释启动固件、VirtIO、网络及就绪检测的注意事项。

主要功能

  1. 01介绍 CHR 镜像架构与启动方式
  2. 02提供多种 QEMU 启动模式
  3. 03说明 VirtIO 磁盘与网络配置
  4. 04讲解用户态 NAT 与二层网络连接
  5. 05说明加速选择和启动就绪检测

适用场景

  1. 01在 QEMU 中运行 RouterOS CHR
  2. 02构建多实例或集成测试环境
  3. 03连接虚拟机并捕获二层流量
  4. 04排查固件、设备或启动就绪问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tikoci/routeros-skills --skill routeros-qemu-chr

安装后在 Claude Code 中输入/routeros-qemu-chr确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
chr-licensing.md
github-actions-ci.md
known-issues.md
virtio-drivers.md

详细介绍

使用 QEMU 运行 RouterOS CHR

什么是 CHR

云托管路由器(CHR)是 MikroTik 面向虚拟机设计的 x86_64 和 aarch64 架构 RouterOS 镜像。免费许可证允许无限制使用,但速度限制为 1 Mbps,足以满足开发、测试、API 工作和数据包嗅探调试的需求。免费的 60 天试用版可完全移除速度限制(需要注册免费的 mikrotik.com 账号)。有关许可证等级、试用版激活和过期行为的完整详情,请参阅 CHR 许可证参考。

镜像变体

镜像架构启动方式来源
chr-<ver>.imgx86_64SeaBIOS(MBR 链式加载)download.mikrotik.com
chr-<ver>-arm64.imgaarch64UEFI(EDK2 pflash)download.mikrotik.com
chr-efi.img(fat-chr)x86_64UEFI(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 固件位置
平台代码 ROMVars 文件
macOS Homebrew(Apple Silicon)/opt/homebrew/share/qemu/edk2-aarch64-code.fdedk2-arm-vars.fd
macOS Homebrew(Intel)/usr/local/share/qemu/edk2-aarch64-code.fdedk2-arm-vars.fd
Ubuntu/Debian/usr/share/AAVMF/AAVMF_CODE.fdAAVMF_VARS.fd
x86 OVMF(Homebrew)edk2-x86_64-code.fdedk2-i386-vars.fd
x86 OVMF(Linux)/usr/share/OVMF/OVMF_CODE.fdOVMF_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 上进行点对点连接,请使用 TCP listen/connect;目前 macOS 上没有无需 root 权限的多虚拟机第 2 层网段(可使用只需提权一次的 socket_vmnet 实现)。

在主机上抓取访客第 2 层帧(例如 MNDP)

要让主机进程接收访客的广播(首先要处理的是 MNDP 邻居发现,即 WinBox“Neighbors”选项卡使用的协议,UDP/5678),请让主机成为 TCP socket netdev 的一端:

  1. 主机在一个环回端口上运行 TCP 服务器。
  2. CHR 添加第二块网卡:-netdev socket,id=net1,connect=127.0.0.1:<port> — QEMU 是发起连接的一方,因此必须在 QEMU 启动之前开始监听。
  3. QEMU 将每个访客帧传送到主机,帧采用长度前缀格式:4 字节大端长度,后接原始以太网帧。
  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 的启动分为多个阶段:

  1. 连接被拒绝——QEMU 仍在启动
  2. ECONNRESET——HTTP 服务器已启动,但 REST 子系统尚未接受请求
  3. /rest/* 返回 401——身份验证中间件已启动;REST 处理程序可能仍在初始化
  4. 返回 200,但响应正文错误——REST 在路由表稳定之前已初始化;/system/resource 可能会短暂返回一个数组(例如 /user 列表),随后才返回预期的单个对象
  5. 返回 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 API809180tcp::9180-:80
SSH(RouterOS CLI)229122tcp::9122-:22
API 协议87289728tcp::9728-:8728
API-SSL87299729tcp::9729-:8729
WinBox82919291tcp::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 才能共享多播端口,因此本地套接字之间不会传递任何帧(虚拟机无法发现彼此;主机抓包也收不到数据)。点对点连接请使用 TCP socket(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 技能

评论

支持 Markdown