In the previous post we configured just enough basic networking to have connections on both WAN and LAN sides. Before going in to routing territority that will connect the two sides, we’ll first get a basic firewall going. This is sensible not only in terms of security but also in terms of understanding, as we’ll be using the same tool, iptables, for both routing and firewall. And firewall rules, basic ones at least, are easier to wrap your head around than routing ones.
In this post we will set up firewall rules that will allow all traffic outbound from the host itself to the LAN and WAN and next to no traffic to the host (apart from responses to the host’s queries and access to a select few services).
As stated in the previous post, we’re currently only setting up an IPv4 network. This makes this part easier, as I only need to concern myself with IPv4 rules. If you’re wondering why I am using iptables rather than the successor, nftables, or other tools, the answer is simply familiarity.
I should also stress that this is a very, very _basic_ firewall. It will not include rules designed to protect against spoofing, flooding or other advanced attack types. That is not because you will not need them or I don’t advise you include such. It’s just beyond the scope of the post and the series (at least for the moment). However, it is worth noting that most of our rules will be based on references to network devices which will invite a lot less spoofing potential than rules based on source IP’s. Yes, traffic from a host on the physical WAN side can claim to be LAN traffic in packet headers (e.g. source address: 192.168.0.64) but it’s still coming in on a non-LAN NIC and will be treated as such.
Firewall and iptables basics
While I will briefly explain every command and concept introduced, it might be beneficial to give a firewall introduction and an iptables tutorial a quick read first, in case I skip some basics. DigitalOcean can usually be relied on for good technical documentation. What is a firewall and how does it work is a good introcution to basic concepts. How To Set Up a Firewall Using Iptables on Ubuntu 14.04 sounds outdated but isn’t and it’s by the always excellent Justin Ellingwood. The main thrust of this post will be to apply that iptables knowledge practically to the case of building a router.
Still, let’s go over what is needed for our here-and-now-purposes.
First of all what do we mean (and don’t mean) by a ‘firewall’? We are talking about a firewall in the classic sense, i.e. one that operates on the network layer of the OSI model. That means that the information we have available to us is information about network traffic, such as packet headers, IP addresses, etc. I say this because some people think of web filtering as a ‘firewall’. That is not included here. We can absolutely use our firewall to block traffic coming from 31.13.72.36. We cannot use it to block searches about gambling or access to porn. Specifically, anything that relies on DNS (e.g. blocking access to the facebook.com domain) is not within the domain of this kind of firewall.
iptables is a command that sets a rule in the kernel’s Netfilter framework. One iptables command gives you one rule (except for the ones that do other things, like list all current rules). The rules are applied to traffic by the kernel, and the iptables commands directly instruct the kernel in how to deal with traffic. Iptables is also a systemd service (iptables.service) but that service is not ‘the firewall service’. What it does it run all your iptables commands before the network comes up (usually at boot) so that your rules are live before the network is on. And then after the network gets deactivated, the service tears the rules down (‘flushes’ them). In systemd service parlance it’s a “oneshot” service, i.e. not a daemon.
Iptables rules exist within tables and chains. A chain is list of rules applied to specific types of traffic, e.g. outbound traffic. To that traffic you can do various things. Depending on what you want to do to traffic you will look at different tables, e.g. the filter table (which allows for filtering, i.e. deciding on what to let pass) or the mangle table (which allows for ‘mangling’, i.e. making changes to packet headers). A table will comprise various chains because intent crosses types of traffic. For example you may want to block both some inbound and some outbound traffic.
Whenever a packet arrives and gets put in a chain (e.g. an incoming packet that gets put in to the INPUT chain), it will be processed by the rules in that chain one after the other until it either triggers a rule that makes a decision or it reaches the end of the chain (i.e. gets through all the rules without triggering a decision). I believe it’s this serial nature of the rules processing – one at a time – that has led to the designation ‘chain’ but don’t hold me up on that. If no decision gets made by any rule, the chain’s policy (i.e. default rule) will decide what to do with the packet. What kind of “decisions” are we talking about here? As you might expect or guess, it is the basic binary Maxwell’s demon choice: Do we let the packet pass (ACCEPT) or do we deny it (DROP or REJECT). To illustrate: A common policy for the INPUT chain in the filtering table is to DROP the packet. We then implement exceptions for packets that we should ACCEPT. In other words: We maintain a guest list and if the packet is not on the list, it doesn’t get in.
If the paragraphs above make no sense or you are still confused about the exact relationship between rules, chains and tables, fret not. Firstly, it’s much easier to understand in practice, once we get into examples. Secondly, we will only be looking at the filter table today, so there is very little need to understand the whole zoomed out diagram of traffic and all the other ways – apart from filtering – that traffic can be controlled.
How-to and what-not-to
Before we get into actually writing rules, a word on how to proceed and what gaps to mind. As mentioned earlier all commands take effect immediately in your current session and there are no warnings if you’re about to do something stupid, like lock yourself out. Fortunately, a reboot will always reset the rules to those commands that are saved, i.e. read off of a file on disk. Any commands issued in a session do not get written to that file, unless you issue a specific additional command that basically takes a snapshot of the current state of the firewall and uses it as the new base state on the next reboot. I am not even going to mention that command in this post. If during testing, you assemble a number of iptables commands that you want quick access to, put them in a shell script.
Since we are issuing commands directly to the kernel, all iptables commands will need root level privilege both for setting and listing rules. Apart from that, I will note if root is not needed for a command rather than the other way around.
As the default distro for this guide is Arch Linux, you should have a blank slate in the filtering table. Check that assumption by running:
iptables -L -n -v --line-numbers
If you get output like this:
Chain INPUT (policy ACCEPT 639K packets, 2317M bytes)
num pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 416K packets, 51M bytes)
num pkts bytes target prot opt in out source destination
… you’re good to go. If you have rules numbered and listed you should flush them before you proceed. Before you do, though: If you are on (and depend on) an SSH connection, check what the INPUT chain policy is. If it’s anything but ACCEPT, a flush will cut your connection. You can set an ACCEPT policy (for now) by running
iptables -P INPUT ACCEPT
… and then flush existing rules by running
iptables -F
Note that all the three previous commands have altered the filter table. The filter table is the default table, as in ‘you don’t have to explicitly specify it as the target’, presumably because it’s the one most often used. It can also be explicitly stated by adding -t filter to the commands.
Outbound traffic
The first thing we need to establish is what we mean by outbound traffic (which I will use to designate traffic that is in the OUTPUT chain). Traffic in the OUTPUT chain is traffic which originates from the router host itself. Running pacman -Sy on the host will set off a process that will generate traffic from the host to an Arch Linux mirror to check for updates. However, the traffic does not need to be heading towards other hosts to enter the OUTPUT chain. ping localhost is also outbound traffic by this definition (i.e. it originates from the host itself).
Note that the terms outbound and incoming do not relate to your home or organisation, they relate to the router host itself. In other words, traffic from the router host to a host on the LAN side is just as much outbound/OUTPUT chain traffic as traffic headed for the WAN side. Also: Traffic from hosts on the LAN side headed for the WAN side, e.g. the internet, is not outbound traffic by this definition and not in OUTPUT chain since the origin is not the router host. We will cover this kind of traffic when we address the FORWARD chain in the next post.
You might want to restrict outbound traffic for all sorts of reasons. If you have analysed all possible needs for it and narrowed it down to a few legitimate destinations (such as some very static Arch linux mirrors), a DROP policy followed by exceptions can make sense. As this a basic firewall, I will keep things simple and say we trust our own host. Therefore we will simply apply a policy that allows the host to reach out in all directions, WAN and LAN.
iptables -P OUTPUT ACCEPT
(Yes, I know that an OUTPUT ACCEPT policy is part of the assumptions above but here we are making an active choice rather than just a starting point).
Inbound traffic
As a mirror to outbound traffic, inbound traffic (that is traffic in the INPUT chain) is traffic that has our router host as it’s final destination. This can for instance be a response to our earlier pacman -Sy request where the server has noted the origin of the request and uses it to set the destination of the reply. If we – against earlier advice not to – also run, say a file server, directly on the router host, incoming traffic can also be requests from other clients.
Just like in the previous section, inbound/INPUT chain traffic applies equally to the WAN and LAN side. A host on the LAN side pinging the router? Inbound traffic. Also just like with outbound traffic, packets passing through the router, ultimately destined for somewhere else, is not included here, e.g. traffic coming in on the WAN side, headed for a web server on the LAN.
Now, the policy, that is the default rule, on the INPUT side should absolutely be set to DROP. Unlike with OUTPUT we should be expected to know and list the (few) needs for other hosts to talk directly to our router. However, we will not set that policy before we have written some rules that state the exceptions to that policy for what I hope are obvious reasons. (If it’s not obvious to you, I encourage you to set it and see what happens… 😈)
I will go through those needs shortly but first I will just say that the order in which I go though them may not be the right order in which to put them once you actually start using the router. There can be various, possibly conflicting, considerations here, such as efficiency, specificity and clarity. That, however, is a concern for later. For now, just experiment but bear in mind the precautions I mentioned in the ‘How-to and what-not-to’ section.
DHCP server
In the previous post we set up a DHCP server on the LAN side, i.e. attached to the br0 interface. A DHCP server offers it’s services on UDP ports 67 and 68. Were we to block incoming traffic we would have a DHCP server listening on those ports but no traffic would reach it. Let’s carve out exceptions for it:
iptables -A INPUT -i br0 -p udp -m udp --dport 67 -j ACCEPT
iptables -A INPUT -i br0 -p udp -m udp --dport 68 -j ACCEPT
Two lines, one for each port. (Personally, I see no traffic on port 68, only on 67, but it is mentioned in the well-known ports list on Wikipedia)
-A INPUT means append the rule to the rules in the INPUT chain. Append simply adds it to the end of the chain’s list so it comes after every other existing rule. Since this is the first we are adding, it will still be number one.
-i br0 means in-interface br0, the virtual bridge interface that we created in the last post. In other words: The LAN side. The slightly awkard term ‘in-interface’ is used to refer to the network interface that the packet comes in on.
-p udp specifies that only packets using the UDP protocol and not TCP should be matched.
-m udp might seem somewhat incongruous here. Didn’t we just tell iptables to match on the UDP prootocol? Yes, and while the -m flag is technically identical to --match, it might be better to think of it as m for module. The idea is that all protocol specific options are not available outside of the protocol’s extension module. So to match on those you have to first load the module. Which brings us to…
--dport [67, 68] means that we match only packets with destination port 67 and 68, respectively. The options --dport and --sport (for source port) are common to a number of protocol modules (including but limited to tcp and udp) but protocol specific. So you have to specify -p udp in order to load the module and subsequently use the --dport/--sport options. If not iptables responds…
iptables v1.8.13 (nf_tables): RULE_INSERT failed (Invalid argument): rule in chain INPUT
-j ACCEPT means jump to the terminating target ACCEPT. When a rule causes a jump to a terminating target, no further rules in the chain are checked against the packet. So if we imagine a rule further down the chain that would have caused a jump to the terminating target DROP, well, that would not get checked. ACCEPT obviously means that the packet is allowed to pass.
Other services
Other LAN facing services can be dealt with in much the same fasion. Here’s an opening for SSH:
iptables -A INPUT -i br0 -p tcp -m tcp --dport 22 -j ACCEPT
Just like before we append the rule the INPUT chain, we specify an in-interface, a protocol (and that protocol’s extension module) and the well-known port 22 for SSH. Note that I’m restricting access to SSH to the LAN facing virtual bridge. If I wanted to allow access from either LAN or WAN side, I could simply run the command without specifying the in-interface.
Responses
As mentioned above, direct responses to outbound traffic will appear in the INPUT chain. Once we implement a DROP policy, those responses will get blocked if we don’t make an exception for them. But how? There’s no objective marker that says ‘this is a legitimate response to your request’. If there was, it would soon get spoofed. The answer to this problem was the invention of the stateful firewall. A stateful firewall “individually tracks sessions of network connections traversing it”. So to continue with the earlier example of running pacman -Sy, that would send packets from my router host to the first mirror in my mirrorlist with the IP address 194.29.226.88. That connection would get tracked by Netfilter so when a reply arrives from 194.29.226.88, that reply would get recognised as a reply, rather than a random incoming packet.
Similar to the ability to filter by destination port this functionality is part of an extension module that must be loaded to be used. So we run
iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-m conntrack (connection tracking) indicates the extension module. --ctstate is the function we get access to from the conntrack module and we use it to specify that packets that are part of either RELATED or ESTABLISHED connections should be allowed to pass though the INPUT chain.
Note that there is some overlap between our rules already. Traffic coming in in an ongoing, established SSH connection will likely qualify for this rule. But all incoming traffic in the SSH connection goes to port 22. So either rule will allow it. The need for the SSH specific rule arises from the very first attempt at connecting to the port. Those packets are not part of an established or related connection – our client is connecting to the SSH service out of the blue – and so would not be allowed to pass by this latest rule.
Pings
Say I want to be able to check if my router is up, even when I’m not at home. A simple way is to allow for pinging it.
iptables -A INPUT -m icmp --icmp-type 8 -s 38.242.130.44 -j ACCEPT
Because ping uses the ICMP protocol we load the icmp module extension but unlike with UDP and TCP we can match on a specific type rather than a port. Type 8 is an “Echo request (used to ping)”. -s followed by an IP address indicates that pings are only allowed from a specific source IP address, and not the entire internet.
Loopback
The loopback interface is a virtual network interface that is only accessible to the host itself. A web server hosted on 127.0.0.1 or localhost will see inbound traffic (from the host to the host) on the loopback interface. Since the traffic is destined for the router host itself, it qualifies as inbound traffic (ie. it gets in the INPUT chain) and so we need to explicitly allow it or it will fall victim to a policy that defaults to DROP everything.
iptables -I INPUT 1 -i lo -s 127.0.0.1 -j ACCEPT
To switch things up a little, we don’t append this rule but insert it in the INPUT chain’s top spot (-I INPUT 1), i.e. as rule number one. There is no particular necessity for this, it’s just an illustration of another way to add rules. Inserting is often useful if you want to test if a rule will get triggered but the traffic it targets would get jumped earlier in the chain if the rule was just appended to the bottom.
-i lo specifies traffic that comes in on the interface lo, i.e. the loopback interface. -s 127.0.0.1 requires the source IP address to be the localhost’s 127.0.0.1. I believe this is superfluous, as the loopback interface is inaccessible from any other IP address but… I dunno, it doesn’t hurt, I guess.
Listing rules
We’re almost ready to wrap up by implementing a policy. Before that we need to check the rules now in place. I recommend aliasing the following command to something:
iptables -L -n -v --line-numbers
-L is the list command. -n disables reverse DNS lookups. So if you have specified, for instance, allowed source IP addresses, those will be listed rather than any host names associated with them. It also means you will get 0.0.0.0/0 as IP addresses rather than the translate “anywhere”. Finally, it means a more instantaneous response as the output doesn’t have to wait for the lookups to complete. -v is for verbose. The main added informational benefit is that columns listing in-interfaces and out-interfaces gets added. Seeing as we have used the first in our rules, that is very useful indeed. --line-numbers will see to it that each rule gets a number. This is useful for deleting them as you simply refer to them by their chain and number.
Check that the rules needed to access the host when the default policy is no longer to ACCEPT are in place.
Policy
Now that we have pricked select holes in the firewall we can apply a stricter INPUT chain policy. We have choice between REJECT which will inform the origin that the packets were not accepted and DROP which will not inform the origin of our decision. The effect is the same: The packets are not allowed to pass. The general tendency is to use REJECT when it might be ourselves we are blocking and DROP when it’s more likely to be attackers. Which roughly translates to REJECT on the LAN, DROP on the WAN. Since the INPUT policy does not differentiate we go for the safer option, which is DROP. -j REJECT rules can be added to the chain to cover the need for rejection as opposed to dropping.
Before running the command below you might want to consider various safeguards that will ensure that a default ACCEPT policy is reimplemented after a test period. This could be a ‘sleep five minutes and reboot’ script or a ‘sleep and reset policy’ script. Without further ado or safety warnings:
iptables -P INPUT DROP
Conclusion
If everything is working satisfactorily, I recommend saving all needed policy and rule commands to a shell script.
As after the previous post this is still not a router but a ‘Janus box’ – one face towards the WAN side, one face towards the LAN side – with nothing linking the two. It’s just a Janus box with slightly better security. That will change after the next post.
Cover image: A castle on a hill with mountains in the background © Tristan, Unsplash License