skip to content
Hacktivate
Table of Contents

Why you can’t just bridge Wi-Fi

VirtualBox and VMware ship their own proprietary ways of putting a VM on the same network as the host. KVM does not natively bridge over wireless, and that isn’t an oversight. The 802.11 standard forbids a station from sending frames with a source MAC address other than its own, and a normal Layer 2 bridge relays frames from many MAC addresses (one per guest), so the access point just drops them. This is a property of Wi-Fi itself, not of KVM.

The way around it is to move up a layer. Instead of bridging Ethernet frames (Layer 2), we route IP packets (Layer 3) and use proxy ARP to make the guests reachable as if they were on the local network. The good news is that both libvirt and the Linux kernel now do all of this natively. No TAP devices to create by hand, no per-guest routes to babysit, no unmaintained daemons.

There are two supported approaches below. Pick based on whether you need guests on a separate routed subnet (Option A, the simplest and most robust) or on the exact same subnet as the rest of your LAN (Option B).


This is the cleanest modern method. libvirt creates a small dedicated subnet, runs its own DHCP for the guests, and routes between that subnet and your Wi-Fi interface. Everything is declarative, survives reboots, and is managed entirely through virsh or virt-manager.

Guests live on their own subnet and reach the internet and your LAN normally. Other LAN devices reach the guests through the host, which acts as a router. For most use cases (servers, dev VMs, K8s nodes) this is exactly what you want.

1. Define the network

Create a file wifi-routed.xml. Replace wlan0 with your wireless interface, and pick a small subnet that does not overlap your LAN’s DHCP range:

wifi-routed.xml
<network>
<name>wifi-routed</name>
<forward dev="wlan0" mode="route">
<interface dev="wlan0"/>
</forward>
<bridge name="virbr1" stp="on" delay="0"/>
<domain name="wifi-routed"/>
<ip address="192.168.100.1" netmask="255.255.255.0">
<dhcp>
<range start="192.168.100.10" end="192.168.100.100"/>
</dhcp>
</ip>
</network>

2. Register and start it

Terminal window
virsh net-define wifi-routed.xml
virsh net-autostart wifi-routed
virsh net-start wifi-routed

net-define (rather than the older net-create) makes the network persistent so it survives reboots; net-autostart brings it up on boot.

3. Enable IP forwarding

libvirt usually sets this for you, but it’s worth making explicit and persistent. Add to /etc/sysctl.d/99-kvm-wifi.conf:

/etc/sysctl.d/99-kvm-wifi.conf
net.ipv4.ip_forward = 1

Apply it right away with sudo sysctl --system.

4. Attach your VMs

In virt-manager, edit the VM’s NIC and set the network source to wifi-routed. Or from the CLI:

Terminal window
virsh edit <vm-name>
<interface type='network'>
<source network='wifi-routed'/>
<model type='virtio'/>
</interface>

Start the guest. It gets an address from libvirt’s DHCP (e.g. 192.168.100.10), reaches the internet, and is reachable from the host and, thanks to the host’s routing, from the rest of the LAN.


Option B: same-subnet with kernel proxy ARP

Use this when guests must appear on the exact same subnet as your physical LAN (for example, so other machines find them without any routing hop). This still relies on proxy ARP, but on the kernel’s own built-in implementation via sysctl, which replaces the old parprouted daemon entirely.

1. Create a network on the LAN subnet

Suppose your Wi-Fi is on 192.168.1.0/24. Carve out a slice of that range your router’s DHCP does not hand out (say 192.168.1.128/28) so nothing collides. Create wifi-proxyarp.xml:

wifi-proxyarp.xml
<network>
<name>wifi-proxyarp</name>
<forward dev="wlan0" mode="route">
<interface dev="wlan0"/>
</forward>
<bridge name="virbr2" stp="on" delay="0"/>
<domain name="wifi-proxyarp"/>
<ip address="192.168.1.129" netmask="255.255.255.240">
<dhcp>
<range start="192.168.1.136" end="192.168.1.142"/>
</dhcp>
</ip>
</network>

Register and start it as in Option A:

Terminal window
virsh net-define wifi-proxyarp.xml
virsh net-autostart wifi-proxyarp
virsh net-start wifi-proxyarp

2. Enable proxy ARP on both interfaces

This is the key step, and the modern replacement for parprouted. The kernel answers ARP requests for the guest IPs on the guests’ behalf, so other LAN devices believe the guests are local. Enable it on the wireless interface and the libvirt bridge:

Terminal window
sudo sysctl -w net.ipv4.conf.wlan0.proxy_arp=1
sudo sysctl -w net.ipv4.conf.virbr2.proxy_arp=1

Make it persistent by adding to /etc/sysctl.d/99-kvm-wifi.conf:

/etc/sysctl.d/99-kvm-wifi.conf
net.ipv4.ip_forward = 1
net.ipv4.conf.all.proxy_arp = 1
net.ipv4.conf.wlan0.proxy_arp = 1
net.ipv4.conf.virbr2.proxy_arp = 1

Then sudo sysctl --system.

3. Attach VMs and configure addresses

Point each VM’s NIC at the wifi-proxyarp network (same virsh edit block as Option A, with <source network='wifi-proxyarp'/>). Guests in the .136-.142 range get addresses via DHCP; the gateway is the bridge address 192.168.1.129, and DNS can point at your real router (192.168.1.1).

Because these addresses sit inside the LAN’s own /24, other machines on the network can now reach the guests directly, with the host quietly answering ARP and routing the packets.


Legacy method: parprouted + manual TAP (for reference only)

Older guides (including the previous version of this post) built the bridge by hand: create a TAP device with ip tuntap, run the parprouted daemon to shuttle ARP requests between wlan0 and the TAP, add a host route per guest, and launch QEMU with a raw -netdev tap argument.

That approach still works, but there’s little reason to use it now:

  • parprouted is unmaintained, and the kernel’s built-in proxy_arp (Option B) does the same job with no external dependency.
  • Manual TAP devices need one interface per VM and can’t use DHCP without extra helpers.
  • Everything has to be re-created or scripted at each boot.

If you already have a parprouted setup, moving to Option A or B drops the daemon, the manual routes, and the per-VM TAP bookkeeping. There’s no reason to start a new deployment with it today.


Summary

Option A: routed network Option B: same-subnet proxy ARP Legacy: parprouted
Guest subnet Separate, routed to LAN Same /24 as LAN Same as LAN
Proxy ARP by libvirt routing Kernel sysctl parprouted daemon
DHCP for guests Yes (libvirt) Yes (libvirt) No
Per-VM setup None None One TAP each
Maintained Yes Yes No

Start with Option A unless you specifically need guests on the same subnet as the rest of your LAN, in which case use Option B. Both are managed entirely through libvirt and standard kernel sysctl settings, with no unmaintained tooling in the loop.