To do complete port forwarding with netfilter, the firewall introduced linux 2.4, three things are required, and two are optional.

Forwarding and iptables support in the kernel

You need to at least compile support for DNAT and SNAT "Full NAT" and "Connection state match support" (only required for the state matching in the FORWARD-rule) in the kernel or as kernel modules. Just compile everything as modules, and you'll be sure not to miss anything.
The only things i'm sure you won't need are the ipchains and ipfwadm compatibility modules.

Then turn on forwarding with the following line at startup:

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

Your startup scripts already do this if your firewall already is forwarding from the internal network to the external network, so called masquerading or source-nat (SNAT).
This document assumes that this is the case.

The PREROUTING DNAT rule in the nat-table

First, for the forwarding itself, a rule in the PREROUTING chain of the nat-table is required. To forward tcp-port $FW_EXTERNAL_PORT on ip $FW_EXTERNAL_IP to internal ip:port $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT

iptables -t nat -A PREROUTING -p tcp --dport $FW_EXTERNAL_PORT -d $FW_EXTERNAL_IP -j DNAT --to-destination $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT

Means:

"Before deciding where to send packages coming into the machine (PREROUTING), match packets coming in on $FW_EXTERNAL_IP:$FW_EXTERNAL_PORT, and rewrite their destination addresses to $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT".

The FORWARD ACCEPT rule in the filter-table

Now, this is a forwarding, and for forwarding to work, you must also allow this forwarding in the FORWARD chain of the filter table:

iptables -A FORWARD -p tcp --dport $INTERNAL_MACHINE_PORT -d $INTERNAL_MACHINE_IP -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT

Means:

"When deciding whether to allow a packet to be forwarded (FORWARD), that is, while routing, match
packages going to $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT that are either part of a new
connection, an established connection, or related to an established connection, and allow them
through."

The POSTROUTING SNAT rule in the nat-table (optional)

Then, for this to work on the internal network, a rule in the POSTROUTING chain of the nat-table is required. Without this, the machine forwarded to on the internal network wouldn't know to send
responses on the internal network through the internal ip of the firewall, $FW_INTERNAL_IP.

iptables -t nat -A POSTROUTING -p tcp -d $INTERNAL_MACHINE_IP --dport $INTERNAL_MACHINE_PORT -j SNAT --to-source $FW_INTERNAL_IP

Means:

"After deciding where to send packages coming into the machine (POSTROUTING), match packets going
to $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT, and rewrite their source addresses to the internal address of the firewall, $FW_INTERNAL_IP".

The OUTPUT DNAT rule in the nat-table (optional)

To make requests from the firewall to the firewall itself work, yet another rule is needed.
You also have to have support for NAT of locally-generated connections (CONFIG_IP_NF_NAT_LOCAL) in the kernel, which was added in 2.4.19.

iptables -t nat -A OUTPUT -p tcp --dport $FW_EXTERNAL_PORT -d $FW_EXTERNAL_IP -j DNAT --to-destination $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT

Means:

"For packages originating from this machine, the firewall (OUTPUT), match packets coming in on $FW_EXTERNAL_IP:$FW_EXTERNAL_PORT, and rewrite their destination addresses to $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT".

The end result

So, here are the finished lines, slightly reformatted, with some example values:

FW_EXTERNAL_IP=1.1.1.1             # The IP-address of the external interface of the firewall
FW_EXTERNAL_INTERFACE=eth0         # The external interface, if using -i instead of -d.
FW_EXTERNAL_PORT=80                # The port to be forwarded
FW_INTERNAL_IP=192.168.0.1         # The IP-address of the internal interface of the firewall
INTERNAL_MACHINE_IP=192.168.0.2    # The IP-address of the machine on the internal network to be forwarded to.
INTERNAL_MACHINE_PORT=80           # The port to be forwarded to
# Activate forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# Forward packets coming in from the outside
iptables -t nat -A PREROUTING  -p tcp -d $FW_EXTERNAL_IP      --dport $FW_EXTERNAL_PORT      -j DNAT   --to-destination $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT
# Make it work from the firewall itself
iptables -t nat -A OUTPUT      -p tcp -d $FW_EXTERNAL_IP      --dport $FW_EXTERNAL_PORT      -j DNAT   --to-destination $INTERNAL_MACHINE_IP:$INTERNAL_MACHINE_PORT
# Make responses on the internal network go through the firewall
iptables -t nat -A POSTROUTING -p tcp -d $INTERNAL_MACHINE_IP --dport $INTERNAL_MACHINE_PORT -j SNAT   --to-source $FW_INTERNAL_IP
# Allow forwarded packets
iptables        -A FORWARD     -p tcp -d $INTERNAL_MACHINE_IP --dport $INTERNAL_MACHINE_PORT -j ACCEPT -m state --state NEW,ESTABLISHED,RELATED

Use external interface instead?

You can probably replace -d $FW_EXTERNAL_IP with -i $FW_EXTERNAL_INTERFACE, if you want to match incoming packets on the interface instead of the destination address, on all but the OUTPUT DNAT rule.

UDP instead of TCP?

Replace "-p tcp" with "-p udp" in all rules, and remove the "-m state --state NEW,ESTABLISHED,RELATED"-part of the last rule.

See the iptables howto/tutorial at http://iptables-tutorial.frozentux.net/iptables-tutorial.html#TABLE.DNATTARGET for a more thorough explanation of DNAT, and iptables(8) for information on iptables.

History: Port forwarding with netfilter