I got lo (127.0.0.1) and eth0 (172.17.0.8). I want to redirect packets that land on 127.0.0.1:80 to 172.17.42.1:80 (route from eth0).

I tried

iptables -t nat -A OUTPUT -p tcp --dport 80 -d 127.0.0.1 -j DNAT --to 172.17.42.1:80

But when I do curl localhost:80 I get no response, when I do curl 172.17.42.1:80 it just works.


When you are trying to reach localhost, your source address is 127.0.0.1, as is your destination address. So, packet looks something like this:

 | SRC        |  DST       |
 | 127.0.0.1  |  127.0.0.1 |

Since packets that are locally generated first traverse OUTPUT chain, you modify the packet with DNAT rule:

iptables -t nat -A OUTPUT \
         -d 127.0.0.1 \
         -p tcp --dport 80 \
         -j DNAT \
         --to 172.17.42.1:80

After OUTPUT chain, packet looks like this:

 | SRC        |  DST        |
 | 127.0.0.1  |  172.17.42.1 |

So, the first error you see is that even if this packet gets routed outside the source device, destination will not know how to correctly return it. Thus, you also need to add additional SNAT rule:

iptables -t nat -A POSTROUTING \
         -d 172.17.42.1 \
         -p tcp --dport 80 \
         -j SNAT \
         --to-source <your_ip_addr>

Now, packet will look pretty much correct to exit on the network (source address will be the public address of this device and not localhost address).

But, this won't solve your pain just yet.

Routing decisions for packets generated on a local machine are taken in two places.

  • before the OUTPUT chain
  • after the OUTPUT chain and before POSTROUTING

Decision for this packet will be made in the second stage, before the source IP is rewritten in POSTROUTING chain...

So, kernel security mechanism will drop the packet because by default kernel refuses to route packets with src of 127.0.0.1. That means that even mangle coupled with fwmark wouldn't help us here. What's needed for this to work is to enable route_localnet. That's the per-iface variable that enables the use of 127/8 for local routing purposes (default is 0 - aka disabled).

sysctl -w net.ipv4.conf.all.route_localnet=1

You can safely change 'all' for the name of the outgoing interface in your case.

Now, packets will get routed by the previous rules in the table, and since we had the POSTROUTING SNAT, we will fix the source ip of 127.0.0.1 and rewrite it.

Hope this helps.

PS. sorry before 2-3 edits, it's 5AM here and I'm still awake :)


Before one jumps straight into firewall rules, one should also perform a simple forwarding check. Rather like when one checks the power cord is plugged in before taking apart the hardware.

Run:

cat /proc/sys/net/ipv4/ip_forward

If you get a zero, IPv4 will not forward. You'll need to turn this on.

To turn it on immediately and ephemerally to verify behavior:

echo 1 > /proc/sys/net/ipv4/ip_forward

The above turns it on for the machine but is simply modifying a kernel setting on the fly and will not be "saved".

Edit the sysctl.conf file to make the proper permanent change and ensure the following setting:

net.ipv4.ip_forward = 1