Firewall setup

From freebsd.xn--wesstrm-f1a.se

Jump to: navigation, search

Contents

Overview

In this section of the tutorial you will configure some basic functionality in your firewall. Further refinements and functionality will be added in the following guides.

The basic functionality

  • will provide NAT to the clients on your internal LAN.
  • will allow (almost) all traffic from your LAN to pass to the Internet.
  • will block all traffic from the Internet to your LAN except for ssh and data that is a response to outgoing connections from your LAN.

pf is a level 3, stateful firewall and a small explanation is in place. Level 3 refers to the OSI model and means that pf will inspect the data packets arriving to it and decide whether it should be passed or blocked based on IP addresses, port numbers, protocol, flags and some other things. It will also keep track of TCP and UDP on level 4. It can not inspect the content of the data packets and make decisions from that - you would need a level 7 proxy for that kind of filtering.

Stateful inspection means that for every packet that is evaluated by the firewall rules and allowed to pass, some attributes of the packet are copied and stored in a separate table called the state table. These attributes include the sequence number and the IP addresses and port numbers of both the sending and receiving end. The next time a packet arrives at the firewall, it will first be checked against this table. If a match is found, the packet will be considered part of an ongoing connection and allowed to pass. Checking packets against the state table is much faster than evaluating the firewall rules every time. It also brings the huge benefit that we only have to create our firewall rules one way since the state will match the packets regardless of its direction.

TCP traffic fits this philosophy very well because it has well defined "states" with a handshake to establish a connection and rules on how to close the connection properly. For security reasons, pf will only create a state when it sees the initial handshake and the state will be removed when the connection closes. If the connection is interrupted, a timer will remove the state after a while even if there has been no controlled closure of the connection.

UDP traffic does not have any states and many people therefore claim you can't keep states for UDP. This may be true but won't prevent pf from keeping states for UDP anyway. pf will simply register a record in the state table when it sees data transferred via UDP and remove the state if no data is transferred over that connection in a certain amount of time.

Instructions

pf.conf

Start by creating the configuration file for the firewall.

# ee /etc/pf.conf

This file should be empty at this point so copy and paste the following content into the file but do not save it yet - you need to make a few adjustments first.

# MACROS

ext_if   = "em1"
int_if   = "em0"
up_limit = "2000Kb"
comp1    = "192.168.69.5"

# TABLES

table <hackers> file "/usr/local/etc/hackers"

# OPTIONS

set loginterface $ext_if
set skip on {lo0 $int_if}
set state-policy if-bound

# TRAFFIC NORMALIZATION

scrub on $ext_if all no-df random-id

# QUEUING

altq on $ext_if cbq bandwidth $up_limit queue {q_def, q_pri}
queue q_def bandwidth 10% qlimit 400 cbq( borrow default )
queue q_pri bandwidth 90% cbq( borrow ) {q_p2p, q_p1, q_p2}
queue  q_p2p bandwidth 10% priority 3 cbq( borrow )
queue  q_p1  bandwidth 20% priority 5 cbq( borrow )
queue  q_p2  bandwidth 70% priority 7 cbq( borrow ) 

# TRANSLATION

nat on $ext_if from ! ($ext_if) to any -> ($ext_if) static-port
rdr on $ext_if proto { tcp, udp } from any to any port 6881 -> $comp1

# PACKET FILTERING

# Block unwanted traffic immediately
block drop in log quick on $ext_if inet proto icmp from any to any icmp-type redir
block drop in log quick on $ext_if from <hackers> to any

# Incoming to router
pass in quick on $ext_if inet proto icmp from any to ($ext_if) keep state queue q_p2
pass in quick on $ext_if inet proto tcp  from any to ($ext_if) port ssh flags S/SA synproxy state (max-src-conn-rate 3/60, overload <hackers> flush global) queue (q_p1, q_p2)

# Incoming to computer 1
pass in quick on $ext_if inet proto tcp from any to $comp1 port 6881 flags S/SA synproxy state queue (q_def, q_p2)
pass in quick on $ext_if inet proto udp from any to $comp1 port 6881 keep state queue q_def

# Global outgoing prioritized
pass out quick on $ext_if inet proto tcp from any to any port http     flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto tcp from any to any port https    flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto tcp from any to any port ssh      flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto tcp from any to any port pop3     flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto tcp from any to any port 1863     flags S/SA modulate state queue (q_p1, q_p2)
pass out log quick on $ext_if inet proto tcp from any to any port smtp flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto udp from any to any port domain keep state queue q_p1
pass out quick on $ext_if inet proto udp from any to any port 27960  keep state queue q_p1

# Global outgoing non-prioritized (default)
pass out quick on $ext_if inet proto tcp from any to any flags S/SA modulate state queue (q_def, q_p2)
pass out quick on $ext_if inet proto udp from any to any keep state queue q_def
pass out quick on $ext_if inet proto icmp from any to any keep state queue q_p2

# Block everything else
block drop log all

The file is divided into 7 sections labeled MACROS, TABLES, OPTIONS, TRAFFIC NORMALIZATION, QUEUING, TRANSLATION and PACKET FILTERING. It's vital that these sections exist in that order for the firewall to work properly. I will guide you through them one at a time - explaining them and letting you make necessary adjustments.

MACROS

Here you define variables you can refer to in the firewall rules.

Instead of hardcoding the names of your network interfaces into every firewall rule, you should use variables instead. If you for some reason have to swap any of the interfaces to another brand in the future, you only have to change its name in one place. Edit ext_if to match the name of your external interface and int_if to match the name of your internal interface.

The up_limit variable is critical for the traffic shaping to work. This value must not exceed your true and sustainable upload bandwidth. Right now the exact value isn't critical but you'll have to measure and adjust this value later in the tutorial.

comp1 refers to a computer on your internal LAN. It will only be used here to show an example of port forwarding. You can call it what you like but your firewall rules of course have to use the same designation and the IP address have to match what you are actually using on your computer.

TABLES

In this section you define tables that you can fill with tens of thousands of IP addresses and IP address ranges. You can the create rules that use these tables as arguments. In this example there is a table called hackers and it will be filled with the IP addresses stored in the file /usr/local/etc/hackers. You have to create an empty file with that name and the quickest way to do that is to type:

# touch /usr/local/etc/hackers

To do this without leaving the editor, you can just open another ssh session to the router. It's a multi user system after all...

OPTIONS

You can tune many aspects of how pf behaves and this is the section for it. Defining the external interface as a loginterface tells pf to collect statistics you can use to make some nice graphs later on. The skip option tells pf to not interfere with data packets on the interfaces listed between the braces. Data entering and leaving these interfaces will always be passed. Your internal LAN can probably be trusted and you don't want to have to write rules for it unless you absolutely have to. state-policy if-bound means that states always have to match the interface they were created on. A floating state policy is the default and useful if you want load balancing between several network interfaces. But on a home router you have no use of it so a more secure policy is chosen in this example.

TRAFFIC NORMALIZATION

It's possible to manufacture data packets with odd combination of flags and other nasty attributes that shouldn't be able to exist under normal circumstances. Such a packet could potentially reveal a weak spot in the router and allow an attacker inside. The scrub directive will try to normalize every packet before letting it through to the firewall rules and it will even drop some packets if they look too weird.

QUEUING

This is where you make the definitions for the traffic shaper. You will need some background information to understand what is going on here.

As described briefly in the wiki's goals, the very first thing that has to be explained and understood is that you can only shape traffic leaving your router. The reason is simple: when data is received by your router it has already crossed your DSL or cable connection and used all its bandwidth. What you do with the received data at that point doesn't change in what order or how fast it was sent to you. Any traffic shaping of your incoming data has to be done at the upstream router at your ISP and for obvious reasons you have no control over it. The traffic shaping described here is therefore focusing on the data you upload.

Graph showing how download (green) almost dies when an upload (blue) starts and no traffic shaping is used.
The same situation with traffic shaping. Fully utilized upload (blue) has no effect on the download (green) starting a little later

You have probably experienced how your download speed drops if you're using your upload heavily at the same time. When you download a file from another computer it's split into smaller packets with a maximum size of 1500 bytes. For every packet you receive, your computer has to send an acknowledgement back to the other computer telling it that the data packet was received correctly. This packet is 40 bytes long and is called a TCP ACK. The other computer will need these acknowledgements fairly quickly, within a few hundred milliseconds at most. If it doesn't receive them promptly it will stop sending data to you while it waits for them to arrive. This will make your download progress in bursts with periods of no data in between making your overall download speed decrease rapidly. So why isn't the TCP ACKs returned fast enough? Modern network equipment (network cards, routers, DSL and cable modems) all have memory inside them acting as a buffer. Your computer can send data much faster than the average DSL or cable connection can cope with it. If the amount of data to be sent is limited, the buffer memory will act as a temporary storage where your computer can deliver the data and continue with other things while the buffers take care of putting the data on the DSL/cable line as soon as bandwidth is available. But if your computer is sending data continuously, these buffers will be filled constantly and they are usually so big they can contain several seconds worth of data combined. If your computer has to send a TCP ACK while the buffers are filled, it will end up last in the buffer chain and it will take several seconds for it just to traverse the buffers before it can even begin it's journey on the Internet. When that happens, the computer you're downloading from will have stopped sending data a long time ago. This phenomenon is much more apparent on an asynchronous Internet connection where there is a huge difference between upload and download speed.

To remedy this situation two things must be accomplished. First me must make sure that data is never leaving the router faster than the Internet connection can handle it. We do this by putting the maximum available upload bandwidth in the altq definition. This is what the up_limit variable is for and why it is so important to get it right. Correctly tuned, this will make sure the buffers, in the router's network card and in the network equipment outside it, will never fill up. The data packets will through this manoeuvre queue up inside the router's main memory instead, but here the operating system can access them and manipulate them and that is the second thing. When the router sees a TCP ACK packet entering the queue it will physically move it and put it first in the queue. Then it will immediately be sent out on your Internet line since the buffers in your upstream equipment are now empty. The only delay is now the ping time to the computer you're downloading from and you can't affect that.

With the queue directive you define how the available bandwidth should be distributed. In this example, two main queues are created first:

  • q_def is the default queue. All bulk traffic like file sharing will be put here and 10% of the bandwidth is allocated to it. 10% might seem small but don't worry, it will be able to use more as you will learn soon.
  • q_pri will be used for the prioritized traffic and it receives the remaining 90% of your bandwidth.

This strict allocation will only be used when both queues are fully utilized. The borrow option makes sure the queues can borrow unused bandwidth from each other. The q_pri queue will not see any real traffic because it's split into three child queues where the actual traffic will be put.

  • q_p2p will be used later to solve one of this wiki's secondary goals, the one were you'll give priority to selected people.
  • q_p1 will handle all your low bandwidth traffic like web browsing, email, chat and online gaming.
  • q_p2 is where your TCP ACKs will be put. This is the main purpose of this queue but it will also serve ICMP, like ping and traceroute.

You may wonder why 70% of your bandwidth is allocated to TCP ACKs and the simple answer is that it probably doesn't have to be. The problem is that it's a bit difficult to calculate exactly how much bandwidth you need. It depends both on the ratio of your upload/download bandwidth as well as the size of the data packets you receive. On my DSL connection, which has 17Mbit/s download and 2Mbit/s upload, I regularly use 25% of my upload bandwidth for TCP ACKs when my download is fully utilized. This is mainly with large packet sizes but with smaller packets I would need even more of my upload bandwidth for the TCP ACKs since the number of packets per second increases. By giving this queue a fairly large piece of the bandwidth you will probably never have any shortage of bandwidth for the TCP ACKs. The borrow option allows these queues to borrow bandwidth from each other too.

As you can see, there's also a priority defined which may look strange since we have split the bandwidth already. But the priority is used in case of an overload and a queue with a higher priority will always be served first if that situation arises.

TRANSLATION

Address translation, also known as NAT, is where your internal IP addresses are exchanged with your router's external address when you communicate on the Internet. With the nat command here you simply tell pf to perform NAT when data pass on the external interface and is not coming from the router itself.

One important thing to know is that NAT is always performed before the firewall rules are evaluated. For incoming packets that means the destination address (which is the same as your router's external address) will be converted to your internal 192.* addresses before checked by the firewall rules and you can use that to control data sent to individual computers on your LAN. For outgoing packets NAT also happens first meaning that your internal 192.* addresses will be converted to the router's external address before the firewall rules can have a look at them. This means you loose the ability to see what internal computer the packets came from. In the example rules above you don't need this ability but if you someday feel you do need it, there is a way of tagging the packets when they enter the internal interface and then use this tag in the firewall rules when checking the packets exiting the external interface.

The static-port option tells pf not to change the source port number when it performs address translation. In most cases this doesn't matter but some applications expect your data to always come from a specific source port and will not function correctly if it's altered by NAT.

Port forwarding is also controlled in this section and the example above shows how to forward port 6881 to a computer on your internal LAN. In this case, both UDP and TCP are forwarded but you could of course settle for just one of them or even another protocol like ICMP or GRE. Remember that every port forward must also have a matching firewall rule that allows the traffic to pass.

Translation in pf is extremely powerful. You can do many nice tricks with it. In a later example you'll be using it's ability to transparently change the destination port on incoming packets.

PACKET FILTERING

You have now reached the actual firewall rules but first a small clarification. /etc/pf.conf is in fact a script that creates the firewall rules. There's not necessarily a 1:1 correspondence between the rows in this file and the true firewall rules because one row here can unwrap to several rules. You also need to understand the quick keyword that appears in every rule. The default behaviour of pf is to select the last rule that matches the data packet currently being evaluated. The quick keyword reverses this behaviour. pf will instead select the first rule that matches the packet and stop parsing further rules at that point. There is a philosophical debate over which method is the preferred one and there are two sides to this as with most other things. I belong to the "first rule match" side and the ruleset above reflects that. What you need to keep in mind because of this is that if you have a general rule allowing all TCP traffic out and you also need to prioritize some of it, like http and mail, those prioritizing rules have to be placed before the general rule. To sum that up - always put specific, detailed rules first and general, broad rules last.

I will now guide you through every rule in the example and describe what they do. You will not learn the details about the syntax here though. The pf.conf manual page is a much better place for that.


Block unwanted traffic immediately
# Block unwanted traffic immediately
block drop in log quick on $ext_if inet proto icmp from any to any icmp-type redir
block drop in log quick on $ext_if from <hackers> to any

The first rules to put in your firewall are those that block any traffic you don't want. There's no meaning in having those packets traverse any further - just get rid of them as quickly as possible. The very first rule blocks any ICMP redirect packets. An ICMP redirect can manipulate your routing table and was a nice thing back in those innocent days when the Internet was young, but today it has become a major security threat. Every decent router should block those packets to protect its users.

The second rule blocks all incoming traffic from the IP addresses stored in the table hackers. This table will primarily consist of IP addresses that have tried to gain access to your router through ssh and failed. You are not interested in having those people poking around so block any connection attempt from them. However, the rule doesn't block outgoing connections to them. They might still provide services that you are interested in so we should still have the option to connect to them. You will see later on how this table can be automatically populated with offending IP addresses.

It's also a good thing to log packets that are blocked and that is accomplished by the log keyword. The log is stored in a binary format in /var/log/pflog and you'll learn later on how to view it.


Incoming to router
# Incoming to router
pass in quick on $ext_if inet proto icmp from any to ($ext_if) keep state queue q_p2
pass in quick on $ext_if inet proto tcp  from any to ($ext_if) port ssh flags S/SA synproxy state (max-src-conn-rate 3/60, overload <hackers> flush global) queue (q_p1, q_p2)

Here are some rules for specific services on the router itself. The first rule here allows any ICMP, like ping and traceroute, to reach the router. Remember that you have already blocked ICMP redirect earlier so those packets will never get here. In this rule you can also see the use of keep state which tells pf to create a record in the state table so that any response will be automatically allowed back out without the need to write a separate rule for it. Keeping state is the default behaviour in pf from FreeBSD version 7.0 but for clarity it's better to include it since there are a few variations on that theme you'll come across later. The packets are also placed in the queue called q_p2 and this might look strange. Didn't I say earlier that you can't prioritize incoming traffic? Well, that is still true but you want the response, to any data matching this rule, to end up in q_p2 and this queue definition will be remembered by the state that is created and any data leaving your router and matching that state will then correctly be placed in this queue. If you don't define a queue here the state will lack a queue definition and outgoing data will end up in the default queue which isn't what you want.

The second rule allows incoming ssh traffic to reach the router. This is a pretty advanced rule so let's break it down a bit.

  • ssh is the name for the service and can be found in /etc/services. You could of course use port number 22 instead.
  • flags S/SA tells pf to only create a state when it sees the initial handshake of a TCP session which is unique to TCP communication. That's why you don't use it in the previous rule since that one was for ICMP. TCP packets that doesn't match a state or aren't part of an initiating connection should be dropped and this is how we do it. It is the default behaviour.
  • synproxy state is a variant of keep state. It adds a certain protection against SYN floods which is a common form of denial-of-service attack nowadays. It can only be used with TCP and pf will act as a proxy during the initiation of the TCP connection and won't let any data through to the actual service until the connection is correctly established with the remote computer. If you for any reason would ever be attacked with a SYN flood, pf can't prevent your download bandwidth from being exhausted but it will protect the services on your LAN from crashing, keeping them available to your internal users at least.
  • max-src-conn-rate will count the number of connection attempts over a certain period of time. If the threshold is exceeded (3 attempts in 60 seconds in this example) the IP address will be put in the table defined after the keyword overload. In this case the table is hackers and any further connection attempts will then be blocked by the rule in the beginning of your ruleset that specifically dropped all packets from IP addresses in the table hackers. This is how you prevent some brute force hackers that try to gain access to your router through ssh.
  • Finally the packets matching this rule are passed into the queue named q_p1 but in this example there's also a second queue defined which needs an explanation. In the IP header is a Type of Service (TOS) field. One of the bits there is named Low Delay and can be set to indicate that this packet should be forwarded promptly. If you have a second queue defined in your rules, any packet that match the rule and has a ToS of Low Delay, will automatically be put in the second queue. This happens to be true for the TCP ACKs you want to prioritize. So simply by defining a second queue you automatically make all TCP ACKs belonging to an established connection to go into the queue with the highest priority while the normal data matching that rule, is still placed in the queue with the lower priority. You have now effectively managed the traffic shaping goal I set up in the beginning of this wiki and this truly shows how powerful pf is.

Incoming to computer 1
# Incoming to computer 1
pass in quick on $ext_if inet proto tcp from any to $comp1 port 6881 flags S/SA synproxy state queue (q_def, q_p2)
pass in quick on $ext_if inet proto udp from any to $comp1 port 6881 keep state queue q_def

These rules are just an example on how to pass traffic to a computer on your internal LAN. The port forward defined under translation needs these rules to work. Both TCP and UDP are forwarded but UDP doesn't have any ACKs so you don't need a second queue for it. Nor does UDP use the flags that TCP does so two rules are created with slightly different syntax. It's possible to combine both protocols in the TCP rule because pf will split them in two separate rules when the config is parsed and is also clever enough to figure out how the UDP rule should look. But once again, for clarity it's recommended to write two rules already in the config.


Global outgoing prioritized
# Global outgoing prioritized
pass out quick on $ext_if inet proto tcp from any to any port http     flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto tcp from any to any port https    flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto tcp from any to any port ssh      flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto tcp from any to any port pop3     flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto tcp from any to any port 1863     flags S/SA modulate state queue (q_p1, q_p2)
pass out log quick on $ext_if inet proto tcp from any to any port smtp flags S/SA modulate state queue (q_p1, q_p2)
pass out quick on $ext_if inet proto udp from any to any port domain keep state queue q_p1
pass out quick on $ext_if inet proto udp from any to any port 27960  keep state queue q_p1

Now time has come to define rules for your outgoing traffic. These rules are for the services you probably want to prioritize and they apply to all computers on the LAN including the router itself.

  • The first five rules are identical except for the services. It's easy to add more services here but make sure to only select services that are low on bandwidth. File sharing does not belong in these rules. Port 1863 is not defined in /etc/services but is the port used for MSN.
  • The sixth rule is identical to the other five except for the extra log. This is for future use. By logging who you send mail to you can automatically have those hosts whitelisted if you want to install some antispam application later on.
  • The last two rules are examples of prioritized UDP traffic, in this case DNS queries and the online game Quake 3 Arena.
  • The modulate state is a variant of keep state that randomizes the sequence number in your TCP connections. This prevents an outside hacker from guessing what operating system you're using since most operating systems have a fairly predictable way of creating sequence numbers. The synproxy state includes this protection too but for your outgoing rules you have no need for SYN flood protection.

For clarity this example has one rule for each service but the first five rules, that are identical, could equally well be combined to something like this:

pass out quick on $ext_if inet proto tcp from any to any port { http https ssh pop3 1863 } flags S/SA modulate state queue (q_p1, q_p2)

Global outgoing non-prioritized
# Global outgoing non-prioritized (default)
pass out quick on $ext_if inet proto tcp from any to any flags S/SA modulate state queue (q_def, q_p2)
pass out quick on $ext_if inet proto udp from any to any keep state queue q_def
pass out quick on $ext_if inet proto icmp from any to any keep state queue q_p2

You're close to the end now. These three rules allow all traffic out if it's either TCP, UDP or ICMP. If it isn't traffic you want to prioritize, it will be allowed to pass with these rules and end up in the default queue, except for ICMP which you always want to put in the high priority queue.

# Block everything else
block drop log all

Nothing else should be allowed to pass through the firewall so the final rule will block and log everything else. Since there are no interfaces, protocols or hosts defined here, it will apply to all that is left.

If these firewall rules look fine you can now exit the editor and save the file. To make absolutely sure the syntax is correct and no errors have found their ways into this file you can and should test it with this command:

# pfctl -nf /etc/pf.conf

Every time you make changes to this file you should test it before reloading the firewall rules. If no errors are reported you can continue.

Crontab

The hackers table will gradually be populated with IP addresses. Unless you prevent it, they will all be lost if you reboot the router. You can use crontab to save the content of the table regularly. This will also be an opportunity for you to briefly see how crontab works. Crontab uses the dreaded line editor vi by default. This is not the time and place to force you to use it so we temporarily change the editor to ee instead.

# setenv EDITOR ee

Now start crontab in edit mode.

# crontab -e

This is root's crontab and it should be empty if this is a new installation. Copy and paste the following line and then exit the editor and save the file.

@daily /sbin/pfctl -t hackers -Tshow > /usr/local/etc/hackers

Crontab should report that it has installed a new crontab and you can check afterwards that the content is correct.

# crontab -l

The command you added will simply list the content of the table hackers and save it in the file /usr/local/etc/hackers, the same file used to populate the table when pf is started. The command will run once a day at midnight.

Adding pf to rc.conf

You need to start pf at boot time of course and this is accomplished by adding the following lines to /etc/rc.conf.

pf_enable="YES"
pflog_enable="YES"

You should be used to handling the editor by now so from now on I will no longer repeat how you start it or that you need to exit and save the file afterwards.

Activating the firewall is a major change in the configuration and you should reboot the router now and monitor carefully that everything starts without errors.

# shutdown -r now

If you see no errors you can shutdown your router. Time has come to install it in it's real place, replacing your old broadband router.

# shutdown -p now

Summary

  • /etc/pf.conf is the configuration file for the firewall.
  • pfctl is the control command for the firewall.
  • crontab -e can be used to schedule tasks that should be run automatically.

Unresolved issues

  • Blocking ICMP redirects could also be accomplished directly in the IP stack by putting icmp_drop_redirect="YES" in /etc/rc.conf which in turn would set sysctl variable net.inet.icmp.drop_redirect to 1. I'm not sure if this is a better way than blocking them in the firewall?

References


Next guide: Going live
Personal tools