Configure libvirt networking

libvirt on Fedora provides virtual network management for KVM/QEMU guests, letting you create NAT, bridged, or isolated networks through virsh or the virt-manager GUI.

You spin up a new virtual machine and try to ping the host. The request times out. You check the VM settings and the network adapter is attached to virbr0, but nothing flows. The host firewall is blocking the bridge, or the virtual network never started after a reboot. This happens more often than you expect because libvirt manages its own isolated network stack that does not automatically sync with NetworkManager or firewalld.

What is actually happening

Libvirt does not route traffic through your physical NICs by default. It creates a virtual Ethernet switch inside the kernel and attaches it to a NAT gateway. Think of it like a home router plugged into your wall. Your VMs get private IP addresses, and libvirt translates their outbound traffic to the host's public IP. The host keeps the default network definition in /etc/libvirt/qemu/networks/. When you boot Fedora, systemd starts the libvirtd service, but the virtual network itself stays down until you explicitly bring it up. NetworkManager sees the virtual bridge as a separate interface and applies its own routing rules unless you tell it otherwise. Firewalld isolates the bridge in a dedicated libvirt zone to prevent VMs from scanning your local LAN. Libvirt also spins up a lightweight dnsmasq instance to handle DHCP leases and DNS forwarding for guests. That instance lives in /var/run/libvirt/network/ and reads its configuration from the network XML you define. If the XML is missing a DHCP range, dnsmasq starts but hands out nothing. If the bridge name conflicts with an existing interface, libvirt aborts silently. Run virsh net-dhcp-leases before guessing. Half the time the problem is a missing range block.

Start the default network

Here is how to check the current state and bring the built-in network online.

sudo virsh net-list --all
# Query libvirt for every defined network and its active status.
# The output shows 'active' or 'inactive' in the STATE column.
# An inactive network means the bridge exists but has no IP address.

If the default network shows as inactive, start it and mark it for automatic startup on boot.

sudo virsh net-start default
# Bring the virtual bridge online and assign it the 192.168.122.1 gateway address.
sudo virsh net-autostart default
# Tell systemd to launch this network automatically when libvirtd starts.

Reboot before you debug. Half the time the symptom is gone after the autostart flag takes effect.

Create a custom NAT network

You might need a separate subnet for a specific project or to isolate development traffic from production VMs. Define the network in an XML file before handing it to libvirt.

<network>
  <name>dev-net</name>
  <!-- Name the network for virsh commands and virt-manager dropdowns -->
  <forward mode='nat'/>
  <!-- Route guest traffic through the host using masquerading -->
  <bridge name='virbr1' stp='on' delay='0'/>
  <!-- Create a new virtual bridge and enable spanning tree protocol -->
  <ip address='192.168.100.1' netmask='255.255.255.0'>
    <!-- Set the gateway IP that guests will use for outbound traffic -->
    <dhcp>
      <!-- Enable the built-in dnsmasq DHCP server for this subnet -->
      <range start='192.168.100.2' end='192.168.100.254'/>
      <!-- Define the pool of assignable addresses for virtual machines -->
    </dhcp>
  </ip>
</network>

Save the file to /tmp/dev-net.xml and register it with the daemon.

sudo virsh net-define /tmp/dev-net.xml
# Parse the XML and store the configuration in /etc/libvirt/qemu/networks/
sudo virsh net-start dev-net
# Activate the bridge and start the dnsmasq instance for DHCP and DNS
sudo virsh net-autostart dev-net
# Persist the active state across host reboots

Edit /etc/ files. Never edit /usr/lib/ files. Libvirt ships default network templates in /usr/share/libvirt/networks/ for reference only. Manual edits to those paths get overwritten on package updates. Trust the package manager. Manual file edits drift, snapshots stay.

Create a bridged network

NAT keeps VMs hidden behind the host. Bridging makes them appear as independent devices on your physical LAN. You must configure the host bridge through NetworkManager first, then point libvirt at it.

sudo nmcli connection add type bridge ifname br0 con-name br0
# Create a new bridge interface named br0 in NetworkManager
sudo nmcli connection add type ethernet ifname enp3s0 master br0
# Attach your physical NIC to the bridge as a slave port
# Replace enp3s0 with your actual interface name from ip link
sudo nmcli connection up br0
# Activate the bridge and push the configuration to the kernel

NetworkManager will drop your SSH session if you are connected over the network you just bridged. Run these commands from a local console or a second network interface. After the bridge is up, define the libvirt network.

<network>
  <name>lan-bridge</name>
  <!-- Human-readable name for the virtual network -->
  <forward mode='bridge'/>
  <!-- Disable NAT and pass traffic directly to the physical switch -->
  <bridge name='br0'/>
  <!-- Bind to the NetworkManager bridge you just created -->
</network>

Define and start it the same way as the NAT example. Libvirt will detect the existing bridge and skip creating a new one. If the boot menu is gone, GRUB rescue is your friend, not your enemy. Keep a serial console open when testing bridge configurations.

Apply firewall rules

Fedora ships with firewalld enabled by default. The libvirt zone handles traffic on virtual bridges, but you must verify it is active and matches your setup.

sudo firewall-cmd --zone=libvirt --list-all
# Display the active rules for the libvirt zone
# Check that the target is ACCEPT and the interfaces list includes virbr0

If you added a custom bridge, firewalld might not automatically attach it to the libvirt zone. Assign it explicitly and reload the daemon.

sudo firewall-cmd --zone=libvirt --add-interface=br0 --permanent
# Bind the physical bridge to the libvirt firewall zone permanently
sudo firewall-cmd --reload
# Apply the persistent configuration to the running firewall without dropping connections

Fedora convention: always run firewall-cmd --reload after changing persistent rules. The runtime configuration and the persistent configuration will diverge otherwise, and your changes vanish on reboot. Run firewall-cmd --state to confirm the daemon is active before adding interfaces.

Verify it worked

Ping the gateway from a guest and check the host's routing table.

ping -c 4 192.168.122.1
# Test basic connectivity from the guest to the libvirt gateway
ip route show table all | grep virbr
# Verify the host kernel has created the expected routing entries

Check the DHCP lease table to confirm dnsmasq handed out an address.

sudo virsh net-dhcp-leases default
# Query the built-in DHCP server for active leases on the default network
# Look for the MAC address of your guest in the EXPIRES column

If the lease table is empty, dnsmasq is running but the range block is misconfigured. Check the XML again. Run journalctl -xeu libvirtd.service to see startup warnings. Read the actual error before guessing.

Common pitfalls and what the error looks like

The virsh net-start command will refuse to proceed and print Error: Network is already defined with uuid .... The bridge is already active. Run virsh net-destroy first if you need to reapply a modified XML file.

If you see [FAILED] Failed to start libvirtd.service during boot, your network configuration probably references a missing interface name or a conflicting IP range. Check the journal for the exact failure point.

journalctl -xeu libvirtd.service
# Read the recent log lines with explanatory context
# The x flag adds human-readable hints to systemd exit codes
# The e flag jumps to the end of the journal for quick scanning

Guests cannot reach the internet even though ping to the gateway works. IP forwarding is disabled in the kernel. Libvirt usually enables it automatically, but manual sysctl edits or hardened security profiles can override it.

sysctl net.ipv4.ip_forward
# Check the current kernel setting for IPv4 packet forwarding
# A value of 0 means the host drops routed packets between interfaces

If the value is 0, create a drop-in configuration file and apply it.

echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-libvirt-forward.conf
# Write the forwarding rule to a persistent sysctl directory
sudo sysctl -p /etc/sysctl.d/99-libvirt-forward.conf
# Reload the sysctl configuration without rebooting the host

SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. Libvirt expects specific file contexts on /var/lib/libvirt/ and /etc/libvirt/. Manual edits outside those paths trigger AVC denials that silently break network startup. Snapshot the system before the upgrade. Future-you will thank you.

When to use this vs alternatives

Use NAT when you want isolated guest networks that share the host internet connection without exposing VMs to the local LAN. Use bridging when your VMs need static IP addresses from your physical router or must communicate directly with other hosts on the network. Use the default libvirt network when you are testing software and do not need custom subnets or DNS forwarding. Use NetworkManager directly for bridging when you are managing complex VLAN tagging or bond interfaces that libvirt does not fully support. Stay on the upstream libvirt defaults if you only deviate from the configuration occasionally.

Where to go next