Generating Firewall Rules with Perl
Listing 5. ports.conf
wan tcp 22 # ssh wan tcp 25 # smtp wan tcp 80 # http wan udp 53 # dns wan udp 1194 # openvpn wan udp 5060 # sip wan udp 4569 # iax2 wan udp 10000:20000 # rtp lo all lan tcp 22 # ssh lan tcp 25 # smtp lan udp 53 # dns lan tcp 53 # dns lan udp 67 # dhcp lan udp 68 # dhcp lan tcp 80 # http lan tcp 111 # portmapper lan udp 111 # portmapper lan tcp 143 # imap lan tcp 443 # https lan tcp 2049 # nfs lan udp 2049 # nfs lan tcp 3306 # mysql lan udp 4569 # iax2 lan udp 5060 # sip lan tcp 5432 # postgresql lan tcp 10000 # webmin lan all gig all tun all wifi udp 1194 # openvpn voip udp 5060 # sip voip udp 4569 # iax2 voip udp 53 # dns voip tcp 22 # ssh voip udp 10000:20000 # rtp voip tcp 80 # http
You'll notice that I have a rule that allows all traffic on the lo, or loopback, interface. This is important, because without this rule, many programs break in ways that are hard to diagnose. You also may be asking why I go to the effort of creating so many firewall rules for my LAN interface, only to have an all at the end. I do this for several reasons. The primary reason is that until my kids grow up and get on the Internet, I trust the traffic on my local network. However, by having rules for each service I run, I'm able to extract statistics about how much traffic each service generates. Also, security is an iterative process. Over time, I'll add rules that will further tighten my firewall; eventually, I'll remove the final all from the policy.
Now, back to the add_rules() function. Even though this is the longest function in the entire program, it's still not too hard to understand. The sections of code that deal with tcp and udp rules simply create two rules for each rule in the ports.conf file. One rule is tied to the destination port number; the other rule is tied to the source port number. At first, this may seem odd, because we are policing only inbound traffic. What we're doing is making sure that both inbound and outbound connections are allowed to pass. For example, a packet coming in on the WAN interface with the destination port set to 80 corresponds to an inbound connection to my Web server. On the other hand, an incoming packet on my WAN interface with the source port set to 80 is coming from an outside Web server in response to a request that came from inside my network.
The code that handles the all rules is a special case. In this case, we create a rule on the given interface for each of the protocols. In hindsight, this might be overly complex, but it has an interesting side effect. If the router encounters a packet containing an unknown protocol, such as IPSec, the firewall will fall back to its default policy even though we've asked it to pass all traffic on this interface. So, in a sense, “all protocols” actually means “all known protocols”. I think this is a good thing.
For what it's worth, the script will put firewall rules into the kernel in roughly the same order that they appear in the ports.conf file. I say roughly, because the rules will be put into the appropriate chain depending on which interface and protocol they match against. But within each chain, the rules will be executed in order.
The set_default_action() function creates rules that determine what happens to packets that don't match any previous rules. This sounds very similar to the purpose of the set_default_policy() function, but there is a subtle difference. The set_default_policy() function configures the default firewall policy, and the set_default_action() function creates firewall rules that catch unmatched traffic before the kernel falls back to the default policy, essentially capping each chain. Once these rules match a packet, they create a log entry for the packet, and then they implement whatever policy we want, in this case, DROP. Once again, the log entries allow me to determine what traffic is being dropped and why.
I'm not trying to tell you that this program is perfect, nor will it do everything you want it to do. You might even find bugs in it. In fact, by the time you read this article, I'll probably have made several improvements to the script. As it is right now, the script isn't able to configure any firewall policy for the ICMP protocol. It would be nice to be able to allow outbound ping requests and deny incoming requests, for example. It would also be useful to be able to configure firewall policy for outgoing and routed traffic. And because I'm using VoIP, I'm thinking of changing my script to allow me to configure Quality of Service (QoS). If you come up with useful modifications to this script, I'd like to hear about them.
But there you have it, such as it is. In less than 200 lines of Perl code, I'm able to implement a quite flexible and efficient firewall policy containing potentially hundreds of individual rules. At the same time, making changes to my firewall policy is simple enough that even most beginning Linux users can make correct changes.
Mike Diehl works for SAIC at Sandia National Laboratories in Albuquerque, New Mexico, where he writes network management software. Mike lives with his wife and two small boys and can be reached via e-mail at firstname.lastname@example.org.
Mike Diehl is a freelance Computer Nerd specializing in Linux administration, programing, and VoIP. Mike lives in Albuquerque, NM. with his wife and 3 sons. He can be reached at email@example.com
- March 2015 Issue of Linux Journal: System Administration
- High-Availability Storage with HA-LVM
- DNSMasq, the Pint-Sized Super Dæmon!
- Localhost DNS Cache
- Real-Time Rogue Wireless Access Point Detection with the Raspberry Pi
- Days Between Dates: the Counting
- The Usability of GNOME
- You're the Boss with UBOS
- Multitenant Sites
- Linux for Astronomers