Ssh – Linux iptables ssh port forwarding (martian rejection)

forwardingiptableslinuxssh

I have a Linux gateway performing NAT for my home network. I have another network which I'd like to transparently forward packets to, but only to/from specific IP/ports (ie. not a VPN). Here's some example IP and ports to work with:

Source          Router          Remote Gateway     Remote Target
192.168.1.10 -> 192.168.1.1 ->  1.2.3.4        ->  192.168.50.50:5000

I'd like the Source machine to be able to talk to specific ports on Remote Target as if it were directly routable from Router. On the Router, eth0 is the private network and eth1 is internet-facing. Remote Gateway is another Linux machine which I can ssh into and it can route directly to Remote Target.

My attempt at a simple solution is to set up ssh port forwarding on Router, such as:

ssh -L 5000:192.168.50.50:5000 1.2.3.4

This works fine for Router, which can now connect locally to port 5000. So "telnet localhost 5000" will be connected to 192.168.50.50:5000 as expected.

Now I want to redirect traffic from Source and funnel through the now-established ssh tunnel. I attempted a NAT rule for this:

iptables -t nat -D PREROUTING -i eth0 -p tcp -s 192.168.1.10 --dport 5000 -d 1.2.3.4 -j DNAT --to-destination 127.0.0.1:5000

and since the Router is already my NAT gateway, it already has the needed postrouting rule:

-A POSTROUTING -s 192.168.1.0/24 -o eth1 -j MASQUERADE

Most Q&A on this site or elsewhere seem to deal with forwarding server ports or hairpin NAT, both of which I have working fine elsewhere, neither of which apply to this situation. I certainly could DMZ forward Remote Target ports through Remote Gateway, but I don't want the ports internet-accessible, I want them accessible only through the secure SSH tunnel.

The best answer I can find relates to Martian packet rejection in the Linux kernel:

iptables, how to redirect port from loopback?

I've enabled logging of martians and confirmed that the kernel is rejecting these packets as martians. Except that they aren't: I know exactly what these packets are for, where they're from and where they're going (my ssh tunnel).

The "roundabout" solution presented there is applicable to that original question, but does not apply for my case.

However, while writing/researching this question, I have worked around my problem by using SSH source IP binding like so:

ssh -L 192.168.1.1:5000:192.168.50.50:5000 1.2.3.4
iptables -t nat -D PREROUTING -i eth0 -p tcp -s 192.168.1.10 --dport 5000 -d 1.2.3.4 -j DNAT --to-destination 192.168.1.1:5000

Since I'm not using loopback, this gets around Martian rejection.

I still post the question here for two reasons:

  1. In hope that someone who is trying to do something similar in the future might find this in their searches and this workaround might help them.
  2. I still prefer the idea of keeping my ssh port forwards connection bound only to loopback and being able to route to them through iptables. Since I know exactly what these packets are and where they are going, shouldn't there some way for me to flag them as such so that Linux martian filtering doesn't reject them? All my searching on this topic leads to rp_filter, which didn't help at all in my testing. And even if it did work, it isn't specific to the exact packets I am trying to allow.

I'm interested in contributing my question and workaround to general search to save someone else the hours of searching I did only to come up with dead ends, as well as hopefully having someone answer the loopback/martian part of my question that still remains open to me.

Best Answer

The issue with doing a DNAT to 127.0.0.1:5000 is that when the remote side responds, these packets return into the routing engine as if they were locally originated (from 127.0.0.1) but they have an outside destination address. SNAT/MASQUERADE matching the outside interface would have caught them and rewritten them, but the routing decisions that have to be made for the packets to arrive at that interface come first, and they disallow these packets which are bogus by default. The routing engine can't guarantee you'll remember to do that rewrite later.

The thing that you should be able to do instead is reject any outside connections to 192.168.1.1:5000 at iptables INPUT other than those coming from 192.168.1.10 using the ! argument before the -s source address specification. If you use TCP reset as the rejection mechanism (-j REJECT --reject-with tcp-reset, instead of the default ICMP destination unreachable), it will be largely identical to the situation where nothing was even listening on that address:port combination as far as the outside world is concerned.

Related Question