Linux VPS

Port forwarding to KVM VMs on libvirt's NAT network

Expose services in KVM VMs behind libvirt's default NAT network: give VMs fixed DHCP addresses, add iptables or nftables DNAT rules, make them persistent with a libvirt hook and test.

IntermediateKVM hosts3 min read

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 web01

Add 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 --config

Restart 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:80
iptables -I FORWARD -o virbr0 -p tcp -d 192.168.122.10 --dport 80 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT

The 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
fi

Make it executable and restart libvirt:

chmod +x /etc/libvirt/hooks/qemu && systemctl restart libvirtd

On hosts where libvirt uses the nftables backend (newer releases), write the equivalent nft rules in a separate table instead.

Test the forward

  1. From another machine: curl -I http://HOST_PUBLIC_IP:8080.
  2. On the host: iptables -t nat -L PREROUTING -n -v shows 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 add ufw route allow rules.

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.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.