OpenWrt in QEMU

QEMU is an open-source processor emulator and virtualizer. This document describes how to run OpenWrt as a guest operating system in QEMU. If you are looking to use OpenWrt as a QEMU host, see Running QEMU guests on OpenWrt.

It is mixed descriptions from Windows and Linux, so please read through all of it before starting.

:!: Choosing different emulation settings can affect performance greatly.

Example: 30s iperf-s@openwrt (QEMU running on host) to host

ne2k_pci:0.0-31.3 sec  14.6 MBytes  3.92 Mbits/sec
pcnet: 0.0-30.0 sec  2.38 GBytes   682 Mbits/sec
e1000: 0.0-30.0 sec  6.23 GBytes  1.79 Gbits/sec
vmxnet3: 0.0-30.0 sec  8.67 GBytes  2.48 Gbits/sec
virtio-net-pci:  0.0-30.0 sec  44.6 GBytes  12.8 Gbits/sec
  • FIXME Trunk: test kernel image with rootfs
  • FIXME Trunk: use SD card with rootfs, NFS rootfs, NBD rootfs
  • FIXME Trunk: no sound, pcibus, USB emulation in QEMU possible?

QEMU is compatible with a wide range of operating systems. For most users, the easiest way to install it is through your system's package manager.

Most major Linux distributions (including Debian, Ubuntu, openSUSE, and Fedora) include QEMU in their official repositories.

Full Installation: Install the complete suite to emulate all supported architectures.

# Debian
sudo apt-get install qemu

# Fedora
sudo dnf install qemu

Targeted Architecture: If you only need to run or debug specific virtual machines (e.g., x86_64), you can save disk space by installing only that target:

# Debian
sudo apt-get install qemu-system-x86_64

The most straightforward way to install QEMU on macOS is using Homebrew or MacPorts.

# homebrew
homebrew install qemu

# MacPorts
sudo port install qemu

On Apple Silicon (M1/M2/M3/M4/M5) Macs, QEMU supports the native HVF (Hypervisor.framework) acceleration backend for near-native performance when running ARM-based guest OSes.

Install manually by downloading the latest 64-bit installer from the official QEMU download page, or use a package manager:

# Winget
winget install -e --id SoftwareFreedomConservancy.QEMU

# Chocolatey
choco install qemu

To enable hardware acceleration on Windows, ensure WHPX (Windows Hypervisor Platform) or HAXM is enabled in your system settings.


When running OpenWrt in QEMU, you can either emulate a foreign architecture (e.g., running ARM code on an x86_64 host) or virtualize a matching architecture with hardware acceleration (e.g., running x86_64 on x86_64 with KVM).

  • Native (Accelerated): Uses KVM (Linux), HVF (macOS), or WHPX (Windows) for near-native performance. Requires the host and guest architectures to match.
  • Emulated: QEMU translates every instruction, which is significantly slower but allows running any supported architecture on any host.

For the best experience, use an accelerated setup whenever possible.

Linux hosts can use KVM for full hardware acceleration. The libvirt management framework can also be used to manage QEMU guests more easily. Non-accelerated emulation (without KVM) is also possible but slower.

qemu-system-x86_64 -enable-kvm .. # other QEMU options

On Apple Silicon Macs (M1+), QEMU can use HVF for native virtualization of ARM guests. This provides near-native performance. You must use an armsr/armv8 OpenWrt image for this to work.

qemu-system-aarch64 -accel hvf .. # other QEMU options

OpenWrt provides several types of images. Understanding these is key to choosing the right boot method.

Image Type Description
generic-ext4-combined-efi.img.gz Combined kernel + rootfs. Uses ext4 (read-write). GPT partition table for UEFI boot.
generic-ext4-combined.img.gz Combined kernel + rootfs. Uses ext4 (read-write). MBR partition table for legacy BIOS boot.
generic-ext4-rootfs.img.gz Root filesystem only to use with a separate generic-kernel.bin.
generic-initramfs-kernel.bin Kernel with embedded initramfs. Runs entirely in RAM (non-persistent).
generic-kernel.bin Kernel only. Requires a separate rootfs image (generic-ext4-rootfs.img, generic-squashfs-rootfs.img or local folder on host).
generic-squashfs-combined-efi.img.gz Combined kernel + rootfs. Uses squashfs (read-only) + overlay. UEFI boot.
generic-squashfs-combined.img.gz Combined kernel + rootfs. Uses squashfs (read-only) + overlay. Legacy BIOS boot.
generic-squashfs-rootfs.img.gz Squashfs rootfs only. Use with a separate kernel.
generic-targz-rootfs.tar.gz Rootfs tarball for containers or manual installation.
rootfs.cpio.gz Rootfs in cpio archive format (for initramfs).
rootfs.tar.gz Rootfs tarball (generic).

Open https://downloads.openwrt.org/ and find desired OpenWrt release and architecture. For Arm64 use armsr target.

3.2.1. Initramfs (non-persistent)

Simplest method for testing. Runs entirely in RAM; changes are lost on reboot.

# x86_64
qemu-system-x86_64 -M q35 -m 512 -nographic \
  -kernel openwrt-x86-64-generic-initramfs-kernel.bin

# armv8
qemu-system-aarch64 -M virt-8.2 -m 512 -nographic \
  -kernel openwrt-armsr-armv8-generic-initramfs-kernel.bin

3.2.2. Separate Kernel and Root Filesystem

Boot a kernel and point it to a separate rootfs image via VirtIO block device.

# x86_64
qemu-system-x86_64 -M q35 -m 512 -nographic \
  -kernel openwrt-x86-64-generic-kernel.bin \
  -drive file=openwrt-x86-64-generic-ext4-rootfs.img,format=raw,if=virtio \
  -append "root=/dev/vda console=ttyS0 rootwait"

# armv8
qemu-system-aarch64 -M virt-8.2 -m 512 -nographic \
  -kernel openwrt-armsr-armv8-generic-kernel.bin \
  -drive file=openwrt-armsr-armv8-generic-ext4-rootfs.img,format=raw,if=virtio \
  -append "root=/dev/vda rootwait"

3.2.3. Combined Kernel and Root Filesystem Images

Straightforward persistent method. Use a combined image as a drive.

# x86_64 boot using BIOS
qemu-system-x86_64 -M q35 -m 512 -nographic \
  -drive file=openwrt-x86-64-generic-ext4-combined.img,format=raw,if=virtio

# x86_64 boot using UEFI
qemu-system-x86_64 -M q35 -m 512 -nographic \
  -drive file=openwrt-x86-64-generic-ext4-combined-efi.img,format=raw,if=virtio

# armv8 boot using UEFI (change to your actual path to UEFI firmware image supplied with QEMU)
qemu-system-aarch64 -M virt-8.2 -m 512 -nographic \
  -drive file=openwrt-armsr-armv8-generic-ext4-combined-efi.img,format=raw,if=virtio \
  -bios /opt/homebrew/share/qemu/edk2-aarch64-code.fd

3.2.4. Boot with Local Directory as Rootfs

Use a local directory on the host as the guest's root filesystem via 9p driver.

# x86_64
mkdir -p root-fs && tar xvz -C root-fs -f openwrt-x86-64-rootfs.tar.gz

qemu-system-x86_64 -M q35 -m 512 -nographic \
  -kernel openwrt-x86-64-generic-kernel.bin \
  -fsdev local,id=rootdev,path=./root-fs/,security_model=none \
  -device virtio-9p-pci,fsdev=rootdev,mount_tag=/dev/root \
  -append "rootflags=trans=virtio,version=9p2000.L,cache=loose rootfstype=9p"

# armv8
mkdir -p root-fs && tar xvz -C root-fs -f openwrt-armsr-armv8-rootfs.tar.gz

qemu-system-aarch64 -M virt-8.2 -m 512 -nographic \
  -kernel openwrt-armsr-armv8-generic-kernel.bin \
  -fsdev local,id=rootdev,path=./root-fs/,security_model=none \
  -device virtio-9p-pci,fsdev=rootdev,mount_tag=/dev/root \
  -append "rootflags=trans=virtio,version=9p2000.L,cache=loose rootfstype=9p" \
  -bios /opt/homebrew/share/qemu/edk2-aarch64-code.fd

By default, QEMU's user-mode network stack provides NAT and DHCP/DNS to the guest, so the guest can access the internet by default.

3.3.1. Setting up Network Cards in QEMU

QEMU offers several networking options. The modern approach uses -netdev and -device:

-device virtio-net-pci,netdev=net0 \
-netdev user,id=net0,net=192.168.1.0/24

To provide two interfaces (LAN and WAN) for a more router-like setup:

# LAN_IP=192.168.1.0/24
# WAN_IP=10.1.1.0/24

-netdev user,id=lan,net=${LAN}> \
-device virtio-net-pci,netdev=lan \
-netdev user,id=wan,net=${WAN} \
-device virtio-net-pci,netdev=wan

In OpenWrt, eth0 will be enslaved as a bridge port of br-lan and eth1 will be WAN.

3.3.2. Port Forwarding to SSH and LuCI

-netdev user,id=net0,net=192.168.1.0/24,hostfwd=tcp:127.0.0.1:2222-192.168.1.1:22,hostfwd=tcp:127.0.0.1:8888-192.168.1.1:80 \
-device virtio-net-pci,netdev=net0

This creates a user-mode network with port forwarding for SSH and LuCI.

Access:

Keep in mind that you need to install LuCI from a command line: apk update && apk add luci

If for some reason you want to connect to OpenWrt via WAN adapter - add firewall rules to allow SSH:

uci -q delete firewall.ssh
uci set firewall.ssh="rule"
uci set firewall.ssh.name="Allow-SSH"
uci set firewall.ssh.src="wan"
uci set firewall.ssh.dest_port="22"
uci set firewall.ssh.proto="tcp"
uci set firewall.ssh.target="ACCEPT"
uci commit firewall
/etc/init.d/firewall restart

The x86_64 target is well-supported. Use the q35 machine type for modern PCIe emulation.

Boot combined image with ext4, using KVM:

qemu-system-x86_64 \
  -enable-kvm \
  -M q35 -m 512 -smp 2 -nographic \
  -drive file=openwrt-x86-64-generic-ext4-combined.img,format=raw,if=virtio \
  -netdev user,id=net0,net=192.168.1.0/24,hostfwd=tcp:127.0.0.1:2222-192.168.1.1:22,hostfwd=tcp:127.0.0.1:8888-192.168.1.1:80 \
  -device virtio-net-pci,netdev=net0 \
  -netdev user,id=net1,net=10.1.1.0/24 \
  -device virtio-net-pci,netdev=net1

For OpenWrt 23 and newer, use the armsr target (replaces armvirt). The armv8 subtarget is for 64-bit ARM.

Boot combined image with ext4, using Apple Hypervisor.framework (hvf)

qemu-system-aarch64 \
  -accel hvf \
  -M virt-8.2 -m 512 -smp 2 -nographic \
  -drive file=openwrt-armsr-armv8-generic-ext4-combined-efi.img,format=raw,if=virtio \
  -netdev user,id=net0,net=192.168.1.0/24,hostfwd=tcp:127.0.0.1:2222-192.168.1.1:22,hostfwd=tcp:127.0.0.1:8888-192.168.1.1:80 \
  -device virtio-net-pci,netdev=net0 \
  -netdev user,id=net1,net=10.1.1.0/24 \
  -device virtio-net-pci,netdev=net1 \
  -bios /opt/homebrew/share/qemu/edk2-aarch64-code.fd

The malta target emulates a MIPS development board. It supports 32-bit and 64-bit, big and little-endian variants.

Little-endian 32-bit

qemu-system-mipsel -kernel bin/targets/malta/le/openwrt-malta-le-vmlinux-initramfs.elf \
  -nographic -m 256

Big-endian 32-bit

qemu-system-mips -kernel bin/targets/malta/be/openwrt-malta-be-vmlinux-initramfs.elf \
  -nographic -m 256

Little-endian 64-bit

qemu-system-mips64el -kernel bin/targets/malta/le64/openwrt-malta-le64-vmlinux-initramfs.elf \
  -cpu MIPS64R2-generic -nographic -m 256

Big-endian 64-bit

qemu-system-mips64 -kernel bin/targets/malta/be64/openwrt-malta-be64-vmlinux-initramfs.elf \
  -cpu MIPS64R2-generic -nographic -m 256

RISC-V support in OpenWrt uses the virt machine with an embedded boot flow: OpenSBI → U-Boot → FIT image from NOR flash.

Default boot (auto-detect sysupgrade.bin via U-Boot with FIT on pflash):

qemu-system-riscv64 -nographic -machine virt \
  -smp 2 -m 512 \
  -kernel u-boot.bin \
  -drive file=sysupgrade.bin,format=raw,if=pflash,unit=1

Explicit rootfs with initramfs kernel (legacy mode):

qemu-system-riscv64 -nographic -machine virt \
  -smp 2 -m 512 \
  -kernel openwrt-riscv64-generic-initramfs-kernel.bin \
  -drive file=sdcard.img,format=raw,if=virtio \
  -append "root=/dev/vda2 ro console=ttyS0"

Fallback (no firmware images found): boot initramfs kernel directly via OpenSBI:

qemu-system-riscv64 -nographic -machine virt \
  -smp 2 -m 512 \
  -kernel openwrt-riscv64-generic-initramfs-kernel.bin

For SquashFS images, OpenWrt initializes the writable rootfs_data overlay on first boot using the unused space following the read-only root filesystem. Depending on the available space and configuration, this area is formatted as F2FS or ext4; F2FS is selected automatically when more than 100 MiB is available, unless overridden by the kernel command line.

Perform hard factory reset if F2FS is corrupted or failed to set up properly. The exact expanding method depends on your virtualization system.

5.1.1. KVM/QEMU

If you are using KVM/QEMU virtualization.

Increase base image size to 512M:

gzip -dk openwrt-x86-64-generic-squashfs-combined-efi.img.gz
qemu-img resize -f raw openwrt-x86-64-generic-squashfs-combined-efi.img 512M

Loop mount the base image so it can be modified:

loop_device=$(sudo losetup -f)
sudo losetup $loop_device openwrt-x86-64-generic-squashfs-combined-efi.img

Fix the GPT partition and increase the root partition size to 100% (512M):

echo -e "OK\nFix" | sudo parted ---pretend-input-tty "$loop_device" print
sudo parted "$loop_device" resizepart 2 100%
sudo parted "$loop_device" print

Remove the loop mount device

sudo losetup -d $loop_device

5.1.2. libvirt + KVM/QEMU

If you are using KVM/QEMU with libvirt management framework. Import the image as-is and stop the first boot on the GRUB screen. Then expand the block device online as follows and continue the boot.

virsh blockresize openwrt vda 300M
virsh vol-resize openwrt 300M default

5.1.3. Other virtualization systems

If you are using other virtualization systems like Proxmox, VMWare ESXi/Workstation, VirtualBox, XenServer/XCP-ng, and any other self-respecting virtualization software it is also possible to expand the drive but you will have to look at its own documentation for guidance.

(taken from mailing list post by Philip Prindeville)

On my Centos 7.4 KVM host, I did:

To provision 10 VFs per NIC:

cat << EOF > /etc/modprobe.d/sr-iov.conf
# for SR-IOV support
options igb max_vfs=10
EOF

This will take effect after the next reboot. Alternatively by unloading and reloading the IGB module.

Create XML files for each NIC you want to support virtualization on:

# cat << EOF > /tmp/hostdev-net0.xml
<network>
<name>hostdev-net0</name>
<uuid>$(uuidgen)</uuid>
<forward mode='hostdev' managed='yes'>
<pf dev='eno1'/>
</forward>
</network>
EOF

cat << EOF > /tmp/hostdev-net1.xml
<network>
<name>hostdev-net1</name>
<uuid>$(uuidgen)</uuid>
<forward mode='hostdev' managed='yes'>
<pf dev='eno2'/>
</forward>
</network>
EOF

virsh net-destroy default
virsh net-define /tmp/hostdev-net0.xml
virsh net-autostart hostdev-net0
virsh net-define /tmp/hostdev-net1.xml
virsh net-autostart hostdev-net1

To create the pool of VF interfaces.

Then to add interfaces to VMs, I did:

# cat << EOF > /tmp/new-interface-0.1.xml
<interface type='network'>
<mac address='52:54:00:0d:84:f4'/>
<source network='hostdev-net0'/>
<address type='pci' domain='0x0000' bus='0x07' slot='0x10' function='0x0'/>
</interface>
EOF

# Where the ‘0d:84:f4’ is 3 unique bytes
dd status=none bs=1 count=3 if=/dev/urandom | hexdump -e '/1 "%x"\n'

virsh attach-device my-machine-1 /tmp/new-interface-0.1.xml
  • Last modified: 2026/10/12 18:48
  • by mottl