libvirt's default network puts VMs on a private 192.168.122.0/24 subnet behind NAT. That is safe, but services in the VM are unreachable from the internet. Port forwarding to KVM VMs fixes that when you only have one public IP and cannot bridge.
Before you start
- A KVM host with the default libvirt network active (
virsh net-list). - The host firewall in use (UFW, firewalld or raw iptables/nftables), because it may interact with these rules.
- For public IPs per VM, use a bridge instead; see virtual networking.
Give the VM a fixed address
Find its MAC:
virsh domiflist web01Add a DHCP reservation:
virsh net-update default add ip-dhcp-host "<host mac='52:54:00:aa:bb:cc' name='web01' ip='192.168.122.10'/>" --live --configRestart networking in the guest, or reboot it, and confirm the address.
Add DNAT rules
Forward host port 8080 to the VM's port 80 (replace eno1 with your public interface):
iptables -t nat -I PREROUTING -i eno1 -p tcp --dport 8080 -j DNAT --to-destination 192.168.122.10:80iptables -I FORWARD -o virbr0 -p tcp -d 192.168.122.10 --dport 80 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPTThe FORWARD rule is needed because libvirt's own rules reject new incoming connections to the NAT network.
Make rules persistent with a libvirt hook
libvirt rewrites its firewall rules when networks restart, so add rules when the VM starts. Create /etc/libvirt/hooks/qemu:
#!/bin/bash
if [ "$1" = "web01" ]; then
if [ "$2" = "stopped" ] || [ "$2" = "reconnect" ]; then
iptables -t nat -D PREROUTING -i eno1 -p tcp --dport 8080 -j DNAT --to-destination 192.168.122.10:80
iptables -D FORWARD -o virbr0 -p tcp -d 192.168.122.10 --dport 80 -j ACCEPT
fi
if [ "$2" = "start" ] || [ "$2" = "reconnect" ]; then
iptables -t nat -I PREROUTING -i eno1 -p tcp --dport 8080 -j DNAT --to-destination 192.168.122.10:80
iptables -I FORWARD -o virbr0 -p tcp -d 192.168.122.10 --dport 80 -j ACCEPT
fi
fiMake it executable and restart libvirt:
chmod +x /etc/libvirt/hooks/qemu && systemctl restart libvirtdOn hosts where libvirt uses the nftables backend (newer releases), write the equivalent nft rules in a separate table instead.
Test the forward
- From another machine:
curl -I http://HOST_PUBLIC_IP:8080. - On the host:
iptables -t nat -L PREROUTING -n -vshows packet counters rising.
Troubleshooting
- Connection refused or timeout: the service in the VM listens only on 127.0.0.1, or the guest firewall blocks it.
- Works from outside, not from the host: PREROUTING does not apply to locally generated traffic; test from another machine.
- Rules vanish after restart: use the hook as above, not one-off commands.
- UFW blocks forwarding: set
DEFAULT_FORWARD_POLICY="ACCEPT"or addufw route allowrules.
Frequently asked questions
Can I forward a range of ports?
Yes, use --dport 10000:10100 and a matching destination range.
Is a reverse proxy better for websites?
Often. Run Nginx on the host and proxy by hostname to each VM, so many sites share ports 80/443.
Does this work for UDP?
Yes, repeat the rules with -p udp.
