Building a Transparent Firewall with Linux, Part V
Dear readers, I appear to have set a Paranoid Penguin record—six months spent on one article series. (It has consisted of five installments, with a one-month break between the second and third pieces.) But, we've covered a lot of ground: transparent firewall concepts and design principles; how to install OpenWrt on a Linksys WRT54GL router; how to compile a custom OpenWrt system image; how to configure networking and iptables bridging on OpenWrt; and, of course, how to replace the native OpenWrt firewall script with a customized iptables script that works in bridging mode. This month, I conclude the series by showing how to achieve the same thing using an ordinary PC running Ubuntu 10.04.
At this late stage in the series, I assume you know what a transparent firewall is and where you might want to deploy it on your network. But since I haven't said a word about PC hardware since Part II (in the September 2010 issue of LJ), it's worth repeating a couple points I made then about selecting network hardware, especially on laptops.
If it were ten years ago, I'd be talking about internal PCI network adapters for desktop/tower systems and PCMCIA (PC-card) interfaces for laptops. Nowadays, your system almost certainly has an Ethernet card built in and needs only one more to act as a firewall (unless you want a third “DMZ” network, but that's beyond the scope of this series—I'm assuming you're firewalling off just one part of your network).
If you have a desktop or tower system with a free PCI slot, you've got a plethora of good choices for Linux-compatible Ethernet cards. But, if you have a laptop, or if your PCI slots are all populated, you'll want an external USB Ethernet interface.
Here's the part I mentioned earlier: be sure to select a USB Ethernet interface that supports USB 2.0, because USB 1.1 runs at only 12Mbps and USB 1.0 at 1.5Mbps. (USB 2.0 runs at 480Mbps—plenty fast unless your LAN runs Gigabit Ethernet.) Obviously, you also want an interface that is supported under Linux.
As a general rule, I don't like to shill specific products, but in the course of writing these articles, I had excellent experiences with the D-Link DUB-E100, a USB 2.0, Fast Ethernet (100Mbps) interface. It's supported under Linux by the usbnet and asix kernel modules. Chances are, your Linux system automatically will detect a DUB-E100 interface and load both modules. I love it when a cheap, simple device not only “just works” under Linux, but also performs well, don't you?
You'll remember from the previous two installments that in order to support iptables in bridging mode, your Linux kernel needs to be compiled with CONFIG_BRIDGE_NETFILTER=1, and your /etc/sysctl.conf file either needs to not contain any entries for the following settings or have them set to “1”:
net.bridge.bridge-nf-call-arptables=0 net.bridge.bridge-nf-call-ip6tables=0 net.bridge.bridge-nf-call-iptables=0
Well, if you're an Ubuntu user, you don't have to worry. Unlike OpenWrt, the stock Ubuntu kernels already have CONFIG_BRIDGE_NETFILTER support compiled in, and its default /etc/sysctl.conf file is just fine without needing any editing by you. Odds are, this is true for Debian and other Debian derivatives as well, although I haven't had a chance to verify it myself.
One thing you probably will have to do, however, is install the command brctl by way of either Debian's/Ubuntu's bridge-utils package or whatever package through which your non-Debian-derived distribution of choice provides the brctl command. This is seldom a default package, so if entering the command which brctl doesn't yield a path to the brctl command, you need to install it.
As with OpenWrt, however, you will not need the ebtables (Ethernet Bridging tables) command, unless you want to filter network traffic based on Ethernet header information, such as MAC (hardware) address and other very low-level criteria. Nothing I describe in this series requires ebtables, just plain-old iptables.
If you've got two viable Ethernet interfaces, if your kernel supports iptables in bridging mode, and if your system has bridge-utils installed, you're ready to set up bridging! On Ubuntu Server and other Debian-derived, nongraphical systems, this involves changes to only one file, /etc/network/interfaces—unless, that is, your window manager controls networking. See the sidebar Networking Tips: GNOME vs. You for instructions on disabling GNOME's Network Manager system.
Networking Tips: GNOME vs. You
Normally, any computer you're configuring to act as a network device or server should not run the X Window System for reasons of performance and security. But if, for some reason, such as testing, you want to set up bridging on Ubuntu 10.04 Desktop (or any other GNOME-based distribution), you need to be aware of a few things.
Traditionally, Ubuntu and other Debian derivatives store network interface configurations in the file /etc/network/interfaces. However, GNOME's Network Manager system automatically configures any interface not explicitly described in that file.
In theory, this should mean that if you specify interface and bridge configurations in /etc/network/interfaces, you shouldn't have to worry about Network Manager overriding or otherwise conflicting with those settings. But in practice, at least in my own experience on Ubuntu 10.04, you're better off disabling Network Manager altogether in the System→Preferences→Startup Applications applet, if you want to set up persistent bridge settings in /etc/network/interfaces.
To completely disable Network Manager, you also need to open the System→Preferences→Network Connections control panel and delete all connection profiles under the Wired tab. Even if Network Manager is disabled as a startup service, Ubuntu will read network configuration information set by this control panel, resulting in strange interactions with /etc/network/interfaces.
On my test system, even after disabling the Network Manager service, setting up /etc/network/interfaces as shown in Listing 1 and stopping and restarting /etc/init.d/networking, eth2 kept showing up in my routing table with the same IP address as br0, even though br0 should have been the only interface with any IP address (let alone a route). Clearing out eth2's entry in Network Connections and again restarting networking fixed the problem.
To kill the running Network Manager processes, first find its process ID using ps auxw |grep nm-applet. Then, do sudo kill -9 [PID] (substituting [PID] with the process ID, of course) to shut down Network Manager. This is a good point to configure networking manually by editing /etc/network/interfaces (sudo vi /etc/network/interfaces). Finally, restart networking by entering sudo /etc/init.d/networking restart.
So, let's examine a network configuration for bridged eth1 and eth2 interfaces. (To you fans of Fedora, Red Hat, SUSE and other non-Debian-ish distributions, I apologize for my recent Ubuntu-centrism. But hopefully, what follows here gives you the gist of what you need to do within your respective distribution's manual-network-configuration schemes.)
Listing 1 shows my Ubuntu 10.04 firewall's /etc/network/interfaces file. My test system is actually an Ubuntu 10.04 Desktop system, but I've disabled Network Manager as described in the sidebar.
Listing 1. /etc/network/interfaces
auto lo iface lo inet loopback address 127.0.0.1 netmask 255.0.0.0 auto br0 iface br0 inet static address 10.0.0.253 netmask 255.255.255.0 pre-up ifconfig eth1 down pre-up ifconfig eth2 down pre-up brctl addbr br0 pre-up brctl addif br0 eth1 pre-up brctl addif br0 eth2 pre-up ifconfig eth1 0.0.0.0 pre-up ifconfig eth2 0.0.0.0 post-down ifconfig eth1 down post-down ifconfig eth2 down post-down ifconfig br0 down post-down brctl delif br0 eth1 post-down brctl delif br0 eth2 post-down brctl delbr br0
The first part of Listing 1 shows settings for lo, a virtual network interface used by local processes to communicate with each other. I've explicitly assigned lo its customary IP address 127.0.0.1 and subnetwork mask 255.0.0.0.
The rest of Listing 1 gives the configuration for br0, my logical bridge interface. First, I set the bridge interface's IP address to 10.0.0.253 with a netmask of 255.255.255.0, just as I did with OpenWrt. Note that when you associate physical network interfaces with a logical bridge interface, the bridge interface gets an IP address, but the physical interfaces do not. They are, at that point, just ports on a bridge.
Note that on my test system, eth1 and eth2 are the names assigned to my two USB D-Link DUB-E100 interfaces. It's actually more likely you'd use your machine's built-in Ethernet interface (probably named eth0), and that any second interface you'd add would be named eth1. When in doubt, run the command tail -f /var/log/messages before attaching your second interface to see what name your system assigns to it. You also can type sudo ifconfig -a to get detailed information on all network interfaces present, even ones that are down.
Continuing the analysis of Listing 1, after I configure the bridge IP address and netmask, I actually bring down the two physical interfaces I'm going to bridge, before invoking the brctl command to create the bridge (br0) and add each interface (eth1 and eth2) to it. The last step in bringing the bridge up is to assign to both physical interfaces, eth1 and eth2, the reserved address 0.0.0.0, which has the effect of allowing each of those interfaces to receive any packet that reaches it—which is to say, having an interface listen on IP address 0.0.0.0 makes that interface promiscuous. This is a necessary behavior of switch ports. It does not mean all packets entering on one port will be forwarded to some other port automatically; it merely means that all packets entering that port will be read and processed by the kernel.
The “post-down” statements in Listing 1, obviously enough, all concern breaking down the bridge cleanly on shutdown.
Once you've restarted networking with a sudo /etc/init.d/networking restart, your system should begin bridging between its two physical interfaces. You should test this by connecting one interface on your Linux bridge/firewall to your Internet-connected LAN and connecting the other interface to some test system. The test system shouldn't have any problem connecting through to your LAN and reaching the Internet, as though there were no Linux bridge in between—at least, not yet it shouldn't. But, we'll take care of that!
Now it's time to configure the Linux bridge with the same firewall policy I implemented under OpenWrt. Listing 2 shows last month's custom iptables script, adapted for use as an Ubuntu init script. (Actually, we're going to run it from the new “upstart” system rather than init, but more on that shortly.)
Listing 2. Custom iptables Startup Script
#! /bin/sh ### BEGIN INIT INFO # Provides: iptables_custom # Required-Start: $networking # Required-Stop: # Default-Start: # Default-Stop: 0 6 # Short-Description: Custom bridged iptables rules ### END INIT INFO PATH=/sbin:/bin IPTABLES=/sbin/iptables LOCALIP=10.0.0.253 LOCALLAN=10.0.0.0/24 WEBPROXY=10.0.0.111 . /lib/lsb/init-functions do_start () { log_action_msg "Loading custom bridged iptables rules" # 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 LOG --log-prefix "Dropped by default ↪(INPUT):" $IPTABLES -A INPUT -j DROP $IPTABLES -A OUTPUT -j LOG --log-prefix "Dropped by default ↪(OUTPUT):" $IPTABLES -A OUTPUT -j DROP $IPTABLES -A FORWARD -j LOG --log-prefix "Dropped by default ↪(FORWARD):" $IPTABLES -A FORWARD -j DROP } do_unload () { $IPTABLES --flush $IPTABLES -P INPUT ACCEPT $IPTABLES -P FORWARD ACCEPT $IPTABLES -P OUTPUT ACCEPT } case "$1" in start) do_start ;; restart|reload|force-reload) echo "Reloading bridging iptables rules" do_unload do_start ;; stop) echo "DANGER: Unloading firewall's Packet Filters!" do_unload ;; *) echo "Usage: $0 start|stop|restart" >&2 exit 3 ;; esac
Space doesn't permit a detailed walk-through of this script, but the heart of Listing 2 is the “do_start” routine, which sets all three default chains (INPUT, FORWARD and OUTPUT) to a default DROP policy and loads the firewall rules. The example rule set enforces this policy:
Hosts on the local LAN may send DHCP requests through the firewall and receive their replies.
Hosts on the local LAN may connect to the firewall using Secure Shell.
Only the local Web proxy may send HTTP/HTTPS requests and receive their replies.
Hosts on the local LAN may send DNS requests through the firewall and receive their replies.
This policy assumes that the network's DHCP and DNS servers are on the other side of the firewall from the LAN clients, but that its Web proxy is on the same side of the firewall as those clients.
You may recall that with OpenWrt, the state-tracking module that allows the kernel to track tcp and even some udp applications by transaction state, rather than one packet at a time, induces a significant performance hit. Although that's almost certainly not so big an issue on a PC-based firewall that has enough RAM and a fast enough CPU, I'm going to leave it to you to figure out how to add state tracking to the script in Listing 2; it isn't difficult at all!
I have, however, added some lines at the end of the “do_start” routine to log all dropped packets. Although logging on OpenWrt is especially problematic due to the limited virtual disk capacity on the routers on which it runs, this is just too important a feature to leave out on a proper PC-based firewall. On most Linux systems, firewall events are logged to the file /var/log/messages, but if you can't find any there, they instead may be written to /var/log/kernel or some other file under /var/log.
As you may be aware, Ubuntu has adopted a new startup script system. The old one, the init system, still works, and if you prefer, you can enable the script in Listing 2 the old-school way by making it executable and creating rc.d links by running this command:
bash-$ sudo update-rc.d iptables_custom start 36 2 3 4 5 . ↪stop 98 0 1 6
However, I recommend you take the plunge into the world of the newer “upstart” system by skipping update-rc.d and instead adding the following script, iptables_custom.conf (Listing 3), to /etc/init (not /etc/init.d).
Listing 3. Upstart Configuration File for iptables_custom
# iptables_custom description "iptables_custom" author "Mick Bauer <mick@wiremonkeys.org>" start on (starting network-interface or starting network-manager or starting networking) stop on runlevel [!023456] console output pre-start exec /etc/init.d/iptables_custom start post-stop exec /etc/init.d/iptables_custom stop
Rather than requiring you to figure out which start/stop number to assign to your “rc.” links, upstart lets you just specify what needs to start beforehand (in this example: network-interface, network-manager or networking). As you can see, this iptables_custom.conf file then invokes /etc/init.d/iptables_custom, as listed in Listing 2, to do the actual work of loading or unloading rules. For that reason, /etc/init.d/iptables_custom must be executable whether you use it as an init script or an upstart job.
After saving your /etc/init/iptables_custom.conf file, you must enable it with this command:
bash-$ sudo initctl reload-configuration
Now you either can reboot or enter this command to load the firewall rules:
bash-$ sudo initctl start iptables_custom
And that, in one easy procedure, is how to create a bridging firewall using a Linux PC! I hope I've explained all of this clearly enough for you to figure out how to make it meet your specific needs. I also hope you found the previous few months' foray into OpenWrt to be worthwhile.
The Paranoid Penguin will return in a couple months, after I've had a short break. In the meantime, go forth and secure things!
Resources
Peter de Jong's iptables script for the upstart init system is available at 4pdj.nl/2010/01/11/custom-firewall-under-ubuntu-karmic-koala-with-upstart.
See also my book: Bauer, Michael D. Linux Server Security, second edition. Sebastopol, California: O'Reilly Media, 2005. Chapter 3 explains iptables in detail, and Appendix A contains two complete iptables scripts. Although focused on “local” (“personal”) firewalls rather than Internet or LAN firewalls, this material nonetheless should be helpful to iptables beginners.
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”.