Here's my ipfirewall facility. I consider it to still be beta quality mostly because the various interfaces are pretty crude. Here's some information that you'll probably find useful (in roughly the order in which you'll need to know it). Some of this will be absurdly simplistic. Better safe than sorry... This software was written for BSD/386. The current version has been ported to BSD/386 v1.1. The context diffs are with respect to that version of BSD/386. If you don't have access to BSD/386 v1.1 and can't make sense out of the diffs, contact me and I'll send you the entire files (they are copyrighted by UC-Berkeley with very 'friendly' conditions). Speaking of copyrights, here's mine: /* * Copyright (c) 1993 Daniel Boulet * * Redistribution and use in source forms, with and without modification, * are permitted provided that this entire comment appears intact. * * Redistribution in binary form may occur without any restrictions. * Obviously, it would be nice if you gave credit where credit is due * but requiring it would be too onerous. * * This software is provided ``AS IS'' without any warranties of any kind. */ Enough introductory stuff, here we go... 1) The file IPFIREWALL1 is the standard GENERIC kernel configuration file with the IPFIREWALL and GATEWAY options enabled. You may not find it useful. About the only key things are that it enables the IPFIREWALL option and the GATEWAY option. IPFIREWALL turns on my stuff and GATEWAY turns your machine into an IP router. I've never tested the IPFIREWALL1 file. Just to be safe, I've also included my config file (IPFIREWALL2). There is nothing magical about the name of this file or the ident name for the kernel. 2) The files ip_firewall.c and ip_firewall.h are new files that should be placed into the /usr/src/sys/netinet directory. 3) The files ip_input.c-diffs, in.h-diffs and raw_ip.c-diffs are context diffs which should be applied to the files by the 'same' names in the /usr/src/sys/netinet directory. 4) Add the line "netinet/ip_firewall.c optional ipfirewall" to the file /usr/src/sys/conf/files. This tells the config program to include the netinet/ip_firewall.c file if the IPFIREWALL option is defined for a kernel. 5) The Makefile and ipfirewall.c files should go into directory probably back in your home directory tree somewhere. If this ever becomes a part of the system then they should go into the (newly created) directory /usr/src/sbin/ipfirewall. Note that you won't be able to compile the ipfirewall program unless you arrange to make the ip_firewall.h and the new in.h header files accessible (either by editing the Makefile or putting the header files into "/usr/include/netinet"). 6) Build yourself a kernel, make a backup of the current kernel and and install the new one. It should behave in a completely normal fashion since you won't have defined any firewalls yet. 7) Explore the ipfirewall program. The smartest way to do this is to compile the program and then run it from your regular user id. It will detect that it isn't being run by root and will run in a debug mode in which it describes the setsockopt call that it would have made with the specified parameters if it had been run as root. The ipfirewall program takes command line parameters and (assuming they are valid) issues a single appropriate setsockopt call. If you're defining 5 firewalls then you'll have to run the program 5 times. See below for a description of the command syntax of ipfirewall. 8) Once you're pretty sure that you know what you're doing, switch to the root user id and define some real firewalls. The ipfirewall program can be used to put your machine into a very disfunctional state. Make sure that you never setup the program as setuid root. Instead, always run it from the root command line or from "/etc/rc.local" as part of the boot process. 9) Use the "ipfirewall" checkb or checkf command (see below) to pass some test packets through the firewalls that you've defined. 10) You may find it useful to create a file in which the first line is "ipfirewall flush" to flush any existing firewalls and the remaining lines are the ipfirewall commands needed to define the firewalls that you want to use. This will ensure that you're always working from a known state. 11) If you've gotten this far then you're probably ready to let the critter see prime time. Copy your file of ipfirewall commands into the /etc/rc.local file and reboot the system. Once you're up, use the ipfirewall list command to see that you've got the firewalls that you wanted and try to test the firewall with real packets from trusted and untrusted hosts. Enough of that. Here's the syntax for the ipfirewall command. It is rather complex and yet simple at the same time (if you know what I mean). There are six sub-commands. Probably the easiest way to get into this is to give you a roughly BNF style grammar for the command (curly brackets are used for precedence, alternatives are separated by |, optional things are enclosed in square brackets, white space is required if it appears below and must not appear if there isn't any between the tokens below (i.e. no white space around periods, colons or slashes, whitespace required between all other tokens)): command ::= ipfirewall | | | ::= list ::= flush ::= { checkb[locking] | checkf[orwarding] } ::= { addb[locking] | addf[orwarding] } ::= from to ::= tcp | udp ::= ... | ::= a host name from /etc/hosts ::= | ::= a service from /etc/services ::= a non-negative integer ::= { accept | deny } { | } ::= all from to ::= { / } | { : } | ::= integer in the range 0 to 32 inclusive ::= from to ::= ::= [ : ] ::= [ ] Although I think that the above grammar is complete, it isn't exactly what one would call easy to comprehend! Here's the basic idea along with what each of the forms mean: The "ipfirewall list" command prints a list of the firewalls on both the forwarding and blocking chain onto the console (using printf's in the kernel - sigh). The "ipfirewall flush" command empties the two firewall chains. This is the only way (so far) to remove a firewall once it has been defined! There is no way (other than flushing and starting over) to change a firewall. The "ipfirewall addblocking" and "ipfirewall addforwarding" commands take a firewall description and add the firewall to the appropriate firewall chain. There are two basic kinds of firewall descriptions. Universal firewall descriptions match all IP packets between specified pairs of hosts. Universal firewalls only check IP addresses (i.e. they match any combination of protocol and port numbers). Protocol-specific firewalls match either TCP/IP or UDP/IP packets between specified pairs of hosts. In addition to host descriptions, protocol-specific firewalls optionally take a description of which port numbers to match. A host description consists of an IP address and a mask. The IP address is specified as either a name from /etc/hosts or in the familiar nn.nn.nn.nn format. The mask indicates how much of the IP address should be looked at when vetting packets. There are two ways to specify the mask. The first way is to suffix the IP address in the firewall with a slash and an integer in the range 0 through 32 inclusive. This integer is taken to be the number of high order bits of the IP address which are to be checked (for example, 192.153.211.0/24 checks the top 24 bits of the IP address, 192.153.211.17/32 checks all the bits and 0.0.0.0/0 checks none of the bits (i.e. all IP addresses are matched by this example)). The second way to specify a mask is to suffix the IP address with a colon followed by another IP address. This second address is the mask. Specifications equivalent to the above three examples using this syntax would be 192.153.211.0:255.255.255.0 192.153.211.17:255.255.255.255 0.0.0.0:0.0.0.0 The first form is taken from the syntax accepted by a Telebit NetBlazer. The second form is more along the lines of how a netmask is specified in /etc/netmasks. Finally, if no mask is specified then a mask of all 1's is supplied (i.e. no mask is equivalent to /32 or :255.255.255.255). The optional description of port numbers to mask can take three forms. The simplest form is to omit the list in which case all port numbers match. The next form is to specify a list of port numbers (either as positive integers or service names from /etc/services). The final form is actually a special case of the second form in which the first pair of port numbers is separated by a colon instead of white space. This pair specifies a range of port numbers (i.e. x:y specifies that all ports between x and y inclusive should match). A port description matches a particular port number if any of the following is true: - the port description is null - the first pair of port numbers is a range and the port number is in the range (inclusive) - the port number is equal to any of the port numbers in the list There is a limit of a total of 10 port numbers in the source and destination port lists. This limit is arbitrary and easy to increase. It is determined by the value of the IP_FIREWALL_MAX_PORTS #define variable in ip_firewall.h. Each increase of 1 for this value adds two bytes to the size of each firewall. Since the size of a firewall is only slightly over 30 bytes right now, this limit of 10 could probably be increased by quite a bit before it became a concern. I've been thinking of increasing it to 20 which would be longer than any reasonable firewall would need and would only consume 20 more bytes per firewall. The counter argument to any increase is that it is always possible to construct an equivalent set of two or more firewalls that behaves like a single firewall with a really long port list. This probably all sounds hopelessly complicated. It is actually not all that tricky (I'm just not very good at explaining it yet). A few examples will probably help a lot now: Block all IP packets originating from the host badnews: ipfirewall addb deny all from badnews to 0.0.0.0/0 Block all telnet packets to our telnet server from anywhere: ipfirewall addb deny tcp from 0.0.0.0/0 to mymachine/32 telnet Don't forward telnet, rlogin and rsh packets onto our local class C network: ipfirewall addf deny tcp from 0.0.0.0/0 to ournetwork/24 telnet login shell Don't let anyone on the local machine or any machine inside our local network ftp access to games.com: ipfirewall addb deny tcp from games.com ftp to 0.0.0.0/0 This last one might look a little strange. It doesn't prevent anyone from sending packets to the games.com ftp server. What it does do is block any packets that the games.com ftp server sends back! The "ipfirewall checkblocking" and "ipfirewall checkforwarding" commands take a description of an IP packet and check to see if the blocking or forwarding chain of firewalls respectively accept or reject the packet. It is used to make sure that the firewalls that you've defined work as expected. The basic syntax is probably best understood by looking at a couple of examples: ipfirewall checkb from bsdi.com 3001 to mymachine telnet checks to see if the blocking firewall will block a telnet packet from a telnet session originating on bsdi.com to the host mymachine will be blocked or not. Note that someone connecting to our telnet server could be using practically any port number. To be really sure, the firewall used to prevent access should be as simple as possible and/or you should try a variety of port numbers in addition to the rather arbitrarily chosen port of 3001. One final note on the check* and add* command syntax. The noise word "to" exists in the syntax so that I can detect the end of a list of port numbers in the from description. Since I needed a noise word to detect this case, I added the noise word "from" in front of the from case for consistency. Finally, have a look at the file "filters". It is the set of filters that I run at home. Now for a bit of a description of how the firewalls are applied (i.e. what happens in the kernel): When an IP packet is received, the ipintr() routine in ip_input.c is called. This routine does a bit of basic error checking. If it detects any errors in the packet it generally drops the packet on the floor. The idea behind the ipfirewall facility is to treat packets that we don't want to accept as bad packets (i.e. drop them on the floor). The ipfirewall facility intercedes in the normal processing at two points. Just after the basic sanity checks are done, we pass any packets not targeted at the loopback network (127.0.0.0/8) to the firewall checker along with the chain of blocking firewalls. If the firewall checker tells us to block the packet then we branch to the "bad:" label in ipintr() which is where all bad packets are dropped on the floor. Otherwise, we allow normal processing of the packet to continue. The exact point at which we intercede was chosen to be after the basic sanity checking and before the option processing is done. We want to be after the basic sanity checking so that we don't have to be able to handle complete garbage. We want to be before the option processing because option processing is done in separate rather complex routine. Why bother doing this special processing if we might be dropping the packet? The second point at which we intercede is when a packet is about to be forwarded to another host. All such packets are passed to the ip_forward routine. The ipfirewall code is at the very top of this routine. If the packet isn't targetted at the loopback interface (is it possible that it could be when we reach this point? I doubt it but safety first) then pass the packet to the firewall checker along with the forwarding firewall chain. If the firewall checker indicates that the packet should not be forwarded then we drop in (using code copied from a few lines further into the routine which drops broadcast packets which are not to be forwarded). There are a couple of consequences of this approach: 1) Packets which are blocked are never forwarded (something to keep in mind when designing firewalls). 2) Packets targeted at the loopback interface (127.0.0.0/8) are never blocked. Blocking packets to the loopback interface seems pointless and potentially quite confusing. It also makes a possibly common case very cheap. 3) The sender of a packet which is blocked receives no indication that the packet was dropped. The Telebit NetBlazer can be configured to silently drop a blocked packet or to send back a "you can't get there from here" packet to the sender. Implementing the later would have been more work (possibly quite a bit more, I don't really know). Also, I don't see any reason to give a potential hacker any more information than necessary. Dropping the packet into the bit bucket seems like the best way to keep a hacker guessing. Now for some details on how the firewall checker works: The firewall checker takes two parameters. The first parameter is a pointer to the packet in question. The second parameter is a pointer to the appropriate firewall chain. At the present time, the firewall checker passes these parameters to a second routine which is the real firewall checker. If the real checker says NO then an appropriate message is printed onto the console. This is useful for debugging purposes. Whether or not it remains in the long term depends on whether it is considered useful for logging purposes (I'm a little reluctant to leave it in since it provides a hacker with a way to commit a "denial of service" offense against you by filling up your /var/log/messages file's file system with error messages. There are ways of preventing this but ...). A return value of 0 from this routine (or the real firewall checker) indicates that the packet is to be dropped. A value of 1 indicates that the packet is to be accepted. In the early testing stages you might want to make the top level firewall checker always return 1 even if the real checker returns 0 just in case the real firewall checker screws up (or your firewalls aren't as well designed as they should be). In fact, this might be a useful optional feature (providing a way to leave a door unlocked doesn't seem all that wise but it has to be balanced against the inconvenience to legitimate users who might get screwed up by poorly designed firewalls). The real firewall returns 1 (accept the packet) if the chain is empty. If efficiency is a concern (which it is in this code), this check should be done in ip_input.c before calling the firewall checker. Assuming that there is a firewall chain to scan through, the real firewall checker picks up the src and dst IP addresses from the IP packet. It then goes through the firewall chain looking for the first firewall that matches the packet. Once a matching firewall has been found, a value of 1 is returned if the firewall is an accept firewall and a value of 0 is returned otherwise. The following processing is done for each firewall on the chain: 1) check the src and dst IP addresses. If they don't match then there isn't any point in looking any further at this firewall. This check is done by anding the packet's IP addresses the with appropriate masks and comparing the results to the appropriate addresses in the firewall. Note that the mask is NOT applied to the address in the firewall. If it has any 1 bits that are 0 bits in the mask then the firewall will never match (this will be checked in ipfirewall soon). If the addresses match then we continue with the next step. 2) If the firewall is a universal firewall then we've got a match. Return either 0 or 1 as appropriate. Otherwise, continue with the next step. 3) Examine the IP protocol from the packet. If we havn't had to look at it before then we get it and set a local variable to IP_FIREWALL_TCP for TCP/IP packets, IP_FIREWALL_UDP for UDP/IP packets, IP_FIREWALL_ICMP for ICMP packets, and IP_FIREWALL_UNIVERSAL for all other packet types. Also, if the packet is a TCP/IP or a UDP/IP packet, save the source and destination port numbers at this point (taking advantage of the fact that the port numbers are stored in the same place in either a TCP/IP or a UDP/IP packet header). If the packet is neither a TCP/IP or a UDP/IP packet then this firewall won't match it (on to the next firewall). If this packet's protocol doesn't match this firewall's protocol (which can't be universal or we wouldn't be here) then on to the next firewall. Otherwise, continue with the next step. 4) We're checking either a TCP/IP or a UDP/IP packet. If the firewall's source port list is empty or the packet's source port matches something in the source port list AND if the firewall's destination port list is empty or the packet's destination port matches something in the destination port list then we've got a match (return 0 or 1 as appropriate). Otherwise, on to the next firewall. As indicated above, if no packet on the chain matches the packet then it is accepted if the first firewall was a deny firewall and it is rejected if the first firewall was an accept packet. This is equivalent to the default behaviour of a Telebit NetBlazer. They provide a way to override this behaviour. I'm not convinced that it is necessary (I'm open to suggestions). That's about it for the firewall checker. The ipfirewall program communicates with the kernel part of the firewall facility by making setsockopt calls on RAW IP sockets. Only root is allowed to open a RAW IP socket. This ensures that only root uses ipfirewall to manipulate the firewall facility. Also, somewhere in the kernel source or on a man page, I read that the RAW IP setsockopt calls are intended for manipulating the IP protocol layer as opposed to manipulating any particular instance of a socket. This seems like a reasonable description of what the firewall setsockopt command codes do. There are six setsockopt command codes defined by the firewall facility (in netinet/in.h). They are: IP_PRINT_FIREWALLS print both firewall chains onto the console. IP_FLUSH_FIREWALLS flush (i.e. free) both firewall chains. IP_ADD_FORWARDING_FIREWALL add firewall pointed at by optval parm to the end of the forwarding firewall chain. IP_ADD_BLOCKING_FIREWALL add firewall pointed at by optval parm to the end of the blocking firewall chain. IP_CHECK_FORWARDING_FIREWALL pass the IP packet do the firewall checker along with the forwarding firewall chain. Return 0 if packet was accepted, -1 (with errno set to EACCES) if it wasn't. IP_CHECK_BLOCKING_FIREWALL pass the IP packet do the firewall checker along with the blocking firewall chain. Return 0 if packet was accepted, -1 (with errno set to EACCES) if it wasn't. The IP_ADD_*_FIREWALL command codes do a fair bit of validity checking. It is quite unlikely that a garbage firewall could get past them that would cause major problems in the firewall checker. It IS possible for a garbage packet to get past the checks which causes major grief because it either blocks or accepts packets according to unusual rules (the rules will conform to the ones described above but will probably come as quite a surprise). The IP_CHECK_*_FIREWALL command codes expect the optval parameter to point to a struct ip immediately followed by a header appropriate to the protocol value described in the ip_p field of the ip header. The exact requirements are as follows: - The length of the optval parameter must be at least sizeof(struct ip) + 2 * sizeof(u_short) since this is the amount of memory that might be referenced by the firewall checker. - The ip_hl field of the ip structure must be equal to sizeof(struct ip) / sizeof(int) since this value indicates that the tcp/udp/??? header immediately follows the ip header (appropriate for the purposes that this interface is intended for). Failure to follow these rules (for either the IP_ADD_*_FIREWALL or the IP_CHECK_*_FIREWALL commands) will result in a return value of -1 with errno set to EINVAL (for now, it will also result in an appropriate message on the console). That's about all that I can think of for now. There are a couple of details that are worth reading about in the ip_firewall.h file. Other than that, let me know how you do. If you have any problems, give me a call at home (403 449-1835) or send me e-mail at "danny@BouletFermat.ab.ca". If you call, please keep in mind that I live in the Canadian Mountain timezone (GMT-0600). -Danny