KVM forward ports to guests VM with UFW on Linux

See all Linux Kernel Based Virtual Machine related FAQs/Howtos
My Debian/Ubuntu/CentOS Linux server using KVM as a hypervisor. I would like to forward ports to guests VM with UFW iptables. How do I forward ports on a Linux server running libvirt/KVM to specified ports on VM’s, when using NAT? How do I configure KVM/libvirt forward ports to guests with iptables?

This page shows how to forward ports to guests VM in libvirt/KVM running on CentOS 7 or Debian 9 or Ubuntu Linux LTS server using ufw command. For example, the host has a public IP of 202.54.1.4 . You want to forward all ports to 192.168.122.253. Say if you are using NAT-ed networking. You want to allow external access to services offered by your VMs. This page shows the process of NAT port forwarding to VMs using ufw running on Linux.
Tutorial details
Difficulty level Advanced
Root privileges Yes
Requirements Linux terminal
Category Firewall
Prerequisites ufw command
OS compatibility AlmaLinux Alpine Arch CentOS Debian Fedora Linux Mint openSUSE Pop!_OS RHEL Rocky Stream SUSE Ubuntu
Est. reading time 6 minutes
Advertisement

KVM forward ports to guests VM with UFW on Linux

Our sample setup is as follows:
Sample setup KVM libvirt Forward Ports to guests with UFW Iptables
Where,

  • My server runs CentOS 7 latest
  • KVM installed on CentOS 7 server
  • The server has a total 4 VMs with private IP address
  • CentOS7 server has total 5 public IP address
  • Your task is to forward all ports traffic coming to 202.54.1.4 to 192.168.122.253 CentOS7 VM1.
  • Next forward ssh traffic coming to 202.54.1.5 VM2 at 192.168.122.125.

Find out information about your KVM network

Type the following command:
# virsh net-list
# virsh net-info default
# virsh net-dumpxml default

virsh get info about kvm NAT networking

Understanding KVM networking and default iptables/ufw rules

From the above commands, it is clear that I have a virtual network (virbr0) set in nat mode. In nat mode, all VMs can make outgoing connections. Each guest VM in default network can talk to each other too. The host os can talk to all guest VMs also. However, all connections comming from the Internet or other hosts on LAN blocked. The default rule is set as follows by KVM/libvirt:
# iptables -A FORWARD -d 192.168.122.0/24 -o virbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
You need to update FORWARD as follows to accept new connection for each VM in nat mode:
# iptables -A FORWARD -s 192.168.2.0/24 -d 192.168.122.0/24 -o virbr0 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT

Step 1 – Configure kvm firewall hook

The default KVM NAT config provides a rule, but it omits the NEW state, which is essential for accepting new incoming connections. To solve this problem create a hook as follows:
# cd /etc/libvirt/hooks/
Create a file named qemu using a text editor such as vim command or nano command # vi qemu
Append code as follows:

#!/bin/bash
# Hook to insert NEW rule to allow connection for VMs
# 192.168.122.0/24 is NATed subnet 
# virbr0 is networking interface for VM and host
# -----------------------------------------------------------------
# Written by Vivek Gite under GPL v3.x {https://www.cyberciti.biz}
# -----------------------------------------------------------------
# get count
#################################################################
## NOTE replace 192.168.2.0/24 with your public IPv4 sub/net   ##
#################################################################
v=$(/sbin/iptables -L FORWARD -n -v | /usr/bin/grep 192.168.122.0/24 | /usr/bin/wc -l)
# avoid duplicate as this hook get called for each VM
[ $v -le 2 ] && /sbin/iptables -I FORWARD 1 -o virbr0 -m state -s 192.168.2.0/24 -d 192.168.122.0/24 --state NEW,RELATED,ESTABLISHED -j ACCEPT

Save and close the file in VIM/vi text editor. Set permission using the chmod command:
# chmod -v +x /etc/libvirt/hooks/qemu

Step 2 – Configuring the UFW (Iptables firewall) to port forward

Edit the /etc/ufw/before.rules files using a text editor:
Next forward ssh traffic coming to 202.54.1.5 VM2 at 192.168.122.125.
# vim /etc/ufw/before.rules
Add at the top of the file:

# KVM/libvirt Forward Ports to guests with Iptables (UFW) #
*nat
:PREROUTING ACCEPT [0:0]
-A PREROUTING -d 202.54.1.4 -p tcp --dport 1:65535 -j DNAT --to-destination 192.168.122.253:1-65535 -m comment --comment "VM1/CentOS 7 ALL ports forwarding"
-A PREROUTING -d 202.54.1.5 -p tcp --dport 22 -j DNAT --to-destination 192.168.122.125:22 -m comment --comment "VM2/OpenBSD SSH port forwarding"
-A PREROUTING -d 202.54.1.6 -p tcp --dport 443 -j DNAT --to-destination 192.168.122.231:443 -m comment --comment "VM3/FreeBSD 443 port forwarding"
-A PREROUTING -d 202.54.1.7 -p tcp --dport 80 -j DNAT --to-destination 192.168.122.229:80 -m comment --comment "VM4/CentOS 80 port forwarding"
COMMIT

Save and close the file in a vim/vi text editor. You can reload rules with the following commands or simple reboot the Linux server:
# bash /etc/libvirt/hooks/qemu
# ufw reload

OR use the reboot command/shutdown command to restart the Linux server:
# reboot

Step 3 – Verify that forwarding ports to guests in libvirt/KVM working

You can list iptables rules using the following syntax. First make sure NEW rules at the top of the FORWARD chain:
# iptables -L FORWARD -nv --line-number

KVM forward ports to guests VM with UFW on Linux iptables rules

Click to enlarge image

Next make sure connections from outside forwarded to each VM using DNAT set in /etc/ufw/before.rules file. You can list nat/DNAT prerouting rule using the iptables command:
# iptables -t nat -L PREROUTING -n -v --line-number
# iptables -t nat -L -n -v

centos kvm redirect traffic iptables

Click to enlarge image

Please note that the MASQUERADE rule is automatically added by the KVM/libvirt. Try ping command or curl command or ssh command to access VMs from outside:
$ ping 202.54.1.4
$ curl -I 202.54.1.4
# login to VM2/openbsd
$ ssh vivek@202.54.1.5

Linux forwarding ports to guests in libvirt / KVM iptables rules

If you are not using UFW, here is a dump of actual iptables rules:
# iptables-save -t nat
Sample outputs:

# Generated by iptables-save v1.4.21 on Tue Jul 31 03:14:18 2018
*nat
:PREROUTING ACCEPT [72:5501]
:INPUT ACCEPT [42:3301]
:OUTPUT ACCEPT [32:4304]
:POSTROUTING ACCEPT [32:4304]
-A PREROUTING -d 202.54.1.4/32 -p tcp -m tcp --dport 1:65535 -m comment --comment "VM1/CentOS 7 ALL ports forwarding" -j DNAT --to-destination 192.168.122.253:1-65535
-A PREROUTING -d 202.54.1.5/32 -p tcp -m tcp --dport 22 -m comment --comment "VM2/OpenBSD SSH port forwarding" -j DNAT --to-destination 192.168.122.125:22
-A PREROUTING -d 202.54.1.6/32 -p tcp -m tcp --dport 443 -m comment --comment "VM3/FreeBSD 443 port forwarding" -j DNAT --to-destination 192.168.122.231:443
-A PREROUTING -d 202.54.1.7/32 -p tcp -m tcp --dport 80 -m comment --comment "VM4/CentOS 80 port forwarding" -j DNAT --to-destination 192.168.122.229:80
-A POSTROUTING -s 192.168.122.0/24 -d 224.0.0.0/24 -j RETURN
-A POSTROUTING -s 192.168.122.0/24 -d 255.255.255.255/32 -j RETURN
-A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -p tcp -j MASQUERADE --to-ports 1024-65535
-A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -p udp -j MASQUERADE --to-ports 1024-65535
-A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -j MASQUERADE
COMMIT

And FORWARD rules from kvm hook shell script
# iptables-save -t filter | grep FORWARD
Sample outputs ( replace 192.168.2.0/24 with your public IPv4 sub/net):

:FORWARD DROP [0:0]
-A FORWARD -s 192.168.2.0/24 -d 192.168.122.0/24 -o virbr0 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -d 192.168.122.0/24 -o virbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -s 192.168.122.0/24 -i virbr0 -j ACCEPT
-A FORWARD -i virbr0 -o virbr0 -j ACCEPT
-A FORWARD -o virbr0 -j REJECT --reject-with icmp-port-unreachable
-A FORWARD -i virbr0 -j REJECT --reject-with icmp-port-unreachable

Conclusion

You just learned how to use KVM to forward ports to guests VM with UFW on Linux. If you are a new to UFW, see this page on how to setup UFW on Ubuntu and setup UFW on a CentOS 7 server here.

🥺 Was this helpful? Please add a comment to show your appreciation or feedback.

nixCrat Tux Pixel Penguin
Hi! 🤠
I'm Vivek Gite, and I write about Linux, macOS, Unix, IT, programming, infosec, and open source. Subscribe to my RSS feed or email newsletter for updates.

28 comments… add one
  • Thomas Meier Jul 31, 2022 @ 9:24

    Thanks for this tutorial although this is very far from my comfort zone. I have two KVM VMs (Home Assistant (HA) and ser2net). HA tries to connect to ser2net via it’s IP and port 3001; e.g. 192.168.2.10:3001. However I can’t connect although I can ping 192.168.2.10 from HA.

    I have setup a standard bridge br0 on the host and both VMs networking use br0. Home Assistant automatically scans the LAN for devices and that works fine so the bridge works fine as far as I can tell.

    Other than creating the bridge br0 I have not done anything else on the host or the guests; so everything else is standard.Thanks for the tutorial although that’s outside my comfort zone.

    I have two KVM guests (Home Assistant (HA) and ser2net (ser2net)). Home Assistant needs to access ser2net via port 3001; e.g. 192.168.2.10:3001. I have created a bridge (br0) on the host as HA needs to see the network to discover devices. That works well and both VMs use the bridge.

    I can’t access ser2net from HA although I can ping it from HA. Haven’t done anything on firewalls etc. on the host/guests. I’m not sure if your example is directly applicable as one VM needs to talk to the other as opposed to your example.

    # iptables -L FORWARD -nv –line-number gives the following.

    num   pkts bytes target     prot opt in     out     source               destination
    1        0     0 LIBVIRT_FWX  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    2        0     0 LIBVIRT_FWI  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    3        0     0 LIBVIRT_FWO  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    

    Any idea how to fix set this up?

  • Wilko van der Veen Sep 21, 2022 @ 5:42

    chmod +xv does not work (v is not valid). What should the right settings be?

    • 🛡️ Vivek Gite (Author and Admin) Vivek Gite Sep 21, 2022 @ 5:46

      Updated. The correct command is:
      chmod -v +x /etc/libvirt/hooks/qemu

  • Mahit Dec 8, 2022 @ 4:57

    Thank you so much! I don’t have much experience with iptables and I have been endlessly trying to figure out a way to do this. With your clear article, I was able to enable port forwarding to my KVM in just 20 minutes.

  • Rajat Bakshi Jan 3, 2023 @ 15:59

    This actually worked!!!

    Sir you are GOD!

    This is a very clear and working solution to a problem that has been challenging me for a very long time. Thank you so much!

  • Tukir Feb 1, 2023 @ 9:24

    Can we use this setup if we have only one Public IP?

  • Andrija Radičević Feb 8, 2023 @ 23:32

    Unfortunately this solution didn’t work out for me. My configuration is Ubuntu 22.04. LTS with ubuntu-desktop-minimal. I was always getting ‘Connectoin timed out’.
    However, I have found the solution here https://wiki.libvirt.org/page/Networking. All I had to do is to uncomment the following line in /etc/sysctl.conf :

    net.ipv4.ip_forward = 1

    and to create the /etc/libvirt/hooks/qemu file:

    #!/bin/bash
    
    # IMPORTANT: Change the "VM NAME" string to match your actual VM Name.
    # In order to create rules to other VMs, just duplicate the below block and configure
    # it accordingly.
    if [ "${1}" = "VM NAME" ]; then
    
       # Update the following variables to fit your setup
       GUEST_IP=
       GUEST_PORT=
       HOST_PORT=
    
       if [ "${2}" = "stopped" ] || [ "${2}" = "reconnect" ]; then
    	/sbin/iptables -D FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $GUEST_PORT -j ACCEPT
    	/sbin/iptables -t nat -D PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT
       fi
       if [ "${2}" = "start" ] || [ "${2}" = "reconnect" ]; then
    	/sbin/iptables -I FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $GUEST_PORT -j ACCEPT
    	/sbin/iptables -t nat -I PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT
       fi
    fi
    

    I didn’t need to add any changes to /etc/ufw/before.rooles file.

  • Anonymous Sep 11, 2023 @ 7:22

    How can we modify the script when we want to connect to more than one VM in our qemu script ?

Leave a Reply

Your email address will not be published. Required fields are marked *

Use HTML <pre>...</pre> for code samples. Your comment will appear only after approval by the site admin.