KVM forward ports to guests VM with UFW on Linux

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. 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 ufw on Linux
Est. reading time 6 minutes

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

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 +xv /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
# 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

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

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.


🐧 Get the latest tutorials on Linux, Open Source & DevOps via RSS feed or Weekly email newsletter.

🐧 16 comments so far... add one


CategoryList of Unix and Linux commands
Disk space analyzersdf ncdu pydf
File Managementcat cp mkdir tree
FirewallAlpine Awall CentOS 8 OpenSUSE RHEL 8 Ubuntu 16.04 Ubuntu 18.04 Ubuntu 20.04
Network UtilitiesNetHogs dig host ip nmap
OpenVPNCentOS 7 CentOS 8 Debian 10 Debian 8/9 Ubuntu 18.04 Ubuntu 20.04
Package Managerapk apt
Processes Managementbg chroot cron disown fg jobs killall kill pidof pstree pwdx time
Searchinggrep whereis which
User Informationgroups id lastcomm last lid/libuser-lid logname members users whoami who w
WireGuard VPNAlpine CentOS 8 Debian 10 Firewall Ubuntu 20.04
16 comments… add one
  • David Jul 28, 2020 @ 19:27

    Wow, this is a really great write-up. Thanks!

  • David Jul 28, 2020 @ 20:35

    One question: what’s the significance of the 192.168.2.XXX ip address?

  • Regi Jul 29, 2020 @ 16:41

    I think I’m missing something.

    I’m still confused with the 192.168.2.0/24 referencing. This IP range isn’t stated anywhere in the described sample setup or ‘virsh net-dumpxml’ output.

    • 🐧 Vivek Gite Jul 29, 2020 @ 17:15

      a) 192.168.2.0/24 – My LAN sub/net (replace this with your public IP sub/net)
      b) 192.168.122.0/24 – My bridge sub/net for KVM

  • Brett Burch Sep 18, 2020 @ 17:49

    Next forward ssh traffic coming to 202.54.1.5 VM2 at 192.168.122.125

    what is the ip 202.54.1.5?

    • 🐧 Vivek Gite Sep 18, 2020 @ 20:30

      Public IPv4 202.54.1.5.

      • Toushin Taishi May 24, 2021 @ 4:54

        In your example:
        ## NOTE replace 192.168.122.0/24 with your public IPv4 sub/net ##

        Should this be 192.168.2.0/24 and not 192.168.122.0/24 as 192.168.122.0/24 is the subnet of the KVM network?

        • 🐧 Vivek Gite May 24, 2021 @ 8:56

          Yes. I fixed it to avoid confusion. Thank you!

  • Ninjahopper Oct 30, 2020 @ 8:09

    [ $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

    The above ^^^ from your code On Ubuntu 20LTS gives me an “integer expected” error when i try to run the script on terminal.
    I know your server uses centos7, do you have any advice on how to proceed next?

    • 🐧 Vivek Gite Oct 30, 2020 @ 9:31

      Remove [ $v -le 2 ] && . In other words:
      /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
      Make sure you use correct sub/net and nat interfaces

  • Michael Nov 16, 2020 @ 19:09

    Hey,

    I have some guest with IPv6 only addresses and I want to block OUTGOING traffic for “guest-1”. I want the user can access from public internet the guest (“guest-1”) through VNC, SSH, noVNC (80 port) but block all access to the internet (inclusive WWW, and other TCP/UDP connections). Can you provide me some example please?

    Thank you!

  • Toushin Taishi May 21, 2021 @ 7:20

    Hi. What if you don’t have a LAN subnet? The server only has an Internet IP address and does not have LAN subnet. How to do it?

    • 🐧 Vivek Gite May 21, 2021 @ 8:35

      Subnet will be there. Run
      ip a s
      ip a s eth0

      You will see subnet with your public IP.

  • PW May 28, 2021 @ 7:11

    This was a massive help, thank you.

  • PW May 28, 2021 @ 7:12

    This was a great help, thanks for the quality write up.

Leave a Reply

Your email address will not be published.

Use HTML <pre>...</pre> for code samples. Still have questions? Post it on our forum