All posts

Set Up Virtual OpenWrt on Ubuntu with Simulated Wi-Fi Radios

OpenWrt in a QEMU VM, with working 2.4 GHz and 5 GHz access points you configure from LuCI. No router, no antennas, no wireless card. The radios come from mac80211_hwsim, and OpenWrt treats them as real.

1 · Install QEMU/KVM

sudo apt update
sudo apt install -y qemu-system-x86 qemu-utils tmux
sudo usermod -aG kvm "$USER"

Log out and back in before continuing. Group membership only applies at login, and this is the usual reason -enable-kvm fails.

id -nG | grep -o kvm      # should print: kvm

2 · Download the image

You want x86-64 generic squashfs-combined (non-EFI): squashfs gives you firstboot reset, combined boots directly as a disk, and non-EFI needs no OVMF firmware.

mkdir -p ~/owrt-lab && cd ~/owrt-lab

VER=24.10.8
IMG=openwrt-$VER-x86-64-generic-squashfs-combined.img

wget "https://downloads.openwrt.org/releases/$VER/targets/x86/64/$IMG.gz"
gunzip -f "$IMG.gz"

3 · Networking plan

QEMU user-mode networking (SLIRP) NATs the guest and runs its own DHCP. No bridges, no root, but the guest is unreachable without port forwards.

RoleAddress
Subnet10.0.2.0/24
Gateway10.0.2.2
DNS10.0.2.3
OpenWrt guest10.0.2.15
SSHhost:2222guest:22
LuCIhost:8080guest:80

If your real LAN is also 10.0.2.0/24, switch to something like 10.10.10.0/24 and adjust every address below.

4 · Boot the VM

Start a tmux session so the VM survives a closed terminal:

tmux new -s owrt

Then inside it:

cd ~/owrt-lab
IMG=openwrt-24.10.8-x86-64-generic-squashfs-combined.img

qemu-system-x86_64 -enable-kvm -m 1024 -smp 2 \
  -drive file=$IMG,format=raw,if=virtio \
  -netdev user,id=n1,net=10.0.2.0/24,host=10.0.2.2,dns=10.0.2.3,dhcpstart=10.0.2.15,hostfwd=tcp:127.0.0.1:2222-10.0.2.15:22,hostfwd=tcp:127.0.0.1:8080-10.0.2.15:80 \
  -device virtio-net-pci,netdev=n1 \
  -nographic

Note the 127.0.0.1 in the forwards. Written as tcp::2222 they bind every host interface, and OpenWrt's root account has no password yet, so that offers an unauthenticated root shell to your whole network. Drop the loopback address only if you need access from another machine, and run passwd first.

Two different prefixes, so don't mix them up:

  • Ctrl-b d — detach tmux, VM keeps running
  • tmux attach -t owrt — re-attach
  • Ctrl-a x — quit QEMU, powers the VM off

You land on root@OpenWrt:~#. Press Enter if the console looks idle.

5 · Give it internet

OpenWrt ships as a router: br-lan is a static 192.168.1.1 with no upstream. Inside a VM, QEMU is the router, so switch the LAN to DHCP.

uci set network.lan.proto='dhcp'
uci -q delete network.lan.ipaddr
uci -q delete network.lan.netmask
uci commit network
/etc/init.d/network restart
sleep 5

ip -4 addr show br-lan            # expect 10.0.2.15
ping -c3 downloads.openwrt.org    # route + DNS

Delete ipaddr and netmask. Leaving them alongside proto=dhcp gives you a half-configured interface. The address must land on 10.0.2.15 or the port forwards point at nothing.

6 · Install LuCI and Wi-Fi packages

opkg update
opkg install luci iw-full kmod-mac80211-hwsim wpad-openssl

7 · Create the radios

One phy per box you want to simulate. Two gives you a dual-band AP, or an AP plus a client.

Persistent, so they come back on every boot:

echo 'mac80211_hwsim radios=2' > /etc/modules.d/mac80211-hwsim

And load it now:

insmod mac80211_hwsim radios=2
iw phy | grep Wiphy          # expect phy0 and phy1

8 · Configure the radios

wifi config writes /etc/config/wireless from the radios that exist, so it has to run after the module is loaded.

wifi config

They come out disabled. Set band, channel, country and SSID, then enable:

# radio0 -> 2.4 GHz
uci set wireless.radio0.band='2g'
uci set wireless.radio0.channel='1'
uci set wireless.radio0.htmode='HT20'
uci set wireless.radio0.country='US'
uci set wireless.radio0.disabled='0'
uci set wireless.default_radio0.ssid='MyLab-2G'
uci set wireless.default_radio0.encryption='none'

# radio1 -> 5 GHz, non-DFS channel
uci set wireless.radio1.band='5g'
uci set wireless.radio1.channel='36'
uci set wireless.radio1.htmode='HT20'
uci set wireless.radio1.country='US'
uci set wireless.radio1.disabled='0'
uci set wireless.default_radio1.ssid='MyLab-5G'
uci set wireless.default_radio1.encryption='none'

uci commit wireless

9 · Reboot and verify

Proves both persistence mechanisms took.

reboot

Re-attach after ~30s with tmux attach -t owrt, press Enter:

iw phy | grep -c Wiphy      # 2
iwinfo                      # SSID, band, channel per AP
iw dev                      # the phyN-ap0 interfaces

iwinfo is the real test. Interface listed in iw dev but absent from iwinfo means hostapd refused to start, and logread | grep hostapd will say why. Usually the country code or DFS.

10 · Reach LuCI and SSH

http://localhost:8080           # LuCI
ssh -p 2222 root@localhost      # shell

If LuCI redirects to HTTPS and fails, there's no certificate yet:

uci set uhttpd.main.redirect_https='0'
uci commit uhttpd
/etc/init.d/uhttpd restart

Both radios show up under Network → Wireless, fully managed. Change a channel, add an SSID, turn on WPA2, watch netifd reload hostapd.

What's real and what isn't

  • Real: everything MAC and up. Association, four-way handshake, beacons and probes, VLANs, firewall, the uci → netifd → hostapd chain.
  • Not real: the radio. hwsim copies frames between radios on the same channel, nothing more. No path loss, fading, noise or interference, and RSSI is a constant. Don't draw conclusions about throughput, coverage or roaming.

More on the module itself in Simulate a Full Wi-Fi Network in Software with mac80211_hwsim.

— Gopi Raga