Update: Plain Old Debian Router 2017-07-30
This is script getting fairly old now (digitally speaking) but it still has some use. Jessie is now "oldstable" and ipv6 is gaining some popularity, but I figured someone might still get some tips from this, so I am leaving it up.
Plain Old Debian Router 2014-02-28
Intro
Over the years I have tried a number of different router arrangements. One of the most configurable solutions I have used is to manually build a router with iptables on a Debian box. Debian doesn't have frequent releases or cutting edge versions of applications, but it is heavily tested and solid as hell. You don't need Debian to use iptables (or ipset), pretty well any modern version of Linux should do. The information presented here can be used to turn a crappy PC (with 2 network cards) in to a router, instead of using one of those store bought ones. If you have any comments or questions, feel free to send me an email about it.
A Little Explanation
In the Linux world, iptables is a utility that can be used to control network access to your machine. It is quite powerful and can allow or deny traffic based on a comprehensive list of criteria. It can also be used to make it behave like a router, which passes network traffic on for other computers (perhpas better described as a gateway). Somewhat recently (February 2014) I learned of ipset which allows you to create sets of IP addresses, ports, networks, etc. and use them in your iptables rules. This means you can get much better performance out of your firewall rules and organize your firewall script a little better. A recent blowup with my pfSense router gave me some motivation to go back to my old firewall scripts to see if I could update them to meet my current needs. The product of these efforts is documented here in hopes that it may help others who wish to do the same thing.
I was thinking of writing up a complete description of each section, but realized that this would only chop up the script and make it harder to see the bigger picture. Presenting it as one big script also gives me the opportunity to improve my comments, which will hopefully help me down the road should I consider making any more significant changes.
I should also note that the Wheezy install I am referring to does not include a desktop environment. I personally think it is a good idea to use ssh to access a router so you can avoid the extra overhead of an X session. There's nothing stopping you from including a desktop environment and managing it remotely via VNC or xrdp (which I think is pretty slick)... but it is unnecessary.
The Firewall Script
Okay, it is probably easiest to copy and paste this in to a proper editor rather than trying to read it here.
#!/bin/bash
# This is a script I used to make a NAT masquerading firewall out of a stock
# Debian Wheezy installation. iptables should be included in the default
# install but you will probably need to 'apt-get install ipset' to get the
# added functionality of ipset.
# Last modified 2014-02-28
# Set variables... I have a PPPoE DSL connection so I set my external
# interface to ppp0 (configured with pppoeconf application). If you have a
# connection that directly uses your network adapter (such as DHCP) then
# you could just use eth0 for example.
INTIF="eth1"
EXTIF="ppp0"
# These two lines set variables that can be used if you have any rules that
# are based on IP address
EXTIP="`ifconfig ppp0 | grep 'inet addr' | awk '{print $2}' | sed -e 's/.*://'`"
INTIP="`ifconfig eth1 | grep 'inet addr' | awk '{print $2}' | sed -e 's/.*://'`"
# This line defines your internal network for rules that apply to all of
# your internal machines.
INTNET="192.168.1.0/24"
echo "Internal interface is" $INTIF "with IP address" $INTIP
echo "External interface is" $EXTIF "with IP address" $EXTIP
# These modules are required for iptables to do what you want.
echo "Loading the appropriate modules for iptables..."
depmod -a
modprobe ip_tables
modprobe ip_conntrack
modprobe ip_conntrack_ftp
modprobe ip_conntrack_irc
modprobe iptable_nat
modprobe ip_nat_ftp
modprobe ip_nat_irc
# Enabling IP forwarding and dynamic addresses will help too.
echo "Enabling IP forwarding..."
echo "1" > /proc/sys/net/ipv4/ip_forward
echo "1" > /proc/sys/net/ipv4/ip_dynaddr
# These lines create default policies for the INPUT, OUTPUT and FORWARD
# chains, and then flush out any existing rules.
echo "Clearing out any existing rules and setting default policy..."
iptables -P INPUT DROP
iptables -F INPUT
iptables -P OUTPUT ACCEPT
iptables -F OUTPUT
iptables -P FORWARD DROP
iptables -F FORWARD
iptables -t nat -F
# In the previous section, a policy was set to allow outbound traffic
# from the router, the following lines allow the return traffic in.
iptables -A INPUT -i $EXTIF -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i $INTIF -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow all traffic in and out of the loopback device.
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# These three lines let your internal machines have Internet access
# 1. allow established/related traffic from $EXTIF to $INTIF
# 2. allow internal to external traffic to be forwarded
# 3. masquerade traffic to external destinations (perform nat)
echo "Loading rules to perform NAT and allow outbound connectivity..."
iptables -A FORWARD -i $EXTIF -o $INTIF -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -i $INTIF -o $EXTIF -j ACCEPT
iptables -A POSTROUTING -t nat -o $EXTIF -j MASQUERADE
# ====== Custom Rules Start Here ======
# Everything above can probably be left as-is except for the definitions of
# your interfaces... what follows is the items specific to your needs.
# ====== Port forwarding ======
# These can be used to allow traffic to an internal server such as a web
# server or email server.
echo "Loading additional rules for port forwarding and router access..."
# SMTP
iptables -A PREROUTING -t nat -d $EXTIP -p tcp --dport 25 -j DNAT --to-destination 192.168.1.3:25
iptables -A FORWARD -d 192.168.1.3 -p tcp --dport 25 -j ACCEPT
# SMTP/S
iptables -A PREROUTING -t nat -d $EXTIP -p tcp --dport 465 -j DNAT --to-destination 192.168.1.3:465
iptables -A FORWARD -d 192.168.1.3 -p tcp --dport 465 -j ACCEPT
# IMAP/S
iptables -A PREROUTING -t nat -d $EXTIP -p tcp --dport 993 -j DNAT --to-destination 192.168.1.3:993
iptables -A FORWARD -d 192.168.1.3 -p tcp --dport 993 -j ACCEPT
# http
iptables -A PREROUTING -t nat -d $EXTIP -p tcp --dport 80 -j DNAT --to-destination 192.168.1.3:80
iptables -A FORWARD -d 192.168.1.3 -p tcp --dport 80 -j ACCEPT
# https
iptables -A PREROUTING -t nat -d $EXTIP -p tcp --dport 443 -j DNAT --to-destination 192.168.1.3:443
iptables -A FORWARD -d 192.168.1.3 -p tcp --dport 443 -j ACCEPT
# TeamSpeak
iptables -A PREROUTING -t nat -d $EXTIP -p tcp --dport 9987 -j DNAT --to-destination 192.168.1.3:9987
iptables -A FORWARD -d 192.168.1.3 -p tcp --dport 9987 -j ACCEPT
# ====== Services on the router ======
echo "Allowing access to services setup on the router..."
# Of course it makes sense to allow ssh connections from internal network
iptables -A INPUT -d $INTIP -s $INTNET -p tcp --dport 22 -j ACCEPT
# In many cases it makes sense to have other services also running on your
# router such as DHCP and DNS. Note: this allows DHCP on your internal
# network only, while DNS queries are also allowed from the Internet.
iptables -A INPUT -p udp --dport 67:68 -j ACCEPT
# Allow router to be used as a DNS server from inside and outside
iptables -A INPUT -d $INTIP -s $INTNET -p udp --dport 53 -j ACCEPT
#iptables -A INPUT -d $EXTIP -p udp --dport 53 -j ACCEPT
# ====== IPSET filtering ======
# These lines can be used to block large sets of IP addresses.
# https://www.countryipblocks.net/country_selection.php can be used to
# generate CIDR lists of entire countries while I have manually
# generated the other files as I go through my log files.
echo "Adding IPSET ranges..."
# bad_people: IP's that have specifically offended me with directed attacks or
# bad behaviour, ALL traffic from these addresses is blocked
echo "Blocking all access for bad_people..."
ipset create bad_people hash:net
sed 's:#.*$::g' bad_people.txt > stripped
sed '/^$/d' stripped > noblanks
sed 's:^:add bad_people :' noblanks > importfile
ipset restore < importfile
rm importfile
rm noblanks
rm stripped
iptables -I FORWARD -m set --match-set bad_people src -j REJECT
# CIDR ranges: The following entries are used to block a number of countries
# from which a lot of attacks and spam originate but no legitimate traffic.
# They are blocked from all access as well.
echo "Blocking all access for Brazil..."
ipset create cidr_brazil hash:net
sed 's:#.*$::g' cidr_brazil.txt > stripped
sed '/^$/d' stripped > noblanks
sed 's:^:add cidr_brazil :' noblanks > importfile
ipset restore < importfile
rm importfile
rm noblanks
rm stripped
iptables -I FORWARD -m set --match-set cidr_brazil src -j REJECT
echo "Blocking all access for China..."
ipset create cidr_china hash:net
sed 's:#.*$::g' cidr_china.txt > stripped
sed '/^$/d' stripped > noblanks
sed 's:^:add cidr_china :' noblanks > importfile
ipset restore < importfile
rm importfile
rm noblanks
rm stripped
iptables -I FORWARD -m set --match-set cidr_china src -j REJECT
# Microsoft Bing has been caught lying about its identity while scraping my
# web site, these lines block Bing from doing this.
echo "Blocking web server access for mis-identified Microsoft Bing..."
ipset create microsoft_bing hash:net
sed 's:#.*$::g' microsoft_bing.txt > stripped
sed '/^$/d' stripped > noblanks
sed 's:^:add microsoft_bing :' noblanks > importfile
ipset restore < importfile
rm importfile
rm noblanks
rm stripped
iptables -I FORWARD -p tcp --dport 80:443 -m set --match-set microsoft_bing src -j REJECT
# server_block: A manual list of IP's that belong to VPS and web hosting
# companies. They are to be blocked from my web server because the traffic
# is automated spam rather than human. They still need access to other
# ports though because they could still have legitimate traffic like SMTP.
echo "Blocking web server access for Server Block..."
ipset create server_block hash:net
sed 's:#.*$::g' server_block.txt > stripped
sed '/^$/d' stripped > noblanks
sed 's:^:add server_block :' noblanks > importfile
ipset restore < importfile
rm importfile
rm noblanks
rm stripped
iptables -I FORWARD -p tcp --dport 80:443 -m set --match-set server_block src -j REJECT
A little MORE explanation
A lot of that should be fairly self explanatory, but a couple things may need additional clarification. For example, the lines that include -m state --state ESTABLISHED,RELATED are referring to traffic that is part of an existing connection. This is quite powerful because it allows you to craft your rules specifically for new connections and then simply allow any resulting traffic.
The ipset section has been modified a few times... I admit that my ipset skills are poor, and there is probably a better more efficient way to do this. Seriously, feel free to tell me if you think this could be improved. In short, the ipset sections will strip out all of the comments and blank lines from each text file, leaving only a CIDR formatted list of addresses. Then (thanks to this article) it adds some ipset commands to the beginning of each line. Finally it imports the list in to a "set" which can be used by iptables. The reason I use this method is because (1) the text files are required if the lists are going to survive a reboot and (2) the ipset restore method actually is more than 10 times faster than the for loop methods described in many other tutorials.
DHCP Services
Typical store bought routers include DHCP services so that client devices can obtain IP addresses and talk on the network. To add DHCP services to the firewall described above you need some more software... you can use whatever you like, I happen to like dnsmasq. If you want to use dnsmasq, just apt-get install dnsmasq and then edit /etc/dnsmasq.conf with your favourite editor. It is possible to put all of your configuration options in this file or you can include additional configuration files with the following settings:
conf-dir=/path/dnsmasq.d
conf-file=/path/file.conf
I would suggest that you include some extra config files as it will help you to keep things better organized. To get your DHCP up and running you'll need a config line like this:
dhcp-range=192.168.1.101,192.168.1.110,255.255.255.0,7d
This will obviously create a pool of ten addresses to be handed out to clients and will use a lease duration of a week. If you have a machine that you want to set a reservation for you can do it like this:
dhcp-host=xx:xx:xx:xx:xx:xx,hostname,192.168.1.200,7d
Where "xx:xx:xx:xx:xx:xx" is the MAC address of the network card you wish to assign that address to. Personally I like to create reservations for all of my internal devices and then I know that machines in the normal pool are "unknown". If you need more options to be sent to your clients you can check the dnsmasq detailed documentation, 'cause there's lots.
DNS Services
DNS is the service that turns names like snork.ca in to IP addresses like 1.2.3.4, the good news is that dnsmasq can also do this for the devices on your network (as can any typical store bought router). What a store bought router might not be able to do though, is use DNS to block ads, provide names for your internal devices, and even provide a DNS blacklist for your internal email server. This part is a little more complex than the DHCP part... to simply block your devices from a certain server you can use a config option like this:
address=/www.badhost.com/127.0.0.1
Whenever a device on your network tries to access this address (directly or indirectly as is often the case with garbage web sites) it will be told that the server can be found at 127.0.0.1. In the computer world, that address means "yourself". The device will try to access the web site on "itself" and of course will be rejected. But what if badhost.com has lots of other names instead of www? Like ads1.badhost.com and ads2.badhost.com? Well, you put a dot at the beginning of the name instead of the www in the above config option like this:
address=/.badhost.com/127.0.0.1
Which will point ANYTHING.badhost.com to 127.0.0.1. Now some people might notice that as they surf around, the blocked items are replaced with an error message saying that the server is dead. This is because your local machine is probably not running a web server, and redirecting "bad names" to yourself results in a bunch of "no reply" messages. A cute way to get around this (and hide those error messages) is to use something like Homer from FunkyToad. The bad news is that FunkyToad Software seems to be dead (and bought by some hosting company)... the good news is, you can still get Homer here. The idea is that Homer is a little personal web server that just hosts up an empty one pixel image, or a background colour that will replace the error messages making web pages look much better.
So where can you get a list of junk servers to block from your surfing experience? Well, how about the MVPHosts list? It is a maintained collection of crappy servers that have nothing of value for the digital surfer. Their list is pretty extensive and loaded with comments which makes it a pain to parse through. I found that the best way to deal with that is to not parse through it at all. Create a little shell script called mvp_update.sh and populate it with this:
#!/bin/bash
wget http://winhelp2002.mvps.org/hosts.txt -O mvp_hosts.txt
/etc/init.d/dnsmasq restart
This will download their hosts file, name it mvp_hosts.txt (to make sure it is not confused with any other hosts file) and then restart dnsmasq. To tell dnsmasq that it should use this new file you need to add a line like this to one of your dnsmasq config files:
addn-hosts=/path/mvp_hosts.txt
Now, chmod your script to make it executable and run it. I would suggest adding a cron job that runs the script weekly to update it. I should note that some thanks goes to someone called Knyte who had posted a comment on my old bloggy site on 14-01-23 that got me motivated to get this going (on pfSense at the time).
You can add as many config files as you wish that are full of names you wish to resolve. I have one called ad_hosts.txt that I use to block ads, which is populated manually as I go around surfing and finding more crap. I also have one called surbl.txt which is full of sites I do not wish to be emailed about, and I use with SpamAssassin. I setup the surbl.txt like this:
address=/.850koa.com.snorkbl.local/127.0.0.2
address=/.8m.com.snorkbl.local/127.0.0.2
address=/.960knew.com.snorkbl.local/127.0.0.2
address=/.appfog.com.snorkbl.local/127.0.0.2
and then I configure a SURBL list in SpamAssassin like this:
urirhsbl FU_SNORKBL snorkbl.local A
body FU_SNORKBL eval:check_uridnsbl('FU_SNORKBL')
tflags FU_SNORKBL net
describe FU_SNORKBL This email contains a URI that has been blacklisted by Snork
score FU_SNORKBL 9.0
So if anybody emails me about 850koa.com or appfog.com etc. it will probably be deleted... depending on the final score of the message of course. Another nifty thing about dnsmasq is that it is a caching DNS server which means it will "remember" IP addresses of servers for a little while so that multiple requests for the same address can be answered much faster, which is awesome when doing DNS checks in an application like SpamAssassin.
So why use this?
There are lots of other firewall solutions... one that I have used a lot is pfSense which is packed full of other features that I don't have here like traffic shaping, CARP, PPPoE server, and VPNs. All of these things are possible on a Wheezy box, but would obviously require some additional work to get them going. pfSense also has numerous packages that extend its functionality. It is also probably easier to install/deploy pfSense because it comes in the form of a nice LiveCD with its own installer. However, if you want to learn more about iptables, ipset, and possibly create your own very custom firewall, hopefully you'll find this information helpful. Hopefully if you do find this useful you'll tell me about it so I won't feel like I am just typing at myself.