IPFW: A Stateful NATing Firewall config
I've used iptables and pf, both of which are available on FreeBSD, but I always feel more at home with IPFW. It's powerful, and I think, in some ways more intuitive than the others. However, it is still big and complicated. Some configurations aren't easily revealed in a few google searches.
I came across a post on the FreeBSD forums that helped a few things click for a specific use case I have at hand. That of the stateful NATing firewall with internal services.
To accomplish what we want to do we need to have two NAT entry points. There is one for inbound packets and another for outbound packets. So we end up with a structure something like:
Setup general Allows general Denials inbound NAT outbound rules that jump to skipdest to the outbound nat inbound rules that jump to skipdest to the outbound nat deny left over stuff skipdest outbound NAT allow everything
Environment
In the discussion that follows the environment resembles something like the following (for demonstration purposes we're using a private subnet on the public side):
Setup
So with that in mind let's look a bit more closely at the beast.
We're going to keep our firewall configuration in /etc/ipfw. You will have to create this directory.
Let's include the following in /etc/rc.conf:
firewall_enable="YES" firewall_logging="YES" firewall_nat_enable="YES" firewall_script="/etc/ipfw/ipfw.start
/etc/ipfw/ipfw.start is a shell script that will be run at boot and any time we do a “service ipfw restart”.
We'll start out by defining some handy variables to make the firewall rules a bit less cluttered:
#!/bin/sh ########################################################## # handy variables ########################################################## IPFW=/sbin/ipfw LOGALL="log logamount 0" SKIPOUT="skipto 9000" LAN="dc0" LAN_MASK="192.168.100.0/24" WAN="dc1" WAN_MASK="192.168.1.0/24"
Startup and NAT
Now we will get started with the firewall commands:
# flush any rules that might be in place. Note that IPFW has a set facility, one set # isn't flushed by the following command, it usually contains a deny all rule as # as long as the system is set to deny all bey default $IPFW -f flush # disable one_pass, we may be re-injecting some packets thanks to NAT # this is setting the net.inet.ip.fw.one_pass sysctrl to zero. $IPFW disable one_pass # configure our NAT. Note it is possible to have multiple NAT configs but # we're keeping it simple here. # # the config for NAT is one long line, we're using line continuations # to aid readability # # note fwpublic is the hostname associated with the ip address on the WAN # interface and ourserver is server on the LAN. You could have multiple # internal servers here $IPFW nat 1 config \ if $WAN same_ports unreg_only \ redirect_port tcp ourserver:ssh fwpublic:ssh \ redirect port tcp ourserver:smtp fwpublic:smtp \ redirect port tcp ourserver:http fwpublic:http \ redirect port tcp ourserver:https fwpublic:https \ redirect port tcp ourserver:domain fwpublic:domain \ redirect port udp ourserver:domain fwpublic:domain
Tables
We're going to make use of ipfw tables in our script so we have to set those up. Tables can be used as a source or destination in rules, among other things. They're very fast so we can have many hosts and/or subnets in a rule. We can also make changes to tables on the fly without affecting the rules below.
One table used below is a table of bad actors that is maintained using various lists available on the net.
Note we define a variable we can use to refer to the table in the rules, this just makes things look neater.
########################################################## # table of reserved address spaces, these shouldn't be # landing at our front door ########################################################## TBL_RESERVED="table(reserved)" $IPFW table reserved create $IPFW table reserved add 240.0.0.0/4 $IPFW table reserved add 224.0.0.0/4 $IPFW table reserved add 203.0.113.0/24 $IPFW table reserved add 172.16.0.0/12 $IPFW table reserved add 198.51.100.0/24 $IPFW table reserved add 198.18.0.0/15 $IPFW table reserved add 192.168.0.0/16 $IPFW table reserved add 192.88.99.0/24 $IPFW table reserved add 192.0.2.0/24 $IPFW table reserved add 192.0.0.0/24 $IPFW table reserved add 169.254.0.0/16 $IPFW table reserved add 127.0.0.0/8 $IPFW table reserved add 100.64.0.0/10 $IPFW table reserved add 10.0.0.0/8 $IPFW table reserved add 0.0.0.0/8 ########################################################## # this table has a huge list of bad guys in it, we will # load it from another script. To avoid potential conflicts # with that script we are using the "missing" option so the # table won't complain if it already exists. Don't worry about # that script here - it just manipulates the contents of # the table ########################################################## TBL_BADGUYS="table(badguys)" $IPFW table badguys create missing /etc/ipfw/badguysload.pl /etc/ipfw/badguys.list > /etc/ipfw/badguys.log ########################################################## # we have a table of addresses we don't want to be blocked # by the badguys list accidentally or by the reserved list. # # We use one of the reserved subnets on the WAN side # which connects us to our ISP's router. ########################################################## TBL_WHITELIST="table(whitelist)" $IPFW table whitelist create $IPFW table whitelist add remotehost $IPFW table whitelist add friend $IPFW table whitelist add work $IPFW table whitelist add $WAN_MASK
General Denials
Now lets get into the actual rules (note that I use some odd spacing just to make typos stand out a bit more):
########################################################## # Allow local traffic # We don't want to police our loopback or our LAN ########################################################## $IPFW add 1000 allow all from any to any via lo0 $IPFW add 1010 allow all from any to any via $LAN ########################################################## # some stuff we just don't want to see coming in, your # choices will very ########################################################## # ipfw has a built in that will match traffic that doesn't # match that interface its on, think lan addresses showing # up on your wan interface. See the man page for more # details $IPFW add 1020 deny $LOGALL ip from any to any not antispoof in # my isp doesn't move ip6 traffic yet so my rules don't account # for it so if they suddenly turn it on I want to drop the # packets $IPFW add 1021 deny $LOGALL ip6 from any to any in recv $WAN # bad guy blocking based on an ifpw table that is maintained # outside this script. We will drop anything on the list # but we don't want to accidentally block something important # so we have a white list. # # we also want to block subnets that shouldn't be routable # these may or may not show up on the badguy list $IPFW add 1035 skipto 1040 ip4 from $TBL_WHITELIST to any in recv $WAN $IPFW add 1036 skipto 1040 ip4 from any to $TBL_WHITELIST out xmit $WAN # reserved addresses, not worried about these leaking out so only inbound $IPFW add 1037 deny $LOGALL ip4 from $TBL_RESERVED to any in recv $WAN # drop the bad guys, don't want to see these going in our out (if a LAN device # is compromised we don't want it phoning home) $IPFW add 1038 deny $LOGALL ip4 from $TBL_BADGUYS to any in recv $WAN $IPFW add 1039 deny $LOGALL ip4 from any to $TBL_BADGUYS out xmit $WAN $IPFW add 1040 count // destination for whitelisted skips
NAT and Check-State
Now we're ready to consider the remaining traffic:
########################################################## # Reassembly and NAT # # If we were doing userland NAT than it would take care of # reassembly. As a rule you shouldn't see fragmented packets # but apparently there are some VPN issues that can lead to # them occuring. We are advised to do the reassembly prior # to NAT due to the way NATing works. You could probably # get away with denying them as well. # # You want to do check-state as soon as you can for speed's # sake. At this point we are checking any connection tuples that # have been allowed in the past so packets for things we've # already ok'd are allowed and we're done here # ########################################################## $IPFW add 1098 reass all from any to any in $IPFW add 1099 count // pre check-state just curious $IPFW add 1100 nat 1 ip4 from any to any in recv $WAN $IPFW add 1110 check-state $IPFW add 1111 count // post check-state just curious # this is to prevent flood attacks - things pretending to be established # if they were really established they would have been in the state # table $IPFW add 1113 deny tcp from any to any established in recv $WAN
Inbound/Outbound Services
At this point, we dropped all the known bad stuff, NAT'd the inbound packets, and allowed anything that has already been established. Now its time to determine what packets we want to allow to escape from our LAN. You can be really open with this or really draconian. I have it very open in my rules but you could limit it to only certain protocols (i.e. just allowing http and https outbound for instance). Or, you may wish to be really open with a few exceptions (i.e. open but you don't want to allow telnet and imap outside the LAN). The choices are endless.
When we determine a packet is allowed we will keep-state and jump to the outbound NAT. As a rule I typically only match the setup packet on tcp services I wish to allow.
########################################################## # Authorized outbound packets - pretty much anything goes # note that other protocols will drop through to the NAT # eventually ########################################################## $IPFW add 2000 $SKIPOUT tcp from any to any setup out xmit $WAN keep-state $IPFW add 2010 $SKIPOUT udp from any to any out xmit $WAN keep-state $IPFW add 2020 $SKIPOUT icmp from any to any out xmit $WAN keep-state ########################################################## # services we want to allow in from the Internet # # note that at this point the packet has passed through # the inbound nat so it may have the address of an internal # server # # We're allowing incoming mail, http, and https. The # connection to the ssh and dns server from outside are more # restricted. ########################################################## $IPFW add 3000 $SKIPOUT tcp from work to ourserver ssh in recv $WAN setup keep-state $IPFW add 3010 $SKIPOUT tcp from any to ourserver smtp,http,https in recv $WAN setup keep-state $IPFW add 3020 $SKIPOUT tcp from friend to ourserver ssh,domain in recv $WAN setup keep-state $IPFW add 3030 $SKIPOUT udp from friend to ourserver domain in recv $WAN keep-state
Outbound NAT
And now we're at the end. We're going to drop any inbound tcp or udp traffic from the WAN that we haven't already accounted for. Other protocols that get this far will be allowed to continue on their way.
########################################################## # all other tcp and udp traffic will be dropped # other protocols will fall through to our outbound nat # (inbound icmp for instance) ########################################################## $IPFW add 8998 deny $LOGALL tcp from any to any via $WAN $IPFW add 8999 deny $LOGALL udp from any to any via $WAN $IPFW add 9000 nat 1 ip4 from any to any out xmit $WAN $IPFW add 9001 allow ip from any to any # we should never get to this point but doesn't hurt to have the rule here # # if the firewall is default to deny there will be one more similar rule after # this when you do an "ipfw show". It may get a few packets for things that # arrive before we finish running this script at boot or when we reload the # rules $IPFW add 65000 deny $LOGALL ip from any to any //bad packet