Thursday, December 17, 2015

Notes About IPTables

This is going to be a completely nerdy post. You've been warned.

I recently changed my Internet connection at home to fibre! Which is brilliant and fantastic and ... I'm cheap. So instead of going out and buying a router, I decided instead to use a pi to do it. I landed on my Banana Pi with a USB Ethernet adapter only to find that it was SLOW. Damn USB and Ethernet.

So I then brought a BPi-R1 to deal to the problem. I've still saved some money (I think a off the shelve jobbie was around the $200 mark whereas the BPi-R1 cost me around $150. I didn't realise the New Zealand dollar had dropped against the US dollar). Whoops. Thus, this post. I gain some value back by learning something:

What this is all leading to though is the fact that I finally had to learn a little something about IPTables. If you've never heard of it, this post might not be for you. It's the frontend to the firewall (netfilter) in Linux.

It was a bit of a struggle with very few explanations being around. So here's my attempt to get rid of some of that pain:

Anyone who tells you that it's easy is lying. It's not easy. The information out there is often inconsistent. For example, the terms "table" and "chain" seems to get confused and used interchangeably. So let's get this straight. A table is not a chain and a chain is not a table.

The tables I needed to concern myself with were the filter table and the nat table.

The next really confusing bit. LOADS of pages talk about using the command:

 iptables -L

This doesn't list out all of your rules (and the output is pretty much useless anyway). So here's the thing... When you use the iptables command, and you don't specify a table, you're using the filter table. So 'iptables -L' ONLY lists out the filter table.

Instead, you're much better off using:

 iptables-save  

Or, if you want it to be a little less... blerg (the comments and information you don't need), use:

 iptables-save | sed 's/\[[0-9]*:[0-9]*\]// ; /^#/d'  

So for all of this talk about tables and chains... what does it really mean? Well... the table names don't really seem to mean a great deal. They give you a sense of the purpose of the chains BUT the way traffic goes though the various set of chains isn't implied by the table name.


In this diagram, PREROUTING is part of the nat table. INPUT, FORWARD and OUTPUT are part of the filter table and POSTROUTING is part of the nat table. Confused?

Let's go through that diagram. When a packet comes in, a routing decision is made. Is that packet intended for the machine the firewall is on? If it is, it goes to the INPUT chain. Otherwise, it goes to the FORWARD chain. When a packet is going from the firewall machine out, it goes through the OUTPUT chain.

We need to make some decisions. When we're talking firewalls, we're talking about protection. In my case, I chose to only protect my network from the Internet. I don't really care what traffic is going out.

Which means I've already already got my first 3 rules! (They're not technically rules - they're policies. But let's not let that get in the way of our sense of accomplishment)

 iptables --table filter --policy INPUT drop  
 iptables --table filter --policy FORWARD drop  
 iptables --table filter --policy OUTPUT accept  

By default, drop packets going to the INPUT chain (if not matched by a rule), drop packets being forwarded (ditto), and 'accept' (allow to pass through) anything going outwards.

For the rest of this document, I'm going to be using:
  • lanDev as my LAN device in the rules. This will normally look something like eth0 or eth1 etc. On the Banana Pi it's eth0.102 OR br0. Totally irrelevant. My set up is likely to be different from yours.
  • wanDev as my WAN (Wide Area Network i.e. Internet) device.
  • 1.1.1.1 as an example internal IP address. In reality, this will normally be something along the lines of 192.168.0.1, 172.16.1.1 or 10.1.1.1.
  • 2.2.2.2 will be our external IP. If you've got a dynamic IP, read right to the end of this post.
  • I've kept the long form of the commands to make it clear what we're doing i.e. all of the examples specify the table they're using rather than relying on the default.
This hopefully makes it a little clearer.

Next we need to allow the computer to talk to itself (it does this more often than you'd think on the loopback - lo - device).

 iptables --table filter --append INPUT --in-interface lo --jump ACCEPT  

And because I don't want to limit the server from the internal network, I also do the same thing with my internal network:

 iptables --table filter --append INPUT --in-interface lanDev --jump ACCEPT  

If I wanted to harden this up a bit, I could:
  • Lock down the IP addresses that can talk to the server:

    iptables --table filter --append INPUT --source 1.1.1.0/24 --in-interface lanDev --jump ACCEPT 

    This allows anything with an IP address between 1.1.1.0 and 1.1.1.255 access to the router.

    If I wanted to lock down those addresses a little more, I could use the "iprange" extension (I tend to think of it as a module given that you use '-m' on the command line to use an extension):
     iptables --table filter --append INPUT --match iprange 1.1.1.100-255 --in-interface lanDev -j ACCEPT   
    
  • Or ONLY give access to particular services, i.e. ssh:
    iptables --table filter --append INPUT --source 1.1.1.0/24 -in-interface lanDev --protocol tcp --dport 22 --jump ACCEPT 
And finally, we want the Internet to be able send information back to the router BUT only when we've initiated a connection of some kind (when visiting a web page, I want the web page to be able to load):

  iptables --table --append INPUT --match conntrack --ctstate ESTABLISHED,RELATED --jump ACCEPT   


Here we use the conntrack extension to make a match on state. The router can tell which packets are part of an established or related connection.

Let's set up NAT (Internet Sharing). Everyone clear on what NAT is and does?

The needed rules are:

 iptables --table nat --append POSTROUTING --out-interface wanDev --jump MASQUERADE  
 iptables --table filter --append FORWARD --match conntrack --ctstate ESTABLISHED,RELATED --jump ACCEPT  

The first rule does all the real magic. When a device on your local network sends out a packet, the IP address is changed to the external ip address. When a packet comes in, the router then changes the IP address back to that of the local device.

Remember how we dropped everything by default in the forward chain? The second line there just does what we did with the INPUT chain i.e. if it's part of an established or related connection, let it through.

The other thing you'll probably need to check is that IP forwarding is enabled in the kernel. Edit /etc/sysctl.conf and uncomment or add the following line:

 net.ipv4.ip_forward=1  

This will take effect when you reboot. To enable it immediately, run the following:

 sysctl net.ipv4.ip_forward=1  

Now we have a router that's relatively locked down and is doing some routing!

You may have read previous posts where I talk about CG-NAT. The evilness of it meant that I couldn't use my system the way that I normally do. Which is to say that if I'm on holiday or working somewhere away from home, I normally remote into my system. This saves me maintaining multiple development environments.

I spent another $50 on getting a static IP. Basically, I always have the same address when connecting to the Internet and it gets me past the CG-NAT stuff. Neat! (Except that it probably makes it a bit easier to track me).

What if I want to run a service on my router? I would need to open a port:

 iptables --table filter --append INPUT --protocol tcp --dport 80 --jump ACCEPT   

To enable remote access to my system, I needed port forwarding. Say I want to connect to a web server externally from the Internet. To do this, I need the following rules:

 iptables -t nat -A PREROUTING -i wanDev -p tcp --dport 80 -j DNAT --to 1.1.1.1:80  
 iptables -t filter -A FORWARD -p tcp -d 1.1.1.1 --dport 80 -j ACCEPT  

This tells the PREROUTING chain to send traffic on port 80 coming in from the wanDev device to port 80 on 1.1.1.1 and the FORWARD chain to accept traffic coming in on port 80 destined for 1.1.1.1.

I could (but it seems pointless) only accept NEW connections and rely on the earlier "ESTABLISHED, RELATED" rules to continue the connection. To do this, I would replace:
 -A FORWARD -p tcp -d 1.1.1.1 --dport 80 -j ACCEPT  
with:
 -A FORWARD -p tcp -d 1.1.1.1 -m conntrack --cstate NEW --dport 80 -j ACCEPT  

The other thing to note is that the external port doesn't have to match the internal one. Imagine I want to listen to ssh connections on port 12345 but I want to still run ssh connections on 22 internally. In which case, I would use the following rules:

  iptables -t nat -A PREROUTING -i wanDev -p tcp --dport 12345 -j DNAT --to 1.1.1.1:22   
  iptables -t filter -A FORWARD -p tcp -d 1.1.1.1 --dport 22 -j ACCEPT   

Not content with that, I went a step further. The problem with all of this is that the forward porting doesn't work from inside my own LAN. That means that there are differences to how I access things externally.

Imagine you're using a Dynamic DNS service like noip.com. That way you don't need to memorize your IP address (it's an absolute godsend if you have a dynamic IP). From inside my own LAN, I'd be able to use that address to access the various services (the machine I ssh into isn't the same machine hosting the web page for example).

To do this, I need the following rules:
 iptables -t nat -A PREROUTING -p tcp -s 1.1.1.0/24 -i eth0.102 -d 2.2.2.2 --dport 80 -j DNAT --to-destination 1.1.1.1:80  
 iptables -t nat -A POSTROUTING -p tcp -s 1.1.1.0/24 -o eth0.102 -d 1.1.1.1 --dport 80 -j SNAT --to 2.2.2.2:80  
 iptables -t nat -A PREROUTING -p tcp -s 1.1.1.0/24 -i eth0.102 -d 2.2.2.2 --dport 12345 -j DNAT --to-destination 1.1.1.1:22  
 iptables -t nat -A POSTROUTING -p tcp -s 1.1.1.0/24 -o eth0.102 -d 1.1.1.1 --dport 22 -j SNAT --to 2.2.2.2:12345  

Uh oh! We've actually needed the external ip address! Which, if you have a dynamic IP,  you don't always know. I'll address that in a second.

We need to save our rules and make sure they are loaded. These instructions are for a debian based system though I think Arch Linux uses the same method. I'm pretty sure this won't work on a Redhat based system where they don't use the /etc/network/interfaces file for network configuration.
Save the rules:
 iptables-save > /etc/iptables.conf  

To tell the network configuration to restore those rules before bringing up the network, edit /etc/network/interfaces and add the line:
 pre-up iptables-restore < /etc/iptables.rules  


If you have a dynamic IP and you're using port forwarding from inside your own network:

This is where things get tricky. The problem is we won't know what the IP address will be until AFTER the interface is up. To get around this (I'm not sure if this is the recommended way, but it's a solution) we'll need to create a script.

By now you should have a file in /etc called iptables.conf. We need to remove the stuff that's reliant on having an external address to it's own file. We should be able to do this by running:
  echo '*nat' > /etc/iptables-extIP.rules ; grep '2.2.2.2' /etc/iptables.rules >> /etc/iptables-extIP.rules && sed '/2\.\2\.2\.2/d /etc/iptables.rules -i  ; echo 'COMMIT' >> iptables-extIP.rules

This:
  • Copies out all of the lines relevant to the external IP address to a file called /etc/iptables_extIP.rules and makes that file suitable for iptables-restore.
  • Removes those entries from /etc/iptables.rules.
Now we need to change that external IP address to something we can easily search for:
  sed 's/2\.2\.2\.2/EXT_IP/' /etc/iptables-extIP.rules -i

And finally, we need to run create a script to fill in these bits after the WAN device is up. Create a script in /etc/network/if-up.d/extIPRules with the following contents:

 #!/bin/bash  
 wan_device="wanDev" #replace with the name of your external device i.e. ppp0  
 iface=$1  
 if[ "$iface" == "$wan_device" ] ; then  
    ext_ip_addr=$(ip addr show $wan_device | grep inet | egrep '([0-9]{1,3}\.){3}[0-9]{1,3}' -o | head -n 1)  
    sed "s/EXT_IP/$ext_ip/g" | iptables-restore -n  
 fi  
(This is untested. If you do try this and it works, please let me know in the comments.)

Make it executable:

 chmod +x /etc/network/if-up.d/extIPRules  

And it should "Just Work™". If it doesn't, let me know in the comments and I'll try and update this post with any corrections that are needed.