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:
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 Iptables
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
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.
Pingback: VPS: How to forward traffic to devices from public IP | DL-UAT
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
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.
Pingback: Linux Networking | Plesk Issues and fixes.
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”
Kudos for the clear and well made article! Exactly what I was looking for and well explained by providing the right examples.
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 :(
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.
It would be helpful if you add topic on “How to delete iptable rules”
Thank you very much
Awesome guide,
Many thanks,
Great article! The diagram is perfect!
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
Great article.
The link to the new version (using nftables) is broken ;)
Cheers
Hi Alex,
Thanks for the heads-up. Fixed the link. Should be working now.
Greetings,
Jens
Thanks For this blog. Finally i got for what i looking for, and thanks for making tutorial on Centos & Ubuntu. It’s really helpful.
Great article, thanks a lot, works perfectly !
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
Pingback: IP forwarding with iptables - Boot Panic
Pingback: iptables NAT HTTPS on external IP to internal IP – debian - Boot Panic
Pingback: Resolved: IPTables forward traffic coming from port + subnet-mask to another port - Resolved Problem
Great Article …..really helpful for me….Thanks a lot ^^