Firewalling

From securityrouter.org, an OpenBSD-based firewall
Jump to: navigation, search

The firewall is a stateful layer 3 (e.g. IP) to layer 4 (eg. TCP) packet filter. Together with proxies and VPN flows they are what defines the router's security policies. The firewall is dual-stack by default, meaning that all rules that doesn't explicitly specify an address family works for both IPv4 and IPv6.

Clear-text configuration

The firewall can, just like the rest of the product, be configured by both the graphical user interface, and in clear-text. It is based on, and slightly extends, OpenBSD's PF which makes the pf.conf manual page a great source of information. Not every possible feature is interpreted by the graphical editor.

Conceptual

There are a few fundamental concepts which are good to know about:

  • The firewall's ruleset is evaluated every time a packet goes in or out of an interface
    • As a consequence, a packet passing through the firewall is matched against the ruleset two times; when it arrives (in) and leaves (out)
  • Interfaces can be hardware ports, VLAN interfaces, etc.
  • The last matching rule wins, unless a rule is marked by the quick keyword (the graphical web interface makes rules quick by default)
  • Rules are dual-stack by default, meaning that pass on lan will allow both IPv4 and IPv6 traffic

States

Most firewalls today are "stateful". A state may be though of as an active connection or session. A stateful firewall keeps track of all active sessions in a state table. Apart from filtering bad packets that are not associated with a connection (eg. TCP connection), states also help speeding up the evaluation of packets. When a consecutive packet arrives, it's first matched against the state table (based on the protocol, addresses and ports). If an active session is found, the packet is allowed without further evaluation. The state table also keeps tracks of IP translations such as nat-to and rdr-to.

That being said, the ruleset is only evaluated for a packet if no matching state is found. See the flush states section.

This is a example of a (minimal) state table.

all tcp 192.168.2.12:7000 <- 192.168.1.3:57152       ESTABLISHED:ESTABLISHED
all tcp 10.2.0.207:52370 (192.168.1.3:57152) -> 192.168.2.12:7000       ESTABLISHED:ESTABLISHED
all tcp 212.37.18.209:993 <- 192.168.1.3:57165       ESTABLISHED:ESTABLISHED

If you for some reason don't want to create states (and thus evaluate every packet) for a particular rule, use the no state modifier.

Increasing the state limit

For busy firewall with lots of RAM, the default state limit might be too low. Keep in mind that some firewall models have very little RAM. The current limit can be displayed using the command

pfctl -sm

You can monitor the state usage by looking at the firewall-state-entires graph, and increase it (to whatever your RAM allows for) using the set firewall keyword, like:

firewall {
	set limit states 1000000
	...

Adjusting timeout values

You can show the current TCP/UDP timeout values by using the command

pfctl -s timeouts

To adjust these values you can use the set firewall keyword, like:

firewall {
	set timeout { udp.first 60, udp.single 30, udp.multiple 60 }
	...

Flushing states

Changes to the firewall ruleset does not implicitly flush the state table. Therefore, changes in configuration only applies to new connections per default. If you are having problems with a newly created or modified rule you believe should be working, but don't, you could suspect the state table (the ruleset is only evaluated for a packet if no matching state is found). You can kill individual states, but most users will probably just flush the entire table using the firewall flush CLI command or using the "Flush states" button on the firewall page (might be hidden in an actions menu).

Logging

All blocked packets are logged by default. The CLI command show firewall log shows the log. If no rule or state is matched for a packet, it's logged with the label "Block all". If you add your own block rules, you may or may not want to enable logging (as logging has a small performance penalty). A good practice is however to do so, if you believe you will need to troubleshoot the rule in the future. If available, the rules' label is logged with the packet. The label may also be used to filter which entires are seen in the log.

The log keyword is used to enable logging on rules. pass rules may also log traffic, however, for stateful rules, only the first packet will be logged (as the rest of the session will be allowed by the state table).

firewall {
   block out quick log proto tcp to port 25 label block-smtp
}

In the CLI, blocked packets can be viewed and filtered using tcpdump[1] syntax:

> show firewall log label block-smtp
> show firewall log port 25
> show firewall log host 1.2.3.4 and port 25

An example log entry as displayed from the CLI:

Apr 11 11:16:43.072920 label block-smtp: rule 3/(match) block out on em0: 10.2.0.208.40619 > 1.2.3.4.25: ...

Logging to syslog

As detailed in OpenBSD FAQ, logging decoded firewall logs (using for example tcpdump) to syslog has a few disadvantages. First of all, it creates a denial-of-service target. Secondly, tcpdump has a less than perfect security history (OpenBSD has however added privilege separation to it). If you understand its implications, but still need to enable it, you can do so using root access and skeleton files by adding the text below to /cfg/skel/rc.local

tcpdump -l -q -n -e -i pflog0 action block | logger -t pf -p authpriv.info &

Groups

For enhanced readability and flexibility, it's encouraged to perform filtering on interface groups (e.g. "wan") rather than the interfaces' device name (e.g. "em0"). This example, taken from the groups page, demonstrates groups:

interface em0 {
   group "wan"
   address 212.16.179.42/30
   route default 212.16.179.41
}
interface em1 {
   group "lan"
   address 192.168.1.1/24
}
firewall {
   pass out on wan nat-to (wan) label outgoinginternet
   pass on lan label localnetwork
}

Rulesets

A ruleset consists of a group of rules. By default rules are added to the global ruleset firewall {, however every interface also has a configurable ruleset interface em0 { firewall { where rules may be added. The evaluation order of rulesets are:

  • Automatic rules
  • Interfaces rules
  • Global rules

Interface rules

An alternative to interface groups, which can be useful in some scenarios, are interface rules. The firewall scope is simply placed in the interface's scope; thus inheriting the interface's name while providing a natural grouping. They may come handy when there is a benefit of tying the rules to the interface. They are grouped accordingly in the graphical user interface. Below is an example of a self-contained IPv6 tunnel:

interface gif0 {
   group "ipv6tunnel"
   address 2001:469:32:359::2/128 2001:469:32:359::1
   tunnel 82.214.54.212 216.66.80.90
   route default 2001:469:32:359::1
   firewall {
      pass out
   }
}

and this is an example with servers on VLAN interfaces (note that no rule is needed no the parent interface):

interface em1 {
   interface vlan10 {
      address 215.122.12.9/29
      firewall {
         pass in
         pass out proto tcp to 215.122.12.10 port { 25 587 }
      }
   }
   interface vlan11 {
      address 215.122.12.17/29
      firewall {
         pass in
         pass out proto tcp to 215.122.12.18 port { 25 587 }
         pass out proto tcp to 215.122.12.19 port { 110 143 }	
      }
   }
}

another common usage for interface rules are to "skip" (disable the firewall) on certain interfaces.

interface gif0 {
   firewall {
      skip
   }
}

Automatic rules (built-in)

Many local services automatically add rules to the "Automatic"-rulset in order to function as expected. The pptp-proxy automatically add rules to take over the PPTP traffic on defined interfaces. And the PPTP/L2TP service add pass rules inbound on wan for the appropriate protocols and ports needed. Another noticeable service is the ipsec { which allows all traffic on tunnels by default (on the enc0 interface). However if the rule isn't required to be quick it may be overridden by a user-defined rule. So to undo the IPsec behavior. Add a firewall rule

firewall {
   ...
   block log on enc0
   ...
}

Packets paths and chaining

Since mosts packets goes in on one interface (e.g. WAN) and out on another (e.g. LAN) it's often desired to create chained rules. Depending on how permissive versus denying the configuration is, different methods produces the smallest, most clear configuration. For example, if we want to restrict who is allowed to access a certain network, the received-on keyword is very useful. In the example below, the "filtering" is performed "out" on the LAN interface:

interface em1 {
   group "lan"
   ...
}
interface em2 {
   group "admin"
   ...
}
firewall {
   pass in on admin
   pass out on lan received-on admin
   ...
}

Another scenario when it is useful to perform filtering "out" on an interface is when multiple sources are going to have the same access, as in this example where both WAN and LAN should access the DMZ on just a few ports:

interface em0 {
   group "wan"
   ...
}
interface em1 {
   group "lan"
   ...
}
interface em2 {
   group "dmz"
   ...
}
firewall {
   serverports = 25 53 80 110 143 443 587 
   table <servers> { 212.37.18.198 212.37.18.199 }
   pass in on wan to !self
   pass in on lan
   pass out on dmz to <servers> port $serverports
}

Macros

Macros are most commonly used to name and group services (ports) and hosts (addresses). In the graphical web interface the macros are listed in a table below the rule table. Macros are implemented without any contextual limit and may be used to replace virtually anything in the firewall grammar. And since the replacement are done before the ruleset is evaluated, it all comes down to that the result must be a valid configuration. Reserved keywords may not be used in macro names such as ("pass" or "log") unless typed within quotes. Below is a clear-text example of how macros can be used.

firewall {
  web_ports = 80 443
  web_server_a = 192.168.0.10
  web_server_b = 192.168.0.11
  table <web_servers> { $web_server_a $web_server_b }
  pass in on wan proto tcp to port $web_ports rdr-to <web_servers>
}

Ports and services

The most suitable way to create "services" (TCP/UDP port definitions) are using macros. Several ports can be listed separated by comma, space or newline, and the operators in the table below can be used to create port ranges.

Operator Function Example  Comment
= Equal =21 This is the default operator, which is used if an operator is omitted
!= Unequal !=21 0 to 20 and 22 to 65535
< Less than <1000 0 to 999
<= Less or equal than <=1000 0 to 1000
> Greater than >1000 1001 to 65535
>= Greater or equal than >=1000 1000 to 65535
: Range including boundaries 1000:2000 1000 to 2000
>< Range excluding boundaries 1000><2000 1001 to 1999
>< Except range 1000<>2000 0 to 999 and 2001 to 65535

To create a service containing two TCP/UDP ports in the graphical web interface, follow the steps below:

  1. Go to the page Network > Firewall
  2. Locate the macro list (below the rules) and press the plus icon
  3. Click the "new_macro" text and change the name to something like "web_ports"
  4. Click the column next right to the name (the value) and enter something like "80 443"
  5. Use the macro, for example by by clicking on a rule's "Service" column and use it as "Destination port"

Protocols

When building firewall rules with or without $macros it's important to not confuse protocols (eg. ICMP, TCP and UDP) with ports (eg. 80 and 443).

If you were to create a service for PPTP (TCP port 1723 and GRE), two different rules are needed.

firewall {
   server = 192.168.0.10
   pass on lan
   pass in on wan proto tcp to port 1723 rdr-to $server
   pass in on wan proto gre rdr-to $server
}

Tables

Tables are used to group addresses and networks together into a common structure. Tables are very fast in term on address lookup, regardless if the address is matched within a subnet (due to it's optimized internal data structure). It's possible exclude certain hosts from networks (using the ! prefix unary operator). Addresses may be used in rules where one or more addresses are expected (such as after "from", "to" or "rdr-to").

firewall {
  head_office = "123.123.123.0/24"
  sales_dep = 123.123.123.55
  halon_support = 213.213.213.213
  table <admins> { $head_office !$sales_dep $halon_support }
  pass in on wan proto tcp from <admins> to port $mgmt_ports
}

To name individual hosts or network within a table, use macros and put multiple macros in the table.

firewall {
  web_serverA = 192.168.0.10
  web_serverB = 192.168.0.11
  table <web_servers> { $web_serverA $web_serverB }
}

Redirection and port forwarding

Redirection (or port forwarding) is used to change packets' destination, possibly depending on conditions such as ports. It's commonly used to pass traffic through NAT (network address translation) to an internal server. On the graphical web interface's firewall page, there is a port forwarding button that simplified the creation of port forwards in the most simple cases (it makes a lot of assumptions). When working with the clear-text configuration, the rdr-to modifier should in most cases be used on inbound rules (pass in or match in).

firewall {
   pass on lan
   pass in on wan proto tcp to port 80 rdr-to 192.168.0.10
}

For protocols supporting ports (eg. TCP and UDP) it's possible to change the destination port as well.

firewall {
   pass on lan
   pass in on wan proto tcp to port 80 rdr-to 192.168.0.10 port 8080
}

Forwarding to an FTP server

If you have an FTP server behind NAT, that you want to make accessible from the Internet (for example), you may find that passive mode doesn't work. It's because of the way FTP works, and all decent FTP servers have options for passive mode behind NAT. In addition to port 21, a passive port range should be forwarded to the server:

firewall {
   pass quick on lan label "Open LAN"
   pass in quick on wan proto tcp to port { 21, 5000:5100 } rdr-to 192.168.0.10 label "FTP Forward"
}

and the passive port range and passive address should be configured in the server. If you are using vsftpd, these options (where EXTERNAL_IP is substituted with the external IP address that the server is readable on) should do:

pasv_address=EXTERNAL_IP
pasv_max_port=5100
pasv_min_port=5000

Reflection

Trying to access a service that is being port forwarded from the LAN (inside of NAT) on its external (WAN) address will not work (because of the way that NAT works). This is sometimes referred to as redirect reflection, split-horizon or the NAT loopback problem. There is a chapter about this in OpenBSD's FAQ[2].

The most simple way to solve this, is by using (or introducing) an internal (split horizon) DNS server. In case you are using a Microsoft Active Directory, you can use it as DNS server. Simply override the domain name, so that it points to the server's internal IP. Although not recommended, you can even use the firewall's built-in DNS cache for this purpose.

Other vendors solve this by a combination of internal redirects and NATs. We don't recommend this method, because it adds complexity, overrides the natural routing, and hides the client's address from the server. If you understand these issues, but still like to do so, use the example below:

firewall {
   pass in quick to $server_external_ip port { 80 443 } rdr-to $server_internal_ip
   pass in quick to $server_external_ip port 3389 rdr-to $server_internal_ip
   pass out quick on lan received-on lan nat-to (lan:0)
   ...
}

Make sure the rules are placed above any "pass quick on lan" or similar rules, that would otherwise win.

Load balancing redirection

If you want to do static load balancing (instead of the built-in load balancing functionality) it's possible to redirect to an address table. For more information, see the pf.conf manual[3].

firewall {
   table <servers> { 192.168.0.10 192.168.0.11 }
   pass on lan
   pass in on wan proto tcp to port 80 rdr-to <servers>
}

Redirect outbound traffic

If you want to redirect outbound traffic to another host except the intended one (eg. force all outbound DNS queries to a specific DNS server). This works for both UDP and TCP.

firewall {
   pass in quick on lan proto udp to port 53 rdr-to 8.8.8.8 tag dns
   pass out quick on wan tagged dns nat-to (wan:0)
}

Network address translation (NAT)

NAT allows for one (using binat-to) or many (using nat-to) hosts to be translated into one or many addresses. Most commonly, internal (private) addresses are translated into an external (public) IP address. In other words, to share a WAN address with many clients on a LAN. Other uses exist as well, including on tunnels (eg. gif) to hide the internal network so that no additional routes has to be added.

NAT should normally not be used with IPv6, since the problem NAT solves doesn't exist for IPv6 addresses.

In the default configuration, an outbound NAT rule is included:

firewall {
   match out on wan inet nat-to (wan:0) label "Outbound NAT"
   ...
}

Problems with NAT

Some network protocols are incompatible with NAT (some precede NAT, or were just poorly designed), and therefore needs special handling using proxies, eg. PPTP, FTP and SIP.

Session initiation protocol (SIP)

Most SIP (VoIP) implementations have problems traversing NAT outbound. This is due to a security feature in NAT to randomize the source port (in order to improve security and prevent source port conflicts). However this feature may be turned off using the "static-port" directive.

firewall {
   match out on wan inet nat-to (wan:0) static-port label "Outbound NAT"
   ...
}

Many "home" and "small businesses" (SMB) routers have this feature off by default, in order to "make it work" with no regards to security.

IPsec filtering

IPsec tunnels are encapsulated (aggregated) on the enc0 (an encapsulating interface). By default there is an automatic rule that allows all traffic on enc0; there is still a need for pass out rules on the appropriate interfaces. Conceptually IPsec flows (from/to networks) are "firewall policies" in a sense; so if you don't need any more fine grained policies, this default behavior should be fine. Automatic IPsec tunnels have a "firewall tag" that may be applied and used for filtering.

In some cases it's desirable to perform filtering not only on the devices's port, but also on enc0. Below are two examples that overrides the default behavior.

Address-based filtering

"Undo" the automatic allow rule on enc0, and only allow access to Windows RDP on a local machine.

firewall {
   block log on enc0
   pass in on enc0 proto tcp from 10.0.0.0/24 to 192.168.10.1 port 3389
}

Policy-based filtering

In case you have a ruleset where you restrict traffic out on interfaces. You should allow all traffic in, and conditionally outbound.

firewall {
   block log on enc0
   pass in on enc0
   pass out quick on lan to 192.168.0.10 received-on enc0
}

Note: By default there is an "open LAN" rule that allows everything going in and out on the LAN interface. Therefore, the first (address-based) example is normally the most suitable.

Policy routing

Let's say you have two internet connections, and want to make traffic from different internal networks use different internet connections (without completely separating the networks using routing domains). In that case, you can use policy-based routing; the route-to firewall keyword. In the example below, the "primary" WAN (em0) is used for the firewall itself, and for computers on the "dmz" (which also have a port forwarding to them). The "secondary" WAN (em1) is used for clients on the "lan". The routing table says that everything should be routed to the (fictive) gateway 1.1.1.1 on the primary WAN, but the second last firewall rule overrides this for clients on the "lan", saying that they should use the (once again fictive) gateway 2.2.2.1 on the secondary WAN.

Note: When using the route-to keyword you need to use the real name of the interface, you can't use an interface group as this is ambiguous and therefore not allowed.

interface em0 {
   group "primary"
   address 1.1.1.2/24
   route default 1.1.1.1
}
interface em1 {
   group "secondary"
   address 2.2.2.2/24
}
interface em2 {
   group "lan"
   address 192.168.0.1/24
}
interface em4 {
   group "dmz"
   address 10.0.0.1/16
}
firewall {
   $dmz_server1 = 10.0.0.100
   pass out on primary nat-to (primary:0)
   pass out on secondary nat-to (secondary:0)
   pass in on primary to port 80 rdr-to $dmz_server1
   pass in on lan route-to (em1 2.2.2.1)
   pass out on dmz
}