Writing the ipfw Log Lines to a Different File

The system.log file is a general dumping ground for all log messages, but it has the facility to write specific messages to separate files. In this case, we're going to write the log lines from ipfw into /var/log/ipfw.log. This file already exists on my system, but at the moment, the /etc/syslogd.conf is not set up to send the messages to it. First, I must edit the /etc/syslogd.conf and add the following to the beginning of the file:

# Exclude ipfw from the main log
!-ipfw

This stops the ipfw log lines from being written to the system.log. Now add the following to the end of the file to get the ipfw log lines written to the correct file:

# Log ipfw to its own log
!ipfw
*.* /var/log/ipfw.log

Now it's time to restart syslogd so that the new configuration is loaded. There are two ways of doing this: the Windows way or the Unix way. Rebooting your computer is the Windows way, so we'll do it the Unix way.

sudo kill -s HUP `cat /var/run/syslog.pid`

The preceding line reads the pid of the existing syslogd process and gets it to reload its configuration file. All that remains is to turn logging on, sudo sysctl -w net.inet.ip.fw.verbose=1, generate some traffic, and look at the contents of the /var/log/ipfw.log file. No lines are being written to the system.log file any more. The main reason for this is that the ipfw logging generates so many lines in the system.log file that it makes finding other, non ipfw, messages much harder.

Writing Your Own Rules

Each packet that wants to pass through the firewall, in or out, is checked against the rules in the order in which they are numbered. Once a packet matches a rule, the assigned action is taken, and the next packet is processed. This is why rule 65535 is hard-coded into the firewall; it will match anything that does not match any other rule. Let's walk through the rules we created earlier.

02000 allow ip from any to any via lo*

The first rule allows any ip packet to any address via the loopback interface. The computer makes use of the loopback interface to talk to itself. Trust me when I say that this is useful.

02010 deny ip from 127.0.0.0/8 to any in
02020 deny ip from any to 127.0.0.0/8 in
02030 deny ip from 224.0.0.0/3 to any in
02040 deny tcp from any to 224.0.0.0/3 in

These rules, and others like them, are there to stop packets with spoofed IP addresses from entering. A more comprehensive set of rules would ban all private IP ranges. The anti-spoofing rules are placed here to stop any packets before they get to the rules that might let them in. If we wanted to bar known bad addresses, this would be the place to do it.

02050 allow tcp from any to any out
02060 allow tcp from any to any established

Here we allow any outbound packets through and follow this up by allowing any previously established connections back in. The firewall is "state-full"—that is to say it doesn't just process a packet and forget about it as it moves onto the next one. It remembers that it allowed a connection from my computer to my ISP's mail server and therefore can identify incoming packets as being part of the same connection and allow then back in without a whole host of new rules.

02070 allow tcp from any to any 22 in
02080 allow tcp from any to any 80 in
02090 allow tcp from any to any 427 in

This rule allows any inbound packet to the ports of the services that we are running in. This leaves just one more rule.

12190 deny tcp from any to any

This rule denies any tcp packet that has gotten this far.

Let's review. We have thrown out the spoofed addresses, we have allowed all outbound packets and all established inbound connections, and we have allowed inbound packets to the ports of the services we are running. All that is left are inbound packets to ports we have not allowed. It's a good thing we have stopped them here as rule 65535 would have let them in!

A quick overview of the rule set:

Allow loopback.

Deny spoofing and people we don't like.

Allow outbound connections.

Allow established connections.

Allow specific inbound connections.

Deny everything else.

All rules have the same basic structure, and a lot can be achieved with the simplest of rules. The basic shape of a rule, more information on which can be found with man ipfw, is quite simple:

[number] action [log] proto from src to dst [interface-spec]

If a rule is given without a number, the next number in sequence is taken. If a number is given, the rule will be positioned among the existing rules. If a rule with that number already exists, then the new rule will be added after the old one, and you will have two rules with the same number. The action can take many values, but for now we're only interested in allow and deny.

Allow will allow any matching packet to pass through the firewall, and deny will just drop them onto the floor.

The optional log parameter will write a line to the logfile for each packet that matched the rule. Lines are only written to the logfile if the system variable net.inet.ip.fw.verbose is set to 1. So we don't have to change the rules when we want to turn logging on and off.

The protocol is the type of packet. The only ones we are concerned with are tcp or ip (which is another way of saying all of them). As a rule of thumb all the protocols are layered over ip, and the most common one we encounter is tcp, with icmp and udp after that.

If you look in /etc/protocols you will see that there are a large number of protocols, most of which we will never encounter outside of this file. Most traffic is tcp, and our rules concentrate on this unless we are blocking something wholesale, in which case we use ip to mean any protocol. But a point of caution: In our "Deny everything else" rule, 12190, we only deny the tcp packets; changing this to deny ip packets may stop your computer from working as many behind-the-scene services still need to get through, such as DNS.

The src and dst are the source and destination addresses that can be defined as either:

any - any ip address

a specific ip address, 207.68.172.246, or hostname, msn.com

a netmask, 207.68.128.0/18 or 1.2.3.4:255.255.240.0

The address can be prefixed by not. Additionally, the address can be followed by a list of port numbers or service names (22 or ssh). For example, the following rules allow FTP connections:

allow tcp from any to any 20-21 in
allow tcp from any 20,21 to any 1024-65535 in

The two rules read:

Allow in any inbound tcp packets to ports 20 and 21 (ftp-data and ftp).

Allow in any inbound tcp packets coming from ports 20 or 21 that are trying to connect to ports 1024 to 65536.

The final element of the rule is the interface-spec. For simple rules, in and out are sufficient and, if omitted, the default is to allow both in and out. When added to a rule they limit the action of the rule to check only the inbound or outbound packets. The interface-spec allows much more control than we are showing here, for example, a rule about being bound to particular interfaces such as an Airport card rather then the normal RJ45 socket.

If we run ifconfig -l, we will get a list of interfaces that are available on our computer. Mine lists six, even though only three are active. The rules we are using will apply to any packet passing through any valid interface.

Our Own Startup Script

The statement "It's not possible to override the firewall built into Mac OS X" isn't entirely true. We could go in and hack around with the kext files,
but this is more work than we really need to undertake. What we can do is create
our own startup script that runs after the existing firewall to implement our rules.
We need to create a directory called /Library/StartupItems/Firewall and
include in it two files. The first is a generic startup script called Firewall.

The real work is undertaken by the /etc/rc.firewall script where the actual calls to ipfw are made. For a moment, let's just look at the Firewall script. The service will only start if the environment variable FIREWALL is set to YES. The advantage is that if FIREWALL is undefined, it will default to NO. This allows us to try out new firewall rules by running the /etc/rc.firewall script by hand. But if we have to reboot our computer, our rules will not be automatically loaded until we add the line FIREWALL=-YES- to the /etc/hostconfig file. This is a useful safety net when we are developing our own rules. Once we run our own rules, the firewall tab under services will not be usable until we run sudo ipfw flush to remove our rules.

Finally, we need to fill in /etc/rc.firewall. A copy of rules from earlier will do the job. With the changes made to the /etc/hostconfig file, our custom-made firewall rules will be loaded as part of the normal boot sequence. We now know nearly all we need know to play with our own rules.

How to Test Your Firewall

To test your rules, you need a few resources on hand. First, and most important, you should be working at your Macintosh. If you manage to create a set of rules that stops you from accessing the Internet, you will still be able to open up a terminal, take down the rules, and reestablish your connections. Failing that, you can at least get to the reboot switch. If you are tempted, as I have been, to ssh into your computer from work and make a couple safe changes, you'll discover that you've locked yourself out and will have to wait until you get home before you can fix anything.

Second, you will need access to a second computer that is outside your network. My Macintosh is plugged into a router and exposed as a DMZ host. I can plug my laptop into the same router and access the services on my Macintosh as any other computer would. This allows me to check that the services that I think I am exposing are in fact available.

This second computer also allows us to run nmap, our third resource. nmap is a port-scanning tool with a considerable pedigree. We are using it here to establish which ports are visible and accessible to the outside world. We know what services we intended to be available, but it can't do any harm to check.

The only port numbers that turned up were the ones we set up, and port 427 is closed to the outside world. Everything is looking good. Nmap is a powerful tool with many, varied options and is a useful program to master.

Why Would You Write Your Own Rules?

Given that the firewall that comes with the Macintosh does such a good job and is well integrated with the services that you can run, why would you want to write your own rules? If you have a lone Macintosh that connects to the Internet via an Ethernet cable, then you really don't need to write your own rules. However, if your Macintosh is part of a small network, you may well have services that you only want local machines to connect to. This is the situation I have where I allow remote control of my Macintosh via VNC from local machines but do not want anyone else to know that I even run the service, let alone have access to it.

Playing with a firewall can be quite scary, especially when you see what is attacking you for every moment that you are connected to the Internet. Your most prudent course of action at this point is to remove any custom rules that you have written and let the Sharing Control Panel take care of things. It was doing a perfectly good job before you knew what it was dealing with and will continue to do so going forward.

Peter Hickman
is currently working as a programmer for Semantico,
which specializes in online reference works and Access Control Systems.
When not programming or reading about
programming he can be found sleeping.