mcpField
R
routeros-qemu-chr

routeros-qemu-chr

github.com/tikociクラウド・DevOps8343 回インストール5 日前に更新
リポジトリ

routeros-qemu-chr について

QEMU で x86_64 および aarch64 の RouterOS CHR 仮想マシンを動かす、ネットワーク技術者や開発者向けのガイドです。コマンドライン、スクリプト、プログラムからの起動方法に加え、ファームウェア、VirtIO、ネットワーク、起動確認の注意点を扱います。

主な機能

  1. 01CHR イメージのアーキテクチャと起動方式の解説
  2. 02複数の QEMU 起動パターンの提示
  3. 03VirtIO ディスクとネットワーク設定の説明
  4. 04ユーザーモード NAT と L2 接続の解説
  5. 05アクセラレーション選択と起動確認の説明

活用シーン

  1. 01QEMU での RouterOS CHR 実行
  2. 02複数インスタンスや統合テスト環境の構築
  3. 03仮想マシンの接続と L2 トラフィックのキャプチャ
  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 とは

Cloud Hosted Router(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:インライン引数(最も簡単。スクリプト向け)

すべてをコマンドラインで指定します。状態を一か所ですべて確認できるため、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 ファームウェアの場所
プラットフォームコード 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 であり、管理(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)を想定しています。

  1. ホストで、ループバックポート上に TCP サーバーを起動します。
  2. CHR に 2 つ目の NIC を設定します:-netdev socket,id=net1,connect=127.0.0.1:<port>。QEMU が接続側なので、QEMU の起動前にホストがリッスンしている必要があります。
  3. QEMU はすべてのゲストフレームを、長さのプレフィックス付きでホストにストリーミングします。4 バイトのビッグエンディアン長の後に、生の Ethernet フレームが続きます。
  4. ホストでプレフィックスを取り除き、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 の起動は段階的に進みます。

  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 を含む単一オブジェクト)で成功するプローブが2 回連続することを確認してください。認証のみのチェック(プロビジョニング前の admin)では、401/403 を「REST レイヤーが応答した」として受け入れてください。/ が HTTP 200 を返すかのチェックは「起動しているか」の監視には適していますが、「今すぐ REST の呼び出しを開始できるか」の確認には適していません。

ポートフォワーディング

一般的な RouterOS サービス向けの QEMU ユーザーモードネットワーク(-netdev user,hostfwd=...):

サービスゲストポートホストポートの例hostfwd
WebFig/REST API809180tcp::9180-:80
SSH(RouterOS CLI)229122tcp::9122-:22
API プロトコル87289728tcp::9728-:8728
API-SSL87299729tcp::9729-:8729
WinBox82919291tcp::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 同士が相互に検出できず、ホストでのキャプチャにも何も表示されません)。ポイントツーポイント通信には 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 スキルを参照してください

コメント