Libvirt manages the networking of KVM/QEMU virtual machines through named virtual networks, each backed by a Linux bridge on the host and, for NAT and isolated networks, a dnsmasq instance that hands out DHCP leases and answers DNS. In this tutorial you will inspect the default NAT network, reserve a fixed IP address for a guest, create an isolated network for private traffic between VMs, and put guests directly on your LAN or public network with a host bridge. All commands are for Ubuntu 24.04 LTS.

Prerequisites

To follow this guide you need:

  • A physical server or bare metal host running Ubuntu 24.04 LTS with hardware virtualization enabled (Intel VT-x or AMD-V). Standard VPS instances usually do not expose nested virtualization.
  • A non-root user with sudo privileges.
  • KVM and libvirt installed, and at least one virtual machine defined. This guide uses a guest called vm1.
  • Out-of-band console access (IPMI, KVM over IP or a provider console) before you do Step 5, because changing the host's main interface can cut your SSH session.

If libvirt is not installed yet, install it now:

sudo apt update
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients virtinst

This guide runs virsh with sudo so that it always talks to the system instance (qemu:///system), which is where networks and VMs created by virt-install live.

Step 1 - Inspecting the default NAT network

On installation, libvirt creates a network called default. It uses NAT: guests get addresses from 192.168.122.0/24, can reach the internet through the host, but cannot be reached from outside without extra rules. List the networks:

sudo virsh net-list --all
 Name      State    Autostart   Persistent
--------------------------------------------
 default   active   yes         yes

If default shows as inactive, start it and mark it to start at boot:

sudo virsh net-start default
sudo virsh net-autostart default

Show its definition:

sudo virsh net-dumpxml default
<network>
  <name>default</name>
  <uuid>...</uuid>
  <forward mode='nat'>
    <nat>
      <port start='1024' end='65535'/>
    </nat>
  </forward>
  <bridge name='virbr0' stp='on' delay='0'/>
  <mac address='52:54:00:...'/>
  <ip address='192.168.122.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='192.168.122.2' end='192.168.122.254'/>
    </dhcp>
  </ip>
</network>

The key elements are <forward> (the mode: nat, route, bridge, or absent for an isolated network), <bridge> (the host bridge device) and <ip> with its <dhcp> range. The bridge exists on the host as a normal interface:

ip -brief addr show virbr0
virbr0           UP             192.168.122.1/24

Step 2 - Reserving a fixed IP address for a guest

Servers inside the NAT network are easier to manage with a stable address. Instead of configuring a static IP inside the guest, add a DHCP reservation keyed on the guest's MAC address. First get the MAC of vm1:

sudo virsh domiflist vm1
 Interface   Type      Source    Model    MAC
-------------------------------------------------------------
 vnet0       network   default   virtio   52:54:00:6b:3c:58

Add a host entry to the network. The --live --config flags apply the change to the running network and save it to the persistent definition:

sudo virsh net-update default add ip-dhcp-host \
  "<host mac='52:54:00:6b:3c:58' name='vm1' ip='192.168.122.10'/>" \
  --live --config

Pick an address inside the network's subnet. The guest gets it on its next DHCP renewal; the quickest way to force that is to reboot the guest:

sudo virsh reboot vm1

After a few seconds, list the active leases to confirm:

sudo virsh net-dhcp-leases default
 Expiry Time           MAC address         Protocol   IP address          Hostname   Client ID or DUID
----------------------------------------------------------------------------------------------------------
 2026-09-25 13:42:11   52:54:00:6b:3c:58   ipv4       192.168.122.10/24   vm1        ...

Because dnsmasq also serves DNS on 192.168.122.1, other guests on the same network can resolve vm1 by name.

Step 3 - Creating an isolated network

An isolated network has no <forward> element: guests on it can talk to each other and to the host, but traffic is never routed off the host. It is a good fit for database backends, replication links or lab environments.

Create the definition file:

nano ~/isolated.xml
<network>
  <name>isolated</name>
  <bridge name='virbr1' stp='on' delay='0'/>
  <ip address='10.10.10.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='10.10.10.100' end='10.10.10.200'/>
    </dhcp>
  </ip>
</network>

Choose a subnet that does not overlap with any network the host already uses. Define the network, start it and enable autostart:

sudo virsh net-define ~/isolated.xml
sudo virsh net-start isolated
sudo virsh net-autostart isolated
Network isolated defined from /home/your_user/isolated.xml

Network isolated started

Network isolated marked as autostarted

Check that the bridge came up with the gateway address:

ip -brief addr show virbr1
virbr1           DOWN           10.10.10.1/24

The state shows DOWN until a guest is connected, which is normal.

Step 4 - Attaching a VM to a network

Add a second virtual NIC to vm1 on the isolated network. --live hot-plugs it into the running guest and --config keeps it after a shutdown:

sudo virsh attach-interface vm1 --type network --source isolated \
  --model virtio --live --config
Interface attached successfully

Verify from the host:

sudo virsh domiflist vm1
 Interface   Type      Source     Model    MAC
--------------------------------------------------------------
 vnet0       network   default    virtio   52:54:00:6b:3c:58
 vnet1       network   isolated   virtio   52:54:00:1f:a2:07

Inside the guest, the new interface appears (for example as enp7s0). Ubuntu cloud images and server installs configure the first NIC with netplan, so you may need to enable DHCP on the new one. Inside the guest, find its name with ip -brief link, then create a netplan file for it:

sudo nano /etc/netplan/60-isolated.yaml
network:
  version: 2
  ethernets:
    enp7s0:
      dhcp4: true
      dhcp4-overrides:
        use-routes: false

use-routes: false stops the isolated network from installing a default route that would compete with the NAT interface. Apply it inside the guest:

sudo chmod 600 /etc/netplan/60-isolated.yaml
sudo netplan apply
ip -brief addr show enp7s0
enp7s0           UP             10.10.10.143/24

Repeat the attach step for a second VM and ping between the two 10.10.10.x addresses to confirm they can reach each other. To remove an interface later, use sudo virsh detach-interface vm1 --type network --mac 52:54:00:1f:a2:07 --live --config.

Step 5 - Putting guests on the physical network with a bridge

With NAT, guests are hidden behind the host. When guests need their own address on your LAN or their own public IP, create a Linux bridge on the host, move the physical interface into it, and let guests plug into the same bridge.

Find the name of the physical interface and the current address, gateway and DNS settings:

ip -brief addr
ip route show default
ls /etc/netplan/

Replace the existing netplan configuration (commonly /etc/netplan/50-cloud-init.yaml or /etc/netplan/00-installer-config.yaml) with a bridge definition. Back up the old file first:

sudo cp /etc/netplan/50-cloud-init.yaml ~/50-cloud-init.yaml.bak
sudo nano /etc/netplan/50-cloud-init.yaml
network:
  version: 2
  ethernets:
    eno1:
      dhcp4: false
  bridges:
    br0:
      interfaces: [eno1]
      addresses: [203.0.113.10/24]
      routes:
        - to: default
          via: 203.0.113.1
      nameservers:
        addresses: [1.1.1.1, 9.9.9.9]
      parameters:
        stp: false
        forward-delay: 0

Replace eno1, 203.0.113.10/24, 203.0.113.1 and the DNS servers with your values. The host's IP now lives on br0, not on the physical interface. If the file was generated by cloud-init, also stop cloud-init from overwriting it on reboot:

echo 'network: {config: disabled}' | sudo tee /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg

Test the configuration. Press Enter within 120 seconds to keep it; otherwise it reverts:

sudo netplan try

Confirm that br0 holds the address and that eno1 is a member:

ip -brief addr show br0
bridge link show
br0              UP             203.0.113.10/24
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 100

Now register the bridge as a libvirt network so guests can select it by name:

nano ~/host-bridge.xml
<network>
  <name>host-bridge</name>
  <forward mode='bridge'/>
  <bridge name='br0'/>
</network>
sudo virsh net-define ~/host-bridge.xml
sudo virsh net-start host-bridge
sudo virsh net-autostart host-bridge

Libvirt does not run DHCP on a bridged network. Guests attached to it take an address from your LAN's DHCP server, or you configure a static IP inside the guest, exactly as for a physical machine. On a hosted server, each guest needs an IP address routed to you by your provider, and the upstream network must accept the guest's MAC address; check with your provider how additional IPs are delivered before bridging.

Attach a guest and verify:

sudo virsh attach-interface vm1 --type network --source host-bridge \
  --model virtio --live --config
bridge link show

The guest's vnetN tap device now appears as a member of br0.

Step 6 - Limiting bandwidth per interface

Libvirt can shape traffic on a guest's interface with <bandwidth>. Edit the VM:

sudo virsh edit vm1

Inside the <interface> block you want to limit, add:

<bandwidth>
  <inbound average='12500' peak='25000' burst='1024'/>
  <outbound average='12500' peak='25000' burst='1024'/>
</bandwidth>

average and peak are in kilobytes per second (12500 KB/s is about 100 Mbit/s) and burst is in kilobytes. inbound and outbound are from the guest's point of view. The change applies the next time the VM starts:

sudo virsh shutdown vm1
sudo virsh start vm1
sudo virsh domiftune vm1 vnet0
inbound.average: 12500
inbound.peak   : 25000
inbound.burst  : 1024
...

domiftune can also change the values on a running guest, for example sudo virsh domiftune vm1 vnet0 --inbound 25000,50000,2048 --live --config.

Troubleshooting

Guest gets no IP on a NAT or isolated network. Check that the network is active with sudo virsh net-list --all and that dnsmasq is running for it with ps aux | grep dnsmasq. Look at the host logs with sudo journalctl -u libvirtd -u virtqemud --since "10 min ago". Inside the guest, confirm the interface has DHCP enabled.

net-start fails with "Network is already in use by interface". The subnet in your XML overlaps with an existing host interface or another libvirt network. Pick a different range, then sudo virsh net-undefine and net-define again.

Guests on NAT lose internet after installing a firewall. Libvirt inserts its own NAT and forwarding rules when a network starts. Tools that flush the ruleset (a firewall reload, iptables -F) remove them. Restart the affected networks with sudo virsh net-destroy default && sudo virsh net-start default, which recreates the rules.

Guests on br0 cannot reach the network. Confirm that the tap device is a bridge member (bridge link show) and that the guest has a valid address and gateway for that segment. On hosted servers this almost always means the provider has not routed the IP or does not accept the guest's MAC address.

Conclusion

You now have a NAT network with fixed DHCP reservations, an isolated network for private guest traffic, a host bridge that gives guests direct network access, and per-interface bandwidth limits. From here you can move on to live migration between KVM hosts, tune guest performance with CPU pinning and NUMA placement, or define IPv6 subnets in your networks with an additional <ip family='ipv6'> element.