|

When Libvirt NAT Goes Rogue: A Tale of Fedora, Firewalls, and Virtual Mayhem

Like any sensible cybersecurity professional with entirely normal hobbies, I occasionally spin up a small digital nation-state of VMs on my Fedora desktop. Usually they behave. Usually.

Then one day, without warning, they staged a coordinated rebellion.

This post chronicles how my Fedora workstation decided to reinvent network segmentation, how libvirt lost its memory of the “default” world order, and how I spent an hour unraveling a problem that absolutely should not have been this complicated.

Spoiler: NAT rules, firewalld, and iptables all had something to say—and none of it was helpful.


🚨 Symptoms: The “Absolutely Not” Networking Situation

My KVM/QEMU VMs were spinning up normally. The virtual CPUs? Happy. The virtual disks? Present.
The virtual NICs? Well… they were experiencing an existential crisis.

Inside the VMs:

  • DHCP worked (mostly)
  • DNS worked, and resolve everything normally
  • VMs could ping the host’s IP (192.168.101.51)
  • VMs could ping each other
  • But they could not ping anything on my LAN
  • And definitely not the Internet
  • And every traceroute looked like it fell off a cliff after hop 1

This made precisely zero sense… until it made all the sense. See by considering what wasn working from a networking point of view literally only comms between the host and guests, as well as guest to guest. So the bridge must be working, right?


🔎 Initial Investigation: The “You’ve Got to Be Kidding Me” Phase

First, I checked the basics.

Is IP forwarding enabled?

cat /proc/sys/net/ipv4/ip_forward

Output:

1

Forwarding: ON.
So far, so good.

What does firewalld show?

sudo firewall-cmd --get-active-zones

Initial output:

FedoraWorkstation (default)
  interfaces: enp90s0
docker
  interfaces: docker0 br-423d0aa52ca9
libvirt
  interfaces: virbr0

Alright, libvirt has its own zone. Great.

What about NAT?

sudo iptables -t nat -L POSTROUTING -n -v

The output:

146  8760 MASQUERADE  all  --  *      !br-423d0aa52ca9  172.18.0.0/16        0.0.0.0/0           
0     0 MASQUERADE  all  --  *      !docker0           172.17.0.0/16        0.0.0.0/0           

Notice anything missing?

Yep. No MASQUERADE rule for 192.168.122.0/24 — the default libvirt NAT network.

This meant the VMs were basically screaming into the void.


🧨 The Big Clue: virbr0 Was DOWN

Here’s the fun part: checking virbr0 showed this at first:

ip addr show virbr0
virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> ... state DOWN
    inet 192.168.122.1/24

DOWN? No carrier?
It’s a virtual bridge—what is it waiting for, an Ethernet cable!?

But this actually turned out to be important: virbr0 was up but not functional because libvirt’s backend services weren’t working correctly.


🧩 Plot Twist: The Default Network XML Was Missing

At this point I tried to destroy and recreate the default network:

sudo virsh net-destroy default
sudo virsh net-undefine default
sudo virsh net-define /etc/libvirt/qemu/networks/default.xml

Which gave me this gem:

error: Failed to open file '/etc/libvirt/qemu/networks/default.xml': No such file or directory

The entire default libvirt network definition was missing.
Just gone.
Vanished.

Fedora decided it didn’t need it anymore, apparently.


🛠️ Rebuilding the Network from Scratch

I recreated /etc/libvirt/qemu/networks/default.xml manually with the standard libvirt NAT definition and redefined/started the network.

Now virbr0 existed again.

Still down… but progress.


🔥 The Moment of Truth: Adding Forwarding Rules

The FORWARD chain looked like this:

sudo iptables -L FORWARD -n -v
Chain FORWARD (policy DROP 2610 packets, 167K bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER-USER
    0     0 DOCKER-FORWARD

Nobody said “ACCEPT.”
Nobody was letting my VMs leave the house.

So I manually added the three standard libvirt rules:

sudo iptables -A FORWARD -i virbr0 -o virbr0 -j ACCEPT
sudo iptables -A FORWARD -i virbr0 -j ACCEPT
sudo iptables -A FORWARD -o virbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

And voilà:

ACCEPT     all  --  virbr0 virbr0
ACCEPT     all  --  virbr0 *
ACCEPT     all  --  *      virbr0     ctstate RELATED,ESTABLISHED

Now the VMs had both:

  • Permission to leave
  • Permission to come back

Just like well-trained packets should.


🎉 Suddenly Everything Worked Again

Once a VM restarted and reattached, virbr0 sprang to life:

ip addr show virbr0
virbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc htb state UP

That LOWER_UP felt like a warm hug.

DNS resolved.
VMs could ping the LAN.
VMs could ping Google.
Existence restored.


🧠 Conclusion: Fedora, You Chaotic Neutral Beast

What happened?

  • Fedora’s firewalld + Docker nuked libvirt’s NAT rules
  • The default libvirt network vanished from disk
  • virbr0 came up but with no backing rules
  • No forwarding = no routing = sad VMs
  • Rebuilding the default network + manually adding FORWARD and MASQUERADE rules fixed everything

This was a perfect storm of:

  • firewall changes
  • Docker being Docker
  • libvirt assuming everything is fine
  • Fedora being… Fedora

 

If there’s a moral here, it’s this:
When Fedora updates drop, maybe—just maybe—we should read the changelog before casually updating to the latest release and assuming all is well with the world…

But hey, where’s the fun in that?

Written by David, Made funnier with AI.