OpenBSD PF - Building a Router [Contents]



Overview

This example will demonstrate how to turn an OpenBSD system into a router that performs the following duties: Two wired em(4) NICs and an athn(4) wireless card will be used, with the end goal looking something like this:
 [ comp1 ] ---+
              |
 [ comp2 ] ---+--- [ switch ] --- em1 [ OpenBSD ] em0 --- [ internet ]
              |                  athn0
 [ comp3 ] ---+                        )))))
                                       ((((( [ comp4 ]
Replace the em0, em1, and athn0 interface names as appropriate.

Networking

The network configuration will use a 192.168.1.0/24 subnet for the wired clients and 192.168.2.0/24 for the wireless.
# echo 'net.inet.ip.forwarding=1' >> /etc/sysctl.conf
# echo 'inet autoconf' > /etc/hostname.em0 # or use a static IP
# echo 'inet 192.168.1.1 255.255.255.0 192.168.1.255' > /etc/hostname.em1
# vi /etc/hostname.athn0
Add the following, changing the mode/channel if needed:
media autoselect mode 11n mediaopt hostap chan 1
nwid AccessPointName wpakey VeryLongPassword
inet 192.168.2.1 255.255.255.0
OpenBSD defaults to allowing only WPA2-CCMP connections in HostAP mode. If support for older (insecure) protocols is needed, they must be explicitly enabled.

DHCP

The dhcpd(8) daemon should be started at boot time to provide client machines with IP addresses.
# rcctl enable dhcpd
# rcctl set dhcpd flags em1 athn0
# vi /etc/dhcpd.conf
The following dhcpd.conf(5) example also provides static IP reservations for a laptop and server based on their MAC addresses.
subnet 192.168.1.0 netmask 255.255.255.0 {
	option routers 192.168.1.1;
	option domain-name-servers 192.168.1.1;
	range 192.168.1.4 192.168.1.254;
	host myserver {
		fixed-address 192.168.1.2;
		hardware ethernet 00:00:00:00:00:00;
	}
	host mylaptop {
		fixed-address 192.168.1.3;
		hardware ethernet 11:11:11:11:11:11;
	}
}

subnet 192.168.2.0 netmask 255.255.255.0 {
	option routers 192.168.2.1;
	option domain-name-servers 192.168.2.1;
	range 192.168.2.2 192.168.2.254;
}
Any RFC 1918 address space may be specified here. The domain-name-servers line in this example specifies a local DNS server that will be configured in a later section.

Firewall

OpenBSD's PF firewall is configured via the pf.conf(5) file. It's highly recommended to become familiar with it, and PF in general, before copying this example.
# vi /etc/pf.conf
A gateway configuration might look like this:
wired = "em1"
wifi  = "athn0"
table <martians> { 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16     \
	 	   172.16.0.0/12 192.0.0.0/24 192.0.2.0/24 224.0.0.0/3 \
	 	   192.168.0.0/16 198.18.0.0/15 198.51.100.0/24        \
	 	   203.0.113.0/24 }
set block-policy drop
set loginterface egress
set skip on lo0
match in all scrub (no-df random-id max-mss 1440)
match out on egress inet from !(egress:network) to any nat-to (egress:0)
antispoof quick for { egress $wired $wifi }
block in quick on egress from <martians> to any
block return out quick on egress from any to <martians>
block all
pass out quick inet
pass in on { $wired $wifi } inet
pass in on egress inet proto tcp from any to (egress) port { 80 443 } rdr-to 192.168.1.2
The ruleset's various sections will now be explained:
wired = "em1"
wifi  = "athn0"
The wired and wireless interface names for the LAN are defined with macros, used to make overall maintenance easier. Macros can be referenced throughout the ruleset after being defined.
table <martians> { 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16     \
	 	   172.16.0.0/12 192.0.0.0/24 192.0.2.0/24 224.0.0.0/3 \
	 	   192.168.0.0/16 198.18.0.0/15 198.51.100.0/24        \
	 	   203.0.113.0/24 }
This is a table of non-routable private addresses that will be used later.
set block-policy drop
set loginterface egress
set skip on lo0
PF allows certain options to be set at runtime. The block-policy decides whether rejected packets should return a TCP RST or be silently dropped. The loginterface specifies which interface should have packet and byte statistics collection enabled. These statistics can be viewed with the pfctl -si command. In this case, the egress group is being used rather than a specific interface name. By doing so, the interface holding the default route (em0) will be chosen automatically. Finally, skip allows a given interface to be omitted from packet processing. The firewall will ignore traffic on the lo(4) loopback interface.
match in all scrub (no-df random-id max-mss 1440)
match out on egress inet from !(egress:network) to any nat-to (egress:0)
The match rules used here accomplish two things: normalizing incoming packets and performing network address translation, with the egress interface between the LAN and the public internet. For a more detailed explanation of match rules and their different options, refer to the pf.conf(5) manual.
antispoof quick for { egress $wired $wifi }
block in quick on egress from <martians> to any
block return out quick on egress from any to <martians>
The antispoof keyword provides some protection from packets with spoofed source addresses. Incoming packets should be dropped if they appear to be from the list of unroutable addresses defined earlier. Such packets were likely sent due to misconfiguration, or possibly as part of a spoofing attack. Similarly, clients should not attempt to connect to such addresses. The "return" action is specified to prevent annoying timeouts for users. Note that this can cause problems if the router itself is also behind NAT.
block all
The firewall will set a "default deny" policy for all traffic, meaning that only incoming and outgoing connections which have been explicitly put in the ruleset will be allowed.
pass out quick inet
Allow outgoing IPv4 traffic from both the gateway itself and the LAN clients.
pass in on { $wired $wifi } inet
Allow internal LAN traffic.
pass in on egress inet proto tcp from any to (egress) port { 80 443 } rdr-to 192.168.1.2
Forward incoming connections (on TCP ports 80 and 443, for a web server) to the machine at 192.168.1.2. This is merely an example of port forwarding.

DNS

While a DNS cache is not required for a gateway system, it is a common addition to one. When clients issue a DNS query, they'll first hit the unbound(8) cache. If it doesn't have the answer, it goes out to the upstream resolver. Results are then fed to the client and cached for a period of time, making future lookups of the same address quicker.
# rcctl enable unbound
# vi /var/unbound/etc/unbound.conf
Something like this should work for most setups:
server:
	interface: 192.168.1.1
	interface: 192.168.2.1
	interface: 127.0.0.1
	access-control: 192.168.1.0/24 allow
	access-control: 192.168.2.0/24 allow
	do-not-query-localhost: no
	hide-identity: yes
	hide-version: yes
	prefetch: yes

forward-zone:
        name: "."
        forward-addr: X.X.X.X  # IP of the preferred upstream resolver
Further configuration options can be found in the unbound.conf(5) manual. Outgoing DNS lookups can also be encrypted with the dnscrypt-proxy package or with unbound's built-in DNS over TLS support.

If the router should also use the caching resolver, its /etc/resolv.conf file should contain

nameserver 127.0.0.1

Once the changes are in place, reboot the system.