6

I would like for network traffic that arrives on 192.168.0.1:80 to be redirected to 127.0.0.1:3000. And, I would like the mapping of the response to be handled as well. My complete NAT and Filter table rules are pasted below.

I am able to receive connections on port 80. However, I have been unable to redirect the traffic to localhost:3000.

add table inet filter
add chain inet filter input { type filter hook input priority 0; policy accept; }
add chain inet filter forward { type filter hook forward priority 0; policy accept; }
add chain inet filter output { type filter hook output priority 0; policy accept; }
add rule inet filter input ct state related,established  counter accept
add rule inet filter input ip protocol icmp counter accept
add rule inet filter input iifname "lo" counter accept
add rule inet filter input ct state new  tcp dport 80 counter accept
add rule inet filter input ct state new  tcp dport 4489 counter accept
add rule inet filter input ct state new  tcp dport 8080 counter accept
add rule inet filter input iifname "tun0" ct state new  tcp dport 139 counter accept
add rule inet filter input iifname "tun0" ct state new  tcp dport 445 counter accept
add rule inet filter input ct state new  udp dport 1194 counter accept
add rule inet filter input counter reject with icmp type host-prohibited
add rule inet filter forward counter reject with icmp type host-prohibited
add table nat
add chain nat prerouting { type nat hook prerouting priority -100; }
add chain nat postrouting { type nat hook postrouting priority 100; }
add rule nat prerouting redirect
add rule nat prerouting tcp dport 80 redirect to 3000
add rule nat prerouting iifname eth0 tcp dport { 80, 443 } dnat 127.0.0.1:3000
add rule nat postrouting oifname eth0 snat to 192.168.0.1
stackhatter
  • 73
  • 1
  • 7

3 Answers3

7

I'll try to address and complete OP's own working answer and further comments, which include some remaining questions:

  • why is net.ipv4.conf.eth0.route_localnet=1 needed?
  • Why does port 3000 need to be allowed on eth0 rather than lo?

and will also address a minor security concern while at it.

First here's a mandatory schematic about Packet flow in Netfilter and General Networking:

Packet flow in Netfilter and General Networking

This schematic was made with iptables in mind, but nftables can (and does in most default rulesets) use the same hooks in the same places.

When a packet arrives in the network layer (IP layer 3), it is handled by various subsystems. Normally there would be only the routing stack, but here Netfilter provides hooks for itself (conntrack, including NAT handling after initial packet) or for nftables.

Netfilter (conntrack) or nftables don't care about routing (unless if for example nftables uses specialized expressions related to routing), they leave this to the routing stack: they manipulate addresses and ports and nftables then checks available properties like interfaces, addresses and ports.

So:

  • a packet in a new connection (thus also traversing ip nat prerouting) arrives from eth0 with (for example) source address 192.0.2.2 and port 45678 destination to address 192.168.0.1 and port 80 (or 443).

  • the ip nat prerouting dnat rule matches and tells netfilter (its conntrack subsystem) to change the destination address to 127.0.0.1 and the destination port to 3000. This doesn't change any other property of the packet. In particular the packet still arrived from eth0.

  • the routing stack (routing decision in the schematic) doesn't depend on Netfilter, so is logically kept independent of it and not aware of the previous alteration. It now has to handle a packet from 192.0.2.2 and destination 127.0.0.1.

    This is an anomaly: it would allow an address range reserved for loopback to be seen "on Internet", as stated in RFC 1122:

    (g) { 127, <any> }

    Internal host loopback address. Addresses of this form MUST NOT appear outside a host.

    which is explicitly handled in Linux kernel's routing stack: treat it as martian destination (ie: drops the packet), unless relaxed by using route_localnet=1 on the related interface. That's why for this specific case net.ipv4.conf.eth0.route_localnet=1 must be set.

  • likewise, the next nftables rule, this time from filter input hook, sees a packet with input interface still eth0 but with destination port now 3000. It must thus allow destination port 3000, and doesn't have anymore to allow 80 (or 443) to accept it. So the rule should be shortened to:

    iifname "eth0" tcp dport {4489, 3000} counter accept
    

    because it will never see packets from eth0 with destination tcp port 80 or 443: they were all changed to port 3000 in the previous nat prerouting hook. Moreover, for the sake of explanation, supposing such packets were seen, they would be accepted but as there would be no listening process on ports 80 or 443 (it's listening on port 3000), the tcp stack would emit back a TCP reset to reject the connection.

    Also while the routing stack enforces some relations between 127.0.0.0/8 and the lo interface (further relaxed with route_localnet=1), as told before this doesn't concern netfilter or nftables which don't mind anything about routing. In addition if such was the case, for the input interface this would be the source address which didn't change, not the destination address which would relate to the output interface which doesn't even have a real meaning in the input path: oif or oifname can't be used here. The mere fact to be in the filter input hook already means the evaluated packet is arriving on the host for a local process, as seen on the schematic.

    UPDATE: Actually the previously given rule should be further changed for security reasons: port 3000 gets allowed, but not just for destination 127.0.0.1. A connection to 192.168.0.1:3000 can thus receive a TCP RST which hints there's something special here, rather than not getting any reply. To address this case:

    • either use this (which includes a very strange looking 2nd rule):

      iifname "eth0" tcp dport 4489 counter accept
      iifname "eth0" ip daddr 127.0.0.1 tcp dport 3000 counter accept
      

      which, because route_localnet=1, still allows a tweaked system in the same 192.168.0.0/24 LAN to access the service without using NAT at all, by sending packets with 127.0.0.1 on the wire, even if there's probably no gain doing this. For example an other Linux system, with these 4 commands:

      sysctl -w net.ipv4.conf.eth0.route_localnet=1
      ip address delete 127.0.0.1/8 dev lo # can't have 127.0.0.1 also local
      ip route add 127.0.0.1/32 via 192.168.0.1 # via, that way no suspicious ARP *broadcast* for 127.0.0.1 will be seen elsewhere.
      socat tcp4:127.0.0.1:3000 -
      
    • or instead, also protecting for the case above, way more generic and to be preferred:

      iifname "eth0" tcp dport 4489 counter accept
      ct status dnat counter accept
      
      • it keeps the unrelated port 4489/tcp allowed as before
      • ct status dnat matches if the packet was previously DNATed by the host: it will thus accepts any prior alteration without having to restate explicitly which port it was (it's still possible to also state it or anything else to further narrow the scope of what is accepted): now the port value 3000 also doesn't have to be explicitly stated anymore.
      • it thus also won't allow direct connections to port 3000 since this case wouldn't have been DNATed.
  • just to be complete: the same things happens in (not quite) reverse order for output and replies. net.ipv4.conf.eth0.route_localnet=1 allows initially generated outgoing packets from 127.0.0.1 to 192.0.2.2 to not be treated as martian sources (=> drop) in output path's routing decision, before they have a chance to be "un-DNATed" back to the original intended source address (192.168.0.1) by netfilter (conntrack) alone.


Of course, using route_localnet=1 is kind of relaxing security (not really relevant with adequate firewall rules, but not all systems are using a firewall) and requires associated knowledge on its use (eg: copying the nftables ruleset alone elsewhere won't work anymore without the route_localnet=1 setting).

Now that the security concerns were addressed in the explanations above (see "UPDATE"), if the application were allowed to listen to 192.168.0.1 (or to any address) rather than only 127.0.0.1, an equivalent configuration could be done without enabling route_localnet=1, by changing in ip nat prerouting:

iif eth0 tcp dport { 80, 443 } counter dnat 127.0.0.1:3000

to:

iif eth0 tcp dport { 80, 443 } counter dnat to 192.168.0.1:3000

or simply to:

  iif eth0 tcp dport { 80, 443 } counter redirect to :3000

which don't differ much: redirect changes the destination to the host's primary IP address on the interface eth0 which is 192.168.0.1, so most cases would behave the same.

A.B
  • 9,037
  • 2
  • 19
  • 37
1

In order to get this to work, I decided to resort to the age old art of reading the documentation. When that didn't work, I used my newly obtained knowledge to fiddle with everything enough in order to get it to work, but I have no idea why this configuration works.

With the following nftables configuration, and entering sysctl -w net.ipv4.conf.eth0.route_localnet=1 at the shell prompt, I am able to connect to the service listening on localhost:3000 by connecting to the hypothetical external IP address (192.168.0.1:80) that is associated with eth0. However, it isn't clear to me why this works.

Including port 3000 in the line iifname "eth0" tcp dport {4489, 80, 443, 3000} counter accept is necessary in order for this to work.

flush ruleset

table inet filter {
        chain input {
                type filter hook input priority 0; policy drop;

                ct state established,related accept

                iif lo counter accept

                icmp type echo-request counter accept

                iifname "eth0" tcp dport {4489, 80, 443, 3000} counter accept
  }
}

table ip nat {
        chain prerouting  {

                type nat hook prerouting priority -100;

                iif eth0 tcp dport { 80, 443 } counter dnat 127.0.0.1:3000
        }
}
stackhatter
  • 73
  • 1
  • 7
  • @A.B Why does port 3000 need to be allowed on eth0? The dnat rule dnats the incoming connection to 127.0.0.1, which is an IP address assigned to lo. – stackhatter Jun 19 '20 at 02:43
  • @A.B. This explanation makes sense. If you want to write it up into an answer that answers the question, I will mark it as correct. The distinction between routing and the nftables operations is instructive. – stackhatter Jun 19 '20 at 03:15
  • @A.B. If you don't post an answer within a week, I will edit my answer, quoting your explanation. I thought that you may want to answer the question as I thought you should get credit for the clear explanation; I've already used this reasoning in other contexts - very helpful. – stackhatter Jun 19 '20 at 14:18
  • I changed a bit my answer to address also a minor security concern about your current firewall rules, search for the word "UPDATE". – A.B Jun 20 '20 at 14:24
0

You can use iptables-translate if you already have a functioning iptables rule and want to see its nftables equivalent.

For example, a functioning iptables rule for this redirect would be:

-t nat -A PREROUTING -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 3000

Feed that to iptables-translate and you get:

[root@vmtest-centos8 ~]# iptables-translate -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 3000
nft add rule ip nat PREROUTING tcp dport 80 counter redirect to :3000

No other nat rules should be needed for this, though it sounds like you might have other redirects you want to put in place also. Do the same for them.

Michael Hampton
  • 237,123
  • 42
  • 477
  • 940