0

I am attempting to solve the following problem:

I have a system which I will henceforth refer to as "Local" that hosts a service on port 80 and port 443, and depends on sending outgoing requests on port 25. It also hosts a separate service on port 22.
I have a system which I will call "Global" that has a globally accessible static IP address and has DNS configured for it, and is capable of accepting incoming requests on port 80, 443, 25, and 222.

Local and Global are connected (via a VPN interface, if it matters) on the reserved subnet 10.0.0.0/24

I want all incoming requests on Global ports 80 and 443 to redirect to Local on ports 80 and 443 respectively. I also want incoming requests on Global port 222 to redirect to Local on port 22 (yes, that is an intentionally different port). In addition, I want all outgoing requests to port 25 from Local to redirect to Global on port 25.

Both Local and Global are modern linux systems with apt, iptables, nftables, and ufw available.
I have tried a variety of iptables configurations with no success.

As far as I can tell a configuration that /should/ work (but doesn't!) is as follows:

Global:
    /etc/ufw/before.rules (excerpt)
        *nat
        :PREROUTING ACCEPT [0:0]

        # forward port 222 to Local:22
         -A PREROUTING -p tcp --dport 222 -j DNAT --to-destination <Local IP on 10.0.0.0/24 Subnet>:22

        # forward port 80 to Local:80
         -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination <Local IP on 10.0.0.0/24 Subnet>:80

        # forward port 443 to Local:443
         -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination <Local IP on 10.0.0.0/24 Subnet>:443

        # and forward the responses the other direction
         -A POSTROUTING -s 10.0.0.0/24 ! -d 10.0.0.0/24 -j MASQUERADE

        COMMIT

Local:
    /etc/ufw/before.rules (excerpt)
        *nat
        :PREROUTING ACCEPT [0:0]

        # forward outgoing port 25 to Global:25
        -A OUTPUT -p tcp --dport 25 -j DNAT --to-destination <Global IP on 10.0.0.0/24 Subnet>:25

        COMMIT

I realize that incoming requests for HTTP can be routed using nginx or apache configuration, but I want a generic solution that is not protocol-dependent and could be used for ssh or other protocols, as this traffic will not necessarily always be HTTP.

Does anyone know how this can be done?
Is there some reason that this type of configuration isn't possible?

  • Have you tried to use the FORWARD chain on the default 'filter' table rather than doing the nat rules in the PREROUTING chain? The masquerade rule looks right, the others might need to be changed. – datUser Mar 16 at 18:29
  • I tried setting it to use FORWARD but that didn't seem to work either. Interestingly however it does work with PREROUTING when I route to a different port on the same host, but it doesn't seem to forward to external hosts successfully. – Dan Mar 16 at 18:53
  • Yeah, so the PREROUTING chain is not for sending to other hosts, that would be FORWARD. – datUser Mar 16 at 21:35
0

Okay so after much consternation I was able to solve my original problem and can therefore post the correct solution here for anyone who might come across this in the future. Application layer configuration is outside the scope of the original question.

Firstly, if you have ufw set up you will need to allow these rules to apply without being blocked by the firewall. This will need to be done on BOTH hosts, if they both have ufw. This is done with the steps described at https://help.ubuntu.com/lts/serverguide/firewall.html under section heading "IP Masquerading", and they are summarized below

0) edit /etc/default/ufw to set DEFAULT_FORWARD_POLICY="ACCEPT"
1) edit /etc/ufw/sysctl.conf to set net/ipv4/ip_forward=1 and net/ipv6/conf/default/forwarding=1
2) put the desired iptables rules in /etc/ufw/before.rules using ufw's syntax, starting with *nat and ending with COMMIT
3) restart ufw 

To redirect incoming packets, and proxy them through to Local, use the following configuration on Global:

 #ensure forwarding is enabled, just for sanity's sake (for ufw sysctl.conf covers this)
 sysctl -w net.ipv4.ip_forward=1

 #rewrite incoming port 222 to Local:22
 iptables -t nat -A PREROUTING -p tcp --dport 222 -j DNAT --to-dest <Local IP on 10.0.0.0/24 subnet>:22

 #having rewritten the destination, also rewrite the source for all packets that now have a destination of Local:22
 #rewriting the source means that the ACKs and other bidirectional data gets sent back to Global instead of attempting to go from Local directly to the originator
 iptables -t nat -A POSTROUTING -d <Local IP on 10.0.0.0/24 subnet> -p tcp --dport 22 -j SNAT --to-source <Global IP on 10.0.0.0/24 subnet>

 #repeat the above for ports 80 and 443, as in the original question
 iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-dest <Local IP on 10.0.0.0/24 subnet>:80
 iptables -t nat -A POSTROUTING -d <Local IP on 10.0.0.0/24 subnet> -p tcp --dport 80 -j SNAT --to-source <Global IP on 10.0.0.0/24 subnet>

 iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-dest <Local IP on 10.0.0.0/24 subnet>:443
 iptables -t nat -A POSTROUTING -d <Local IP on 10.0.0.0/24 subnet> -p tcp --dport 443 -j SNAT --to-source <Global IP on 10.0.0.0/24 subnet>

That covers the incoming proxy connection. For outgoing connections, we'll need the following configuration on Local:

 #rewrite outgoing port 25 to Global:25, UNLESS it's meant for localhost (assumes lo is set up for ipv4, and not just ipv6)
 iptables -t nat -A OUTPUT -p tcp '!' -d 127.0.0.1/32 --dport 25 -j DNAT --to-destination <Global IP on 10.0.0.0/24 subnet>:25

 #having rewritten the destination, also rewrite the source for all packets that now have a destination of Global:25
 iptables -t nat -A POSTROUTING -p tcp '!' -d 127.0.0.1/32 --dport 25 -j SNAT --to-source <Local IP on 10.0.0.0/24 subnet>

We needed to allow localhost so that it's possible to connect to a local server on that port, as that is how SMTP and many other programs (including DNS) that do proxying at the protocol level need to operate. Therefore we forwarded everything that wasn't bound for localhost.

That's it! That's the complete configuration at this level of the stack, and it will get your packets where they need to be. Application level configuration is outside the scope of this question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.