Easy NAT without scripting

I was once searching the internet for a NAT script to one of my machines to do my routing. Hopefully, I will be able to explain how to make a simple, fast, and scalable iptables rule set (and all without scripting it++); Iptables in linux does not have to be as complicated and obscure as many sites and people make it out to be. It's all really just a complex set of simple rules.

Every single rule in an IPtables chain is simple, it is broken out into no more than 5 main parts: table, chain action, chain name, options, action.
A chain action would be how you are going to manipulate a chain, Append, delete, insert, new chain, etc which is documented in the man page by -A. -X, -I, -N, respectively. A chain is a container or rules. Don't think of these as a table as tables are already used, we'll discuss tables in a bit. In a chain, you can define any number of rules but remember, the number of rules you add per chain will increase the amount of CPU time spend to process your chain. Try to always keep your chains as short as possible, or maybe even make your long set of strict matches to something generally specific. I'll go into more detail about being "generally specific" when we get into design. Keeping your rules short and clean also makes them easier to manage. I've dealt with many scripts that are suppose to make managing IPtables easier, harder and less secure. I've successfully converted a scripted firewall of over 1000 rules to just 50 lines, was more secure, and a lot easier to manage.

Options are the raw power of the chain you are manipulating. Here we can match every single bit in a packet after it's been striped of it's frame. We can even drop it at the interface with -i. So yes, IPtables does more than just IP. For example, lets Add a filter to our INPUT chain that drops all traffic from ppp0

iptables -A INPUT -i ppp0 -j DROP

Congrats! You've just rendered ppp0 a hole in the internet for any inbound packets. Anything that enters on ppp0 will be dropped and the sending host will never get a response as to the success or failure of the packet. Lets flush that chain so we can play around some more:
iptables -F INPUT

You can set the policy of only INPUT,OUTPUT, and FORWARD as these are the built-in chains. You should only really ever use ACCEPT or DROP for a policy, anything else just over complicates the matter. Only built-in (non-user-defined) chains can have policies, and neither built-in nor user-defined chains can be policy targets, says man 5 iptables. So if we defined a chain called badinternet, you can't set a policy on it to drop.

# WRONG!!!
iptables -N badinternets
iptables -P badinternets DROP

# Right!
iptables -P INPUT DROP
iptables -P INPUT ACCEPT

Actions are nothing more than what to do when iptables matches a packet to your rule. -j TARGET is all you need. If the packet does not match, the next rule in the chain is the examined; if it does match, then the next rule is specified by the value of the target, which can be the name of a user-defined chain or one of the special values ACCEPT, DROP, QUEUE, or RETURN. I've never dealt with QUEUE, but I'm sure it has it's purpose.

Lets build a few simple rules. Since this is a linux box running a little blog, lets add SSH and HTTP to our rules

# -p means protocol (icmp,tcp,udp,gre,ah,ipv6,etc)
# --dport means destination port for the protocol
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT

Any TCP traffic coming into our system will be explicitly accepted if it is for port 22 or 80. IPtables is on a first-match-apply matching policy meaning that if it finds a rule that matches your packet, it will be applied and --jump to wherever you said (or accept, drop, reject, queue). Lets specify some VERY specify ICMP rules while we are at it.
# -m means MATCH man iptables and look for MATCH, there are a ton of goodies here
#  We can only specify --icmp-type because -m icmp preceded it. 
iptables -A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT
iptables -A INPUT -p icmp -m icmp --icmp-type 11 -j ACCEPT
iptables -A INPUT -p icmp -j DROP

These specific IP table rules are based off the ICMP response types (which you can also get from /sbin/iptables -p icmp -h). ICMP type 8 is echo (so we exist on the internet) and ICMP type 11 is time exceeded (which is used in traceroutes). The --icmp-type comes directly from the MAN page when you do a search for 'icmp'. You should probably take this time to review the MATCH EXTENSIONS section of the man pages for iptables. You will find lots of handy matching tools that you will never use :) .

OK, so lets get NAT to work by enabling MASQUERADE on the external interface.

# -t means TABLE
iptables -t nat -A PREROUTING -o eth0 -j MASQUERADE

Yeah, thats it. You've got NAT enabled for eth0 (which your cable should be hooked into. For xDSL would be ppp0 or ppp1). Your probably wondering where the hell I got MASQUERADE. Well, it's because it's not a standard action, it's an EXTENSION. Look up TARGET EXTENSIONS in man 5 iptables. There are a ton of goodies in there, but most of them you will see are for special purposes while other can be kinda handy. Unfortunately, all this does for you is route outgoing traffic only! People can still access your router on your machine and your SSH is fully exposed.

We need to filter that inbound traffic without restricting established connections and making it easy to manage this firewall in the future for additional add-ons.



My homemade-from-scratch firewall with NAT that is easy to manipulate and manage. There is no need for shell scripting a well made set of iptable chains.

###
# NAT & Port Forwarding
###

*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:passit - [0:0]

# passit rule set
# use passit to forward packets to an arbitrary host
-A PREROUTING -i eth3 -j passit

# DMZ
# -A passit -p all -j DNAT --to-destination 172.16.22.3
#
# SSH ports
#-A passit -p tcp -m tcp --dport 9002 -j DNAT --to-destination 172.16.22.3:22 
# -A passit -p tcp -m tcp --dport 9003 -j DNAT --to-destination 172.16.22.13:22 
# -A passit -p tcp -m tcp --dport 9004 -j DNAT --to-destination 172.16.22.12:22 
-A passit -p tcp -m tcp --dport 6700 -j DNAT --to-destination 172.16.22.6:22

# Civ 4
-A passit -p udp -m udp --dport 6500 -j DNAT --to-destination 172.16.22.3 
-A passit -p udp -m udp --dport 2056:2082 -j DNAT --to-destination 172.16.22.3 
-A passit -p udp -m udp --dport 13139 -j DNAT --to-destination 172.16.22.3 

# RB6:Vegas
# -A passit -p udp -m udp --dport 3074:3174 -j DNAT --to-destination 172.16.22.6
# -A passit -p tcp -m tcp --dport 3074:3174 -j DNAT --to-destination 172.16.22.6

# Hamachi
#-A passit -p udp -m udp --dport 26000 -j DNAT --to-destination 172.16.22.3
#-A passit -p tcp -m tcp --dport 26000 -j DNAT --to-destination 172.16.22.3

# End passit
-A passit -j RETURN
# End passit

# Making NAT work!
-A POSTROUTING -o eth3 -j MASQUERADE

# For transparent VPN tunneling
# this is needed when you are using peer to net and not
# when using net to net.
-A POSTROUTING -o ppp0 -j MASQUERADE
-A POSTROUTING -o ppp1 -j MASQUERADE

# Use SNAT when you have a static or multiple statics
# -A POSTROUTING -d ! 172.16.0.0/255.240.0.0 -o eth3 -j SNAT --to-source 72.181.255.245
COMMIT

###
# Filtering
###

*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:inside - [0:0]
:outside - [0:0]
:tcpserv - [0:0]
:trusted - [0:0]

# allow established and related connections through
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

###
# Class definentions
###
-A INPUT -i lo -j ACCEPT
-A INPUT -i eth2 -j inside
-A INPUT -i eth3 -j outside

###
# Defacto rule
###

# reject everything that wasn't allowed
# this includes interfaces not explicitly specified
-A INPUT -j REJECT --reject-with icmp-host-unreachable

###
# Class configs
###

# allow DHCP clients 
-A inside -d 255.255.255.255 -p udp --dport 67 -j ACCEPT
-A inside -d 255.255.255.255 -p udp --dport 68 -j ACCEPT

# allow anyone on our network to access us
-A inside -s 172.16.22.0/24 -j ACCEPT
-A inside -j RETURN

# Allow Tracroute & ICMP  ( always good as a diagnositc tool )
-A outside -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A outside -p icmp -m icmp --icmp-type 11 -j ACCEPT
-A outside -p udp -m udp --sport 6277 --dport 1024:65535 -j ACCEPT
-A outside -p ipv6 -j ACCEPT

# Well defined TCP services
-A inside -p udp --dport 67 -j ACCEPT
-A inside -p udp --dport 68 -j ACCEPT
-A outside -p tcp --sport 1024:65535 -m state --state NEW -j tcpserv
-A outside -j RETURN

# Out external TCP services 
# Note: PREROUTING rules are processed before *filter rules
#    so you don't need to ACCEPT any forwarded ports here
-A tcpserv -p tcp --dport 22 -j trusted
-A tcpserv -p tcp --dport 6701 -j ACCEPT
-A tcpserv -p tcp --dport 80 -j ACCEPT
# -A tcpserv -p tcp --dport 443 -j ACCEPT
# -A tcpserv -p tcp --dport 9001 -j ACCEPT
# -A tcpserv -p tcp --dport 21 -j ACCEPT
# -A tcpserv -p tcp --dport 25 -j ACCEPT
# -A tcpserv -p tcp --dport 3690 -j ACCEPT
# -A tcpserv -p tcp --dport 8222 -j ACCEPT
# -A tcpserv -p tcp --dport 8333 -j ACCEPT
# -A tcpserv -p tcp --dport 3000 -j ACCEPT
# go ahead and reject in this chain because we've already 
# established that it is looking for an open port
-A tcpserv -j REJECT --reject-with icmp-port-unreachable

####
## Trusted Hosts
####

# Work
# I'm so not telling you ....

# My personal Server
-A trusted -s 209.62.10.65/28 -j ACCEPT
# always return, don't want to assume any actions
# and there might be another rule that applies
-A trusted -j RETURN

COMMIT