This post continues from Part 2: DHCP

With DHCP running you should be able to connect devices to the router and get access to the internet. Although it may work, you aren't done yet. Right now your router is visible to the entire world. Anyone in the world could try and SSH into your router, ping it, DOS it, whatever. A properly configured firewall is essential to keeping your local network safe from some 400lb guy in his mother's basement. Commercial routers do this for you, but since we're building our own it's up to us.

A firewall is just a set of rules that get run over every packet that comes or goes through the device it's running on. The rules look at things like which interface the packet is coming or going from, what ports it's using, what protocols it's using, the IPs involved, etc.. It uses the packet's properties to determine some sort of action like dropping the packet, rewriting it, or accepting it. When all the conditions of a rule match a packet, the action is applied to that packet. In Linux, the de facto firewall program is called iptables. It's been around for decades and is incredibly powerful. On the other hand it's often criticized for being complex and unforgiving. Alternatives like the "uncomplicated firewall" (ufw) have sprung up to try and make firewall configuration easier, but under the hood all they are doing is writing iptables rules. I don't think the basics of iptables are that complicated though if you just give it a chance. So let me explain how it works at a high level and I'll demonstrate some basic rules that can lock down your router as well, if not better, than a commercial router.

Tables and Chains

Iptables works on these things called tables (obviously). There are 5 different tables it uses: raw, mangle, nat, filter, and security. Each of these tables are applied at a different stage of packet processing and so they can be used to achieve different things. This is just the basics though, so all we're going to focus on are the nat and filter tables.

Each of these tables contain chains, which are just a list of rules. There are 5 default chains called, PREROUTING, INPUT, FORWARD, OUTPUT, and POSTROUTING. Not every chain is applicable in every table though. For example the nat table only uses PREROUTING and POSTROUTING chains and the filter table only uses INPUT, FORWARD, and OUTPUT chains. For now lets focus on INPUT, FORWARD, and OUTPUT.

When a packet goes though the Linux network stack the chain of rules that get applied depends on where the packet is coming from and where it is going. If the packet is destined for the machine the firewall is running on, it would get run through the INPUT chain (the packet is an input to this system). If it is destined for some other machine then it gets run through the FORWARD table (the packet is being forwarded to another system). If the packet originated from the machine the firewall is running on it gets run through the OUTPUT table (the packet is an output of this system).

Like I said before, each of these chains is just a list of rules. Once the appropriate chain is picked based on the packet's source and destination the rules are run in order from the start of the list until a rule is matched. Each rule has a policy associated with it that says what to do with the packet if that rule is matched. The most common policies are ACCEPT and DROP which allow the packet through the firewall or deny it respectively. There are a lot more policies that let you gracefully reject packets, jump around to other rule chains, and more, but again, this is just the basics.

So what happens if no rules are matched? In fact this is what's happening on your router right now. Since you haven't added any rules there are no rules to match. Each chain has a default policy which says what to do if no rules are matched. By default the policy is ACCEPT. See:

$> sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination                    

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

This command lists (-L) the chains, rules, and default policies of an iptables table. By default it lists the filter table. To list a different table like nat you could add -t nat to the command. You can see for that each of the chains INPUT, FORWARD, and OUTPUT the policy if no rules are matched is ACCEPT. This is what I mean by your router is not secure right now. It will literally accept anything, from anywhere, going to anywhere. If you don't understand why this is bad for something connected to the public internet then you probably aren't the type of person that should be building their own router.

A default ACCEPT policy is OK for the OUTPUT table. We'll assume that the router hasn't been hacked and any traffic it wants to send out to the world is acceptable. For the INPUT and FORWARD tables, which process packets from the outside world, you definitely want to change the default policy to DROP. WARNING: DO NOT DO THIS OVER SSH. IT WILL DROP YOUR SSH CONNECTION:

$> sudo iptables -P INPUT DROP
$> sudo iptables -P FORWARD DROP

And see the change:

$> sudo iptables -L
Chain INPUT (policy DROP)
target     prot opt source               destination

Chain FORWARD (policy DROP)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

OK now you're safe from haxors. You're router is dropping all your LAN traffic too though so you're also kind of fucked. We'll need to add some more rules to allow the traffic we actually want.

Adding Rules

By default iptables rules are not persistent. This is good because if you fuck something up you can just reboot the machine and everything will be fine. Once you've got a working set of rules though you'll want them to be applied automatically at boot. There are some packages like iptables-persistent and other bullshit like that to do it for you, but for simple folk there's /etc/rc.local. Just put your commands in there and they'll get run on every boot. OK, now for some rules. I highly recommend running these on the command line and testing everything works before putting them into /etc/rc.local if you're doing this over SSH.

Input Chain Rules

First of all, let's accept anything from the loopback interface:

$> sudo iptables -A INPUT -i lo -j ACCEPT

And lets also allow anything on the LAN to send traffic to the router itself:

$> sudo iptables -A INPUT -i enx0050b617c34f -j ACCEPT

You should now be able to see these rules in the INPUT chain (-v just enables verbose output to show the interface names the rule applies to):

$> sudo iptables -L -v
Chain INPUT (policy DROP 50250 packets, 10M bytes)
 pkts bytes target  prot opt in               out  source    destination
1140K  199M ACCEPT  all  --  lo               any  anywhere  anywhere
  14M 3023M ACCEPT  all  --  enx0050b617c34f  any  anywhere  anywhere
...

These rules go in the INPUT chain because we want them to apply to traffic destined to the router itself. The -A just means append to the end of the current list of rules. Remember the rules are evaluated in order until a rule is matched so the order these things are inserted into the chain matters. The -i option says to match this rule the packet must be coming from the enx0050b617c34f interface (which is my LAN interface). Finally the -j ACCEPT says the policy to apply is to accept the packet. Putting that all together we get "a packet from the LAN interface destined to the router should be accepted".

Next, let's allow traffic from the WAN to the router. We don't just want the router to accept any packets from the WAN though. It should only accept packets that are part of a connection the router itself initiated. This prevents any random person on the internet from sending traffic to the router, but still ensures the router can still receive responses from the internet when it wants to. Since iptables does connection tracking its simple to make a rule like this with just a few more options:

$> sudo iptables -A INPUT -i eth0 -m conntrack \
        --ctstate ESTABLISHED,RELATED -j ACCEPT

Most of those options should make sense already, but let me explain the new ones: -m and --ctstate. The -m option specifies a "match" to use. This is just some extra condition the packet must match for the rule to be applied. In this case we want to match on connection state so the conntrack matching extension is used. The --ctstate option says which types of connections should be matched. To allow traffic from connections initiated by the router the rule needs to match ESTABLISHED and RELATED packets. This means a packet will be accepted if it is part of an already established TCP connection, or if it is related to a TCP connection in the process of being set up (router sends SYN, the SYN/ACK the server responds with is a "related" packet).

Forward Chain Rules

So now we can send traffic from the LAN to the router, from the router to the LAN, and from the router to the WAN. But what about from the LAN to the WAN? For that we will need forwarding rules. Recall rules in the FORWARD table apply to packets from somewhere other than the router going to somewhere other than the router (neither source or destination IP is the router's). The packets are just being forwarded through. The rules are very similar to the ones from before.

To accept traffic being forwarded from the LAN to the WAN:

$> sudo iptables -A FORWARD -i enx0050b617c34f -o eth0 -j ACCEPT

In other words "accept packets coming in the LAN interface and going out the WAN interface". Just like the router, we should accept traffic from the WAN going to the LAN if, and only if, the LAN initiated the connection:

$> sudo iptables -A FORWARD -i eth0 -o enx0050b617c34f -m conntrack \
        --ctstate ESTABLISHED,RELATED -j ACCEPT

Alrighty, that should do it for filter table rules. There's still ony problem though. If a LAN device sends a packet to the WAN the source IP address will be a LAN address (something in the 192.168.0.0/24 space). These IPs are unrouteable on the public internet and will be dropped the second the packet leaves your house. Instead the router should change the source IP address on outgoing packets to its WAN IP. Then when a reply is received it should change the destination IP back to the LAN IP address and forward it along. This procedure is called Network Address Translation, aka NAT, and this is where the nat table comes into play.

NAT

To translate IP addresses between local addresses and publicly routable addresses, we'll need to add some rules to the PREROUTING and POSTROUTING chains of the nat table. These chains allow you to modify packets when they're received and as they are being transmitted respectively. So to translate LAN IP addresses to the router's WAN IP address we will have to add a POSTROUTING rule that rewrites the address just before the packet is sent out. Fortunately, iptables has built in things to make this easy. There is a policy called MASQUERADE that will do all the translation for us. All we need to do is add a rule and iptables will take care of all the NAT rewriting on outgoing and incoming packets that match the rule.

$> sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

To see that it worked you can add -t nat to the iptables -L command to see the nat table rules:

$> sudo iptables -t nat -L -v
Chain PREROUTING (policy ACCEPT 2130 packets, 684K bytes)
pkts bytes target prot opt in out source destination 

Chain INPUT (policy ACCEPT 1640 packets, 661K bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 15342 packets, 1363K bytes)
pkts bytes target prot opt in out source destination

Chain POSTROUTING (policy ACCEPT 15337 packets, 1362K bytes)
pkts bytes target     prot opt in  out   source   destination  
1200 83980 MASQUERADE all  --  any eth0  anywhere anywhere

Boom, that's it, you should have a working router. Test things out and make sure everything is working properly. If so then make sure to add all the iptables rules to your /etc/rc.local so they will be added after every boot. The completed thing should look something like this:

# /etc/rc.local

# Default policy to drop all incoming packets
iptables -P INPUT DROP
iptables -P FORWARD DROP

# Accept incoming packets from localhost and the LAN interface
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -i enx0050b617c34f -j ACCEPT

# Accept incoming packets from the WAN if the router initiated
# the connection
iptables -A INPUT -i eth0 -m conntrack \
    --ctstate ESTABLISHED,RELATED -j ACCEPT

# Forward LAN packets to the WAN
iptables -A FORWARD -i enx0050b617c34f -o eth0 -j ACCEPT

# Forward WAN packets to the LAN if the LAN initiated the
# connection
iptables -A FORWARD -i eth0 -o enx0050b617c34f -m conntrack \
    --ctstate ESTABLISHED,RELATED -j ACCEPT

# NAT traffic going out the WAN interface
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# rc.local needs to exit with 0
exit 0

Now stop, and back things up. Unless you want to do all that configuration over if something goes awry it's a good idea to copy all the configuration files somewhere safe.

So now that you've got a working router there are probably a few other things you'll want to do. First of all you'll probably want to add WiFi, we'll look at that next. In later posts I'll also show how to do some cool DNS tricks, add remote access, web caching, and monitoring. Stay tuned!

Other Posts in This Series