Hack 34 Firewall with OpenBSD's PacketFilter
Use OpenBSD's firewalling
features to protect your network.
PacketFilter,
commonly known as
PF,
is the firewalling system available in OpenBSD. While it is a
relatively new addition to the operating system, it has already
surpassed IPFilter, the system it has replaced, in both features and
flexibility. PF shares many features with Linux's
Netfilter. Although Linux's Netfilter is more easily
extensible with modules, PF outshines it in its traffic normalization
capabilities and enhanced logging features.
To communicate with the kernel portion of PF, we need to use the
pfctl command. Unlike
the iptables command that is
used with Linux's Netfilter, it is not used to
specify individual rules, but instead uses its own configuration and
rule specification language. To actually configure PF, we must edit
/etc/pf.conf. PF's rule
specification language is actually very powerful, flexible, and easy
to use. The pf.conf file is split up into seven
sections, each of which contains a particular type of rule. Not all
sections need to be used�if you don't need a
specific type of rule, that section can simply be left out of the
file.
The first
section is for macros. In this section you can specify variables to
hold either single values or lists of values for use in later
sections of the configuration file. Like an environment variable or a
programming-language identifier, macros must start with a letter and
also may contain digits and underscores.
Here are some example macros:
EXT_IF="de0"
INT_IF="de1"
RFC1918="{ 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }"
A macro can be referenced later by prefixing it with the
$ character:
block drop quick on $EXT_IF from any to $RFC1918
The second section allows you to specify tables
of IP addresses to use in later rules. Using tables for lists of IP
addresses is much faster than using a macro, especially for large
numbers of IP addresses, because when a macro is used in a rule, it
will expand to multiple rules, with each one matching on a single
value contained in the macro. Using a table adds just a single rule
when it is expanded.
Rather than using the macro from our previous example, we can define
a table to hold the nonroutable RFC 1918 IP addresses:
table <rfc1918> const { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }
The const keyword ensures that this table cannot
be modified once it has been created. Tables are specified in a rule
in the same way that they were created:
block drop quick on $EXT_IF from any to <rfc1918>
You can also load a list of IP addresses into a table by using the
file keyword:
table <spammers> file "/etc/spammers.table"
If you elect not to use the const keyword, then
you can add addresses to a table by running a command such as this:
pfctl -t spammers -T add 10.1.1.1
Additionally, you can delete an address by running a command like
this:
pfctl -t spammers -T delete 10.1.1.1
To list the contents of a table, you can run:
pfctl -t spammers -T show
In addition to IP addresses, hostnames may also be specified. In this
case, all valid addresses returned by the resolver will be inserted
into the table.
The next section of the configuration file contains
options
that affect the behavior of PF. By modifying options, we can control
session timeouts, defragmentation timeouts, state-table transitions,
statistic collection, and other behaviors. Options are specified by
using the set keyword. The number of options is
too numerous to discuss all of them in any meaningful detail;
however, we will discuss the most pertinent and useful ones.
One of the most important options is
block-policy. This option specifies
the default behavior of the block keyword and can
be configured to silently drop matching packets by specifying
drop. Alternatively,
return may be used, to specify that
packets matching a block rule will generate a TCP reset or an ICMP
unreachable packet, depending on whether the triggering packet is TCP
or UDP. This is similar to the REJECT target in
Linux's Netfilter.
For example, to have PF drop packets silently by default, add a line
like this to /etc/pf.conf:
set block-policy drop
In addition to setting the block-policy,
additional statistics such as packet and byte counts can be collected
for an interface. To enable this for an interface, add a line similar
to this to the configuration file:
set loginterface de0
However, these statistics can only be collected on a single interface
at a time. If you do not want to collect any statistics, you can
replace the interface name with the none keyword.
To better utilize resources on busy networks, we can also modify the
session-timeout values. Setting
this to a low value can help improve the performance of the firewall
on high-traffic networks, but at the expense of dropping valid idle
connections.
To set the session timeout (in seconds), put a line similar to this
in /etc/pf.conf:
set timeout interval 20
With this setting in place, any TCP connection that is idle for 20
seconds will automatically be reset.
PF can also optimize performance on low-end hardware by tuning its
memory use regarding how many states may be stored at any one time or
how many fragments may reside in memory for fragment reassembly. For
example, to set the number of states to 20,000 and the number of
entries used by the fragment reassembler to 15,000, we could put this
in our pf.conf:
set limit states 20000
set limit frags 15000
Alternatively, we could combine these entries into a single one, like
this:
set limit { states 20000, frags 15000 }
Moving on, the next section is for traffic
normalization rules. Rules of this type ensure that packets passing
through the firewall meet certain criteria regarding fragmentation,
IP IDs, minimum TTLs, and other attributes of a TCP datagram. Rules
in this section are all prefixed by the scrub
keyword. In general, just putting scrub all is
fine. However, if necessary, we can get quite detailed in specifying
what we want normalized and how we want to normalize it. Since we can
use PF's general filtering-rule syntax to determine
what types of packets a scrub rule will match, we can normalize
packets with a great deal of control.
One of the more interesting possibilities is to randomize all
IP IDs in the packets leaving your
network for the outside world. In doing this, we can make sure that
passive operating system determination methods based on IP IDs will
break when trying to figure out the operating system of a system
protected by the firewall. Because such methods depend on analyzing
how the host operating system increments the IP IDs in its outgoing
packets, and our firewall ensures that the IP IDs in all the packets
leaving our network are totally random, it's pretty
hard to match them against a known pattern for an operating system.
This also helps to prevent enumeration of machines in a
network
address translated (NAT) environment. Without random IP IDs, someone
outside the network can perform a statistical analysis of the IP IDs
being emitted by the NAT gateway in order to count the number of
machines on the private network. Randomizing the IP IDs defeats this
kind of attack.
To enable random ID generation on an interface, put a line such as
this in /etc/pf.conf:
scrub out on de0 all random-id
We can also use the scrub
directive to reassemble
fragmented packets before forwarding them to their destinations. This
helps prevent specially fragmented packets (such as packets that
overlap) from evading intrusion-detection systems that are sitting
behind the firewall.
To enable fragment reassembly on all interfaces, simply put the
following line in the configuration file:
scrub fragment reassemble
If we want to limit reassembly to just a single interface, we can
change this to:
scrub in on de0 all fragment reassemble
This will enable fragment reassembly for the de0
interface.
The next two sections of the pf.conf file
involve packet queuing and address translation, but since this hack
focuses on packet filtering,
we'll skip these. This brings us to the
last section, which contains the actual
packet-filtering rules. In general, the
syntax for a filter rule can be defined by the following:
action direction [log] [quick] on int [af] [proto protocol] \
from src_addr [port src_port] to dst_addr [port dst_port] \
[tcp_flags] [state]
In PF, a rule can have only two actions: block and pass. As discussed
previously, the block policy affects the behavior of the
block action. However, this can be modified for specific rules by
specifying it along with an action, such as block
drop or block return. Additionally,
block return-icmp can be used, which will return
an ICMP unreachable message by default. An ICMP type can be specified
as well, in which case that type of ICMP message will be returned.
For most purposes, we want to start out with a default
deny policy; that way we can
later add rules to allow the specific traffic that we want through
the firewall.
To set up a default deny policy for all interfaces, put the following
line in /etc/pf.conf:
block all
Now we can add rules to allow traffic through our firewall. First
we'll keep the loopback interface unfiltered.
To accomplish this, we'll use this rule:
pass quick on lo0 all
Notice the use of the quick keyword. Normally PF
will continue through our rule list even if a rule has already
allowed a packet to pass, in order to see whether a more specific
rule that appears later on in the configuration file will drop the
packet. The use of the quick keyword modifies this
behavior and causes PF to stop processing the packet at this rule if
it matches the packet and to take the specified action. With careful
use, this can greatly improve the performance of a ruleset.
To prevent external hosts from spoofing internal addresses, we
can use the antispoof keyword:
antispoof quick for $INT_IF inet
Next we'll want to block any packets from entering
or leaving our external interface that have a nonroutable RFC 1918 IP
address. Such packets, unless explicitly allowed later, would be
caught by our default deny policy. However, if we use a rule to
specifically match these packets and use the quick
keyword, we can increase performance by adding a rule like this:
block drop quick on $EXT_IF from any to <rfc1918>
If we wanted to allow traffic into our network destined for a web
server at 192.168.1.20, we could use a rule like this:
pass in on $EXT_IF proto tcp from any to 192.168.1.20 port 80 \
modulate state flags S/SA
This will allow packets destined to TCP port 80 at 192.168.1.20 only
if they are establishing a new connection (i.e., the
SYN flag is set), and will enter the connection
into the state table. The modulate keyword ensures
that a high-quality initial sequence number is generated for
the session, which is important if the operating system in use at
either end of the connection uses a poor algorithm for generating its
ISNs.
Similarly, if we wanted to pass traffic to and from an email server
at the IP address 192.168.1.21, we could use this rule:
pass in on $EXT_IF proto tcp from any to 192.168.1.21 \
port { smtp, pop3, imap2, imaps } modulate state flags S/SA
Notice that multiple ports can be specified for a rule by separating
them with commas and enclosing them in curly braces. We can also use
service names, as defined in /etc/services,
instead of specifying the service's port number.
To allow traffic to a DNS server at 192.168.1.18, we can add a rule like this:
pass in on $EXT_IF proto tcp from any to 192.168.1.18 port 53 \
modulate state flags S/SA
This still leaves the firewall blocking UDP DNS traffic. To allow this
through, add this rule:
pass in on $EXT_IF proto udp from any to 192.168.1.18 port 53 \
keep state
Notice here that even though this is a rule for UDP packets we have
still used the state keyword. In this case, PF
will keep track of the connection using the source and destination IP
address and port pairs. Also, since UDP datagrams do not contain
sequence numbers, the modulate keyword is not
applicable. We use keep state instead, which is
how to specify stateful inspection when not modulating ISNs. In
addition, since UDP datagrams do not contain flags, we simply omit
them.
Now we'll want to allow connections initiated from
the internal network to pass through the firewall. To do this,
we'll need to add the following rules to let the
traffic into the internal interface of the firewall:
pass in on $INT_IF from $INT_IF:network to any
pass out on $INT_IF from any to $INT_IF:network
pass out on $EXT_IF proto tcp all modulate state flags S/SA
pass out on $EXT_IF proto { icmp, udp } all keep state
As you can see, OpenBSD has a very powerful and flexible firewalling
system. There are too many features and possibilities to discuss
here. For more information, you can look at the excellent PF
documentation available online or the pf.conf
manpage.
|
No comments:
Post a Comment