8 min read

Set up networking for an OpenBSD 7.4 firewall device

Table of Contents

I had a few wrinkles because I’d already set up networking, and I didn’t want to have to go through and redo all my static IPs. But I wanted to make my network more rational. I had devices on 192.168.150.*/24 and I wanted something bigger, so I decided to go with /20. That gave me a range of 92.168.144.1–192.168.159.254. (I admit I used an IP subnet calculator for that.) My ISP gives me IPv4, so I’m using that and not worrying about IPv6.

First things first: let’s make the box forward packets:

# sysctl net.inet.ip.forwarding=1
# echo 'net.inet.ip.forwarding=1' >> /etc/sysctl.conf

Configure Network Adapters

I have two Intel NICs. I’m connecting igc0 to my cable modem. I’m connecting igc1 to a 16-port switch which is my internal network. I’ve got another network port that was autodetected, but I’m not using it yet, so I’ll remove it.

# echo "inet autoconf" > /etc/hostname.igc0
# echo "inet 192.168.144.1 255.255.240.0 NONE" > /etc/hostname.igc1
# rm /etc/hostname.igc2
# sh /etc/netstart.sh

Setting up the PF packet filter

Now, set up PF. I got my instructions for this mostly from home.nuug.no/~peter/pf/en/ftpproblem.html. Here’s how I edited /etc/pf.conf:

#---------------------------------#
# Macros
#---------------------------------#
ext_if="igc0"
lan_if="igc1"

ftpproxy="127.0.0.1"
ftpproxyport="8021"

#---------------------------------#
# Tables
#---------------------------------#

table <localonly> { \
 # Addresses that can talk to the local network but not to the rest of the world
 \
 # Local printer
 192.168.158.1/32 \
}

# This is a table of non-routable private addresses.
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 }

#---------------------------------#
# Protect and block by default
#---------------------------------#

set skip on lo0
set block-policy drop

# Spoofing protection for all NICs.
block in from no-route
block in quick from urpf-failed

# Block non-routable private addresses.
# We use the "quick" parameter here to make this rule the last.
block in quick on $ext_if from <martians> to any
block return out quick on $ext_if from any to <martians>

# Default blocking all traffic in on all LAN NICs from any computer or device
# attached.
block return in on { $lan_if }

# Default blocking all traffic in on the external NIC from the Internet/ISP,
# we'll log that too.
block drop in log on $ext_if

# Don't allow ICMP from outside. Commented out this section.
# Yeah, I know people hate that.
#
#match in on $ext_if inet proto icmp icmp-type {echoreq } tag ICMP_IN
#block drop in on $ext_if proto icmp
#pass in proto icmp tagged ICMP_IN max-pkt-rate 100/10

# We need the router to have access to the Internet, so we'll default allow
# packets to pass out from our router through the external NIC to the Internet.
pass out inet from $ext_if

#---------------------------------#
# LAN Setup
#---------------------------------#

# Allow any computer or device on the LAN to send data packets in through the NIC.
# This means any computer attached to this network interface can pass in data
# reaching anywhere, i.e. the Internet or any of the computers attached to the
# router.
pass in on $lan_if

# Always block DNS queries not addressed to our DNS server.
block return in quick on $lan_if proto { udp tcp } to ! $lan_if port { 53 853 }

# Block localonly from seeing the internet
block in quick on lan_if from <localonly>

# Allow data packets to pass from the router out through the NIC to the
# computers or devices attached to it on the lan NIC.
# Without this we can't even ping computers attached to the lan NIC from
# the router itself.
pass out on $lan_if inet keep state

#---------------------------------#
# FTP
#---------------------------------#
# allow ftp clients to work
anchor "ftp-proxy/*"
pass in quick on $lan_if inet proto tcp to port ftp divert-to $ftpproxy port $ftpproxyport
# no ftp servers so don't pass out
# pass out proto tcp from $ftpproxy to any port ftp

#---------------------------------#
# NAT
#---------------------------------#
pass out on $ext_if inet from $lan_if:network to any nat-to ($ext_if)

Then you can test things out and restart pf. I think my network connection dropped at this point:

# pfctl -n -f /etc/pf.conf
# pfctl -F all
# pfctl -f /etc/pf.conf

Configure dhcpd

Now I need to configure serving IPs from this machine. (My old firewall is still on the network at this point and serving real IPs as well as doing DNS, but I need to get the new firewall enabled.) So time to edit /etc/dhcpd.conf:

#       $OpenBSD: dhcpd.conf,v 1.1 1998/08/19 04:25:45 form Exp $
#
# DHCP server options.
# See dhcpd.conf(5) and dhcpd(8) for more information.
#

# This is for 192.168.144.* to 192.168.159.*
subnet 192.168.144.0 netmask 255.255.240.0 {
    option domain-name "lan.example.net";
    option domain-name-servers 192.168.144.1;
    option routers 192.168.144.1;
    ########################################
    # Dynamic IP addresses 
    ########################################
    range 192.168.145.1 192.168.149.254;

    ########################################
    # Fixed IP machines that humans don't
    # (normally) see - WAPs etc.
    ########################################
    host wap {
        hardware ethernet dc:9f:44:11:22:33;
        fixed-address 192.168.144.13;
    }

    ########################################
    # Fixed IP machines that humans see.
    ########################################
    # These are machines I normally ssh
    # or telnet to, or run servers on.
    # 
    host fileserver {
        hardware ethernet 22:33:44:55:66:77;
        fixed-address 192.168.150.171;
	option host-name "fileserver";
    }
    host weather {
        hardware ethernet 88:99:00:AA:BB:CC;
	fixed-address 192.168.150.177;
	option host-name "weather";
    }
#... etc...
    # I'm putting my laptop on a non-150 address for testing purposes.
    # That way I can plug it into the server and get a DHCP address that
    # should route on to the internet.
    host laptop {
        hardware ethernet 00:22:44:66:88:ff;
	fixed-address 192.168.161.2;
    }
} # subnet

Next, start serving DHCP:

rcctl enable dhcpd
rcctl start dhcpd

At this point I could plug my laptop into the new firewall and get an IP address.

Start Unbound for DNS

On my old firewall I was using bind. That was heavyweight, so I decided to do something different here. Originally I thought I needed NSD and Unbound for DNS (I was looking at some instructions at jamsek.dev/posts/2019/Jul/28/openbsd-dns-server-with-unbound-and-nsd/) but eventually realized I could get away with just unbound.

# rcctl enable unbound
# rcctl start unbound

Next, I changed my DNS server so the new firewall was getting it locally instead of going to the old firewall. In /etc/resolv.conf:

nameserver 127.0.0.1
lookup file bind

After that, configure DNSSEC:

# unbound-anchor -a "/var/unbound/db/root.key"
# ftp -S do -o /var/unbound/db/root.hints https://www.internic.net/domain/named.root

With root.key downloaded, I could set up my /var/unbound/etc/unbound.conf:

# $OpenBSD: unbound.conf,v 1.21 2020/10/28 11:35:58 sthen Exp $
server:
	interface: 192.168.144.1
	interface: ::1

	# override the default "any" address to send queries; if multiple
	# addresses are available, they are used randomly to counter spoofing
	access-control: 0.0.0.0/0 refuse
	access-control: ::0/0 refuse
	access-control: 127.0.0.0/8 allow
	access-control: 192.168.144.0/20 allow
	access-control: ::1 allow

	hide-identity: yes
	hide-version: yes

	# Perform DNSSEC validation.
	#
	auto-trust-anchor-file: "/var/unbound/db/root.key"
	root-hints: "/var/unbound/db/root.hints"
	qname-minimisation: yes
	val-log-level: 2

	# Synthesize NXDOMAINs from DNSSEC NSEC chains.
	# https://tools.ietf.org/html/rfc8198
	#
	aggressive-nsec: yes

	# Serve zones authoritatively from Unbound to resolver clients.
	# Not for external service.
	#
	local-zone: "lan.example.net." static
#
# Host addresses - infrastructure
#
	local-data: "firewall.lan.example.net.	 IN A	192.168.144.1"
	local-data-ptr: "192.168.144.1		 firewall.lan.example.net"

#
# Host addresses - named servers
#
	local-data: "fileserver.lan.example.net.  IN A	192.168.150.171"
	local-data-ptr: "192.168.150.171	fileserver.lan.example.net"

	local-data: "weather.lan.example.net.	IN A	192.168.150.177"
	local-data-ptr: "192.168.150.177	weather.lan.example.net"
# ... etc...

remote-control:
	control-enable: yes
	control-interface: /var/run/unbound.sock

With all that set up, I could make sure I didn’t have any syntax errors and restart unbound:

# unbound-checkconf
# rcctl restart unbound

I think it was at this point that I made the switch from using the old firewall to the new firewall. I changed the new firewall to have the same MAC as the old one (my ISP wanted to see a specific MAC) and then put the new one in place:

# echo "inet autoconf lladdr 11:22:33:44:55:66" > /etc/hostname.igc0
# sh /etc/netstart

Having two devices with the same MAC is a Bad Thing, and I had a lot of weirdness on my network until I unplugged the old firewall.

This post is part of a series on setting up an OpenBSD 7.4 firewall device.