My home network observes bedtime with OpenBSD and pf

A guide on using OpenBSD and the pf packet filter to automate internet access schedules for a home network, including hardware choices and rule configurations.
The files mentioned in this article are available for viewing or cloning here: the 'pf-bedtime' repo.
Goals
I want my home network to "shut off Internet access" when it’s bedtime.
I need it to be completely automatic and on a schedule.
I also need to make exceptions to allow a couple servers and devices do backups and updates at night.
Bonus: I’d like to be in charge of the DNS on my local network so I can experiment with using it as a DNS sinkhole (like Pi-hole) for unwanted domains and giving my home computers nice local domain names.
To do this, I’m setting up a computer to act as the main router between my ISP and my home network. It performs all of the DHCP, NAT, DNS caching, and "firewall" functions you’d expect from a consumer device, but on familiar PC hardware.
This machine will replace a Ubiquiti UniFi Security Gateway USG-3P. That device worked fine as a plug-and-play router/gateway/firewall and there may even have been some way to get all of this working by shelling into it and hacking a solution. But if I’m going to put in the effort, I’d much rather learn something non-proprietary. I’d rather learn some more OpenBSD.
The computer
(Fun fact: Initially, when I first wrote this article, I had it all set up on the mini PC featured in this article about my home computing setup. However, I ran into trouble with the RealTek ethernet hardware support in OpenBSD, which had been running fine with Linux for years. So I grit my teeth and bought a replacement, the computer in the picture above. It’s a slightly larger box, but the important difference is that it boasts Intel ethernet hardware, which works great on both Linux and OpenBSD.)
A router is just a computer with at least two ethernet ports and the right software. You can turn just about any computer into a router. By running an operating system you already understand, you have a chance of understanding your router. I like understanding things.
The computer is a Qotom Q305p 3205u. It runs a 1.5Ghz Celeron processor with completely passive cooling (the whole computer is a heatsink), 4Gb RAM, and two gigabit ethernet ports. This one came with a 32Gb SSD, which may be standard for the model. Good specs for a router. Well, probably overkill, but I figure this computer will be useful for many years to come. I have no idea how old it is or what it was used for before I bought it.
(Like my other fanless mini computer, this one has a boat-load of serial DE-9 ("COM") ports. These computers are often used for industrial control, embedded kiosks, and always-on digital signs. The industrial intent and lack of moving parts always gives me the feeling that these computers generally operate for a very long time.)
Total price with power adaptor: $60. I think that’s a great value. I love these little computers.
OpenBSD basic router setup
First, I installed the current stable version of OpenBSD:
$ uname -a OpenBSD treebeard.local 7.8 GENERIC.MP#54 amd64
I’ve been learning OpenBSD off and on for a couple years. It’s slowly becoming familiar to me. There’s a half-kidding joke about installing OpenBSD being just a bunch of hitting the Enter key. I’m pretty much at that point now that I understand what the defaults are doing. (Such as: No, the install media is not mounted when it asks, but it’ll find it for you and mount it if you hit Enter some more.)
To get the rest of the router setup working, I followed the OpenBSD Handbook’s excellent guide: Build a Simple Router and Firewall (openbsdhandbook.com).
That’s a surprisingly short page that sets up all the following functionality:
IP forwarding (kernel setting via
sysctl
) - Two configured network interfaces
DHCP services via
dhcpd
DNS caching for the local network via
unbound
NAT (IPv4 address translation) via
pf
Sane minimal packet filtering (the "firewall" part) via
pf
Later, I set up some DNS entries for my home computers, see my related page: Setting up .home.arpa names with OpenBSD’s unbound.
I ended up re-writing my pf.conf
completely from scratch by following The Book of PF. That’s the next section.
Setting up pf
(Sketchbook ink and watercolor by the author: A fearsome Puffy determines which packets shall pass.)
The centerpiece of my setup is the pf
packet filter, which is built into the OpenBSD kernel and originated, like many good things, from OpenBSD.
The bulk of pf configuration is done through /etc/pf.conf
.
I constructed mine from scratch while reading The Book of PF, 4th Ed. (see the references section at the bottom of this page).
(I like to thoroughly document things I won’t be touching frequently, so there are a lot of comments in that file, including instructions on updating pf after I make changes.)
Anyway, I set this up in the recommended fashion: block all
traffic and then let only selected traffic through.
When it’s daytime, I use the rule:
pass proto tcp from <leased_ips>
When it’s bedtime, that rule changes to:
pass proto tcp from <bedtime_exempt>
There are two IP address tables being used:
<leased_ips>
is maintained bydhcpd
when it leases addresses to clients on the local network. -
<bedtime_exempt>
is maintained manually by me. I store the addresses in a text file and load them into the table with a script whenever I make a change.
When it’s bedtime, I only explicitly allow traffic to the exempt computers.
This blocks traffic to everything else because, as you may recall, the default
is block all
!
You’ll notice that I’m only doing this for TCP traffic. I’m handling ICMP and UDP packets in a strict fashion in accordance with the wisdom of the book. We’ll see if I end up needing to make any exceptions.
(Update: Sure enough, I’m going to need to experiment with the daytime rule - the above doesn’t allow Discord voice chat or Roblox to function, which…was not appreciated by certain members of this house.)
Updating tables
Since this is all predicated on the two address tables, how do these tables get updated?
The <leased_ips>
table is initially created in pf.conf
with this placeholder:
table <leased_ips> persist counters
It is populated automatically by dhcpd
from this command line option
set in /etc/rc.conf.local
(also in the repo):
dhcpd_flags="-L leased_ips"
I think it’s great how tables are built right into the OpenBSD kernel and all the tooling understands them. It feels very cohesive and, dare I say it, planned and thought-out?
I store the <bedtime_exempt>
addresses in a text file and update the table
from the file contents with pfctl
:
pfctl -t bedtime_exempt -T replace -f no_bedtime.txt
The text file is a simple list with one address per line. It can also have standard Unix-style comments (line starts with '#'). Again, all of this feels very cohesive and flexible to me. It’s the good parts of the Unix Philosophy.
When you or a program update a table, the changes take place immediately in the running kernel’s tables and you don’t have to tell pf about them.
Anchors
The crux of bedtime enforcement is the ability to schedule a change to the rules that allow traffic from local computers.
Anchors are a grouping for rules in pf.conf
. There are a couple different uses for them, but in my case, I’m using an anchor as a named chunk of rules which I can change from the command line without having to reload anything else.
Initializing an anchor can be as simple as giving it a name:
anchor foo;
In my case, I’m pre-populating my 'bedtime' anchor with the unrestricted Internet access rule so access works when pf starts up with the assumption that it’s currently "daytime":
anchor bedtime { # the default "awake" rule, bedtime not enforced pass proto tcp from <leased_ips> }
Once you have an anchor, you can swap out its rules on the fly (they’ll be parsed and added to the ruleset) from a file or even STDIN at the command line. Here’s an example that uses
Source: Hacker News










