In the last post we used iptables to filter the traffic going out from the router (towards the LAN as well as the WAN) and coming in to the router (again, from both sides). We skipped over the case of traffic going through the router, i.e. traffic that does not have the router itself as neither source nor destination. We will go into that aspect in this post, not just allowing it (using the filter table, much like in the previous post) but actually enabling it (using the nat table).
As in the last post, all commands require root unless stated otherwise.
Filtering traffic passing through
As noted in the last post, the FORWARD chain is used to filter traffic “passing through the router, from somewhere else, destined for somewhere else”. Just like in the previous post, we need to allow some of this traffic to pass and block the rest.
When setting up the firewall rules on the OUTPUT and especially the INPUT chain, we took some pains to make sure we didn’t accidentally lock ourselves out of access to the router. This time round we need not worry so much. Firstly, there is no current functionality that we can disrupt (i.e. the router isn’t yet working as a router that allows for traffic from the LAN to the WAN or vice versa), second, even if there was something that was working, disturbing through-flow would be much less disruptive than inbound traffic. We would still have sufficient access to undo the damage. All of this is a long-winded way of saying that this time, we can start by setting a restrictive policy and clearing any previous rules:
iptables -P FORWARD DROP
iptables --flush FORWARD
Now, we do want packets to get forwarded from the LAN to the WAN, and we will write rules for that. So what kind of packet would this DROP policy apply to? I am unsure if this is pactically relevant but I am going to say traffic going the other way that isn’t in response to a LAN device’s requests, and leave it at that. I see very few packets get caught by this policy but they do happen.
Moving swiftly forward (no pun intended) we put in rule no. 1 which says that traffic from the LAN can flow freely to the WAN:
iptables -A FORWARD -i br0 -o enp1s0 -j ACCEPT
Like in the previous post, we append a rule to a chain (in the default and unspecified filter table), here the FORWARD chain, and we match on the in-interface br0 (the LAN facing virtual bridge we created in the first post) and also on the out-interface enp1s0 (the WAN facing physical interface). Matching traffic is accepted and skips the rest of the chain.
Still mirroring the previous post, we also want to allow for responses going back from the contacted host on the WAN side to the requesting host on the LAN side. The rule allowing for this is almost exactly identical to the one we used last time. I’ll refer to the explanation in that post for details and just point out here, that tracking traffic passing through is also part of what makes this firewall “stateful”.
iptables-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
And that will suffice for filtering/firewalling until we start talking port forwarding and DMZ. That concludes the ‘allowing’ section and we now move on the to ‘enabling’ section.
Kernel IP forwarding
In the next section we will discuss the workings of moving packets from one network to another and therefore from one network interface to another. That requires the cooperation of the kernel and in most linux distributions, including Arch, the kernel is, by default, set to reject such manoeuvres. The simple reason being that most hosts aren’t routers. Fortunately, there is a simple option that can change that. Start by checking what the current setting is:
sysctl net.ipv4.ip_forward
If the answer is net.ipv4.ip_forward = 0 then your kernel is not set as a ‘router’. It can be set for the current boot by running the command above again but this time in write mode:
sysctl -w net.ipv4.ip_forward=1
Or by adding a configuration file to /etc/sysctl.d/ (say, 99-routing.conf)to make the setting permanent:
net.ipv4.ip_forward = 1
Network address translation (NAT)
We have allowed traffic to pass through. Why is that not enough? Why does a host on the LAN need help to access something on a WAN? Let’s imagine what would happen if our router just passed on packets from a LAN device straight out on to the internet, say brokkr.net, without making any changes. In the packets that reach my server the host has included it’s source address. That source address will be the host’s address on the local network, e.g. 192.168.0.105. But that is an address on a private network! Many people have a 192.168.0.1/24 network – this could be anyone’s. How is my server to know which one it should send the reply to? We say that this address is not publically routable.
To assist the device on the local network the router can intercept the packet headed for the WAN and – before sending it on to it’s destination – substitute it’s own WAN side address for that of the host’s in the packet’s source address field. Assuming our router is publically routable, the server can send a reply straight back to the router. (If it’s not, there will just be more layers of NAT at other junctions in the WAN network). When the response arrives with the router’s address in the destination address field, the router – having made a note of it’s earlier IP address swap – knows that the reply isn’t really destined for itself but the device on the LAN. It then mirrors it’s earlier operation and inserts the local network host’s address instead of it’s own in the destination address field and passes the packet on. We also call this whole thing source network address translation or SNAT. We will skip over the exact details of how SNAT works under the hood as it isn’t needed for the following implementation.
NAT in practice
The process described above can be implemented in the POSTROUTING chain in the nat table. Since we are leaving the filter table – which, being the default, did not need to be specified in earlier commands – be aware that all commands here need to explicitly state --table nat. While a complete overview of all tables and chains in iptables is outside of the scope of these posts, it is worth stopping up and asking what ‘POSTROUTING’ means and why this chain in particular.
To answer those questions I will point you in the direction of Phil Hagen’s very good flowchart of packets traversing iptables chains. As is immediately evident there are many more chains that a packet has to go through to make it to the end than just those in the filtering table. The POSTROUTING chain in the nat table is the purple box in the lower right corner, very close to the cloud saying ‘Outgoing packet’. In other words, it’s the last stop before an outbound packet is released. The routing decisions, made by the kernel, potentially moving the packet from the one network to another, have been made at this juncture. This is what ‘post-routing’ refers to – it comes after the kernel’s decision. The decisions can be found ‘upstream’ in the flowchart for both inbound and locally generated packets as white rectangles. As a result of the kernel’s decision, the router now knows which network (in our case, simply either LAN or WAN) a packet will be released onto and, derived from that, which out-interface it will be released on (in our case, br0 or enp1s0).
Why implement SNAT at this late stage? Because in order to do SNAT, we need to know the result of the routing decision. The routing decision tells us what network the packet exits to. That in turn tells us what interface the packet leaves by. If we don’t know the interface the packets is leaving the router on, we cannot substitute in it’s IP address. It is also worth noting that the POSTROUTING chain is downstream of our earlier filtering chains (FORWARD and OUTPUT, marked as green rectangles in the flowchart). I will hazard a guess and say that this is for efficiency. The sooner you can drop unwanted packets, the less computing power you have to expend on them. We definitely don’t want to do costly NAT operations on packets that aren’t going anywhere.
Before moving on to actual commands, we need to address a question of terminology. What I have described so far does qualify as source network address translation or SNAT. Traffic is going from a private network out onto a public network so we first need to substitute the source IP address (and then later the destination in the response but source comes first). However, in iptables parlance, SNAT is a narrower concept, used to mean one of two techniques to accomplish general-concept SNAT, the other being MASQUERADE. The difference is quite simple. When you tell iptables to do SNAT, you have to tell it what source IP address to substitute in. So you look at your WAN side interface and make a note of it’s IP address and add that to the command. Except… unless you’re paying your ISP for a static IP address, it’s unlikely to stay the same, day in, day out. That’s where MASQUERADE looks more attractive because it will check the out-interface’s IP address before adding it to outgoing packets. It does this for every packet. If that sounds a bit laborious, well, that’s because it is. That is why SNAT is recommended to use if you have a static, public IP address and MASQUERADE if you don’t. Supposedly, however, the difference in practice isn’t that noticable. This post will only discuss MASQUERADE as that is what I have personal experience with.
As before we start by setting a policy. When we discussed policies for chains in the filter table (INPUT, OUTPUT, and FORWARD) the issue could be framed simply as a firewall issue: To block or not to block. That works within the confines of the filter table. The more generally applicable question a policy asks, however, is: Do we allow the packet to move on to the next chain or not? In the specific context of POSTROUTING, the last chain before release, the question is if the packet should be released if it isn’t redirected by a rule. My answer is yes, it should. We will set a rule for packets that need SNAT, i.e. packets that are going out on the WAN, but all packets leaving the router enter the POSTROUTING chain. That includes LAN-to-LAN traffic which doesn’t need SNAT. This should obviously not be blocked (and if it should the FORWARD chain in the filter table would probably be the more appropriate place for it).
So we set the policy to ACCEPT and flush the POSTROUTING chain that we will be using shortly of possible older rules.
iptables --table nat -P POSTROUTING ACCEPT
iptables --table nat --flush POSTROUTING
And finally we can add the rule that we’ve been leading up to this entire time:
iptables --table nat -A POSTROUTING --out-interface wan_nic -j MASQUERADE
Once again we’re targetting the POSTROUTING chain in the nat table. -A is again for append; we’re appending a new rule to the now empty chain. The rule has a single match criterion which is that the packets targeted should be only those headed for the exit, i.e. those that leave the router on the WAN facing interface. Note that wan_nic is a placeholder and should be replaced with the actual name of the WAN facing interface, enp1s0 in my case (check output from ip address or networkctl). -j is for jump and MASQUERADE is the target; we’re telling iptables that packets in the chain matching the criterion should not be processed further here but taken care of by the target. The MASQUERADE target then handles everything we described above regarding source NAT automatically, including redirecting the response from the host on the WAN.
In the last post I made a big fuss about differentiating between traffic from the router and passing through the router. You might then reasonably ask: Given that I set a single criteron – a match on the WAN interface as the out-interface – am I not targetting both of those types of traffic? The answer is… possibly yes. It was only on closer inspection of the flowchart, I have linked to, that I realized that locally generated, outbound traffic would also enter the POSTROUTING chain. The only packets that skip it are those that are headed for localhost and so will shortly become ‘incoming packets’. However, if that is the case, it’s not a big problem – it’s just a bit of needless extra work. Partly because the NAT penalty is not that large, partly because the amount of traffic going out from the router itself (and responses to it) is not that large, mostly OS updates.
And that concludes NAT for now. If all has gone according to plan the host should now have all the most basic hallmarks of a router. It should hand out IP addresses to any who ask on the LAN. Those hosts should then be able to make requests to the router itself as well as to hosts on the WAN – and get responses back. In the next post we will return to the nat table to implement some port forwarding using the PREROUTING chain (and the DNAT, or destination NAT, target). That is, however, a separate and more esoteric concern from getting basic SNAT up and running.
Conclusion
As I concluded last time: If everything is working satisfactorily, I recommend saving all needed policy and rule commands to a shell script. Eventually we will get to setting up the system to automatically load the rules on boot but I recommend against it before you’re sure that there are no unintended effects. Better to do it manually at first, so the effects can be undone with a simple restart.
Before we conclude this post, a short aside: NAT was introduced in the 1990’s because by then people started to realize that IPv4 addresses were a scarce commodity. NAT allowed large networks to share a few addresses rather than have one each. It is also sometimes seen as a security measure because hosts behind NAT are not publically routable. Attackers therefore, generally, have to concentrate on the single point of the network that is, i.e. the router. This notion has, however, largely fallen out of favour because a) it just puts the onus on the router instead of the host it self and b) it implies that everything behind that bouncer router is a safe haven which is at odds with current thinking (see: zero-trust). In other words: Consider applying all that firewall knowledge to hosts on the LAN as well, now that you’re at it.
In the next post we will return to the interface I suggested reserving in the first post and build a DMZ on it. It will mostly be retreading covered ground just with some DMZ-specific considerations.
Cover image: a dark cave with a light at the end © Michal Ico, Unsplash License