Programster's Blog

Tutorials focusing on Linux, programming, and open-source

iptables Port Forwarding Generator

Setting up iptables for port forwarding can be a confusing mess. Eventually I had to give up trying to write it out by hand, and created the script below in PHP that has everything written out as verbosely as possible, and makes use of functions to remove the need to repeat oneself.

It's here if I need it again in future. Feel free to use it.

Update 11th April 2023

I had since made some improvements to the script, and decided to publish it to GitHub instead. The original script is still available below in the Appendix if you need it though.

Appendix

Original Script

<?php

define("WAN_IP", "xxx.xxx.xxx.xxx");
define("WAN_INTERFACE_NAME", "enp0s2");
define("HOST_LAN_IP", "192.168.0.1");
define("LAN_INTERFACE_NAME", "enp1s0");


function createPortForwardingRule(
    int $incomingPort, 
    string $desiredInternalServerLanIp, 
    int $desiredPort,
    string $protocol="tcp"
)
{
    $commands = [];

    # Add a rule to accept the connection from the outside world in the first place
    $commands[] =  
        "/sbin/iptables"
        . " --append INPUT" 
        . " --in-interface " . WAN_INTERFACE_NAME 
        . " --destination " . WAN_IP . "/32" 
        . " --protocol {$protocol}"
        . " --dport ${incomingPort}" 
        . " --jump ACCEPT";

    # Add a NAT rule to transform the incoming ip/port to an outgoing ip/port
    $commands[] =  
        "/sbin/iptables"
        . " --table nat"
        . " --append PREROUTING" 
        . " --in-interface " . WAN_INTERFACE_NAME 
        . " --destination " . WAN_IP . "/32" 
        . " --protocol {$protocol}"
        . " --dport ${incomingPort}" 
        . " --jump DNAT"
        . " --to-destination ${desiredInternalServerLanIp}:{$desiredPort}";

    # Add a rule to tell iptables to allow forwarding in this scenario.
    $commands[] = 
        "/sbin/iptables" . 
        " --append FORWARD" . 
        " --protocol {$protocol}" . 
        " --destination ${desiredInternalServerLanIp}" . 
        " --dport ${desiredPort}" . 
        " --match state " . 
        " --state NEW,ESTABLISHED,RELATED" . 
        " --jump ACCEPT";

    return implode(PHP_EOL, $commands);
}


/**
 * Open up a port on this server to the outside world. This is part of
 * forwarding onto other servers, but for this server.
 */
function openPort(int $port, string $protocol="tcp")
{
    $commands[] = 
        "/sbin/iptables"
        . " --append INPUT "
        . " --protocol {$protocol}"
        . " --in-interface " . WAN_INTERFACE_NAME 
        . " --destination " . WAN_IP . "/32" 
        . " --dport ${port}"
        . " --jump ACCEPT"
        ;

    return implode(PHP_EOL, $commands);
}

function main()
{
    $commands[] = "#!/bin/bash";

    # reset by deleting all rules, and then all chains
    $commands[] = "/sbin/iptables --flush";
    $commands[] = "/sbin/iptables --delete-chain";

    # Setting default filter policy
    # Drop any incoming packets unless a subsequent rule whitelists them
    $commands[] = "/sbin/iptables --policy INPUT DROP";
    $commands[] = "/sbin/iptables --policy OUTPUT ACCEPT";
    $commands[] = "/sbin/iptables --policy FORWARD DROP";

    # Accept all traffic coming in and out of the loopback interface.
    $commands[] = "/sbin/iptables --append INPUT --in-interface lo --jump ACCEPT";
    $commands[] = "/sbin/iptables --append OUTPUT --out-interface lo --jump ACCEPT";

    # Allow any already established connections to keep carrying on.
    $commands[] = 
        "/sbin/iptables"
        . " --append INPUT"
        . " --match state"
        . " --state RELATED,ESTABLISHED"
        . " --jump ACCEPT"
        ;

    # Allow any already established connections to keep carrying on forwarding
    $commands[] = 
        "/sbin/iptables"
        . " --append FORWARD"
        . " --match state"
        . " --state RELATED,ESTABLISHED"
        . " --jump ACCEPT"
        ;

    # Add a rule to the input chain that the firewall should accept all packets 
    # coming in on the internal lan interface (e.g. dont filter block)
    $commands[] = 
        "/sbin/iptables"
        . " --append INPUT" # same as -I
        . " --in-interface " . LAN_INTERFACE_NAME
        . " --jump ACCEPT"
        ;

    # Accept the forwarding of all packets that came in on the internal internal 
    # private network for KVM guests.
    # This should go last
    $commands[] = 
        "/sbin/iptables"
        . " --append FORWARD"
        . " --in-interface " . LAN_INTERFACE_NAME 
        . " --jump ACCEPT"
        ;

    # Open up ports to this server here.
    $commands[] = openPort(22); // allow SSH in remotely
    $commands[] = openPort(80); // allow http connections to this server for reverse proxy
    $commands[] = openPort(443); // allow https connections to this server for reverse proxy

    # Add port forwarding rules here. E.g. facilitate remote SSH access to some internal servers
    $commands[] = createPortForwardingRule(4321, "192.168.0.2", 22);
    $commands[] = createPortForwardingRule(1234, "192.168.0.3", 22);

    # All packets being forwarded out of this server to the internet should look 
    #like they are coming from this server.
    $commands[] = 
        "/sbin/iptables"
        . " --table nat"
        . " --append POSTROUTING"
        . " --out-interface " . WAN_INTERFACE_NAME
        . " --jump MASQUERADE"
        ;

    print implode(PHP_EOL, $commands) . PHP_EOL;
}

main();


Last updated: 11th April 2023
First published: 21st July 2021

This blog is created by Stuart Page

I'm a freelance web developer and technology consultant based in Surrey, UK, with over 10 years experience in web development, DevOps, Linux Administration, and IT solutions.

Need support with your infrastructure or web services?

Get in touch