By now we have a basic working router. We can connect new Clients/PCs via the switch, they get an IP address from the DHCP server and they can access the internet.
Next up we will add an iptables based firewall. Linux is currently in a transition from iptables to nf_tables, but it has been like this for years and iptables is still the default, so that is what I will be using. In the last few weeks nf_tables 1.0 and firewalld 1.0 have come out, so maybe with Ubuntu 22.04 I will switch do a newer alternative.
We should first talk about what we will need the firewall for. The first thought many people probable have is, to protect our clients, but this is unnecessary since our clients are not accessible from the internet. Some people will yell at me for this, but when using NAT our clients are as protected from connections from the internet as they would be with a firewall. But this doesn't mean I don't use my firewall as an additional layer of protection. When I add IPv6 later I definitely need the firewall since there is no NAT with IPv6 and our clients would be directly accessible from the Internet.
Where iptables is needed for, is to stop connections to our router itself since the router is directly accessible from the internet. Also we might not want all services that our router provides to the clients to be accessible for guests. I also use iptables for port forwards, to expose services that one of my Linux clients provides to the internet. Lastly I use it to redirect all outgoing DNS request to my unbound DNS server that we will setup in a later blog post.
I have one base script that sets up some defaults, like allowing established connections, localhost connections and ICMP. It also creates additional chains all starting with MAIN- that other scripts can then insert them self into. This is what it looks like:
Since there is no default policy to reject packets only to drop packets, I use accept as the default policy and the reject everything at the end of the script. Dropping can be really annoying to debug when all connections time out, but is fine when you have active malicious traffic you want to block. The script also logs everything that is rejected to journald which makes debugging easier and it can be quite interesting to see what ports some random IPs from the internet try to reach.
My firewall script has three options: on, off and reload.
ON creates some basic rules like allowing ICMP traffic and already established connections. It also creates an additional chain for each of the four chains INPUT, FORWARD, PREROUTING, and POSTROUTING. These additional chains allow me to add and remove rules and then reload the firewall without flushing the default chains which would also flush the REJECT at the end. This would ideally only disable the firewall protection for a brief moment, but if I had an error somewhere in my ON section it could leave me without any rules. This way, when the reload fails, I at least have the default REJECT in place and can fix the problem over my still established ssh connection or through the console.
RELOAD flushes my additional base chains and then executes all scripts inside /usr/local/etc/firewall.
OFF just flushes and deletes all chains.
Inside /usr/local/etc/firewall I then have multiple scripts for different purposes. These are all run by the run-parts command inside the base script.
These scripts all create their own chains, append these chains to the MAIN- chain and then add rules to their own chain.
SSH is allowed from anywhere, but I rate limit the connections so attackers can not try to break in as fast as they like. Not that it really matters since no passwords are allowed, only public keys.
Allow DHCP connections which use Port 67 UDP from inside my two networks. Without this, DHCP would not work.
Notice that we use the virtual br0 and vlan222 interface for iptables and not the physical LAN or OPT1 interfaces.
Since the base script has a default reject target in the forwarding chain, we need to allow forwarding for our clients again. This is what I mentioned at the beginning and would not be necessary, when using a NAT, to protect the clients.
Here I allow my two networks to forwards packets to the internet, but do not allow connections from the internet to reach my clients.
This is also where I put the MSS Clamping and NAT rules from Part 3.
I have a small Linux server behind my router that offers some services to the internet. To be reachable I need to forward some ports that reach the routers public IP to my server.
Here we need to do two things. First we need to allow packets to be forwarded to my server with:
iptables -A PORT-FORWARDS-FORWARD -d 192.168.144.10 -j ACCEPT
And the we can add port to the PREROUTUNG chain using the DNAT target to exchange the destination IP address from the public router IP the the internal server IP.
Running the firewall at boot
To run the base script at boot and allow easy reload, stopping and starting of the rules I created a systemd service. The service uses as much sandboxing as possible, since it is running as root even though it is only running scripts I wrote myself. For a more detailed description and reasoning behind this, look here: https://github.com/stephan13360/systemd-services