Building a Linux firewall
The growth of the Internet has prompted many organizations to become security-conscious. Documented and undocumented incidents of security violations, expanded research about security issues, and even media hype have brought about the potential for at least partial solutions for securing a networked environment—without completely isolating the network from the outside world. Leading the pack of solutions is the firewall. Just about everyone has defined what a firewall is, so I won't be any different. A firewall is a device or collection of devices that restricts the access of “outside” networks to “inside” networks. Not surprisingly, Linux can play a part in this arena.
There are currently three models used to classify firewalls. Fundamentally, the current industry classifications are application proxy gateway, circuit level relay, and packet filter.
An application proxy gateway is what most people think of when they talk about firewalls. Also known as a bastion host, it is used to completely sever the connectivity between outside and inside networks. Connections are made via proxy processes to the bastion host. The bastion host in turn will establish a connection to the real destination and handle communications between the two connections.
There are several advantages to a proxy gateway. First, because the proxies are at the application level, they can take advantage of application protocols. For example, protocols providing authentication—such as TELNET, FTP, and HTTP—can be intercepted at the proxy and stronger authentication mechanisms applied (such as S/Key) without adversely affecting the remote client. Also, protocol-specific rules can be applied by the proxy. A rule can be established that allows FTP GETs through the gateway, but not FTP PUTs. Another advantage is the extensive logging that can be provided at the application level. It is important to note that the bastion performs no IP routing functions. All communications are through proxy processes. The firewall toolkit FWTK, available as freeware from TIS, is an example of a firewall application level gateway.
A circuit-level relay functions in a manner similar to an application proxy gateway, except the proxies employed for a circuit relay are normally not application-aware. Because of this, you lose many of the detailed logging capabilities and precise rule definitions you have in an application proxy gateway. The important concept remains the same in that a connection is established via proxies and IP packets are not forwarded through the firewall. SOCKS is an implementation of a circuit level relay based firewall.
Packet filtering is the most common type of firewall available. It works on the concept of forwarding packets based on rules. Those rules typically take into consideration source and destination IP address, source and destination port numbers, the protocol being transported, TCP flags, IP flags or options, and other information, such as the interface, over which the packet arrived. The primary difference between a packet filtering firewall and the others is IP forwarding. A packet filtering firewall is usually a router, and its function in life is to forward packets. This means that while you can control what machines on the outside can talk to certain machines on the inside (and which applications), you now rely on the application to protect itself from harm. For some applications, this isn't a particularly wise decision. Nonetheless, packet filtering can be very useful, is widely available and typically inexpensive.
A Linux machine can function as any one or as all three (i.e., as a hybrid) of these firewall types) of the firewall types. Without add-ons however, the Linux kernel has the ability to function as a packet filter routing device, using the ipfirewall code written by Daniel Boulet and Ugen J.S. Antsilevich. For most 1.2.x and 1.3.x kernels, the firewall code (ip_fw.c) is based on the port by Alan Cox and Jos Vos. Boulet has released version 2.0e (as of this writing) of his ipfirewall code as shareware. I have yet to install the new release, so any discussion I have is based on the ip_fw.c code—specifically kernel 1.2.13.
In order to use this built-in firewall capability, you need to understand a bit about how TCP/IP works. Trying to set up a firewall from scratch without understanding networking is a sure route to disaster. If you want a “plug and play” firewall solution for Linux, one is mentioned at the end of this article. To learn more about TCP and IP, recommended reading is TCP/IP Illustrated, Volume 1 by W. Richard Stevens. Also, the 3rd edition of Douglas Comer's Internetworking with TCP/IP is excellent bedtime reading.
The firewall code includes three facilities—accounting, blocking, and forwarding. Accounting rules are used for maintaining packet and byte count statistics for selected IP traffic. Blocking rules specify rules for accepting and rejecting packets to and from the firewall itself. Forwarding rules specify which packets will be forwarded by the firewall; this implies a source and destination address of something other than the firewall. You can specify any type of rule based on source and/or destination IP addresses; TCP or UDP ports; protocols such as TCP, UDP, or ICMP; or by combinations of the three.
The services are activated when the kernel boots, and the rules are set and modified with the setsockopt(2) system call. The current accounting statistics and firewall rules can be viewed by looking at the files ip_acct, ip_block, and ip_forward in the /proc/net directory. The contents of ip_acct look like this:
# cat /proc/net/ip_acct
IP accounting rules
C0A80101/FFFFFFFF->00000000/00000000 00000000
0 0 0 386 392946 0 0 0 0 0 0 0 0 0 0
In this example, one rule is present, which basically says to keep statistics on connections from 192.168.1.1 to anywhere for all ports and all protocols.
Changing the accounting and firewall facilities has to be done via a C program, Perl script, or some other language that supports the setsockopt(2) system call. Here is a sample program that will change the default policy for the forwarding rules:
# cat set_policy.c #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/udp.h> #include <linux/ip_fw.h> main(int argc, char **argv) { int p, sfd; struct ip_fw fw; fw.fw_flg = 0; if (strcmp(argv[1], "accept") == 0) { p = IP_FW_F_ACCEPT; } else if (strcmp(argv[1], "reject") == 0) { p = IP_FW_F_ICMPRPL; } else if (strcmp(argv[1], "deny") == 0) { p = 0; } sfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); setsockopt(sfd, IPPROTO_IP, IP_FW_POLICY_FWD, &p, sizeof(int)); } # cat /proc/net/ip_forward IP firewall forward rules, default 4 # set_policy deny # cat /proc/net/ip_forward IP firewall forward rules, default 0
As you can see, it is just a matter of opening up a raw IP socket and using setsockopt() to change the environment. The setsockopt call is setting the default policy for the forwarding rules (IP_FW_POLICY_FWD). The value for the forward policy command is kept in p and determined by the command-line arguments accept, reject, and deny. The words are equated to the defined values of IP_FW_F_ACCEPT, IP_FW_F_ICMPRPL, and 0. The difference between deny and reject is that the deny policy will just throw packets away, while the reject policy will throw packets away, and also respond with an “ICMP port unreachable” message to the originating host.
Writing the C or Perl code to manipulate firewall rules may sound like a lot of fun, but some administrators may not have the time to roll their own firewall interface. ipfw is a utility in the net-tools (version 1.3.6) package that will allow the root user to add, delete, or list information dealing with the accounting and firewall rules. Figure 1 shows the output of the blocking rules in the current host.
Obviously, this is a lot better than looking at the direct contents of the /proc/net/ip_block file. The -n option just tells ipfw not to resolve addresses to names.
Adding a rule is done quite easily. Let's say we wanted to accept SNMP queries from a remote management station (note that the author does not really advocate this kind of behavior—this is just an example). We could add the rule:
# ipfw add b accept udp from 0.0.0.0/0 \
to 20.2.51.105 161
to give us the new list shown in Figure 2.
Before we move on, let me explain some of the notation in the ipfw command given above. The arguments tell us we're adding a blocking (b) rule for the UDP protocol. The rule is accepting UDP datagrams from any host (0.0.0.0/0) to 20.2.51.105, but only those destined for port 161. 20.2.51.105 is the address for one of the interfaces on the firewall.
Assume the above rule for SNMP is not what we wanted. Let's say we only wanted to allow SNMP queries from one particular network—for example 20.2.61.0 (subnetted of course). We can delete the rule we just added and then put in our new rule.
# ipfw del b accept udp from 0.0.0.0/0 \
to 20.2.51.105 161
# ipfw add b accept udp from 20.2.61.0/24 \
to 20.2.51.105 161
Figure 3 shows our new rule set. The syntax 20.2.61.0/24 allows you to specify a netmask with the address. The /24 says there is a 24 bit netmask or 255.255.255.0.
The more astute reader at this point may be asking, “What the heck do those rules mean anyway?” I'll get to that, but first, I want to talk about a utility which I find preferable to ipfw.
The ipfwadm (version 1.2) program by Jos Vos (available from www.xos.nl/linux/ipfwadm) is an administrative utility for IP firewalling and accounting which is similar to ipfw. It offers, I think, a slightly more intuitive interface, better output, and a better man page (not everyone reads the source code for documentation).
To list the rules shown in Figure 3, issue this command:
# ipfwadm -B -l -n
IP firewall block rules, default policy: accept
typ prot source destination ports
den tcp 0.0.0.0/0 20.2.51.105 * -> *
den tcp 0.0.0.0/0 192.168.1.1 * -> *
acc udp 20.2.61.0/24 20.2.51.105 * -> 161
rej udp 0.0.0.0/0 192.168.1.1 * -> *
acc udp 0.0.0.0/0 20.2.51.105 53 -> *
rej udp 0.0.0.0/0 20.2.51.105 * -> *
Notice a couple of things. First, ipfwadm always shows the default policy; I like to see this. Second, the type fields for these rules:
rej udp 0.0.0.0/0 192.168.1.1 * -> * rej udp 0.0.0.0/0 20.2.51.105 * -> *
are set for reject, rather than deny, as shown in ipfw's output in Figure 3. Well, they really are set to reject. ipfw only supports the deny and accept policies, not reject.
Given our vast knowledge on setting and listing rules, let's rebuild the table again (except for the SNMP rule) while adding a few more details. But first, we'll define what our network looks like, shown in Figure 4.
We'll call the 20.2.51.0 network the “outside” network and the 192.168.1.0 the “inside” network. Since blocking rules apply to the firewall itself, the rules will be set on deathstar. First, we'll flush any rules we have and set the default policy to accept:
# ipfwadm -B -f
# ipfwadm -B -p accept
Now we'll define what we need to block. The protocols you can choose to block are TCP, UDP, and ICMP. We want to allow ICMP messages to the firewall, so we can't just block everything. We could block TCP by adding the rule:
# ipfwadm -B -a deny -P tcp -S 0.0.0.0/0 \
-D 20.2.51.105
but that would be not be adequate. That rule would end up blocking all traffic from the firewall, as well as traffic to the firewall. Therefore, an administrator could not telnet or ftp from the machine. That may be desirable, but let's assume we'll let out TCP traffic originating from inside. What we would like is to block all connection attempts to the firewall while letting connections go out. We can modify the rule with the -y option. This will do what we want by blocking any TCP segments from any host to the firewall that have the SYN bit set.
# ipfwadm -B -y -a deny -P tcp -S 0.0.0.0/0 \
-D 20.2.51.105
Remembering that the firewall has two interfaces, we block the second interface also:
# ipfwadm -B -y -a deny -P tcp -S 0.0.0.0/0 \
-D 192.168.1.1
That is too restrictive, in that connections from the inside network to the firewall will be blocked also. We can refine the rule to block all TCP connection requests only if they come in over the outside interface (20.2.51.105).
# ipfwadm -B -y -a deny -P tcp -S 0.0.0.0/0 \ -D 20.2.51.105 -I 20.2.51.105 # ipfwadm -B -y -a deny -P tcp -S 0.0.0.0/0 \ -D 192.168.1.1 -I 20.2.51.105
There, that wasn't too bad.
Now that we've taken care of TCP traffic, let's write some rules for UDP. Since UDP has many problems that we won't discuss in detail here, we'll just block it all. The rules will be the same for TCP, except since there's no SYN bit, there's no need for -y, and so we'll reject the packet instead of denying it. The reason we reject it is that ICMP port unreachable messages make sense to UDP based applications, but are ignored by TCP applications. It would be nice if the ip_fw code sent TCP Resets if the rule was for TCP and marked for rejection, but it doesn't. So our rules for UDP will be:
# ipfwadm -B -a reject -P udp -S 0.0.0.0/0 \ -D 192.168.1.1 -I 20.2.51.105 # ipfwadm -B -a reject -P udp -S 0.0.0.0/0 \ -D 20.2.51.105 -I 20.2.51.105
Setting up those rules, we find that blocking all UDP traffic isn't such a good idea after all. If we have telnet, then we will probably want to be able to resolve hostnames. So we open up DNS. Before we do that, though, let's look at the traffic pattern for a DNS query so that we can gauge what we'll need to write. Figure 5 contains tcpdump output of a DNS query from deathstar to mccoy, an “outside” DNS server.
We can see we will have to have two rules—one for the query going out (sending to port 53 on the remote machine) and one for the response (sending to port 53 on the firewall from the remote machine). The rule can be written as:
# ipfwadm -B -a accept -P udp -S 20.2.51.105 \ -D 0.0.0.0/0 53 # ipfwadm -B -a accept -P udp -S 0.0.0.0/0 53 \ -D 20.2.51.105
Since this two-way traffic is common among many protocols there's an option you can set to condense the two rules into one. The -b option sets bi-directional mode, which installs a rule that matches traffic in both directions. We can then get away with
# ipfwadm -B -a accept -b -P udp -S 0.0.0.0/0 53 \ -D 20.2.51.105
Now that we've recreated our table, we can get a listing of the rules with extended (-e) output. This is shown in Figure 6. Notice the extended output contains packet and byte counts for the blocking rules. The firewall code automatically maintains accounting information for the forwarding and blocking rules.
What we have accomplished so far is protecting the machine deathstar to suit our environment. To protect the internal network we will need to develop the proper forwarding rules. Before I just start typing in rulesets, I like to build a table of what's allowed and disallowed. Figure 7 shows the table I put together to establish the blocking rules. Note that the asterisk in the rules table indicates any host address or any port number.
We can build the same type of table for our forwarding ruleset. Building our table should be simple if we have a security policy in place. Let's assume that the part of our security policy that discusses firewalls will allow TELNET and FTP out, and e-mail (SMTP) in both directions. Further, e-mail is only allowed to go to the mailhub (since there's only one machine on the internal network, it will be the mailhub—humor me). Figure 8 shows the generic rules table for the forwarding ruleset.
The stance of the firewall is one of “deny everything”. This is a very common policy for firewalls because the only packets that will be forwarded are the ones which are explicitly allowed.
The rules are fairly straightforward except for the source and destination ports numbered 1024 or greater. The reasons for this are basically historical. Most Unix client programs, such as TELNET, assign their ephemeral port range between 1024 and 5000. Ports 1 through 1023 are known as “reserved ports”. These are for server applications such as telnetd, ftpd, etc. Almost all TCP/IP stacks follow this convention and because of it we can take advantage of it in our ruleset—to help guarantee client-only communications coming into the network. The reason I don't use the range 1024-5000 is because not all devices adhere to this Unix tradition. For example, annex terminal servers start their ephemeral port range at 10000.
Here are the commands to establish our forwarding rules:
# ipfwadm -F -f # ipfwadm -F -p deny # ipfwadm -F -a accept -b -P tcp -S 0.0.0.0/0 23 \ -D 192.168.1.0/24 1024:65535 # ipfwadm -F -a accept -b -P tcp -S 0.0.0.0/0 21 \ -D 192.168.1.0/24 1024:65535 # ipfwadm -F -a accept -b -P tcp -S >0.0.0.0/0 20 \ -D 192.168.1.0/24 1024:65535 # ipfwadm -F -a accept -b -P tcp -S >0.0.0.0/0 25 \ -D 192.168.1.2 1024:65535 # ipfwadm -F -a accept -b -P tcp -S >192.168.1.2 25 \ -D 0.0.0.0/0 1024:65535
To set up deathstar as the firewall machine, the ipfwadm commands would be put into a file and executed as a shell script. Deathstar uses the file /usr/local/etc/set-rules.sh. To bring the machine up properly, it would be wise to establish the rules within the kernel before the network interfaces are brought up. The /etc/rc.d/rc.inet1 on deathstar contains the lines:
# set firewall rules /bin/bash /usr/local/etc/set-rules.sh # bring up ethernet ifconfig eth0 192.168.1.1 192.168.1.255 up # bring up ppp link /usr/lib/ppp/ppp-on
Deathstar is, in reality, my desktop machine. I've loaded just about everything on it, so it doesn't make a very good firewall. A firewall, as we have described it, should run the bare minimum of software in order to function. Normally this means that compilers, X, games, or anything that doesn't have to do with the kernel or communications are taken off of the system.
Even with generic tables to work with, you may not always get the rules the way you want them. It's nice to be able to check your work. The ipfwadm utility offers the -c option to check packets against your rules. For example, to check if a packet from some host can send mail to an internal host other than warbird, we can run:
# ipfwadm -F -c -P tcp -S >195.1.1.1 1024 \ -D 192.168.1.5 25 -I 20.2.51.105
This would yield the response packet denied. When using -c to check a rule, you need to be very specific and supply a source address and port, destination address and port, and an interface address.
The other way to test your environment is with live traffic. If you suspect traffic is not being forwarded because of your ruleset, you can use tcpdump to monitor the traffic coming into and going out of the firewall. It becomes fairly obvious if the firewall is not allowing legitimate traffic to go through. For example, when I set up the rules to allow mail through, I noticed it took an exceptionally long time to send a message. tcpdump revealed that the receiver, mccoy in this case, was sending IDENT messages back to the source but they were being blocked by the firewall. By adding a rule to allow IDENT messages, mail went much faster. Creating this rule is left as an exercise for the reader.
For rudimentary logging, a rule may be set with the -k option, which will cause the kernel to print out a message via syslog for all matching packets. However, setting up the kernel to understand the -k option is not straightforward. The kernel needs to be compiled with CONFIG_IP_FIREWALL_VERBOSE defined. To do this, just add the definition to the Makefile in the net/inet directory in the kernel source directory. Unfortunately, the code defined in the CONFIG_IP_FIREWALL_VERBOSE section of ip_fw.c does not compile cleanly in 1.2.x distributions. The fix is simple and implemented in the latest 1.3.x versions of the kernel.
If you set up the kernel to support the -k option you will receive output in the /var/adm/messages file similar to that shown in Figure 9.
The firewall we just built can be replaced by almost any router you can purchase from a vendor. However, turning a Linux machine into a packet filtering router is a cheap and very effective alternative.
There are several limitations to the firewall code. Its inadequate logging capabilities are a big miss; documentation is lacking; and the inability to filter on IP options do not allow the filtering router to be as flexible as it might be.
For many environments, the firewall facilities of the Linux kernel can be more than sufficient, but for those who need commercial-grade firewall software for Linux, or software that can run under Linux, there are solutions. The shareware ipfirewall code from Daniel Boulet, mentioned earlier in this article, addresses several of the problems just stated. Also, the commercial Mazama Packet Filter from Mazama Software Labs is a real “bells and whistles” product. It comes complete with nice documentation, a filter “language” for defining the rulesets (this is a winner), a GUI for very simple administration, and fixes for the technical problems (such as IP options and TCP SYN/ACK filtering).
One last concept not mentioned in this article is that of IP Masquerading. Very perceptive readers will notice the network warbird is on (192.168.1.0) is a private IP address. That is, it not one assigned by the InterNIC, but can be used for local or private IP-based networks not connected to the Internet. I can get away with using this addressing scheme because the machine called relay is a commercial firewall that performs masquerading (otherwise known as address hiding). All connections going out of the 20.2.51 or 192.168.1 networks have a source address of relay from the perspective of the remote machine. As you might be able to guess from its name, relay is also an application proxy gateway. Linux also has the ability to hide addresses, but that is a topic for another article.
Chris Kostick (ckostick@csc.com) is a Senior Computer Scientist at Computer Sciences Corporation's Network Security Department. He enjoys working with Linux but considers himself a latecomer because he started out at kernel version 1.1.18. As far as computers go, he's not sure if he has more fun debugging TCP/IP problems or playing Doom.