Forward a TCP port to another IP or port using NAT with Iptables

Besides using NAT for accessing the internet with multiple machines using a single IP address, there are many other uses of NAT. One of them is to forward all traffic that is sent to a certain TCP port to another host. In practice, this technique can be used to test a service on a new host without adjusting anything on the client. The users or the clients do not need to be pointed to a new machine in order to test it. When the test would be unsuccessful, removing the NAT-rule is all it takes to switch back.

Since most major distributions switched to nftables instead, I decided to rewrite this article completely. Debian has nftables since Debian 10 (Buster) and CentOS and RHEL since version 8. Ubuntu also very recently, since 20.10 (Groovy Gorilla), moved to nftables. You can find the new version here: https://jensd.be/1086/linux/forward-a-tcp-port-to-another-ip-or-port-using-nat-with-nftables

If you are interested, I also created a YouTube video from this blogpost. If you prefer classic text, you can just follow the rest of this article:

Theoretical explanation

To above scenario is better known as port forwarding and it allows you to forward an incoming packet to another destination. That destination can be another port or IP-address. With the scheme under here, I will try to make things a little more visual and explain how port forwarding works:

nat_beforeafter

The left part of the scheme shows the original situation: the client connects to service A, running on server A (ip: 10.1.1.1). Incoming packets come through service A on server A and the outgoing packets go back to the original source IP, client A. The goal of the NAT-setup is to forward the traffic to service A running on server B (ip:10.2.2.2) without any modifications to client A. Our new setup needs to be transparent for the client.

The right part of the scheme shows our goal: the client still connects to TCP port 9999 to the IP of server A (10.1.1.1) so nothing changed on that side. As soon as a TCP packet with destination port 9999 arrives at server A, it should be forwarded to server B (ip: 10.2.2.2). Before redirecting the TCP-packet to the other machine, the packet needs to be modified so that it get’s sent back to server A before sending back to the original host.

To completely understand this process, we need to take a deeper look into how Iptables works. When a packet passes through Iptables, it passes a set of chains. Decisions made by those chains are called rules and that’s basically how you configure Iptables.

Overview of the chains used by Iptablesiptables_chains

For our setup to work, we need to add a DNAT and SNAT rule (Prerouting and Postrouting). The first one will make sure that the packet gets routed to the other host (ip: 10.2.2.2) and the second one will make sure that the source address of the packet is no longer the original one but the one of the machine who performed the NAT. That way, the packet will be sent back to it’s original source via the host which owns ip 10.1.1.1.

Firewalld

Since CentOS or RHEL 7, iptables has been “replaced” with firewalld. And while you can realize the scenario with firewalld, I will use the classic iptables. To use iptables instead of firewalld on CentOS 7 or RHEL 7, you can find more information in this post.

Preparations

The next steps prepare the system and iptables for NAT. Most commands can be used on CentOS, RHEL and Debian in exactly the same way. If there are some differences, I mention them (so if nothing special is mentioned, the commands are interchangeable).

IP forwarding

NAT uses IP forwarding and by default it’s not enabled in the kernel parameters. First we need to check if IP forwarding is enabled and if it’s not, we need to enable it.

To check if IP forwarding is enabled:

CentOS/RHEL:

[jensd@cen ~]$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0

Debian:

jensd@deb:~$ sudo sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0

The above outputs shows that IP forwarding is not enabled.

To enable IP forwarding persistently (survives a reboot):

CentOS/RHEL:

[jensd@cen ~]$ echo "net.ipv4.ip_forward = 1"|sudo tee /etc/sysctl.d/99-ipforward.conf
net.ipv4.ip_forward = 1

Debian:

jensd@deb:~$ sudo sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/g' /etc/sysctl.conf

To activate the changes immediately:

CentOS/RHEL:

[jensd@cen ~]$ sudo sysctl -p /etc/sysctl.d/99-ipforward.conf
net.ipv4.ip_forward = 1

Debian:

jensd@deb:~$ sudo sysctl -p
net.ipv4.ip_forward = 1

Iptables

The next thing to do is to check if Iptables is running on the system. Iptables is running as a kernel module so it can’t be seen as one of the normal processes.

CentOS/RHEL:

[jensd@cen ~]$ lsmod|grep iptable

Debain:

jensd@deb:~$ lsmod|grep iptable
iptable_nat            12928  0
nf_nat                 18242  1 iptable_nat
nf_conntrack_ipv4      14078  3 nf_nat,iptable_nat
nf_conntrack           52720  3 nf_conntrack_ipv4,nf_nat,iptable_nat
iptable_filter         12536  0
ip_tables              22042  2 iptable_filter,iptable_nat
x_tables               19118  3 ip_tables,iptable_filter,iptable_nat

If there is no output, it means that Iptables isn’t loaded.

To start Iptables:

[jensd@cen ~]$ sudo systemctl start iptables
[jensd@cen ~]$ lsmod|grep iptable
iptable_filter 12810 1
ip_tables 27239 1 iptable_filter

Now that we are sure that Iptables is active on the system, we can check which rules are active.

For the INPUT, FORWARD and OUTPUT-chains:

[jensd@cen ~]$ sudo iptables -L -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

For the NAT-related chains:

[jensd@cen ~]$ sudo iptables -t nat -L -n
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination

As you can see, at this moment, no rules are configured and all traffic is allowed to and from the system without doing anything NAT-related.

In case you already have some rules configured, it’s a good idea (in a testing environment) to flush the current rules:

[jensd@cen ~]$ sudo iptables -F
[jensd@cen ~]$ sudo iptables -t nat -F

Enable port forwarding

After going trough the above steps, we’re ready to active the port forwarding. As an example, I will forward the TCP port 9999 of host 192.168.202.103 to TCP port 80 on host 192.168.202.105.

First I will check that nothing is actually listening on port 9999 of host 192.168.202.103 by doing a telnet to port 9999 on that machine:

jensd@deb:~$ telnet 192.168.202.103 9999
Trying 192.168.202.103…
telnet: Unable to connect to remote host: Connection refused

To be sure that something is listening at port 80 of host 192.168.202.105, where I want to get forwarded, I can do the same check:

jensd@test:~$ telnet 192.168.202.105 80
Trying 192.168.202.105...
Connected to 192.168.202.105.
Escape character is '^]'.
GET /index.html HTTP/1.0

HTTP/1.1 200 OK
Date: Thu, 06 Nov 2014 11:00:49 GMT
Server: Apache/2.2.22 (Debian)
Last-Modified: Thu, 06 Nov 2014 10:48:30 GMT
ETag: "563-b1-5072e6fe83634"
Accept-Ranges: bytes
Content-Length: 177
Vary: Accept-Encoding
Connection: close
Content-Type: text/html

<html><body><h1>It works!</h1>
<p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p>
</body></html>
Connection closed by foreign host.

As you can see, there is a webserver running on port 80 on host 192.168.202.105.

Now, to forward port 9999 on host 192.168.202.103 to port 80 on host 192.168.202.105, we need to add the following rules to the iptables configuration of host 192.168.202.103:

[jensd@cen ~]$ sudo iptables -t nat -A PREROUTING -p tcp --dport 9999 -j DNAT --to-destination 192.168.202.105:80
[jensd@cen ~]$ sudo iptables -t nat -A POSTROUTING -p tcp -d 192.168.202.105 --dport 80 -j SNAT --to-source 192.168.202.103
[jensd@cen ~]$ sudo iptables -t nat -L -n
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:9999 to:192.168.202.105:80

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
SNAT       tcp  --  0.0.0.0/0            192.168.202.105      tcp dpt:80 to:192.168.202.103

To test if my NAT-rule is working, I will repeat the test with telnet:

jensd@test:~$ telnet 192.168.202.103 9999
Trying 192.168.202.103...
Connected to 192.168.202.103.
Escape character is '^]'.
GET /index.html HTTP/1.0

HTTP/1.1 200 OK
Date: Thu, 06 Nov 2014 11:03:07 GMT
Server: Apache/2.2.22 (Debian)
Last-Modified: Thu, 06 Nov 2014 10:48:30 GMT
ETag: "563-b1-5072e6fe83634"
Accept-Ranges: bytes
Content-Length: 177
Vary: Accept-Encoding
Connection: close
Content-Type: text/html

<html><body><h1>It works!</h1>
<p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p>
</body></html>
Connection closed by foreign host.

To permanently save the rules, execute iptables-save

CentOS/RHEL:

[jensd@cen ~]$ sudo iptables-save|sudo tee /etc/sysconfig/iptables
# Generated by iptables-save v1.4.21 on Thu Nov  6 12:02:25 2014
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A PREROUTING -p tcp -m tcp --dport 9999 -j DNAT --to-destination 192.168.202.105:80
-A POSTROUTING -d 192.168.202.105/32 -p tcp -m tcp --dport 80 -j SNAT --to-source 192.168.202.103
COMMIT
# Completed on Thu Nov  6 12:02:25 2014
# Generated by iptables-save v1.4.21 on Thu Nov  6 12:02:25 2014
*filter
:INPUT ACCEPT [862:76022]
:FORWARD ACCEPT [98:11497]
:OUTPUT ACCEPT [666:84508]
COMMIT
# Completed on Thu Nov  6 12:02:25 2014

Debian:

jensd@deb:~$ sudo iptables-save | sudo tee /etc/iptables.up.rules
# Generated by iptables-save v1.4.14 on Thu Nov  6 12:07:27 2014
*nat
:PREROUTING ACCEPT [1:78]
:INPUT ACCEPT [1:78]
:OUTPUT ACCEPT [3:159]
:POSTROUTING ACCEPT [3:159]
-A PREROUTING -p tcp -m tcp --dport 9999 -j DNAT --to-destination 192.168.202.103:80
-A POSTROUTING -d 192.168.202.103/32 -p tcp -m tcp --dport 80 -j SNAT --to-source 192.168.202.105
COMMIT
# Completed on Thu Nov  6 12:07:27 2014
# Generated by iptables-save v1.4.14 on Thu Nov  6 12:07:27 2014
*filter
:INPUT ACCEPT [4424:3400545]
:FORWARD ACCEPT [14:5970]
:OUTPUT ACCEPT [3185:304340]
COMMIT
# Completed on Thu Nov  6 12:07:27 2014

25 thoughts on “Forward a TCP port to another IP or port using NAT with Iptables

  1. Great article, I followed this article and was able to get all my mysql traffic forwarded to a second server from the 10.1.x.x subnet. However my forwarding server has 2 nics. How to achieve this on a centOS 5.11 server with 2 NICs one with 192.168.1.10 as IP and the other with 10.1.1.10 as IP. and the mysql server at 10.2.1.10? I need to do this because people from out side use putty to establish a connection via 192.168.1.10

    Thanks,

    • I would need to test this to be 100% sure but I assume that you can add a -i eth0 to the rules and adjust the source IP to the correct one matching the interface.

  2. Pingback: VPS: How to forward traffic to devices from public IP | DL-UAT

  3. I need same config, but in your configuration, client IP seen as SERVER 1 IP. That’s is a bit problem. What if i need to on SERVER2 client ip ?

    Because by the following rule,

    nat -A POSTROUTING -p tcp -d 192.168.202.105 –dport 80 -j SNAT –to-source 192.168.202.103

    You overwrite source IP of client. And server won’t be able to detect client IP.

    How we can receive client IP as source on SERVER 2?

    • I didn’t really test this but I think that MASQUERADE is what you’re looking for. SO instead of putting the POSTROUTING rule in your Iptables, you could add something like this:
      iptables -t nat -A POSTROUTING -j MASQUERADE

  4. You started with great examples using IPs above and then went off and used completely different IPs in your examples. The post makes sense as you have it but would have been even clearer using your initial IPs form the diagrams.

  5. Pingback: Linux Networking | Plesk Issues and fixes.

  6. I use this command for connect rdp port via vps vpn server but not working
    iptables -t nat -A PREROUTING -p tcp –dport 3389 -j DNAT –to-destination 10.8.0.19:3389
    iptables -t nat -A POSTROUTING -p tcp -d 10.8.0.19 –dport 3389 -j SNAT –to-source “public ip”

  7. client 192.168.1.101
    server 192.168.1.2
    nas 192.168.1.3:80
    I want redirect traffic to 192.168.1.3:80 when I ask server 192.168.1.2:80.
    On server I don’t have apache etc.
    I try iptables -t nat -A PREROUTING -p tcp –dport 80 -j DNAT –to-destination 192.168.1.3:80, but it doesn’t work :(

  8. Thanks – This was useful
    You may need to add a FORWARD rule if your default is to drop forwarded packets, with the source as the originating network, and the destination as Server B with the port as Server B’s listening port. (I needed to do this, anyway.)

    E.g.
    iptables -A FORWARD -s 10.169.16.0/24 -d 192.168.202.103 -p tcp –dport 9999 -j ACCEPT

    In the example above the originating network shows a completely different source network. This locks forwarding down to packets that originate *only* from that network, which is fine.

  9. Thanks For this blog. Finally i got for what i looking for, and thanks for making tutorial on Centos & Ubuntu. It’s really helpful.

  10. Pingback: Resolved: How can I route all traffic through one interface, to another interface, but send certain traffic to another IP with iptables? - Resolved Problem

  11. Pingback: IP forwarding with iptables - Boot Panic

  12. Pingback: iptables NAT HTTPS on external IP to internal IP – debian - Boot Panic

  13. Pingback: Resolved: IPTables forward traffic coming from port + subnet-mask to another port - Resolved Problem

Leave a Reply

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