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
sudoprivileges. - 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.
NoteIf you want a private network that still reaches the internet, add
<forward mode='nat'/>right after the<name>line. That gives you a second NAT network likedefaultwith its own subnet.
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.
WarningThis step reconfigures the host's primary network interface. Keep a console session open and use
netplan try, which rolls back automatically if you lose connectivity.
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.
