Build a transparent firewall using an ordinary PC.
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.
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.
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.)
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).
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!