routeros-qemu-chr について
QEMU で x86_64 および aarch64 の RouterOS CHR 仮想マシンを動かす、ネットワーク技術者や開発者向けのガイドです。コマンドライン、スクリプト、プログラムからの起動方法に加え、ファームウェア、VirtIO、ネットワーク、起動確認の注意点を扱います。
主な機能
- 01CHR イメージのアーキテクチャと起動方式の解説
- 02複数の QEMU 起動パターンの提示
- 03VirtIO ディスクとネットワーク設定の説明
- 04ユーザーモード NAT と L2 接続の解説
- 05アクセラレーション選択と起動確認の説明
活用シーン
- 01QEMU での RouterOS CHR 実行
- 02複数インスタンスや統合テスト環境の構築
- 03仮想マシンの接続と L2 トラフィックのキャプチャ
- 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 とは
Cloud Hosted Router(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:インライン引数(最も簡単。スクリプト向け)
すべてをコマンドラインで指定します。状態を一か所ですべて確認できるため、LLM が組み立てたりデバッグしたりするのも簡単です。
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
メリット: コマンドが 1 つで、読みやすく、変更も簡単です。 デメリット: コマンドラインが長く、バージョン管理が難しく、永続化されません。
パターン B:ラッパースクリプト(再利用向け)
アクセラレーションの検出、ファームウェアパスの処理、PID ファイルの管理を行うシェルスクリプトです。
#!/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
メリット: 移植性があり、プラットフォームの違いに対応し、パラメーター化できます。 デメリット: シェルスクリプトの制約があり、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 であり、管理(REST/SSH/WinBox の hostfwd)に最適です。
ただし、レイヤー 2 を終端します。ブロードキャストや転送されない UDP は、ホストや
他の VM に届きません。L2 を使う用途(VM 間リンク、近隣探索、MAC-Telnet)では、socket netdev に
2 つ目の NIC を追加してください。
L2 ネットワーク:socket netdev(VM 間リンクとホストでのキャプチャ)
QEMU の socket netdev は、生の Ethernet フレームをホストソケット経由で転送します。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
⚠ macOS では
mcastは動作しません。 QEMU の mcast ソケットはSO_REUSEADDRのみを設定しますが、 macOS/BSD ではマルチキャストポートを共有するすべてのソケットにSO_REUSEPORTが必要です。そのため macOS では ローカルソケット間で何も配信されません。同じグループ上の 2 台の VM は互いを検出できず、ホストのリスナーにも フレームが一切届きません。Linux(および CI)では動作します。macOS でポイントツーポイント接続を行うには TCP のlisten/connectを使用してください。 現時点で macOS に root 権限不要のマルチ VM L2 セグメントはありません(その場合は、最初に一度だけ特権が必要なsocket_vmnetを使用してください)。
ゲストの L2 フレームをホストでキャプチャする(例:MNDP)
ゲストのブロードキャストをホストプロセスで受信できるようにするには、ホストを TCP socket netdev の一端にします。
最初の用途として、MNDP 近隣探索(WinBox の「Neighbors」タブで使用されるプロトコル、UDP/5678)を想定しています。
- ホストで、ループバックポート上に TCP サーバーを起動します。
- CHR に 2 つ目の NIC を設定します:
-netdev socket,id=net1,connect=127.0.0.1:<port>。QEMU が接続側なので、QEMU の起動前にホストがリッスンしている必要があります。 - QEMU はすべてのゲストフレームを、長さのプレフィックス付きでホストにストリーミングします。4 バイトのビッグエンディアン長の後に、生の Ethernet フレームが続きます。
- ホストでプレフィックスを取り除き、Ethernet → IPv4 → UDP/5678 → MNDP TLV の順に解析します。
同じ接続で長さプレフィックス付きフレームを書き戻すと、L2 フレームをゲストに注入できます (例:即時応答を促す MNDP リフレッシュ。同じ基本機能を MAC-Telnet も必要とします)。 ループバックのみを使うため、LAN に何も漏れません。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(SVE2 が必要な ARMv9)などの CPU モデルを指定すると、VM の起動前に 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 を含む単一オブジェクト)で成功するプローブが2 回連続することを確認してください。認証のみのチェック(プロビジョニング前の admin)では、401/403 を「REST レイヤーが応答した」として受け入れてください。/ が HTTP 200 を返すかのチェックは「起動しているか」の監視には適していますが、「今すぐ REST の呼び出しを開始できるか」の確認には適していません。
ポートフォワーディング
一般的な RouterOS サービス向けの QEMU ユーザーモードネットワーク(-netdev user,hostfwd=...):
| サービス | ゲストポート | ホストポートの例 | 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 |
1 つの netdev で複数のポートをフォワードする場合:
-netdev user,id=net0,hostfwd=tcp::9180-:80,hostfwd=tcp::9122-:22,hostfwd=tcp::9728-:8728
複数の CHR を実行する場合は、インスタンスごとに異なるホストポート(9180、9181、9182...)を使用してください。
既知の制限事項
- QGA(ゲストエージェント)には KVM が必要です — RouterOS CHR の QGA デーモンは、CPUID を通じて KVM ハイパーバイザーを検出した場合にのみ起動します。HVF(macOS)または TCG(ソフトウェアエミュレーション)では、CPUID 0x40000000 が KVM ベンダー文字列を返さず、0x40000001 も KVM の機能を返さないため、デーモンは起動しません。QEMU は virtio-serial ポートを正しく提供し、PORT_OPEN(イベント 6)を送信しますが、ゲストはポートを開きません(
query-chardevではfrontend-open=falseと表示されます)。これは QEMU のバグではありません。MikroTik のドキュメントでは、QGA は「KVM」セクションにのみ記載されています。QGA のテストには Linux + KVM(例:mikropkl lab)が必要です。 check-installationはすべての QEMU 環境で aarch64 上では失敗します — 解決できないファームウェア/DTB の問題です(既知の問題を参照)- 直接の
-kernel起動はどちらのアーキテクチャでも動作しません — RouterOS には完全なファームウェア起動パスが必要です - クロスアーキテクチャ TCG:aarch64 ホスト上の x86_64 は実用的ではありません — x86 の I/O ポートエミュレーションが遅すぎます(タイムアウトまで約 300 秒以上)。逆方向(x86_64 上の aarch64)は問題なく動作します(約 20 秒)
virtio_mmioドライバーはありません — 常に明示的に-device virtio-blk-pciを使用し、aarch64 ではif=virtioに依存しないでくださいsocket,mcast=による VM 間ネットワークは macOS では動作しません — QEMU が設定するのはSO_REUSEADDRのみです。macOS/BSD ではマルチキャストポートを共有するためにSO_REUSEPORTが必要なため、ローカルソケット間でフレームが配信されません(VM 同士が相互に検出できず、ホストでのキャプチャにも何も表示されません)。ポイントツーポイント通信には 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スキルを参照してください