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.
| Role | Address |
|---|---|
| Subnet | 10.0.2.0/24 |
| Gateway | 10.0.2.2 |
| DNS | 10.0.2.3 |
| OpenWrt guest | 10.0.2.15 |
| SSH | host:2222 → guest:22 |
| LuCI | host:8080 → guest: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-bd— detach tmux, VM keeps runningtmux attach -t owrt— re-attachCtrl-ax— 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→hostapdchain. - 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