Packet Filtering


While packet filtering rules come last in /etc/pf.conf, understanding packet filtering is essential to using PF. People use PF without going near address translation, redirection, or any of the other nifty PF features, but packet filtering is the destination for many people. You could say that the entire function of PF is to "filter packets": allow packets with this TTL or higher, sieve out fragments, and so on. In this context, however, packet filtering has a very specific meaning: providing access control for network packets by source, destination, protocol, and other packet characteristics.

What Packet Filtering Doesn't Do

Packet filtering controls network connections based entirely on TCP/IP protocols and protocol characteristics, such as ports. If you want to stop all connections coming from a particular block of IP addresses, packet filtering is your friend. If you only want to allow connections to a particular TCP/IP port, packet filtering will work for you. If you want to allow entrance only to packets with the ECN flag set, but no other flags, PF will do that without even questioning why you would want to do such a weird thing. You can also filter other protocols that operate at a logical protocol layer such as IPSec, SKIP, VINES, and so on, but only on the logical network protocol. If it's a different protocol layer, PF cannot even see it.

One common question is, "How do I use PF to filter based on Ethernet MAC addresses?" The answer is, "You don't." MAC addresses are part of a physical protocol and are in a different layer. You might as well ask how one could use PF to filter dial-up connections. (Also, don't forget that MAC addresses are easily changed, and filtering based on them is more trouble than the security gained in almost all environments.) Mind you, OpenBSD does have a tool to filter based on MAC address, brconfig(8). But it's not part of PF.

Also, PF doesn't know anything about applications or application protocols. If you allow TCP/IP connections to port 25 on a server within your network, you might think you're allowing connections to the mail server on that host. Actually, you're allowing the connection to whatever daemon happens to be running on port 25 on that host! PF does not and never will recognize a SMTP data stream; it only sees that the connection is going to port 25 on that host, and allows it.

At one time, I had a system on the Net running an ssh daemon on ports 25 (email), 80 (web), 110 (POP3), 443 (secure web), and several other popular TCP/IP ports so that I could saunter past whatever packet-filtering firewall I happened to be behind that day. It made a very effective demonstration of exactly why I thought that company's security system could stand improvement.

Packet Filtering Rule Design

We took a brief look at the design of a PF rule earlier this chapter. Let's completely dissect this same rule to identify each piece.

 1 pass 2 in 3 on fxp0 4 proto tcp 5 from any 6 to 192.168.1.1 7 port 22 8 keep state 

The 1 first part of the rule is the keyword that tells PF how to process this particular rule. Every packet-filtering rule begins with either "pass" or "block." We then state if this rule applies to packets 2 entering (in) or leaving (out) the system. This rule applies to a 3 particular interface.

We then have several statements to define the characteristics of the connection that this rule matches — a regular expression for TCP/IP, as it were. This rule applies to 4 TCP connections 5 from any IP address, if the connection is made 6 to the IP address 192.168.1.1 on 7 port 22. Finally, if the rest of the rule matches and the connection is allowed, we 8 keep state for the connection.

Each sort of rule has a slightly different syntax, depending on the type of protocol being filtered. For example, ICMP has no port numbers, so the rules are written slightly differently. Their own modifiers can follow some keywords. We'll see examples of all of these throughout this chapter.

Pass and Block

Each packet-filtering rule begins with one of two keywords: pass or block. Pass rules allow traffic that matches the pattern specified in the rule to continue. The trick lies in specifying the pattern that matches only the packets you want to pass.

Block rules are similar, but they have a wide range of possible responses. What, exactly, should happen when a packet is rejected? PF can give several types of responses, depending on the protocol you are using and your desired behavior.

The default is to silently drop rejected packets, but this can be adjusted on a global level by the block-policy option (see "Blocked Packet Policy"). You can also decide how to respond to blocked packets on a rule-by-rule basis.

Dropped packets are simply rejected without notifying the client. The effects of this vary widely depending on the sort of connection and the type of client. In most cases, the client will wait until the protocol times out and then complain that it could not make a connection or that the connection was lost. While this is the default, you can set it explicitly to override a default policy.

 block drop in all 

You can make your system respond politely to TCP requests and say, "No, I'm not going to accept that connection" by using the "return-rst" response. As it implies, this returns a RST (reset) for any matching attempted connection. This only works for rules that only match TCP packets — if you do not specify the protocol, you will get a syntax error. The following example sends a RST for every incoming TCP connection request:

 block return-rst in proto tcp all 

This only affects TCP packets, however. PF includes the "return" keyword, which returns an RST for TCP connections and an ICMP UNREACHABLE message for UDP requests and silently drops everything else. This is the same as the "block-policy" option.

 block return in all 

You can also specify particular types of ICMP responses for matching packets. This defaults to the standard "port unreachable" ICMP type 3 message, but you can choose to override it and return a more specific ICMP code. The effects of this will vary depending on the error code returned; if you are not conversant with ICMP error messages, either take the default and like it or learn the proper ICMP code to return in each circumstance. Getting this wrong is an excellent way to annonuce that you have a misconfigured firewall, you don't know what you're doing, and would some kind hacker please show you the error of your ways?

ICMP Code

PF Name

Description

0

net-unr

Network Unreachable

1

host-unr

Host Unreachable

2

proto-unr

Protocol Unreachable

3

port-unr

Port Unreachable

4

needfrag

Fragmentation Needed

5

srcfail

Source Route Failed

6

net-unk

Destination Network Unknown

7

host-unk

Destination Host Unknown

8

isolateSource

Host Isolated

9

net-prohib

Destination Network Administratively Prohibited

10

host-prohib

Destination Host Administratively Prohibited

11

net-tos

Network Unreachable for Terms of Service

12

host-tos

Host Unreachable for Terms of Service

13

filter-prohib

Communication Administratively Prohibited by Filtering

14

host-preced

Host Precedence Violation

15

cutoff-preced

Precedence Cutoff in Effect

If you just want the standard "icmp unreachable" message, use the "return-icmp" statement.

 block return-icmp in all 

Alternately, if you know which ICMP code you want to return, you can specify the code name or number in parenthesis after the return-icmp statement. If you to return a polite message telling clients that they may not connect to your network, for example, you might want to use ICMP code 13, "filter-prohib."

 block return-icmp(filter-prohib) in all 

If you're seriously interested in ICMP filtering, uses, and the effect of filtering and returning various sorts of ICMP responses, I recommend you check out Ofir Arkin's "ICMP Usage in Scanning" at http://www.sys-security.com/html/projects/icmp.html.

Default Pass or Default Block

Out of the box, PF uses a "default pass" stance. This is very simple to change with two rules at the beginning of /etc/pf.conf.

 block in all block out all 

Now, nothing will go in or out unless you explicitly create a rule allowing it.

Additional Actions in Rules

You can use a couple of keywords here to specify actions that the PF system should take upon matching a packet to a rule. If you specify the "log" keyword immediately after the "in" or "out," a log message is sent to the pflogd(8) daemon (see "PF Logging"). If pflogd is not running, no log is kept. If this rule includes a "keep state" or "modulate state" statement; only the packet that establishes state is logged. If you want to log all the packets in stateful inspection connections, use the "log-all" option instead of just plain "log." All packets that match that rule, not just the initial packet, will be logged.

 block return in log-all 

You can tell PF to stop processing the rules when a packet matches a particular rule. Remember, rules are processed in order: If you have a rule that allows a connection and a later rule that disallows that connection, you can use the "quick" keyword to prevent PF from ever reaching that later rule. This makes it safe to have a "block all" rule last in /etc/pf.conf, and it can accelerate PF on long rule lists. Remember, all rules are processed in order.

 pass in quick proto tcp from any to $ext_if port 22 keep state 

The "quick" keyword must appear after the "log" or "log-all" keyword or the "direction" keyword if you are not logging. The PF developers discourage use of the quick keyword, as you should be able to achieve the same results with a properly-written ruleset.

Packet Pattern Matching

One of the most intensive parts of PF is the syntax used to describe packets. The next several terms in a rule describe particular sorts of packet by protocol, port, direction, and various other characteristics. PF will compare each arriving packet to these rules. If the rule matches the packet description, it will be treated as you decide. These terms must be specified in the order presented here. A term can be skipped, but the terms that appear must be in order.

Interface Matching

The "on" keyword describes an interface that this rule applies to. You must specify an interface, either explicitly or with a macro. If you want a rule to match every interface on the machine, you can use the "all" interface name. Here we stop all traffic coming in on interface fxp0 and allow traffic out on whatever interface is represented by the macro $external_if:

 block in on fxp0 pass out on $external_if 

Address Families

You could list an protocol address family, either "inet" for IPv4 or "inet6" for IPv6, to state that this rule only applies to packets in that type of address. The inet address family includes the IP, ICMP, TCP, and UDP protocols. The inet6 family includes the IPv6, ICMPv6, TCP, and UDP protocols. While TCP and UDP are common to both families, IPv6 and ICMPv6 are extremely different from IP and ICMP. The following rules allow standard IP traffic but deny IPv6 traffic:

 pass in inet block in inet6 

Network Protocol

In addition to the inet and inet6 families, PF can recognize almost any network protocol by number or name. The "proto" keyword tells PF to filter by protocol. Network protocols can be listed by name, as given in /etc/protocols, or by protocol number. For example, here is a rule that allows SKIP traffic (protocol 57) to pass through the packet filter:

 pass in proto 57 

Obviously, this functionality somewhat overlaps the inet and inet6 statements — you could have a statement that explicitly allowed the IP, ICMP, TCP, and UDP protocols.

Address and Port

The next set of statements is the most commonly used type of packet-filtering rule, identifying source and destination addresses and ports. The syntax is very simple:

 from source-address port source-port to destination-address port destination-port 

Addresses here can be specific IP addresses or netblocks in CIDR notation (such as /24, /22, and so on, as discussed in Chapter 8). You could also use the keyword "any," meaning "any address." For example, this rule allows connections from anywhere on the Internet to the IP address 192.168.1.5:

 pass in from any to 192.168.1.5 

You could also specify an address as "no-route," meaning "any address which is not currently routable." If your OpenBSD machine does not know how to get to an IP address, the no-route address matches. It's usually a good idea to drop packets for unreachable hosts — even if they are legitimate, your system cannot respond to them. (This can only happen on systems without a default route, of course.) See Chapter 8 for a discussion of routing.

Interface and host names can also be used in the address space. PF will automatically translate these to IP addresses when loading the rules. Here, for example, we are allowing the machine 192.168.1.200 to connect to anything on the fxp0 interface:

 pass in from 192.168.1.200 to fxp0 

When you activate these rules and check them within PF, you'll see that the live rules contain the actual IP addresses on interface fxp0.

Ports can be specified as numbers or as names as found in /etc/services. Port numbers exist only in the TCP and UDP protocols, so when you specify a port you must specify the protocol being used. Use of the port keyword is optional in both source and destination; if you don't care which port a connection is coming from or going to, do not include a port statement. Here, we tighten the previous rule to specify that only TCP connections to port 22 are permitted:

 pass in proto tcp from any to 192.168.1.5 port 22 

Remember, this is not the same as only allowing SSH connections! PF doesn't know what application protocol you're using; it only knows about the TCP/IP port you are permitting.

If you know the source port of a connection, you can easily specify that on the rule line. For example, many old firewalls make all of their outbound DNS queries from UDP port 53. Here, we allow the nameserver running on our PF-protected host to make queries of our ISP's nameserver. We're also specifying that packets may return back to this service:

 1 pass out proto udp from 192.168.1.5 port 53 to 10.15.3.8 port 53 2 pass in proto udp from 10.15.3.8 port 53 to 192.168.1.5 port 53 

Note that we specify 1 "pass out" in the first rule so that packets can leave this system and 2 "pass in" on the second rule so that the responses may come back. As DNS queries run over UDP, we restrict this to UDP queries only. (In a little bit, we'll see how to write this sort of rule more securely and as a single line with stateful inspection.)

User and Group

PF allows you to filter by the user and group of the socket opened by the program trying to access the network. This functionality can only work when connections originate with or terminate at the PF device; you cannot use this on a network-protecting firewall to protect hosts within the network. In other words, if you're running a web server on your OpenBSD machine, you can use PF only allow connections to port 80 if the web server user has opened the connection, but if you're using your OpenBSD machine to protect another web server the user and group ID will not work. Also, this only works with TCP or UDP protocols; if you try to filter other protocols such as ICMP by user, the restriction will be ignored.

A "user" or a "group" keyword followed by a name or UID/GID activates this functionality, such as:

 user username 

Add this after your packet address and port statement. For example, here we allow anyone in the wheel group to make outbound SSH connections from this machine:

 pass out on fxp0 proto tcp from any to any port 22 group wheel 

Be careful restricting user functionality; you might be surprised what sorts of connectivity a shell user might need! If you don't have interactive shell users, and none of your users should be running programs on the server, you might consider a rule such as this:

 block out on fxp0 proto tcp from any to any group customers 

In the event of a user account compromise, or if some FTP-only customer even manages to break out into a shell, he won't be able to initiate any outbound connections from your system. This will seriously restrict the intruder's ability to use your server to attack other servers, which is always nice. The downside is, he'll have time to spend trying to break root on your server instead. Are you certain all of your programs are secure? [3]

Packet Flags

PF allows you to filter based upon TCP packet flags. A packet flag is a flag that indicates the state of the connection that the packet claims to be a part of. For example, when a client sends a SYN packet to a server, that packet includes a flag that says "I'm a SYN packet." The returning SYN+ACK packet will have two flags set, the SYN flag and the ACK flag. TCP packets have many different possible flags, each with its own purpose.

If you're going to start complex filtering based on TCP flags, you must be completely intimate with TCP/IP. You will see many sites on the Internet that claim that if you filter on such-and-such flags, you will get a particular useful behavior. Research these claims carefully. The problem is that every TCP/IP stack has been modified by vendors for their own purposes [4] and behaves in a slightly different way. You may find a description of a neat filtering-on-flags trick that can prevent port scans from working, for example, but then discover that some of your desktop systems crash whenever they try to access the Internet. If you want to really get into flag-based filtering, get a copy of all three volumes of TCP/IP Illustrated and don't just read it; assimilate it, live it, commune with it, become one with it. Ambitious flag-based filtering is of questionable efficacy, and (in my opinion) the single part of firewall construction most laden with uncertainty, error, and superstition.

Simple flag-based packet filtering help manage multi-interface firewalls. If you want to filter connections through one machine to your sales office, your financial network, your DMZ, the factory in Shaolin China, and the public Internet, the only way to differentiate traffic flows is on which flags are set in each packet. PF recognizes the following flags.

Flag

Name

F

FIN

S

SYN

R

RST

P

PUSH

A

ACK

U

URG

E

ECE

W

CWR

We will discuss certain precise combinations of flags that can be used in various situations. Using other combinations of flags leaves you on your own.

The general syntax of a flag statement is:

 flags 1 set / 2 list 

This rule matches a packet that has certain flags 1 set out of a 2 list of flags. One popular example is:

 flags S/SA 

PF will check the "S" and "A" flags on every packet. If the "S" flag is set, and the "A" flag is not set, this rule matches. This rule doesn't care if, say, "R" or "E" or any other flag is set, because it's not in the list. This is a typical "connection creation" rule; packets that are sent by one machine requesting access to a another have the SYN flag set, but the ACK flag not set. This is commonly referred to as a "SYN packet."

You can also specify lists that cannot be set, such as this:

 flags /SFRA 

A matching packet has none of the flags S, F, R, and A set.

If you're only concerned about one flag, you can specify it on both sides of the slash. Here, matching rules have the SYN flag set, and the other flags are irrelevant:

 flags S/S 

Add the flag statement after the address and port statement in your PF rule. Here, we are allowing any machine on the Internet to request a TCP connection to our web server:

 pass in proto tcp from any to 192.168.1.5 port 80 flags S/SA 

If the packet has the SYN flag set, and the ACK flag is not set, the connection will be allowed. If both the SYN and ACK flags are set, this rule does not match.

Filtering on Flags and Port Scanners

In most cases, filtering based on flags is just over-engineering with very little net benefit. One popular use for flag-based filtering is to confound network scanners such as nmap that use the characteristics of an operating system's responses to bad packets to identify an operating system. While this is of questionable efficiency — the intruder will just use some other method to identify the operating system — PF can provide some basic protection against these sorts of probes with the following rules:

 block in quick proto tcp all flags SF/SFRA block in quick proto tcp all flags SFUP/SFRAU block in quick proto tcp all flags FPU/SFRAUP block in quick proto tcp all flags /SFRA block in quick proto tcp all flags F/SFRA block in quick proto tcp all flags U/SFRAU block in quick proto tcp all flags P 

ICMP Types and Codes

Much like TCP flags, ICMP has types and codes. Despite what you might hear, it is not proper to unilaterally block ICMP. ICMP has a vital role to play in many sorts of connections. Fortunately, most ICMP needs are handled very easily by the provided block policies.

You can allow particular types of ICMP traffic with the "icmp-type" keyword.

 icmp-type type code code 

For example, here's how you allow incoming ping requests. This doesn't allow the responses, but it will let the requests into your system. (Adding stateful inspection would let the transaction complete, but we haven't discussed stateful inspection yet.)

 pass in inet proto icmp icmp-type 8 code 0 

Everything I said about filtering by TCP flags applies doubly to filtering ICMP. You must really understand TCP/IP before you start playing with ICMP filtering.

IP Options

Unless you specifically allow packets containing IP options, PF will block them. The "allow-opts" keyword tells PF to accept matching packets if they contain IP options.

With the packet description language presented here, you can describe almost any TCP/IP packet and give basic descriptions of packets of most other protocols. While passing and blocking is theoretically all you need to do with packets, PF includes some options to make managing packets easier.

Type of Service

Every TCP packet includes a Type of Service field. The setting of this field varies with the application protocol; each application requests a particular Type of Service setting. You will need to study your particular application to determine what Type of Service your application uses (or, just drop a packet sniffer on the network and see what it says). PF can match packets on Type of Service with the "tos" keyword. Here we allow an application protocol to pass when it has a Type of Service of 0x10, but reject it when it has a Type of Service of 0x08:

 pass  out on fxp0 proto tcp from any to any port 88 tos 0x10 block out on fxp0 proto tcp from any to any port 22 tos 0x08 

Applications can change the requested Type of Service after the connection is set up, making filtering based on Type of Service very tricky. In "Bandwidth Management," we'll see an example of using Type of Service to offer different service levels to different parts of an application.

Labels

Your /etc/pf.conf file should certainly be well commented, with labels for each major section describing the desired effect so your coworkers can have some idea what was going through your mind when they get paged at 3 a.m. to diagnose a problem. But you can also use labels visible to pfctl(8). These labels have no operational effect whatsoever, but can help you manage your system. Labels can be useful for parsing rule output for billing purposes, for example. To use a label, add the "label" keyword and a label name to the end of your rule.

 pass out proto tcp from any to any port 22 1 label 2 ssh 

We have the label at the 1 end of the rule and a simple 2 label name. In addition to these simple text names, you can use macros within label names. PF provides several macros for label names. When you use a macro, the label name must be in double quotes (").

$if

Interface name

$srcaddr

Source IP address

$dstaddr

Destination IP address

$srcport

Source port description

$dstport

Destination port description

$proto

Protocol name

$nr

Rule number

These macros are parsed when the rules are first loaded; you can't dynamically build a list of labels from the packets that pass through your firewall. If a macro references a field with an "any" value, the macro also shows up as "any." For example, our sample label rule above doesn't list an interface name. Let's use the $if macro in the rule name:

 pass out proto tcp from any to any port 22 label "$if:ssh" 

When you load the rules and run them, the label will show up as "any:ssh." The rule doesn't mention an interface, so the rule applies to any interface, and the label macro knows it. Macros for IP address and port are most useful when applied to macros for groups of servers or ports: Each generated rule will be separately labeled.

 pass in proto tcp from any to $Webservers port 22 label 1 "ssh:$dstaddr" 

This will generate a separate rule for each web server, each with its own label named after the IP address of the web server.

One thing to note is that you can use the same label multiple times within a rule. I could put the string "label ftp" in several FTP-related rules, and they would each show up as a separate lines with the same name when viewing the statistics, like this. Or, if I have a rule with brackets to allow multiple protocols or addresses, it will expand to have multiple rules with the same label. For example, this rule creates multiple labels called "web":

 pass out proto tcp from ($ExtIf) to any port {80, 443} label web 

If you want separate labels for these, try combining the label with the macro for the destination port, like this:

 pass out proto tcp from ($ExtIf) to any port {80, 443} label "web:$dstport" 

This would create two labels, called "web:80" and "web:443."

Anchors and Named Rulesets

PF supports the ability to add attachment points for rules. You can define an attachment point, or anchor, so that when a packet reaches the anchor point in the rules list, the rules in that attachment point are interpreted. At first glance, this seems rather pointless — why not just put the rules you want to have executed where you want them executed, instead of using an attachment point and some fancy redirection?

The clever bit is that outside programs can add rules into anchor points! Your IDS can add rules to block an IP address when it detects an attack, although with the current state of IDS technology this tends to block legitimate activities too frequently. Your mail server can use this to block known spam sources (see spamd(8)). Your authentication system can add rules to your firewall when a user logs in. We discuss OpenBSD's integrated authpf(8) features in Chapter 19, which makes heavy use of anchors. Most of these functions require a bit of programming, or at least shell scripting.

PF supports the following types of anchors.

nat-anchor

An anchor for NAT rules

rdr-anchor

An anchor for redirection rules

binat-anchor

An anchor for bi-directional NAT rules

anchor

An anchor for packet-filtering rules

In pf.conf, an anchor appears like this:

 anchortype anchorname 

For example, an anchor called "intrusion-detection" would appear as:

 anchor intrusion-detection 

You cannot put anchors within anchors.

We'll look at an example of anchor use in Chapter 19.

[3]Of course, if the user account can use CGI programs, the intruder could just run a program with the permissions of the "www" user. Isn't security fun?

[4]I have been told that this purpose is not "Make Michael's life difficult," but am still awaiting hard evidence to back this assertion.




Absolute Openbsd(c) Unix for the Practical Paranoid
Absolute OpenBSD: Unix for the Practical Paranoid
ISBN: 1886411999
EAN: 2147483647
Year: 2005
Pages: 298

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net