I’ve never had much luck with home broadband routers from Linksys and the like. That’s not to say that they’re bad - for most users they’re more than adequate. I just happen to have expectations for a router that SOHO appliances can’t meet. About five years ago a friend of mine introduced me to Smoothwall. I built a smoothie on old parts I had from crap PCs I bought at thrift stores. The case and power supply were from an old Midwest Micro tower, the innards from an old Compaq desktop. While by today’s standards, an AMD K6 with 144MB of RAM is pathetic it has served me well as my router, firewall, DHCP server, etc for the last five years without even flinching.
Alas, I discovered that the poor old IDE drive was finally giving up the ghost. I discovered this quite by accident. I logged into it to troubleshoot a network issue on a game console and wanted to see if the device was even reaching the gateway. I quickly discovered that I couldn’t even run ls
, much less tcpdump
. It must have died in the last few days because I know I’ve logged into it as recently as a week ago. The crazy thing is, because of the stability of Linux and the narrowly-defined responsibilities of this box, it’s continued to function as a router and firewall with no issue for at least a few days, despite a dead hard drive. If I rebooted it, I’d probably be in bad shape but for now I’m still online.
For a while I’ve wanted to replace the Smoothwall with a custom made router/firewall based on Debian Linux and iptables. I don’t dislike Smoothwall. What you get out of the box with Smoothwall is stellar. For my purposes, the guts are too complicated for the customizations I’d like to have and there’s a lot of cool features that I am not interested in using. For my purposes, a few shell scripts and a couple services will be more than sufficient and easier for me to manage.
IMPORTANT NOTE
My Internet service includes a static IP. This router configuration will not pull DHCP on the WAN (it will provide DHCP to the LAN). It will not connect via PPPoE either.
Build Environment
VMWare
I’ve got a system running VMWare Server 2.0 that I use for development. I created two virtual machines: one (frankenstein-test) has the first NIC bridged to my LAN, the other was on a “Host Only” network internal to VMWare. The second virtual machine only had a NIC on the “Host Only” network. This second VM (router-client) has no access to my physical network so if it’s going to reach the Internet it must do so through frankenstein-test.
Network
- Real LAN: 192.168.1.0/24
- Real Gateway: 192.168.1.1
- Host Only LAN: 192.168.128.1/24
- frankenstein-test eth0: 192.168.1.180
- frankenstein-test eth1: 192.168.128.1
- router-client eth0: 192.168.128.50
I had three main areas that I needed to get working. The first was routing with NAT, the second was TCP and UDP port forwarding, the third was DHCP with dynamic and static assignments.
Debian VM Install
I chose Debian because after using it on servers at work I’ve found it very intuitive as an administrator. I feel that default configuration choices and locations for things is very sensible. That’s not to speak ill of any other distribution. Regardless, the core ideas should be easy to implement on any Linux distribution.
I have a Debian Etch net install iso on my VMWare server to do installs from. I went through the install configuring the primary network interfaces manually with the desired IPs. My preference for partitioning on frankenstein-test was to have a large /var
partition as I might set up a caching squid proxy later. That’s outside the scope of this article and I might cover it later. I have a caching squid proxy on my network already to speed package downloads so I pointed my systems to that. When I got to the point of selecting what I wanted installed, I deselected everything. I installed the router-client system the same way and I already had frankenstein-test working as a router when I went to build the client so it was able to install of the proxy on my real LAN.
The Build
At the time I began writing this I had a working router in my test environment that meets all three of my requirements. I copied the config files that I changed off of frankenstein-test, issued a soft-shutdown, and took a VMWare snapshot. The snapshot is so that when I rebuild if I find that I missed anything I can go back and grab it. I like snapshots of powered off VMs because I know there won’t be any funky “missing time” issues.
I power the VM up with it configured to boot off of my ISO to begin a clean install. My choices by screen are as follows:
- (Choose Language) English
- (Choose Language) United States
- (Choose Keyboard Layout) American English
- (Configure the Network) - Found two NICs, I chose eth0
- (It came up via DHCP with no default route, so I say //No// to //Continue without a default route//.
- Configure Network Manually
- (IP Address) //192.168.1.180//
- (Netmask) //255.255.255.0//
- (Gateway) //192.168.1.1//
- (Nameserver)
- (Hostname) //frankenstein-test//
- (Domain name) //internal.lub-dub.org//
- (Partitioning) Manual Partitioning - this is an 8GB virtual disk
- 100MB, Primary, /boot
- 2GB, Primary, /
- 512MB, Primary, swap
- (remainder), Primary, /var
- (Partition Disks) Write Changes to Disk
- (Configure time zone) - Pacific
- (Root Password) - //insecure//
- (Confirm Root Password) - //insecure//
- (User Name) - //Jason Mansfield//
- (Username) - //jason//
- (Password) - //insecure//
- (Confirm Password) - //insecure//
- (Use a network mirror) - Yes
- United States
- ftp.us.debian.org
- (Proxy) - //http://proxy.internal.lub-dub.org:3128/ // - You’ll probably leave this blank.
- (Popularity Contents) - No
- (Software Selection) - Uncheck //Standard System//
- (Install GRUB to MBR) - Yes
Configuration
I log in as root and want to install openssh but our sources are still pointing to the CD. I comment out the deb cdrom
line from /etc/apt/sources.list
. Before I can install anything I need to update my local apt database:
apt-get update
I can now install openssh:
apt-get install openssh-server
I edit /etc/ssh/sshd_config
and set:
ListenAddress 192.168.128.1
and restart sshd.
Router/Firewall/Port Forwarding
I alas, my network configuration is still based on DHCP. Here’s what I have for /etc/network/interfaces
:
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).
# The loopback network interface
auto lo
iface lo inet loopback
# The primary network interface
allow-hotplug eth0
iface eth0 inet dhcp
I change it to this:
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).
# The loopback network interface
auto lo eth0 eth1
iface lo inet loopback
# The primary network interface
iface eth0 inet static
address 192.168.1.180
netmask 255.255.255.0
network 192.168.1.0
broadcast 192.168.1.255
gateway 192.168.1.1
# dns-* options are implemented by the resolvconf package, if installed
dns-nameservers
dns-search .internal.lub-dub.org
iface eth1 inet static
address 192.168.128.1
netmask 255.255.255.0
up /etc/firewall/iptables.sh
You’ll notice the up
line in eth1
. That’s the script the script that does the magic for our router/firewall. The /etc/firewall/
directory doesn’t exist by default so I had to create it. The script /etc/firewall/iptables.sh
looks like this:
#!/bin/bash
IPTABLES=/sbin/iptables
EXTIF=eth0
INTIF=eth1
EXTIP=192.168.1.180
INTIP=192.168.128.1
INTCIDR=/24
PORTFWFILE=/etc/firewall/portfw
for i in filter nat mangle
do
${IPTABLES} -t ${i} -F
done
${IPTABLES} -t nat -A POSTROUTING -o ${EXTIF} -j SNAT --to ${EXTIP}
${IPTABLES} -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
${IPTABLES} -A INPUT -m state --state NEW -i ${INTIF} -j ACCEPT
${IPTABLES} -P INPUT DROP
${IPTABLES} -A FORWARD -i ${EXTIF} -o ${EXTIF} -j REJECT
${IPTABLES} -N spoof
${IPTABLES} -A spoof -j LOG --log-prefix 'SPOOF' ${LOGOPTIONS}
${IPTABLES} -A spoof -j REJECT
for PRIVNET in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 127.0.0.0/8
do
${IPTABLES} -t filter -A FORWARD -i ${EXTIF} -s ${PRIVNET} -j spoof
done
echo 1 > /proc/sys/net/ipv4/ip_forward
for RULE in `cat ${PORTFWFILE} | grep -v ^# | grep -v '^[ \t]\{0,\}$'`
do
INPORT=`echo ${RULE} | cut -d: -f 1`
OUTIP=`echo ${RULE} | cut -d: -f 2`
OUTPORT=`echo ${RULE} | cut -d: -f 3`
PROTO=`echo ${RULE} | cut -d: -f 4`
${IPTABLES} -t nat -A PREROUTING -d ${EXTIP} -p ${PROTO} --dport ${INPORT} -j DNAT --to ${OUTIP}:${OUTPORT}
${IPTABLES} -A INPUT -p ${PROTO} -m state --state NEW --dport ${INPORT} -i ${EXTIF} -j ACCEPT
${IPTABLES} -t nat -A POSTROUTING -p tcp -s ${INTIP}${INTCIDR} -d ${OUTIP} --dport ${OUTPORT} -j SNAT --to ${EXTIP}
done
You’ll need to make the script executable. Here’s what it all does:
IPTABLES=/sbin/iptables
EXTIF=eth0
INTIF=eth1
EXTIP=192.168.1.180
INTIP=192.168.128.1
INTCIDR=/24
PORTFWFILE=/etc/firewall/portfw
Define our internal and external interfaces and internal and external IPs. The PORTFWFILE
is used to define port forwarding rules as explained later. The CIDR netmask is optionally used in port forwarding later on.
for i in filter nat mangle
do
${IPTABLES} -t ${i} -F
done
We want to clear our existing rules for each of the filter, nat, and mangle tables.
${IPTABLES} -t nat -A POSTROUTING -o ${EXTIF} -j SNAT --to ${EXTIP}
Traffic going out the external interface should be NATted to the external IP.
${IPTABLES} -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
${IPTABLES} -A INPUT -m state --state NEW -i ${INTIF} -j ACCEPT
Allow traffic through that we’ve already decided is OK. Allow all outbound new connections.
${IPTABLES} -P INPUT DROP
${IPTABLES} -A FORWARD -i ${EXTIF} -o ${EXTIF} -j REJECT
Anything we haven’t matched with a prior rule gets dropped. Any traffic that would be routed in and out the external interface gets rejected.
${IPTABLES} -N handle_spoof
${IPTABLES} -A handle_spoof -j LOG --log-prefix 'SPOOF' ${LOGOPTIONS}
${IPTABLES} -A handle_spoof -j REJECT
for PRIVNET in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 127.0.0.0/8
do
${IPTABLES} -t filter -A FORWARD -i ${EXTIF} -s ${PRIVNET} -j handle_spoof
done
Here we want to deal with traffic coming in from the Internet that shouldn’t actually originate from the Internet because it’s on private/reserved/loopback IP space. These are known as Martian packets.
echo 1 > /proc/sys/net/ipv4/ip_forward
Allow the system to function as a router.
for RULE in `cat ${PORTFWFILE} | grep -v ^# | grep -v '^[ \t]\{0,\}$'`
do
INPORT=`echo ${RULE} | cut -d: -f 1`
OUTIP=`echo ${RULE} | cut -d: -f 2`
OUTPORT=`echo ${RULE} | cut -d: -f 3`
PROTO=`echo ${RULE} | cut -d: -f 4`
${IPTABLES} -t nat -A PREROUTING -d ${EXTIP} -p ${PROTO} --dport ${INPORT} -j DNAT --to ${OUTIP}:${OUTPORT}
${IPTABLES} -A INPUT -p ${PROTO} -m state --state NEW --dport ${INPORT} -i ${EXTIF} -j ACCEPT
${IPTABLES} -t nat -A POSTROUTING -p tcp -s ${INTIP}${INTCIDR} -d ${OUTIP} --dport ${OUTPORT} -j SNAT --to ${EXTIP}
done
This sets up port forward rules based on the contents of PORTFWFILE
. The file has one rule per line and allows comments beginning with #
and blank/whitespace-only lines. Fields are separated by colons. The first is the port coming into the firewall. The second is the IP on the internal network the packet should be sent to. The third is the port it should be sent to. The last is the protocol the rule applies to (tcp or udp). The first iptables command performs the port manipulation, the second allows the packet.
The last is funky and optional. If you have services hosted on your internal network but other clients on the internal network access them by the external IP (probably via DNS) weird routing can happen. Packets are not coming in the external interface so they don’t get source NATted. They are directed at the external IP so they get destination NATted. They are sent to the internal server but the source address is the internal client. The internal server responds to the internal client directly. The internal client was expecting responses to come from the external IP, not the internal server IP so it discards the reply packets. The last rule source NATs traffic from the internal network destined to the external IP to the external IP. You could source NAT it to the internal IP of the router. I prefer the internal IP because it makes it easy differentiate between traffic originating from the internal network and traffic originating from the router itself.
The /etc/firewall/portfw
file looks like this:
# inport:dest IP:dest port:(tcp|udp)
22:192.168.128.50:22:tcp
53:192.168.128.50:53:udp
53:192.168.128.50:53:tcp
25:192.168.128.50:25:tcp
Two of my networking objectives are knocked out. I initiated a soft reboot and found that router-client could get out just fine. When I ssh to 192.168.1.180 I end up logging in to router-client.
DHCP
Now, on to DHCP. I used ISC DHCP (the same group that makes BIND.
apt-get install dhcp3-server
I only want to serve DHCP on my internal network, not for my ISP. I edit /etc/default/dhcp3-server
:
INTERFACES="eth1"
Then to configure the actual DHCP daemon, I made the following edits to /etc/dhcp3/dhcpd.conf
:
option domain-name "internal.lub-dub.org";
option domain-name-servers ns1.internal.lub-dub.org;
...
authoritative;
...
subnet 192.168.128.0 netmask 255.255.255.0 {
range 192.168.128.100 192.168.128.200;
option domain-name-servers ns1.internal.lub-dub.org;
option domain-name "internal.lub-dub.org";
option routers 192.168.128.1;
option broadcast-address 192.168.128.255;
default-lease-time 600;
max-lease-time 7200;
}
Restart DHCP:
/etc/init.d/dhcp3-server restart
I configured router-client for DHCP and restarted networking to verify that it would get an IP address, which it did. My port forwarding seemed to break here because router-client was no longer at 192.168.128.50.
Next is to make static DHCP assignments.
I create a file called /etc/dhcp3/static-dhcp.conf
with these contents:
host router-client {
hardware ethernet 00:0c:29:ae:37:10;
fixed-address router-client.internal.lub-dub.org;
}
The ethernet address is that assigned by VMWare. The fixed-address
line needs to resolve for the DHCP host so I put an entry in my /etc/hosts
file for router-client.internal.lub-dub.org pointing to 192.168.1.50. On my actual network, everything has proper forward and reverse records. I add this line to my dhcpd.conf:
include "/etc/dhcp3/static-dhcp.conf";
I restart dhcpd on frankenstein-test. I then restart networking on router-client and find that it gets 192.168.128.50. My port forwards now work again.
Conclusion
My test router works in my virtual environment and is ready to be loaded on my hardware with a good hard drive. This article only covered making a basic firewall router. There are countless enhancements that could be applied to this system. One possibility is a caching proxy on the internal interface. You can also provide DNS service directly off this system. It’s my opinion that the best decision for security is to have as few services on the firewall as you can get away with and those that you do have on should only listen on your internal interfaces.
There are also a number of security enhancements you can apply to this system. It can be configured to not return ICMP, run Snort or other IDS/IPS applications. Keep in mind that the more you add to this security appliance the more potential security issues there are. As I make major changes to my firewall I’ll try and produce articles to match.
My next objective is to take all the explanation on this page of the iptables script and make it into comments in the script itself, where that information should be. I’ll also be doing an internal NTP server and arpwatch or something like it.
Check back on this page to see these enhancements. The bottom of the page features a timestamp of when this page was last edited. There’s also an RSS feed link at the bottom.