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:
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
Each of these tables contain chains, which are just a list of rules. There are 5 default chains called,
POSTROUTING. Not every chain is applicable in every table though. For example the
nat table only uses
POSTROUTING chains and the
filter table only uses
OUTPUT chains. For now lets focus on
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
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
$> 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
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.
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
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.
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 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
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.
To translate IP addresses between local addresses and publicly routable addresses, we'll need to add some rules to the
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!