You get a bonus - 1 coin for daily activity. Now you have 1 coin

The IPFW firewall in FreeBSD - configuring from scratch, a simple config example

Practice



In this note I will explain and show the basics of one of the most popular Unix firewalls - IPFW. This is precisely the firewall considered the "default" one for FreeBSD.


So, I assume that you generally understand what a firewall is for and how it works. In the simplest terms: firewalls look at all the traffic passing through a computer (be it a simple home desktop, a router, or a server), compare each packet against the rules set by the administrator, based on where the packet is coming from and which port, and where it is going and to which port, and decide what to do with that packet.

We will not touch here on bandwidth limiting via "pipes", kernel NAT (we'll make do with NATd), and other things that no longer fall into the category of "initial setup". Here is a config for setting up the firewall for someone who is not very familiar with this firewall and who needs a "push" so they can then figure out on their own - what to write where so that it works the way they want.


1. General provisions

So, we have a computer with 2 (in our story we'll talk about two) network cards. For simplicity we'll call them eth0 and eth1, although seeing such network interface names in FreeBSD is, in a way, a rarity.

In our example, network card eth0 will "look" into our protected network. And card eth1 - outward, to the internet. Again, here we're describing a particular case - when the router has the internet as its outbound "hole", and not some other network of ours.

Now let's talk about how rules are specified in IPFW and how the firewall goes through them.

Rules are described one after another. The firewall runs each packet through the rules one after another, until it finds one whose conditions the packet matches. In most cases, processing stops after that (i.e. the first matching rule ends the packet's path through the list).

For example, if we have two rules:

allow all from 192.168.0.1 to any dst-port 1000-2000
deny all from 192.168.0.1 to any dst-port 1050

The second rule will not fire in our case. It would seem that if you add them up at the level of human logic - a packet coming from address 192.168.0.1 to anywhere should be allowed through to ports 1000 through 2000, except port 1050. But no - the allow rule (the 1st one) will also fire for port number 1050 (this port falls within the specified range 1000-2000) and IPFW simply won't check the next rule!

Each rule has its own unique number, starting from a small number (100 by default). The last allowed rule number is 65535, which is occupied by the "default rule". By default, the "default rule" is "deny all".

When specifying rules we can either specify their number or not. In the latter case IPFW will automatically increment the number of the next rule and we'll get a list with automatically assigned rule numbers.

Generally speaking, it only makes sense to specify rule numbers explicitly if you want to "jump" between rules during processing. For example, when a packet matches a rule's condition - it needs to skip past a certain set of deny rules and end up further down the list. There is no other way to "jump" to a specific rule in this case - and for that we need to know exactly what number the needed condition has.

One more note about numbering. IPFW is fine with gaps in the list. For example, it is quite "normal" for it if you number the rules something like this:

100 ...
200 ...
300 ...
10000 ...
10100 ...
10200 ...

That is, rule numbers don't have to go with a strict increment. You can even do this:

100 ...
200 ...
201 ...
202 ...
300 ...


2. How rules are built and the main firewall commands

Each rule consists of:
a) A number
b) A command
c) Command parameters, if any
d) A protocol specification (tcp, udp, icmp... the words all and ip mean - any protocol)
e) A "from" rule
f) A "to" rule
g) Additional conditions

The "from" and "to" rules consist of:
a) The word "from" or "to" respectively
b) An IP address or host name (the word any means any IP, and the word me means any IP address among those currently configured on this computer)
c) Port(s). If a port is not specified - it is considered to be "any port".

If you put "not" before an IP address or port - it will invert the condition.

Again - this is heavily simplified. In fact, if you're going to count traffic, tag packets, etc. - the IPFW syntax expands further, but that is beyond the scope of this article.

Here's an example:
allow tcp from 192.168.0.1 33333 to 192.168.0.5 dst-port 80

Here we:
a) Do not specify a rule number - meaning IPFW will assign it itself
b) Specify the "allow" command - allow the rule to pass through the firewall
c) There are no additional parameters for "allow" - so none are shown here
d) Specify a condition that the packet must be a TCP packet (not UDP, not ICMP - specifically TCP)
e) The "from" rule - from computer 192.168.0.1, port 33333
f) The "to" rule - to computer 192.168.0.5, port 80
g) No additional conditions

Another example:
500 skip 10000 all from any to 192.168.0.100 dst-port 3389 out via eth1

Here we:
a) Tell IPFW that this rule has number 500
b) Specify the "skip" command - jump to another rule
c) Specify the skip command's parameter - the rule number to jump to - 10000
d) Specify the protocol condition - any protocol (all)
e) From: from any computer (any) on any port (port not specified)
f) To: to the computer with IP 192.168.0.100 on port 3389
g) Additional condition - direction: we specify that the condition will only fire if the packet must exit through network card eth1 (out eth1)


If you need to specify several IP addresses or ports in one rule - see the following tip.

a) You can use ranges:
allow all from any to 192.168.0.1 dst-port 5000-9000
allow all from any to 192.168.0.1-192.168.0.100

b) You can list them comma-separated (and group with ranges)
allow all from any to 192.168.0.1,192.168.0.2 dst-port 25,110,143,1024-2048
allow all from any to 192.168.0.1-192.168.0.10, 192.168.0.15

c) You can use curly braces (only 1 level of braces!):
allow all from any to { 192.168.0.1 or not 192.168.0.5 or 192.168.0.10 }


How do you specify subnets (a mask)?

a) Using "/":
allow all from any to 192.168.0.0/24

b) Using ":":
allow all from any to 192.168.0.0:255.255.255.0



Main commands:

allow or pass or accept or permit
Allow the packet to pass and end the check. That is, the packet is given a "green" light.
allow tcp from 192.168.0.1 to 192.168.0.2
allow udp from 192.168.0.1 to 192.168.1.0/24 dst-port 80,443
allow all from 192.168.0.5 to any dst-port 80,443,25,110,143,8080

deny or drop
Deny the packet passage and end the check. The packet is not passed through the router.
deny tcp from 192.168.0.1 to not 192.168.0.1/24
deny all from any to 192.168.0.5

divert
Pass the packet to the redirection socket on the specified port. The check ends, the packet, instead of the specified destination, reaches a daemon on the local machine that is "listening" on the specified port.
divert natd ip from 10.0.0.0/8 to any out via eth1
# here we pass packets leaving through eth1 from network 10.0.0.0/8 to the NATd daemon

fwd or forward
Redirect the packet to the specified IP address instead of where the packet was heading. That is, we catch the packet and tell it to "roll" to the computer we need. The check ends.
It should be noted that IPFW does not change the packet itself, so the receiving computer will indeed see that the packet was not addressed to it at all and must be prepared for that.
fwd 10.0.0.1,3128 tcp from 10.0.0.0/8 to not me 80
# Here we pass to the transparent proxy at address 10.0.0.1 and port 3128 everything coming from network 10A to anywhere except the router itself on port 80

reset
Deny the packet passage and, if it is a TCP packet, try to send the sender a TCP "reset" message. A more "humane" reply instead of "simply an unanswered denial", but it spends traffic on the reply. The check ends.
reset all from any to 192.168.0.1 dst-port 25,110,143

skipto
On matching the condition - jump to the specified packet number. The check continues from the condition to which the packet was redirected.
skipto 3000 tcp from 192.168.1.0/24 to 192.168.0.0/16

tee
On matching the condition - send a copy of this packet to the specified redirection socket (divert socket) port. The original packet continues through the list of conditions.
tee 40000 tcp from 192.168.0.15 to any
# For example, on port 40000 on the router there sits a daemon that records all traffic coming from 192.168.0.15

unreach
Deny the packet passage and send back an ICMP "unreach" reply with the specified code. The packet's passage ends.
unreach net all from any to 192.168.5.0/24


Additional conditions placed at the end of a rule:

bridged
The packet is a Layer2 packet.

diverted
The packet was generated by a divert socket

diverted-loopback
The packet was generated by a divert socket and placed back into the IP input stack for delivery.

diverted-output
The packet was generated by a divert socket and placed back into the IP output stack for delivery.

dst-ip ip addresses
The packet is sent to one of the specified IP addresses.

dst-port port numbers
The packet is sent to one of the specified ports.

established
The packet belongs to an already established TCP connection.

frag
The packet is fragmented and is not the first part of a fragmented datagram.

jail jail ID
The packet is sent or received by a "jail" - a virtual machine in FreeBSD.

icmptypes types
The packet matches one of the specified ICMP types.

in or out
The packet is incoming or outgoing (mutually exclusive conditions).

iplen sizes
The packet has a size from the "sizes" list, which is specified in the same way as the port list.

layer2
The packet is a Layer2 packet.

MAC source MAC destination MAC
The packet was sent from a network interface with the "source MAC" MAC address and must arrive at a network interface with the "destination MAC" MAC address.

proto protocol
The packet matches the specified protocol.

via interface
The packet enters or exits through the specified interface.

setup
The packet has the TCP SYN flag set, but not ACK.

dst-ip ip addresses
The packet comes from one of the specified IP addresses.

dst-port port numbers
The packet comes from one of the specified ports.

tcpflags flags
The packet has the specified TCP flags.
If an exclamation mark (!) precedes a flag - that flag must not be set for the rule to fire.


These are only the main conditions here. You can read much more in the help page

man ipfw.


3. Configuring the firewall

So, here I'll give a very small firewall configuration file, which has several examples with comments.

#!/bin/sh

# Command for talking to ipfw
fwcmd="/sbin/ipfw -q"

# eth0 is the internal network interface
# with IP address 192.168.0.1
iip="192.168.0.1"
imask="255.255.255.0"
inet="192.168.0.0"
iif="eth0"

# eth1 is the internet interface
# IP, mask, subnet, etc. - purely for example
oip="11.22.33.44"
omask="255.255.255.0"
onet="11.22.33.0"
oif="eth1"

# Clear the current rule set
${fwcmd} -f flush

### Rules before NAT

# Through eth1 there cannot be traffic with private IPs
${fwcmd} add deny all from 10.0.0.0/8 to any in via ${oif}
${fwcmd} add deny all from 172.16.0.0/12 to any in via ${oif}
${fwcmd} add deny all from 192.168.0.0/16 to any in via ${oif}

${fwcmd} add deny all from any to 10.0.0.0/8 via ${oif}
${fwcmd} add deny all from any to 172.16.0.0/12 via ${oif}
${fwcmd} add deny all from any to 192.168.0.0/16 via ${oif}

${fwcmd} add deny all from any to 0.0.0.0/8 via ${oif}
${fwcmd} add deny all from any to 169.254.0.0/16 via ${oif}
${fwcmd} add deny all from any to 192.0.2.0/24 via ${oif}
${fwcmd} add deny all from any to 224.0.0.0/4 via ${oif}
${fwcmd} add deny all from any to 240.0.0.0/4 via ${oif}

# And on the internal interface - where would external traffic come from
${fwcmd} add deny all from ${onet}:${omask} to any in via ${iif}

# Allow traffic to travel within our networks
${fwcmd} add allow all from ${inet}:${imask} to ${inet}:${imask}

### NAT
# Outgoing traffic to the internet - pass through NAT
${fwcmd} add divert natd ip from ${inet}:${imask} to any out via ${oif}

# Incoming traffic to ${oip} - i.e. to the router's own IP address
# also send into NATD - for unmasking
${fwcmd} add divert natd ip from any to ${oip} in via ${oif}


### Rules after NAT

# All traffic originating from this router - is allowed. This same rule
# fires during masking - if you disable it, nothing will
# fly out to the internet after NAT
${fwcmd} add allow all from me to any



# We don't check packets that belong to an already established,
# and accordingly already verified, TCP connection
${fwcmd} add allow all from any to any established

# Deny long fragmented echo requests
${fwcmd} add deny icmp from any to any frag

# Allow the second and subsequent parts of fragmented packets to pass through
# (we've already checked their headers)
${fwcmd} add allow all from any to any frag


# Allow users to connect to mail via SMTP and receive it via
# POP3, IMAP
${fwcmd} add allow tcp from ${inet}:${imask} to any dst-port 25,110,143,465,995
${fwcmd} add allow tcp from any 25,110,143,465,995 to ${inet}:${imask}

# Allow resolving DNS names
${fwcmd} add allow all from any to any dst-port 53
${fwcmd} add allow all from any 53 to any

# Allow SSH traffic
${fwcmd} add allow all from any to any dst-port 22
${fwcmd} add allow all from any 22 to any

# Allow time updates via NTP
${fwcmd} add allow all from any to any dst-port 123
${fwcmd} add allow all from any 123 to any

# Allow FTP access only for certain IPs
${fwcmd} add allow all from 192.168.0.5 to any dst-port 20,21,49152-65535
${fwcmd} add allow all from any 20,21,49152-65535 to 192.168.0.5

# Allow connecting via RDP (Windows Terminals)
${fwcmd} add allow all from any to any dst-port 3389
${fwcmd} add allow all from any 3389 to any

# Allow HTTP/S traffic
${fwcmd} add allow all from any to any dst-port 80,443
${fwcmd} add allow all from any 80,443 to any

# Allow all pings from the inside
${fwcmd} add allow icmp from any to any via ${iif}

# Deny other pings
${fwcmd} add deny icmp from any to any

# And deny certain types of pings from outside
${fwcmd} add allow icmp from any to any icmptypes 0,3,4,8,11,12 via ${oif}

# Allow ICQ
${fwcmd} add allow all from any to any dst-port 5190
${fwcmd} add allow all from any 5190 to any

#Allow UDP
${fwcmd} add allow udp from ${inet}:${imask} to any

# Deny IDENT
${fwcmd} add reset tcp from any to any 113 in via ${oif} setup

# Deny all connection establishment from outside
${fwcmd} add deny tcp from any to any in via ${oif} setup

# Deny broadcast via the external interface
${fwcmd} add deny ip from any to 0.0.0.255:0.0.0.255 in via ${oif}

# Deny SMB connections from outside
${fwcmd} add deny udp from any to any 137-139 via ${oif}

# Deny everything else from outside
${fwcmd} add deny ip from any to any via ${oif}


4. Chewing through NAT - the double travel of packets

So, what happens when a packet, in the course of IPFW processing, encounters the "off you go through NAT" rule on its path? I.e. the "divert natd" command fires?

Let's break it down step by step. Let's start with a packet flying out of the local network into the internet.
1) The packet flies into IPFW with a private IP and passes all checks up to the rule with NATD.
2) The packet is redirected to the NATD daemon and the IPFW check ends.
3) The packet is transformed (masked) by the NATD daemon and the packet's IP takes the value of the router's outbound interface IP address.
4) The NATD daemon throws the packet back into the IPFW stream and the firewall starts checking the packet FROM THE FIRST RULE! I.e. for IPFW this is a new packet, which it hasn't looked at yet and which has nothing in common with the packet that originally flew in from the local network.
5) The NATD rule no longer fires, since the packet no longer has a local IP and must exit through the external network card (out via eth1).
6) The packet reaches the point where it is allowed to exit, since it is supposedly sent by the router itself (allow all from me to any). And off it flies into the internet.

Now about a packet that flies in from the internet and is heading to an internal machine:
1) The packet flies into IPFW with the sending computer's public IP and starts going through the rules.
2) The packet reaches the NATD rule, which says that if the packet arrived specifically at the router's external IP address and came in through the external network card - then it needs to be run through NATD.
3) The packet is redirected to NATD and the IPFW check ends - the firewall is no longer interested in this packet.
4) NATD unmasks the packet (if it can) and sends it back into IPFW, which starts checking the unmasked packet FROM THE VERY BEGINNING OF THE RULES - for it this is already a new packet, with nothing in common with the first one.
5) IPFW performs its usual checks, now taking into account that the packet is flying not to the router's own public address, but to a private address inside the local network. Therefore the NATD rule doesn't fire and the packet continues its path through the rules.

This is how we see that every packet that goes through masking passes through the firewall twice - once as itself, and then again as the mask. We also see that each such repeat pass starts not right after the NATD rule, but from the very first IPFW rule.


5. Enabling the firewall in FreeBSD

1) Create a file with the list of rules, for example from the example above, and place it at:

/usr/local/etc/ipfw.conf

2) Make the file executable:

# cd /usr/local/etc
# chmod +x ipfw.conf

3) Open /etc/rc.conf and add these lines:

...
firewall_enable="YES"
firewall_script="/usr/local/etc/ipfw.conf"
...

4) Restart the router

# reboot

ATTENTION! Be near the router in case you forgot to specify something correctly somewhere (some IP) and access to the router turns out to be blocked after booting. That is, be in front of the console.


6. What if we need DIVERT or FORWARD?

By default, divert and forward are not included in the kernel. They can be added by rebuilding the kernel (and while you're at it, unconditionally enable IPFW - not as a loadable module, but as part of the kernel).

# cd /usr/src/sys/i386/conf

Here you copy the kernel you need (the GENERIC kernel is used by default) into another file and add the following lines after all the options:

options IPFIREWALL
options IPFILTER
options IPDIVERT
options IPFIREWALL_FORWARD

And rebuild the kernel and install it:

# cd /usr/src
# make buildkernel KERNCONF=MYKERNEL && make installkernel KERNCONF=MYKERNEL

instead of MYKERNEL, of course, put the name of your kernel configuration file.

That's it, reboot, and Divert and Forward will work through IPFW.


7. What else can IPFW do?

A lot. It can create "pipes" inside which you can limit traffic bandwidth, emulate packet loss; it can count traffic by given rules; it can work with tagged packets... Believe me, the configuration file above is only the bare minimum config.

You can read a lot in "man ipfw". A lot - in the FreeBSD handbook (by the way, it's also available in Russian). This note was not intended to describe all of IPFW's capabilities - it is for those who are just starting to work with this firewall and who need a basic understanding of where, what, and how to write to get one result or another.


8. So why not the kernel NAT?

Because as of FreeBSD 8.1 the kernel NAT was buggy - it had a very small command buffer that did not allow using a large number of port-forwarding (D-NAT) rules. And patching the kernel NAT source code and rebuilding all of that didn't appeal to me (I already have enough work as it is), so personally I still (even on 8.2) live with non-kernel NAT. Maybe it's already been fixed by now - I haven't tested it yet.

In any case, if kernel NAT starts working properly - I will describe how to use it.

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Computer networks"

Terms: Computer networks