Published on (
 See this if you're having trouble printing code examples

Network Filtering by Operating System

by Avleen Vig

You manage a heterogeneous network and want to provide different Quality of Service agreements and network restrictions based on the client operating system. With pf and altq, you can now limit the amount of bandwidth available to users of different operating systems, or force outbound web traffic through a transparent filtering proxy. This article describes how to install pf, altq, and Squid on your FreeBSD router and web proxy to achieve these goals.

Mission Objective

In an ideal environment, there would be no need for bandwidth shaping, OS fingerprint-based filtering, or even Quality of Service (QoS). Several factors in the real world require a change of game plan. Bandwidth is not free, and many ISPs charge customers based on bandwidth usage. Worms, viruses, and compromised systems can all lead to higher bandwidth costs. In the wake of the W32.Slammer worm, which saturated the connections of infected networks, many companies saw their monthly connectivity bills skyrocket due to the worm's traffic.

Filtering your connections based on operating system can go partway to helping keep such situations from running away. While I will focus on filtering traffic from Windows systems, this process can equally apply to BSD, Linux, Mac OS, or a host of other operating systems listed in the pf.os file on your system. This may be especially useful to people running older versions of OSes that have not or cannot be patched but still require some network connectivity.

As an extension of transparent filtering, content filtering is also possible, with tools such as squidGuard allowing children and corporate desktops alike to browse in relative safety.

Tools of the Trade

During my research for this article, several people asked me why I chose to use BSD, pf, altq, and Squid for this task. Other tools come close to providing the required functionality, but none offers to fill the requirements as readily as these. Linux and iptables can work with Squid to provide a transparent proxy but cannot filter connections by operating system. Though other proxy servers exist, Squid is one of the best available today.

It is important to note that OS fingerprinting works only on TCP SYN packets, which initiate TCP sessions, and not on currently established connections or UDP sessions. While this will not be a problem for most systems and network administrators, you may want to pay more attention to your UDP filtering rules.

Installing pf and altq

pf and altq provide packet filtering and bandwidth shaping, respectively. Their relationship is not unlike that between IPFIREWALL and DUMMYNET, where the same rules file configures both pf and altq.

While pf is universally usable, altq requires a supported network card. The good news is that most network cards in common use are supported. Look at the Supported Devices section of man 4 altq to find a list of supported network cards.

Once you've confirmed you have a supported device, add pf and altq to your kernel. You will need to recompile your kernel as described in the FreeBSD Handbook. First, add a few options to the end of your kernel configuration file:

device pf
options ALTQ
options ALTQ_CBQ
options ALTQ_RED
options ALTQ_RIO
options ALTQ_HFSC
options ALTQ_CDNR
options ALTQ_PRIQ

Note: If you are installing altq on a multiprocessor system, add options ALTQ_NOPPC to your configuration before you recompile your kernel.

After you have recompiled your kernel and rebooted, test pf to make sure it installed correctly with the command pfctl -s rules. If you see the error pfctl: /dev/pf: No such file or directory, pf did not install correctly. If you see the error No ALTQ support in kernel ALTQ related functions disabled, pf is working but altq is not. In the latter case, you will still be able to force users through a transparent proxy, but you won't be able to limit bandwidth using altq.

Installing Squid with Transparent Filtering Support

Install Squid with the command:

% cd /usr/ports/www/squid && make config install clean

This will present you with a list of options for compiling Squid. To enable transparent proxy support, select SQUID_PF. You can also select or deselect any other option. I often find SQUID_SNMP useful for gathering and graphing statistics using RRDTool. Once Squid is installed, edit /usr/local/etc/squid/squid.conf. Set at least the options:

http_port YOUR_PROXY_IP:3128
http_access deny to_localhost
acl our_networks src YOUR_NETWORK/24
http_access allow our_networks
visible_hostname YOUR_HOSTNAME
httpd_accel_host virtual
httpd_accel_port 80
httpd_accel_with_proxy on
httpd_accel_uses_host_header on

Replace YOUR_PROXY_IP with the IP address your proxy server will listen on, YOUR_NETWORK/24 with your internal network address range (for example,, and YOUR_HOSTNAME with the hostname you want to show to users in error messages. YOUR_HOSTNAME is not required but extremely useful if you have a cluster of proxy servers sharing a common front end such as a load balancer.

While you can get by with changing only these options, you should spend some time going through the remainder of your squid.conf file and tuning it to your needs. Over time, you may need to tune various other options such as cache sizes or connection timeouts. The Squid configuration file is a behemoth; spending an hour now getting familiar with various options may save you time and trouble in the future.

Mastering FreeBSD and OpenBSD Security

Related Reading

Mastering FreeBSD and OpenBSD Security
By Yanek Korff, Paco Hope, Bruce Potter

Content Filtering with squidGuard

This step is optional for those who wish to do content filtering. I use squidGuard to filter content on my home network, in order to prevent my young children from running into sites I feel are unsuitable. The possible applications for this are endless, as squidGuard offers blacklists for ads, adult content, drugs, gambling sites, hate sites, and more.

Installing and configuring squidGuard

Install squidGuard with the command:

% cd /usr/ports/www/squidguard && make install clean

Next to Squid, squidGuard is very simple to configure--which is probably a good thing, as you spent the last two hours configuring Squid, right?

Copy /usr/local/etc/squid/squidGuard.conf.sample to /usr/local/etc/squid/squidGuard.conf and open it in your editor of choice. If you wish to filter by time of day, read the Configuring squidGuard guide. For now, the filters should always be enabled. Remove the existing source sample-clients block, and create a new block in its place with your own network range:

source localnet {

At the end of the file, replace the existing acl block with a new block:

acl {
    default {
            pass !ads !drugs !gambling !porn all
            redirect http://YOUR_WEBSERVER/cgi-bin/squidGuard.cgi? \

Note: Do not include the line break in the redirect line.

This ACL will deny access to any URLs listed in the ads, drugs, gambling, and porn databases. Other databases are also available; there is a list in the configuration file before the acl blocks from which to choose. Pay particular attention to the redirect statement, which makes reference to a CGI. In the event that a user visits a restricted site, squidGuard will redirect the request to this URL. A sample squidGuard.cgi is available for you to download. Place this on a web server and alter the redirect to change YOUR_WEBSERVER to the name of your web server.

You must also configure Squid to use squidGuard with the redirect_program directive in /usr/local/etc/squid/squid.conf. Open this file one last time and search for the line:

#  TAG: redirect_program

Below this, add the command:

redirect_program /usr/local/bin/squidGuard

Configuring pf and altq

The time has come to bring all the installed programs and changes together by configuring the packet filter and bandwidth shaper. By default, FreeBSD keeps pf's configuration in /etc/pf.conf. The sample configuration file is very well documented. To start, consider an example of filtering with Network Address Translation (NAT). This example assumes an internet connection with available bandwidth of 3Mb downstream and 512Kb upstream:

ext_if        =  "fxp0"
external_addr =  ""
int_if        =  "fxp1"
internal_net  =  ""
proxy_server  =  ""
altq on $ext_if bandwidth 512Kb cbq queue { windows_out, trusted_out }
queue windows_out bandwidth 20%
queue trusted_out bandwidth 80%
altq on $int_if bandwidth 3Mb cbq queue { windows_in, trusted_in }
queue windows_in bandwidth 20%
queue trusted_in bandwidth 80%

rdr on $int_if inet proto tcp from $internal_net os "Windows" to any \
  port www -> $proxy_server port 3128
nat on $ext_if from $internal_net to any -> ($ext_if)

pass out quick on $ext_if inet proto tcp from $proxy_server \
  to any port www keep state queue windows_out
pass out quick on $ext_if inet proto tcp from $internal_net os "Windows" \
  to any keep state queue windows_out
pass out quick on $ext_if inet proto tcp from $internal_net os "unknown" \
  to any keep state queue windows_out
pass out on $ext_if inet proto tcp from $internal_net \
  to any keep state queue trusted_out
pass out quick on $int_if proto tcp from any to $proxy_server \
   queue windows_in
pass out on $int_if proto tcp from any to $internal_net queue trusted_in

The first five lines declare variables that the rule set will use repeatedly. Variables are quite helpful in configuration files. By using them correctly, you can save much time in the future if you need to change IP addresses, network interfaces, protocols, or almost anything else. When you declare a variable in your rules file, access it later as $variable_name.

The six altq configuration lines that follow set the amount of available bandwidth. Bandwidth is controlled on the interface where packets leave the router. Packets going from the network to the internet leave the router on $ext_if, so we set $ext_if to 512Kb. Similarly, packets coming into the network from the internet leave the router in $int_if, so the rules set $int_if to 3Mb. pf understands b, Kb, Mb, and Gb, which represent bits, kilobits, megabits, and gigabits per second, respectively.

The altq line then specifies the type of scheduler to use to queue packets. The example uses cbq. The schedulers decide which order the queues are processed in. Class Based Queuing (CBQ) splits the available network bandwidth between two or more queues, where each queue has packets assigned to it by source or destination addresses, port or other identifiable factor (in this case, operating system). Queues can also have a priority to deal with some packets before others. The OpenBSD Packet Queuing page has more detail on the different types of schedulers.

The end of this line specifies which queues to limit.

The next two lines define the queues themselves that altq will handle. The format is quite understandable and explicitly states how much bandwidth is available to each queue. You can specify this either as a percentage or a fixed amount. If you think of your internet connection as a road, a queue defines how many lanes different packets can travel on. The more lanes they can use, the more data gets transferred. This configuration then repeats for the incoming bandwidth.

The rdr line is the key to the filtering objective. It specifies that all TCP traffic (proto tcp) from the internal network ($internet_net) that comes from a Windows system and is going to any other address on port 80 (os "Windows" to any port www), should redirect to the proxy server on port 3128 ( -> $proxy_server port 3128).

The nat line sets up network address translation, which lets the internal network communicate with the internet.

Notice that each line of the pf rules also ends in either windows_in, windows_out, trusted_in, or trusted_out. These are the four queues set up previously as part of the altq rules; this lets pf know which queues pf and altq should use when processing the packets. These queues are entirely optional. Leaving them out of the pf rules would prevent altq from limiting the bandwidth of any packets matching those rules.

The simplest format of the pf rules is:

<pass|block> <in|out> on <interface> from <src> to <dst>
    [keep state] [queue <queue_name>]

In addition, you can also give port numbers and protocols.

This rule set includes a special rule for unknown OS traffic. Patches to operating systems and IP stacks can change the fingerprint of packets. Filtering all unrecognized traffic through the Windows queue helps avoid future problems.

The last two rules control inbound traffic. Because it is not possible to know whether the destination system is Windows, pf cannot filter the incoming traffic based on an operating system not already using the proxy. In practice, this does not pose a serious problem, as all of the web traffic from these systems is already traveling across the bandwidth-restricted proxy. If the Windows systems run peer-to-peer software such as BitTorrent, that traffic will not go through the proxy. In this case, queue such traffic by giving the port numbers used, as follows:

bittorrent_ports = "6881:6999"
pass out quick on $int_if proto { tcp, udp } from any to any \
  port $bittorrent_ports queue windows_in

Further Reading

Squid configuration guide

squidGuard home page

pf manual

Avleen Vig is a Systems Administrator at Google.

Return to the BSD DevCenter.

Copyright © 2009 O'Reilly Media, Inc.