This is the fourth blog post about my home network improvements series. I am sorry it is taking me so long to write all those posts, but each takes a lot of hours to write and I am balancing my life more towards family at the moment. I hope you can bear with me until the end.
In the previous post, we presented installed the OS and set up networking and routing.
We will now see how to add another very important feature the firewall.
- Router features list (published)
- Creating a basic router, defining the network and routing (published)
- Adding a firewall to our router (this post)
- Providing basic network services, DHCP and DNS (to be published)
- Testing the firewall (to be published)
- Extra services (to be published, could be splitted in more than one post)
So today’s post will present a simple but secure firewall installation.
As I have said in a previous article, I want to try out nftables instead of using iptables. But we will continue iterating on the previous post and use iptables instead one more time. I want to have a working router and then I can think of switching to nftables and solving integration with other tools.
A Basic Firewall
We will use iptables command line to populate the firewall rules. As changing those rules from the command line is not persistent, a simple reboot will restore your OS in the previous configuration so if things do not workout or if we get locked out by a wrong rule, just reboot and restart to setup your firewall. Once we will be happy with the firewall, we will save the rule set and make it permanent.
For rules, we obviously do not want any traffic coming from the WAN to establish new connections inside our LAN or on our router. Only established connections should be allowed through, e.g. an HTTP response is allowed through the firewall so that we can browse the internet. We want some network services to still function, like ICMP or DNS messages to pass through the firewall. We do not want to filter the outgoing traffic for the moment, so everything from the LAN is allowed to reach the WAN.
I like to set default policies for the different iptables chains instead of relying on the last rule to do the policy for me. However, in order to avoid getting locked out, we will set those policies at the very end and always start by defining what is allowed. In order to define our firewall, we will work first with the main chains of the filter table (the default one). Mostly caring of incoming packets and IP forwarding rules.
As a reminder, the WAN interface was defined in the previous article as wan1, and the LAN interface as lan1.
Incoming packets – INPUT
When working on the INPUT chain, one must always keep in mind that it applies to all incoming packets relative to an interface. So for the LAN interface, this is incoming packets from our LAN, but from the WAN interfaces this is packets coming from the WAN, and so on. So in our incoming rules, we will need to distinguish between those coming from outside (lowest trust) and those coming from the inside (higher trust).
Thus let’s authorise all incoming packets on the loopback interfaces – coming from services on the router host – and from the LAN. This is of course when you feel that you can trust all machines on your LAN. Maybe in a future post I will provide better advises here.
$ sudo iptables -A INPUT -i lo -j ACCEPT $ sudo iptables -A INPUT -i lan1 -j ACCEPT
Our firewall will be a basic stateful firewall, so we will use the conntrack module to analyse connection states. Such firewall requires usually more resources memory and cpu. Indeed, in order to keep track of connection states, the firewall will need to keep them in memory. The amount of memory depends a lot on architecture and kernel version. On my new router with Ubuntu 18.04.1, when tracking the maximum amount of connections – which is
net.netfilter.nf_conntrack_max = 262144 – the memory should be less than 100MB. And of course managing this list is time consuming. We will see in a future blog post how it performs and how we can tweak it for better performance.
For the WAN interface, we want to accept certain packets only. We want to allow established connections, those are connections already created or which another rule allowed to create, so we consider them already authorised. We also want to accept new valid connections which are considered related to established ones, like ICMP messages related to a connection. We want all invalid traffic to be dropped.
$ sudo iptables -A INPUT -i wan1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT $ sudo iptables -A INPUT -i wan1 -m conntrack --ctstate INVALID -j DROP
I could not find which ICMP message are identified as related by conntrack, but during my testing it seems many of the ICMP traffic is handled as related. Therefore I assume we have most ICMP cases cover, especially for ICMP destination unreachable (type 3) – which also includes DF code or don’t fragment – and ICMP time exceeded (aka TTL expiration, type 11). So the only rule missing is with ICMP echo request (aka ping, type 8), we are going to allow it:
$ sudo iptables -N ICMP-RULES $ sudo iptables -A INPUT -i wan1 -p icmp -j ICMP-RULES $ sudo iptables -A ICMP-RULES -i wan1 -o wan1 -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT $ sudo iptables -A ICMP-RULES -j DROP
Now we will add 2 new chains for managing the allowed UDP and TCP traffic. I do not know why using one chain for each type of traffic, it seems to be the recommend way to have better efficiency. That’s something on my vast todo list to understand this better. I’m using it because it is neater in my humble opinion.
$ sudo iptables -N WAN-SRVC-UDP $ sudo iptables -N WAN-SRVC-TCP $ sudo iptables -A INPUT -p udp -m conntrack --ctstate NEW -j WAN-SRVC-UDP $ sudo iptables -A INPUT -p tcp --syn -m conntrack --ctstate NEW -j WAN-SRVC-TCP $ sudo iptables -A WAN-SRVC-UDP -j DROP $ sudo iptables -A WAN-SRVC-TCP -j DROP
If you have services on your router you wish to expose, you would add them now. For example, we will add the SSH connection service on the LAN interface. It is not necessary as we authorise everything from LAN, but this is just to illustrate how to use those chains.
$ sudo iptables -I WAN-SRVC-TCP 1 -i lan1 -p tcp --dport 22 -j ACCEPT
We are now going to add the last rule of our INPUT chain and it is to log all unfiltered packets and see how we can improve our firewall rules. This rule is temporary, and especially before applying the next rule you should check system logs if you see any legitimate message with “INPUT packet unfiltered”, if yes it means one or more of the above rules are incorrect (e.g. typos or wrong interface name, etc.). By legitimate message, I mean a message about for example your current remote SSH connection, you do not want to see it in your logs because when you apply the drop rule it means you will be cut out.
$ sudo iptables -A INPUT -m limit --limit 3/minute --limit-burst 3 -j LOG \ --log-prefix "INPUT packet unfiltered: "
Before applying our last rule on the INPUT chain, make sure you have checked the logs for any mention of “INPUT packet unfiltered”. Use
sudo journalctl -rb to see the logs since last boot in reverse chronological order, so newer first. If no such logs are visible then your are pretty safe. Now it is time to set the default policy for the INPUT chain:
$ sudo iptables -P INPUT DROP
IP forwarding packets – FORWARD
We now need to setup the forwarding chain to filter packets which would be otherwise forwarded between interfaces (see the routing chapter above).
We want to explicitely authorise all traffic from
lan1 at destination of
lan1 to be forwarded. This will be useful when we will have several network interface cards (NIC) attached to our
lan1 bridge. It will allow traffic on this bridge. We also want all traffic from
wan1 to be forwarded.
$ sudo iptables -A FORWARD -i lan1 -o lan1 -j ACCEPT $ sudo iptables -A FORWARD -i lan1 -o wan1 -j ACCEPT
Port forwarding is the ability for our router to redirect traffic directed to the
wan1 interface on a specific port to a different destination (e.g. inside the LAN). Port forwarding is sometimes known as virtual server. It can be used to redirect incoming traffic from one port to the other on the same host, or to another host. Port forwarding rules will be identified by conntrack as
In addition, connection which have been already accepted by another rule should be allowed to be forwarded.
$ sudo iptables -A FORWARD -i wan1 -m conntrack --ctstate DNAT,RELATED,ESTABLISHED -j ACCEPT
Now we can set our default policy for the FORWARD chain.
$ sudo iptables -P FORWARD DROP
Here is an example how to do port forwarding. Let’s consider that the host 192.168.9.2 on the LAN offers a certain web service on port 443 (HTTPS). We want to redirect incoming traffic on the router from port 8443 to the mentioned LAN host. The following rule will create the redirection. Usually it is not enough to work as packets arriving on the
wan1 and redirected to the LAN host will need to be forwarded to the
lan1 interface. But we do not need an explicit
FORWARD rule which would allow that, we are using conntrack to monitor our communications and this communication is now marked as
DNAT, and we already have a conntrack rule in our
FORWARD chain which will handle this traffic.
$ sudo iptables -t nat -A PREROUTING -i wan1 -p tcp --dport 8443 -j DNAT --to-destination 192.168.9.2:443
Outgoing packets – OUTPUT
We do not specify any special rules here. We allow all packets outgoing on all interfaces.
$ sudo iptables -P OUTPUT ACCEPT
IPv6 firewall – blocked
We do not have any IPv6, so let’s build a simple IPv6 firewall which blocks everything:
$ sudo ip6tables -P INPUT DROP $ sudo ip6tables -P FORWARD DROP $ sudo ip6tables -P OUTPUT DROP
We do now have a basic firewall, not as sophisticated as the great Chinese firewall, but functional enough for home usage. For convenience, I have summarised all of the commands from the previous chapters in one “script” hereafter. I use this format so I can add some comments within the lines as reminder of what was explained before. If you are happy with this configuration and are using the same interface name, then you could directly run that script. But I would advise you run one command at a time.
Do not forget that those commands are not permanent. So if it messes up with your system, simply reboot it and you will be able to start fresh.
#!/bin/bash set -eu # The WAN interface is wan1 # The LAN interface (a bridge) is lan1 # ## INPUT chain # Accept everything from the host or the LAN iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -i lan1 -j ACCEPT # Accept all existing or already approved connections iptables -A INPUT -i wan1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # Drop all invalid messages iptables -A INPUT -i wan1 -m conntrack --ctstate INVALID -j DROP # Create new chain for ICMP rules iptables -N ICMP-RULES iptables -A INPUT -i wan1 -p icmp -j ICMP-RULES iptables -A ICMP-RULES -i wan1 -o wan1 -p icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT iptables -A ICMP-RULES -j RETURN # Create new chains for UDP and TCP rules iptables -N WAN-SRVC-UDP iptables -N WAN-SRVC-TCP iptables -A INPUT -p udp -m conntrack --ctstate NEW -j WAN-SRVC-UDP iptables -A INPUT -p tcp --syn -m conntrack --ctstate NEW -j WAN-SRVC-TCP iptables -A WAN-SRVC-UDP -j DROP iptables -A WAN-SRVC-TCP -j DROP # Uncomment the next line to allow SSH access from the LAN # iptables -I WAN-SRVC-TCP 1 -i lan1 -p tcp --dport 22 -j ACCEPT iptables -A INPUT -m limit --limit 3/minute --limit-burst 3 -j LOG \ --log-prefix "INPUT packet unfiltered: " # Default to block iptables -P INPUT DROP # ## FORWARD chain # Allow LAN to LAN forwarding iptables -A FORWARD -i lan1 -o lan1 -j ACCEPT # Allow LAN to WAN forwarding iptables -A FORWARD -i lan1 -o wan1 -j ACCEPT # Allow forwarding for accepted connections, including DNAT (port forwarding) iptables -A FORWARD -i wan1 -m conntrack --ctstate DNAT,RELATED,ESTABLISHED -j ACCEPT # Default to block iptables -P FORWARD DROP # Uncomment the next line to do port forwarding from 8443 (outside) to 192.168.9.2:443 (inside) #iptables -t nat -A PREROUTING -i wan1 -p tcp --dport 8443 -j DNAT --to 192.168.9.2:443 # ## OUTPUT chain # Default to accept iptables -P OUTPUT ACCEPT # ## IPv6 all blocking firewall ip6tables -P INPUT DROP ip6tables -P FORWARD DROP ip6tables -P OUTPUT DROP
For the moment we won’t validate our firewall, I personally did it but describing it requires a lot of time and effort so I am postponing that part for now. We also need to setup other services, mainly DHCP and DNS in order to easily connect clients on the LAN side. Then we will be able to verify fully our firewall and test it with real world application. You will see that as we are not supporting UPnP or equivalent, some applications do not perform at their top. This can easily be fixed by setting proper forwarding rule on your firewall.
It is possible that this firewall can be improved. But it is a good start and basis on which to build securely your router.
All product names, trademarks and registered trademarks are property of their respective owners.
Image credits: All images are credited Vera & Jean-Christophe Magical-World.eu CC BY-SA.