The firewall in Mac OS X 10.5 Leopard is confusing, to say the least. It is not enabled by default, which is a huge mistake, in my humble opinion. Also, the graphical user interface offers less flexibility than in previous version while trying to configure it. Besides allowing you to independently control the blocking of incoming ICMP Echo Requests and logging via syslog, it has the following main operation modes:

  • Allow all incoming connections.

    Seems obvious that choosing this setting means that the firewall is essentially disabled and any traffic can freely flow in and out of the computer.

    I would not recommend this setting, even for trusted networks. It is easy for a laptop computer to attach to a different network at any time and have the user forget to re-enable the firewall, posing as a potential victim for other users or computers. For a desktop computer it is more and more common to be attached to a network that has some sort of wireless bridging or routing device that might be used by untrusted users to get access to the network, or by trusted users that carry (maybe without their knowledge) malware, for example.

    This setting offers little or no security, but makes very easy to share contents or provide external services.

  • Block al incoming connections.

    This seems the safest choice, but might not always be desirable if you are sharing content or have services running that you want to make accessible from the outside (like an SSH server, or even the iTunes music collection).

    This is my preferred setting, I have to say. This setting offers adequate security, but stops you from sharing content or offering access to services externally. However, it seems that Leopard’s firewall does not block all services, and some of them are still accessible from the outside (see at the end of the post for more information).

  • Set access for specific services and applications.

    This is by far the most confusing setting. First, because it doesn’t seem to add any rules to IPFW. I’m starting to believe that Leopard offers two layers of filtering. It seems IPFW is one of them, but it defaults to use a ruleset with only default accept entry in it (sequence 65535):

    # ipfw list
    65535 allow ip from any to any
    

    (NOTE: if you enable the Stealth functionality, a IPFW rule is added before the default entry, and it looks like this:

    33300 deny icmp from any to me in icmptypes 8
    

    This blocks ICMP Echo Requests, but be aware that Stealth mode is not enabled by default).

    About the other filtering layer, I’m not sure what it is used or how it is implemented, but it seems to be the result of some sort of kernel-level and user-space cooperation. The second reason why I think this setting can be confusing is because it seems to only affect a particular user (normally the logged user), but not the system or other users like root.

    For example, I compiled synergy and ran it as user root. Then, I configured the Leopard’s firewall for access to specific services and applications, but I left the list empty. I verified that I could reach the synergys server at port 24800 from other computer in the same network segment. I was really surprised, to be honest.

    The next thing I tried is to kill synergys and to re-run it as my currently logged in user. This time, Leopard presented me with a dialog box that asked whether to allow synergys to accept incoming connections:

    Do you want the application “synergys” to accept incoming network connections?

    Clicking Deny may limit the application’s behavior. This setting can be changed in the Firewall pane of Security Preferences.

    This caused the expected behavior: any attempt to connect to port 24800 from the outside was blocked by Leopard’s firewall:

    22:02:30.989097 IP 10.42.242.16.49950 > 10.42.242.12.24800:
      S 3207271609:3207271609(0) win 65535
      <mss 1460,nop,wscale 3,nop,nop,timestamp 699234003 0,sackOK,eol>
    22:02:31.913244 IP 10.42.242.16.49950 > 10.42.242.12.24800:
      S 3207271609:3207271609(0) win 65535
      <mss 1460,nop,wscale 3,nop,nop,timestamp 699234012 0,sackOK,eol>
    22:02:32.914300 IP 10.42.242.16.49950 > 10.42.242.12.24800:
      S 3207271609:3207271609(0) win 65535
      <mss 1460,nop,wscale 3,nop,nop,timestamp 699234022 0,sackOK,eol>
    22:02:33.915334 IP 10.42.242.16.49950 > 10.42.242.12.24800:
      S 3207271609:3207271609(0) win 65535
      <mss 1460,sackOK,eol>
    22:02:34.916398 IP 10.42.242.16.49950 > 10.42.242.12.24800:
      S 3207271609:3207271609(0) win 65535
      <mss 1460,sackOK,eol>
    22:02:35.917319 IP 10.42.242.16.49950 > 10.42.242.12.24800:
      S 3207271609:3207271609(0) win 65535
      <mss 1460,sackOK,eol>
    22:02:37.919629 IP 10.42.242.16.49950 > 10.42.242.12.24800:
      S 3207271609:3207271609(0) win 65535
      <mss 1460,sackOK,eol>
    22:02:41.924491 IP 10.42.242.16.49950 > 10.42.242.12.24800:
      S 3207271609:3207271609(0) win 65535
      <mss 1460,sackOK,eol>
    

    Note how the first three segments had the time-stamp and window scaling options activated. The remaining SYN segments didn’t. This is probably due to Leopard thinking that there was congestion or that it was talking to an old system.

    Once I chose Always allow, Leopard added an entry into the Firewall list named synergys. From here on, I was able connect to port 24800. This is the the network capture that tcpdump displayed when trying to connect to port 24800 from another compuer after synergys had been granted access:

    22:24:20.674656 IP 10.42.242.16.50055 > 10.42.242.12.24800:
      S 2514913060:2514913060(0) win 65535
      <mss 1460,nop,wscale 3,nop,nop,timestamp 699247086 0,sackOK,eol>
    22:24:20.675544 IP 10.42.242.12.24800 > 10.42.242.16.50055:
      S 812848149:812848149(0) ack 2514913061 win 65535
      <mss 1460,nop,wscale 1,nop,nop,timestamp 473026159 699247086,sackOK,eol>
    22:24:20.675591 IP 10.42.242.16.50055 > 10.42.242.12.24800:
      . ack 1 win 65535 <nop ,nop,timestamp 699247086 473026159>
    22:24:20.677911 IP 10.42.242.12.24800 > 10.42.242.16.50055:
      P 1:16(15) ack 1 win 33304 <nop ,nop,timestamp 473026159 699247086>
    22:24:20.677959 IP 10.42.242.16.50055 > 10.42.242.12.24800:
      . ack 16 win 65535 <nop ,nop,timestamp 699247086 473026159>
    

    Funny enough, keeping my telnet client running while trying to connect at port 24800, waiting for Leopard to remove the time-stamp and window scaling options, then allowing this traffic, didn’t make Leopard to try to re-negotiate the time-stamp and window scaling options.

    Another thing worth mentioning is that once synergys had been granted access to incoming traffic at port 24800, removing it from the list of allowed applications and services didn’t stop it from receiving traffic. To block synergys from receiving traffic it was necessary to change Allow incoming connections to Block incoming connections (removing its entry from the list did still allow it to receive incoming traffic).

    Another interesting feature is that Leopard’s seems to be aware of service dependency. For example, enabling the Screen Sharing service also starts a local Kerberos 5 KDC which is used to authenticate users. This is fascinating since users of Screen Sharing, for example, will first request a TGT to this local Kerberos 5 KDC (and will be able to decrypt that TGT if they know the correct password), then using the TGT request a service ticket for the Screen Sharing service that can present to it in order to get authenticated. But moreover, trying to connect to the Screen Sharing service for a second time will not require the user to provide his credentials again because a Kerberos service ticket is already available (provided that it didn’t expire). This is Single Sign-On in its true form.

Conclusion

In conclusion, I can’t say that the graphical user interface provides a lot of room for configuration, neither does it offer any way to configure egress filtering (that is, traffic that originates on the local computer and is to be sent out externally via a network interface).

In general, Leopard’s default administration tools are probably on par with those from Windows Vista. Even though the underpinnings of the newest member of the Mac OS X family are powered by IPFW, I feel Leopard’s firewall has a long way to go to in order to get closer to the powerful functionality and configurability of PF or Netfilter/IPtables, for example.

Also, not enabling the firewall by default is a big mistake in my opinion. One of my Mac OS X laptops is listening on port 111/tcp, 1020/tcp and 1021/tcp. The first one is iCalAlarmAgent and the other two are related to launchd. Even if you configure the firewall to block all traffic, these three ports are still accessible from the outside.

The Cisco Linksys WRT54G/GS/GL is made up of a six-port configurable switch, a standard Ethernet controller (usually a Broadcom controller named eth0) and a Wireless controller (usually a Broadcom controller named eth1).

The following diagram tries to illustrate the different components that made up the Cisco Linksys and how are they interconnected:

                                            Linksys rear
 Trunk    Internet    1     2     3     4   port number
  ---        ---     ---   ---   ---   ---
  |5|        |4|     |3|   |2|   |1|   |0|  switch port number
  ---        ---     ---   ---   ---   ---
  |           |       |                 |
  |         vlan1     |----- vlan0 -----|
  |
  | Miniswitch
  ----------------------------------------
  | Linux
  |
  |           ---- vlan0 -> LAN
  |           |
  |----- eth0 -
              |
              ---- vlan1 -> Internet/WAN

The standard Ethernet controller is attached to the sixth port (port #5) of the switch and is configured as a 802.1q VLAN trunk port. This allows running several VLANs using a single connection to the switch.

By default, OpenWRT configures two per-VLAN network interfaces:

  • vlan0:

    stands on the VLAN0 (the Local Area Network which comprises the four ports labeled as 1, 2, 3 and 4 at the rear of the box).

  • vlan1:

    stands on the WAN network (the port labeled Internet at the rear of the box).

The VLAN configuration is controlled using NVRAM variables. The variable labeled vlan0ports defines which switch ports are assigned onto the VLAN0, while vlan1ports defines which switch ports are assigned onto the VLAN1.

This is the default NVRAM configuration:

nvram set vlan0ports="3 2 1 0 5*"
nvram set vlan0hwname=et0
nvram set vlan1ports="4 5"
nvram set vlan1hwname=et0
  • vlan0ports:

    states that ports #3, #2, #1 and #0 (the ports labeled as 1, 2, 3 and 4 at the rear of the box) are assigned onto VLAN0. Additionally, port #5 is also assigned onto VLAN0.

    The asterisk sitting besides the 5 means VLAN0 is the default, native VLAN for this port, so any untagged traffic is considered to belong to VLAN0.

  • vlan1ports:

    states that port #4 (the port labeled as Internet at the rear of the box) is assigned onto VLAN1. Additionally, port #5 is also assigned onto VLAN1 since it’s a trunk port.

    The lack of an asterisk means VLAN1 is not the default, native VLAN for this port.

NOTE: vlannhwname needs to have a value assigned to it, even when it’s value is never used by the init scripts. This value is usually et0.

NOTE: Care must be exercised as ports numbers are zero-based, as illustrated before, and the sixth-port (port #5) must be assigned to every VLAN, since it is an VLAN trunk port.

The following code snippet from /etc/init.d/S10boot shows how the init script tells the switch which ports are onto which VLANs:

# configure the switch based on nvram
[ -d /proc/switch/eth0 ] && {
  for nr in $(seq 0 15); do
    vp="$(nvram get vlan${nr}ports)"
    [ -z "$vp" -o -z "$(nvram get vlan${nr}hwname)" ] || {
        echo "$vp" > /proc/switch/eth0/vlan/$nr/ports
    }
  done
}

We can also see that up to sixteen VLANs are supported by the switch.

Custom VLANs

The Linksys and OpenWRT combination is so flexible that we can configure additional VLANs. In fact, I was looking to add an additional administrative VLAN (VLAN2) granting me full access to the box while I could restrict access from the LAN and WAN to the minimum — for example, by using additional firewall rules.

This is depicted in the following figure:

                                            Linksys rear
 Trunk    Internet    1     2     3     4   port number
  ---        ---     ---   ---   ---   ---
  |5|        |4|     |3|   |2|   |1|   |0|  switch port number
  ---        ---     ---   ---   ---   ---
  |           |       |     |           |
  |         vlan1   vlan2   |-- vlan0 --|
  |
  | Linksys
  ----------------------------------------
  | Linux
  |
  |           ---- vlan0 -> LAN
  |           |
  |----- eth0 ---- vlan1 -> Internet/WAN
              |
              ---- vlan2 -> Administrative VLAN

To achieve this configuration, we need to remove port #3 (labeled as 1 at the rear of the box) from VLAN0 and assign it onto VLAN2. We also need to add port #5 to the VLAN2 since it is the VLAN trunk port used to carry the traffic from the switch to Linux through the standard Ethernet controller:

nvram set vlan0ports="2 1 0 5*"
nvram set vlan0hwname=et0
nvram set vlan1ports="4 5"
nvram set vlan1hwname=et0
nvram set vlan2ports="3 5"
nvram set vlan2hwname=et0

I’ve defined three custom NVRAM variables that will get used by an additional init script to configure the VLAN2 as an administrative VLAN, granting full access to the box:

  • adm_ifname:

    defines the Linux network interface name assigned to the administrative VLAN, in the form of vlann, where n is the VLAN number.

  • adm_ipaddr:

    defines the IP address for the administrative interface.

  • adm_netmask:

    defines the network mask for the administrative interface.

For example:

nvram set adm_ifname=vlan2
nvram set adm_ipaddr=192.168.0.100
nvram set adm_netmask=255.255.0.0

I’ve also coded up an additional init script, named /etc/init.d/S41network, used to bring up the administrative interface. I’ve decided not to fiddle with /etc/init.d/S40network to avoid breaking things and having problems during upgrades.

These are the contents of /etc/init.d/S41network:

#!/bin/sh
IFNAME=$(nvram get adm_ifname)
VLAN=${IFNAME##vlan}
IPADDR=$(nvram get adm_ipaddr)
NETMASK=$(nvram get adm_netmask)
vconfig add eth0 $VLAN
ifconfig vlan${VLAN} up ${IPADDR} netmask ${NETMASK}

Testing

To test this custom configuration, I recommend disabling the firewall, my removing the executable permission bit from /etc/init.d/S45firewall and /etc/init.d/S41network just to prevent being locked out from the box in case problems arise.

Firewalling

I’ve also replaced the firewalling init script, /etc/init.d/S45firewall, with my own version. This allows for a fine-grained and thighter configuration.

Since the box will act as a routing firewall, and since it has 3 VLANs, I wanted to apply the following policy:

  • Any traffic coming from or going to the administrative VLAN (VLAN2) is allowed:

    This rule allows administering the box from a computer attached to the VLAN2, while blocking administrative access from other VLANs.

  • Incoming ICMP Echo Requests and ICMP Time Exceeded control messages are allowed from any interface:

    This rule allows certain ICMP control messages to reach the box. ICMP Echo Request is needed in order for the box to respond to ping and ICMP Time Exceeded (TTL) so we don’t break the PMTU discovery algorithm.

  • Any other incoming traffic from the LAN is rejected:

    This rule rejects any other traffic which does not match previous rules. Traffic is explicitly rejected, so we avoid having clients blocked waiting for an RST TCP segment.

  • Any other incoming traffic from the WAN is dropped:

    This rule silently drops any traffic coming from the WAN which does not match any previous rule. This will make external scan attacks much slower.

  • Local DNS queries coming from the local box going to configured DNS servers are allowed:

    This rule allows the local machine to resolve DNS queries sent against configured DNS servers (those configured in the wan_dns NVRAM variable). This is rarely needed, but the ipkg command requires a working DNS name resolution.

  • HTTP traffic from the local machine to the WAN is allowed:

    This rule allows upgrading and installing packages using the ipkg command.

  • Outgoing ICMP Echo Requests and ICMP Time Exceeded control messages are allowed from any interface:

    This rule allows certain ICMP control messages to depart from the box. ICMP Echo Request is needed in order for the box to invoke ping and ICMP Time Exceeded (TTL) so we don’t break the PMTU discovery algorithm.

  • Forwarding SSH/NX traffic coming from WAN to the designated SSH/NX server in the LAN:

    This rule allows accesing the SSH/NX traffic from the WAN. In addition, I apply SNAT to make IP datagrams appear to come from the firewall box since I have multiple DSL links.

  • Forwarding HTTP and HTTP/S traffic coming from the LAN targeted to the WAN:

    This rule allows using HTTP and HTTP/S services from the LAN.

  • DNS queries coming from the LAN going to configured DNS servers are allowed:

    This rule allows the machines in the LAN to resolve DNS queries sent against configured DNS servers (those configured in the wan_dns NVRAM variable).

  • Forwarding ICMP Echo Requests coming from the LAN to the WAN:

    This allows pinging external hosts from the LAN. ICMP Time Exceeded, however, is not forwarded, since the firewall sits in the middle between the LAN and the WAN (and I do use SNAT and DNAT).

Here is the complete /etc/init.d/S45firewall script:

#!/bin/sh
IPTABLES=/usr/sbin/iptables
FW_INET_IFACE=$(nvram get wan_ifname)
FW_INET_IP=$(nvram get wan_ipaddr)
FW_PRIVATE_IFACE=$(nvram get lan_ifname)
FW_PRIVATE_IP=$(nvram get lan_ipaddr)
FW_ADM_IFACE=$(nvram get adm_ifname)
NX_IP=10.200.0.10

$IPTABLES -F
$IPTABLES -t nat -F

# Configure SNAT/DNAT/MASQUERADE
$IPTABLES -t nat -A PREROUTING -i ${FW_INET_IFACE} -p tcp \
                               -d ${FW_INET_IP} --dport 179 \
                               -j DNAT --to-destination ${NX_IP}:22
$IPTABLES -t nat -A POSTROUTING -o ${FW_PRIVATE_IFACE} -p tcp \
                                -d ${NX_IP} --dport 22 \
                                -j SNAT --to-source ${FW_PRIVATE_IP}
$IPTABLES -t nat -A POSTROUTING -o ${FW_INET_IFACE} -j MASQUERADE

# Configure input firewall filtering:
# Allow:
#   - Traffic flowing from the loopback interface
#   - Traffic coming from the administrative VLAN
#   - ICMP Echo Request coming from WAN
#   - ICMP Time Exceeded (TTL) coming from WAN
#   - Traffic from an already established or related connection
# Block:
#   - Any traffic coming from the WAN
# Reject:
#   - Any other traffic coming from the LAN
$IPTABLES -A INPUT -i lo -j ACCEPT
$IPTABLES -A INPUT -i ${FW_ADM_IFACE} -j ACCEPT
$IPTABLES -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPTABLES -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
$IPTABLES -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
$IPTABLES -A INPUT -i ${FW_INET_IFACE} -j DROP
$IPTABLES -A INPUT -j REJECT

# Configure output firewall filtering:
# Allow:
#   - Traffic flowing to the loopback interface
#   - HTTP traffic
#   - ICMP Echo Request going to WAN
#   - ICMP Time Exceeded (TTL) going to WAN
#   - DNS queries to configured WAN name servers
#   - Traffic from an already established or related connection
# Reject:
#   - Any other traffic
$IPTABLES -A OUTPUT -o lo -j ACCEPT
$IPTABLES -A OUTPUT -o ${FW_INET_IFACE} -p tcp -m tcp \
                     --dport 80 -m state --state NEW -j ACCEPT
$IPTABLES -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPTABLES -A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
$IPTABLES -A OUTPUT -p icmp --icmp-type time-exceeded -j ACCEPT
for ns in $(nvram get wan_dns); do
        $IPTABLES -A OUTPUT -o ${FW_INET_IFACE} -p udp -m udp \
                            -d "$ns" --dport 53 -j ACCEPT
        $IPTABLES -A OUTPUT -o ${FW_INET_IFACE} -p tcp -m tcp \
                            -d "$ns" --dport 53 -j ACCEPT
done
$IPTABLES -A OUTPUT -j REJECT

# Configure forward firewall filtering:
# Allow:
#   - Incoming SSH/NX traffic -> the filtering takes place after the
#     PREROUTING chain has been processed and, since DNAT has been already
#     being performed, the traffic is filtered accordingly to its final
#     destination (the SSH/NX server)
#   - Outgoing DNS queries to configured WAN name servers
#   - Outgoing HTTP and HTTP/S traffic
#   - ICMP Echo Request coming from LAN going to WAN
#   - Trafic from an already established or related connection
# Drop:
#   - Any other traffic
$IPTABLES -A FORWARD -i ${FW_INET_IFACE} -o ${FW_PRIVATE_IFACE} -p tcp -m tcp \
                     -d ${NX_IP} --dport 22 -m state --state NEW -j ACCEPT
$IPTABLES -A FORWARD -i ${FW_PRIVATE_IFACE} -o ${FW_INET_IFACE} -p tcp -m tcp \
                     --dport 80 -m state --state NEW -j ACCEPT
$IPTABLES -A FORWARD -i ${FW_PRIVATE_IFACE} -o ${FW_INET_IFACE} -p tcp -m tcp \
                     --dport 443 -m state --state NEW -j ACCEPT
$IPTABLES -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPTABLES -A FORWARD -i ${FW_PRIVATE_IFACE} -o ${FW_INET_IFACE} \
                     -p icmp --icmp-type echo-request -j ACCEPT
for ns in $(nvram get wan_dns); do
        $IPTABLES -A FORWARD -i ${FW_PRIVATE_IFACE} -o ${FW_INET_IFACE} \
                             -p udp -m udp -d "$ns" --dport 53 -j ACCEPT
        $IPTABLES -A FORWARD -i ${FW_PRIVATE_IFACE} -o ${FW_INET_IFACE} \
                             -p tcp -m tcp -d "$ns" --dport 53 -j ACCEPT
done
$IPTABLES -A FORWARD -j DROP

FreeBSD firewall using PF

November 12th, 2005

FreeBSD supports OpenBSD’s powerful firewall PF since version 5.3. The scenario I was pursuing was firewalling one of my FreeBSD machines:

Incoming firewalling

  • Only incoming SSH connections from known SSH clients should be accepted.
  • Only incoming Syslog traffic from known Syslog clients should be accepted.
  • Only incoming ICMP Echo-Reply, ICMP Echo-Request and ICMP Destination-Unreachable datagrams should be accepted. Any other ICMP datagram is potentially dangerous.
  • No other incoming traffic should be allowed, but should get logged.

Outgoing firewalling

  • Only outgoing DNS queries to known DNS servers should be accepted.
  • Only outgoing NTP traffic to know NTP servers should be accepted.
  • Only outgoing ICMP Echo-Reply, ICMP Echo-Request and ICMP Destination-Unreachable datagrams should be accepted. Any other ICMP datagram is potentially dangerous.
  • No other outgoing traffic should be allowed, but should get logged.

The contents of /etc/pf.conf should look lite this:

scrub in all pass quick on lo0 all icmp_types = "{ echorep, unreach, echoreq }" syslog_sources = "{ 192.168.0.124, 192.168.0.125, 192.168.0.126 }" ssh_sources = "{ 192.168.0.90, 192.168.0.91 }" ntp_servers = "{ 192.168.0.125 }" dns_servers = "{ 192.168.0.124 }" block in log all pass in on rl0 inet proto icmp icmp-type $icmp_types keep state pass in on rl0 proto tcp from $ssh_sources to self \ port { 22 } flags S/SA keep state pass in on rl0 proto udp from $syslog_sources to self \ port { 514 } block out log all pass out on rl0 inet proto icmp icmp-type $icmp_types keep state pass out on rl0 proto udp from self to $dns_servers \ port { 53 } keep state pass out on rl0 proto udp from self to $ntp_servers \ port { 123 } keep state

To enable PF and PF logging support to start automatically during boot, the following lines should be added to /etc/rc.conf:

pf_enable="YES"
pf_rules="/etc/pf.conf"
pf_program="/sbin/pfctl"
pflog_enable="YES"
pflog_logfile="/var/log/pflog"
pflog_program="/sbin/pflogd"