Building a Transparent Firewall with Linux, Part IV
I've been writing a multipart series on building a transparent (bridging) firewall using Linux. Specifically, I'm using the distribution OpenWrt running on a Linksys WRT54GL broadband router, a hardware choice driven mainly by my curiosity about the WRT54GL's built-in five-port Ethernet switch and its ability to run OpenWrt Linux.
So far I've covered installing OpenWrt, recompiling a new OpenWrt image with iptables' bridging functionality enabled and configuring networking using OpenWrt's uci (Unified Configuration Interface) command.
This month, I review the example network topology and finally begin configuring iptables, the heart of the whole undertaking. Before I do so, however, there are a few OpenWrt housecleaning tasks to get out of the way: tweaking the kernel and network configurations, and disabling OpenWrt's native firewall system.
OpenWrt Performance as a Transparent Firewall
In researching this article, I had a nasty surprise. Although in the past I had seen articles and how-tos on making transparent firewalls with OpenWrt, this mode of operation is not supported by default in the Kamikaze and Backfire releases. Reportedly, running iptables in bridging mode under OpenWrt reduces overall system performance by a whopping 40%!
I proceeded writing this series anyhow, because I wanted to see for myself just how big an effect this is, and it seemed to me that the series still would be useful just for the sake of explaining how to install and use OpenWrt, and for explaining how to write iptables rules for transparent firewalls. However, at several points, I've written of my doubts as to the example OpenWrt/WRT54GL's suitability for high-bandwidth/high-availability settings.
Also, hopefully without sounding too grandiose, I hoped that by spurring greater interest in OpenWrt's flawed capability, I might encourage someone to get to the bottom of why OpenWrt performance plunges when run with iptables in bridging mode. Surely there's a reason that this not terribly new kernel feature is problematic in OpenWrt!
I say all this because I want to be clear that although transparent Linux firewalls in general constitute an interesting and useful technology, the specific combination of a $65 broadband router plus OpenWrt running in this mode is probably suitable only in a home or lab setting, not for any situation where you need to move large volumes of packets very quickly and very reliably (which is hopefully unnecessary for me to say, given that the WRT54GL is marketed to home users in the first place). I say it also so you understand why you have to go through the hoops of recompiling the OpenWrt image and editing /etc/sysctl.conf to get iptables bridging working in OpenWrt.
Recompiling the OpenWrt image with CONFIG_BRIDGE_NETFILTER=y set in the Linux kernel is the first of two steps in enabling iptables' bridging mode in OpenWrt. The second step is either to delete the following parameters in /etc/sysctl.conf or set each of them to “1” rather than “0”:
net.bridge.bridge-nf-call-arptables=0 net.bridge.bridge-nf-call-ip6tables=0 net.bridge.bridge-nf-call-iptables=0
In addition, I need to correct an error I made in the OpenWrt network configuration I showed you last time. You may recall that I changed OpenWrt's default configuration, such that all Ethernet ports were assigned to a single VLAN and bridge.
But possibly due to the way the Linux kernel interacts with the bridge hardware on my Linksys WRT54GL, with that configuration, I find that iptables ignores inter-VLAN traffic—that is, traffic between ports on the same VLAN. In order to get iptables to work properly on this hardware and on OpenWrt, I actually need two VLANs: one corresponding to my “uplink” (the Ethernet port connected to the outside world) and my “LAN” (everything else). These two VLANs, however, are still associated with the same bridge interface.
To create a separate VLAN for my uplink port, which is my WRT54GL's “WAN” port (or “port 4” to OpenWrt), I issue these commands on my router:
root@sugartongs:/etc/config# uci set network.eth0_1=switch_vlan root@sugartongs:/etc/config# uci set network.eth0_1.device="eth0" root@sugartongs:/etc/config# uci set network.eth0_1.vlan="1" root@sugartongs:/etc/config# uci set network.eth0_1.ports="4 5"
(Port 5, you'll recall, is a virtual port associated with the kernel, that must be included in all “ports” statements in OpenWrt network configurations, which is why our “...ports” statement is set to “4 5” rather than just “4”.)
To remove the WAN port from the other VLAN (eth0_0) I set up last time, I use this command:
root@sugartongs:/etc/config# uci set network.eth0_0.ports="0 1 2 3 5"
Next, in my bridge configuration, for the network I named “lan”, I associate both VLANs with the bridge:
root@sugartongs:/etc/config# uci set ↪network.lan.ifname="eth0.0 eth0.1"
And finally, I list my new network configuration to make sure everything's correct, commit the changes and reboot:
root@sugartongs:/etc/config# uci show network root@sugartongs:/etc/config# uci commit root@sugartongs:/etc/config# reboot
Listing 1 shows what the resulting /etc/config/network file looks like.
Listing 1. Corrected /etc/config/network
config 'switch' 'eth0' option 'enable' '1' config 'switch_vlan' 'eth0_1' option 'device' 'eth0' option 'vlan' '1' option 'ports' '4 5' config 'switch_vlan' 'eth0_0' option 'device' 'eth0' option 'vlan' '0' option 'ports' '0 1 2 3 5' config 'interface' 'loopback' option 'ifname' 'lo' option 'proto' 'static' option 'ipaddr' '127.0.0.1' option 'netmask' '255.0.0.0' config 'interface' 'lan' option 'type' 'bridge' option 'proto' 'static' option 'netmask' '255.255.255.0' option 'ipaddr' '10.0.0.253' option 'ifname' 'eth0.0 eth0.1'
Note that on your system, sections may be listed “out of order”, for example, with one VLAN section near the top and another near the bottom. Commands within a given section need to be in the correct order, but the sections themselves do not, so don't worry!
You also have to disable OpenWrt's native DHCP and iptables systems. The need for disabling DHCP services is obvious: acting as a DHCP server wouldn't be very “transparent” behavior! So, disable it with these two commands:
root@sugartongs# /etc/init.d/dnsmasq disable root@sugartongs# /etc/init.d/dnsmasq stop
OpenWrt's native iptables script (/etc/init.d/firewall) is fine if you want to use OpenWrt as a standard “Layer 3” (routing) firewall. Leaving this script enabled allows you to use the uci command and the file /etc/config/firewall to manage iptables in a manner very similar to how you manage network configuration and other OpenWrt system settings.
However, this system doesn't lend itself very well to running iptables in bridging mode—to use it that way, you'd need to hack the script extensively, which would be a bewildering task given the large number of custom tables it uses beyond “INPUT”, “OUTPUT” and “FORWARDING”. Therefore, disable it like this:
root@sugartongs# /etc/init.d/firewall disable root@sugartongs# /etc/init.d/firewall stop
Now you can create a custom iptables script more suitable for a transparent firewall.
In order to write a firewall script, you need to consider your network's topology and how the transparent firewall fits in. Figure 1 shows the example home network I sketched out in Part II of this series, with a firewall cabled between the network's Internet uplink (via DSL router or cable modem) and its backbone (which collapses back to a wireless broadband router configured with Internet uplink and LAN on the same logical subnet).
Figure 1. Example Home Network
You could use a number of topologies instead. If you have only a few hosts on your internal network, and your Internet uplink device is already providing DHCP services, you could use your transparent firewall as your broadband router (though configuring WLAN on OpenWrt is outside this series' scope). If your cable modem or DSL router includes a switch and/or wireless LAN access point, you could connect some of your network nodes directly to that and use your transparent firewall to protect other devices.
I'm going to stick with the topology in Figure 1, however, for simplicity's sake. It should be clear enough how to customize my sample iptables script for whatever topology you choose. Let's take a closer look at Figure 1.
The first thing you should notice is that everything on this network resides on the same logical subnet (10.0.0.0/24) except, of course, for the cable/DSL modem's WAN interface (the one connected to the Internet), which has the Internet-routable address 4.3.2.1. That WAN address is strictly illustrative; in actual practice, WAN IP addresses in any residential Internet scenario are assigned by your Internet service provider, often automatically, so please don't attempt to set yours to 4.3.2.1!
Another important point is that on this example network, client PCs are assigned IP addresses via DHCP from the pool 10.0.0.2 through 10.0.0.100. My diagram doesn't indicate which host is providing DHCP services. Is it the cable/DSL modem, the broadband router or the Web proxy?
As a matter of fact, it doesn't matter! Because this entire network fabric is switched, DHCP requests will propagate freely, including through the transparent firewall. However, if the cable/DSL modem acts as the DHCP server, you will need to write rules on the firewall to allow DHCP through in both directions.
Now that you understand what the network looks like, let's decide how to manage its dataflows. In my example scenario, the firewall will have a “deny by default” policy, as any good firewall should. The task, therefore, will be one of anticipating and allowing the dataflows you need the firewall to allow.
First, assuming the LAN's DHCP server is upstream of the firewall, you need to allow DHCP traffic between UDP port 67 (the DHCP server port) and UDP port 68 (the DHCP client port).
Next, you don't want to lock yourself out of the firewall itself! You need to allow traffic from the LAN to TCP port 22 on the firewall.
As you can see in Figure 1, the example network has an outbound Web proxy. Because one of the best uses of a firewall is to enforce use of a Web proxy, you'll for sure want to allow only outbound Web traffic originating from the Web proxy. You'll also allow outbound DNS queries (and corresponding replies).
That's it! Things downstream of the firewall—that is, transactions between hosts connected to the broadband router shown in Figure 1—don't need to be allowed by the firewall. For example, print jobs sent from wired and wireless DHCP clients to the network printer don't need an “allow LPR” rule, because those packets should never reach the transparent firewall in the first place.
(If, however, you have only a few hosts on your LAN and elect to omit the downstream switch or broadband router and cable them directly to the transparent firewall, this will not be the case. You will need to allow for “LAN-to-LAN” transactions of that type.)
Now, finally, you're ready to write a custom firewall script! You could, of course, simply edit the file /etc/init.d/firewall. But, that would make it harder to revert to OpenWrt's native uci-driven firewall system later—better to leave that script alone. I prefer to create a new script from scratch, arbitrarily named /etc/init.d/iptables.custom.
Listing 2 shows what /etc/init.d/iptables.custom needs to look like in order to implement the firewall policy we arrived at in the previous section. Let's dissect it.
Listing 2. Custom iptables Startup Script
#!/bin/sh /etc/rc.common # Customized iptables script for OpenWrt 10.03 START=46 IPTABLES=/usr/sbin/iptables LOCALIP=10.0.0.253 LOCALLAN=10.0.0.0/24 WEBPROXY=10.0.0.111 stop() { echo "DANGER: Unloading firewall's Packet Filters!" $IPTABLES --flush $IPTABLES -P INPUT ACCEPT $IPTABLES -P FORWARD ACCEPT $IPTABLES -P OUTPUT ACCEPT } start() { echo "Loading custom bridging firewall script" # Flush active rules, custom tables $IPTABLES --flush $IPTABLES --delete-chain # Set default-deny policies for all three default tables $IPTABLES -P INPUT DROP $IPTABLES -P FORWARD DROP $IPTABLES -P OUTPUT DROP # Don't restrict loopback (local process intercommunication) $IPTABLES -A INPUT -i lo -j ACCEPT $IPTABLES -A OUTPUT -o lo -j ACCEPT # Block attempts at spoofed loopback traffic $IPTABLES -A INPUT -s $LOCALIP -j DROP # pass DHCP queries and responses $IPTABLES -A FORWARD -p udp --sport 68 --dport 67 -j ACCEPT $IPTABLES -A FORWARD -p udp --sport 67 --dport 68 -j ACCEPT # Allow SSH to firewall from the local LAN $IPTABLES -A INPUT -p tcp -s $LOCALLAN --dport 22 -j ACCEPT $IPTABLES -A OUTPUT -p tcp --sport 22 -j ACCEPT # pass HTTP and HTTPS traffic only to/from the web proxy $IPTABLES -A FORWARD -p tcp -s $WEBPROXY --dport 80 -j ACCEPT $IPTABLES -A FORWARD -p tcp --sport 80 -d $WEBPROXY -j ACCEPT $IPTABLES -A FORWARD -p tcp -s $WEBPROXY --dport 443 -j ACCEPT $IPTABLES -A FORWARD -p tcp --sport 443 -d $WEBPROXY -j ACCEPT # pass DNS queries and their replies $IPTABLES -A FORWARD -p udp -s $LOCALLAN --dport 53 -j ACCEPT $IPTABLES -A FORWARD -p tcp -s $LOCALLAN --dport 53 -j ACCEPT $IPTABLES -A FORWARD -p udp --sport 53 -d $LOCALLAN -j ACCEPT $IPTABLES -A FORWARD -p tcp --sport 53 -d $LOCALLAN -j ACCEPT # cleanup-rules $IPTABLES -A INPUT -j DROP $IPTABLES -A OUTPUT -j DROP $IPTABLES -A FORWARD -j DROP }
First, note the includes file /etc/rc.common at the top: this provides functions like enable, disable and other housekeeping functions that OpenWrt uses to manage startup files.
Next, START=46 specifies the priority/order for running this script at startup. 46 is the same slot that the default OpenWrt “firewall” startup script uses, which is to say, after networking is enabled but before the DropBear SSH server and other network services are started.
Next come some “shorthand” variables we'll use throughout the script. IPTABLES, obviously enough, specifies the full path to the local iptables command. LOCALIP is the firewall's bridge IP address; LOCALLAN is the network address of the local LAN, and WEBPROXY gives the IP address of the Web proxy.
The “stop” function (as in ./iptables.custom stop) causes the script to flush all iptables rules from kernel memory and to load default ACCEPT policies for all three default firewall tables, INPUT, FORWARD and OUTPUT. This does not “stop all traffic”; rather, it stops all restrictions on traffic (thus, the warning message).
Now we come to the heart of the script: the “start” function, containing the firewall policy in the form of a list of iptables commands.
First, flush any active rules and delete any custom tables, so you begin with a clean slate ($IPTABLES --flush and $IPTABLES --delete-chain). Next, set default deny policies for the INPUT, FORWARD and ACCEPT chains. (You could just as easily choose REJECT as the default policy, but because this involves sending ICMP replies to jilted clients, versus DROP's simply ignoring them, there's a slight performance benefit to DROP.)
Next come two rules to allow interprocess communication on the firewall itself, by allowing all packets arriving from and destined for the “loopback” interface. This is followed immediately, however, by an antispoofing rule that blocks traffic addressed to the firewall from the firewall's own IP address.
Next are two rules allowing DHCP requests—that is, packets from UDP port 68 sent to UDP port 67—and DHCP responses—that is, packets from UDP port 67 to UDP port 68. These two rules are necessary only if your DHCP server is on the other side of your firewall from your DHCP clients.
You may have noticed that these two DHCP rules and the subsequent rules for SSH, HTTP proxying and DNS are “stateless”. Rather than invoking the iptables “state” module, which lets you allow, for example, outbound DHCP queries while letting the kernel decide what constitutes a valid response, you're explicitly allowing the reply traffic. This is an admittedly archaic way to write iptables rules.
However, as I mentioned in the sidebar, OpenWrt has significant performance issues when used as a bridging firewall. Because the “state” module imposes still more of a performance hit, and because this firewall policy is simple to begin with, I'm doing it the old-fashioned way. For a bridging firewall on a better-performing distribution/hardware combination, I definitely would take advantage of Linux's state-tracking features!
The next pair of rules in Listing 2 allows SSH connections to the firewall itself, but only from the local LAN. Note that the “incoming” leg of SSH transactions is handled in the INPUT table, whereas the “outbound” leg is processed in the OUTPUT table. If you were using -m state, the OUTPUT leg would be implicit.
Next come two pairs of rules allowing only the Web proxy to send and receive traffic to/from TCP ports 80 and 443, which, of course, correspond to HTTP and HTTPS, respectively.
This wouldn't work unless DNS did also, so next are rules allowing DNS queries to TCP and UDP ports 53 (ordinarily, DNS queries just use UDP, but once in a while they can occur over TCP as well).
Finally, the script ends with three “cleanup” rules that place a “drop all” rule at the bottom of each of the default tables. These are, of course, redundant with the default “DROP” policies I set near the beginning of the start() function, but specifying such cleanup rules are a firewall best practice; sometimes redundancy is desirable!
When you type in any firewall script, be careful! At the very least, double- and triple-check the SSH rules that allow access to the firewall. If there's any problem with those rules, you'll be locked out once you run the script, and you may even need to re-flash your firewall to recover. You can fix other things if SSH works, but if SSH doesn't work, you'll be stuck.
Once you're confident enough to test your rules, save the new script. Be sure to set the “execute” bit on it like so:
root@sugartongs:/etc/init.d# chmod a+x ./iptables.custom
And, enable the script at startup, like this:
root@sugartongs:/etc/init.d# ./iptables.custom enable
Now for the moment of truth—load the rules:
root@sugartongs:/etc/init.d# ./iptables.custom start
Test the rules by making sure the things you want to work still do (connecting back to the firewall via SSH, surfing the Web via your Web proxy and so forth). Also, be sure to test some things you don't expect to work, such as surfing the Web without going through the proxy or connecting to an FTP server using an FTP client application. In my own experience, the challenge with OpenWrt is getting iptables to “see” and act on traffic; the real test is ensuring that it's blocking anything!
And with that, I've completely filled up this month's allotted space. I'll wrap up the series next month with some tips and tricks, and a suitably flowery “Conclusion” paragraph that I promise will be much more worthwhile than this one. For now, I'll simply say, “good luck!”
Resources
Home Page for the OpenWrt Project: www.openwrt.org
OpenWrt's Unified Configuration Interface Documentation: wiki.openwrt.org/doc/uci
The OpenWrt Forum (where you'll end up asking for help sooner or later, if you use OpenWrt more than very casually): https://forum.openwrt.org
Mick Bauer (darth.elmo@wiremonkeys.org) is Network Security Architect for one of the US's largest banks. He is the author of the O'Reilly book Linux Server Security, 2nd edition (formerly called Building Secure Servers With Linux), an occasional presenter at information security conferences and composer of the “Network Engineering Polka”.