FreeBSD home router -- Legacy IPv4: NAT+DHCP


I’ve been writing about setting up a FreeBSD-based eXO router, first the upstream and PPPoE details were covered, then how to setup the gateway, access point and an IPv6-only network. This builds upon a system that can properly route IPv6 and IPv4 as well as has a means to connect other clients and possibly providing them with an IPv6 address and route.

The goal here is to setup the bits of the router needed for everyday usage.

This is part of a series on home routing with FreeBSD.

Table of Contents

Home router

As mentioned on the first post, this setup is based on an APU2d4, with three ethernet interfaces: igb0 for egress/wan, igb1 for local devices and, igb2 as a management interface.

The current state is that the machine can setup the connection to eXO and can correctly accept the IPv6 and IPv4 routes and delegations, furthermore it creates an access point wlan0 that is bridged to igb1 (bridge is called lair) and any devices that connect to these interfaces are able to use only the IPv6 internet.

Local legacy IPv4 networking: NAT (half-rant)

Network Address Translation, one of those things that are secretly quite complex and a bit of a PITA to debug.

The Internet was made to be End-To-End, to have all members of the network be equal in the sense that they can provide and consume services the exact same way [old knowledge; citation needed].

NAT gets in the way of that and it was a necessary evil. Virtually all ISPs that provide you with an internet connection, will also assign you a single IPv4 address (either dynamically or statically), but you surely have more than one device at any given home/company location.

Since, except in very specific cases, it’s not valid for multiple devices to work with the same IP, NAT shows up everywhere.

In a nutshell, NAT makes sure that your 192.168.X.Y IPv4 doesn’t leave the local network (because it’s “non-routable”, aka shouldn’t leave the premises) and then connects on your behalf to whatever other peers you requested.

Then, when the answer comes back, it’s “smart enough” to know that that reply has to go to your 192.168.X.Y instead of to some 192.168.W.Z in the network.

That’s trickier than it sounds and indeed means that, without some kind of permanent port-forwarding, it is not possible to reach devices behind the NAT for better and for worse.

Since RIPE ran out of IPv4 in 2019, this is specially important: now ISPs are starting to use Carrier-Grade NAT, some even “sell it” as a feature because it means they can give you a crappier internet at lower prices.

Crappier internet? Well, if you or someone you know/trust manage your NAT, it is possible to do things like port-forwarding, if in reality, your NAT is behind another NAT that hosts thousands of customers; there is no real option for that.

Random trivia: The Nintendo Switch will happily tell you how crappy your NAT is :-) on a scale from “A: mostly works” to “F: how did this happen?”. CGNAT will automatically place you somewhere between “C” and “D”.

Local legacy IPv4 networking: NAT (implementing)

So, eXO is in this regard only different, in that they assign a static IPv4 address to the connection that is directly addressable by any device connected to the IPv4 internet.

Which means, I do need NAT if I want IPv4 support.

Side note for some other time: NAT64 and DNS64 make it mostly unnecessary to have IPv4 (and NAT!) in the first place. This has been tested out successfully at length by many, but specially by the awesome people at ungleich.ch at last Hack4Glarus and with their IPv6 only hosting Virtual Private Server (VPS) offering.

DHCP: providing the local IPv4 addresses

Before actually translating addresses, we have to provide those addresses to devices in the network. That’s what DHCP is good for.

The along with the DHCP6 client (dhcp6c), DHCP server is the second bit that we need that is not part of the base system.

In this case, I’ll use OpenBSD‘s dhcpd, which is packaged as a port for FreeBSD.

As with dhcp6c, installing dhcpd is a matter of:

pkg install dhcpd

And the magical man pages are man 5 dhcpd.conf and man 8 dhcpd and, even if it is focused on isc-dhcpd-server, the Chapter on DHCP from the FreeBSD Handbook.

Familiar pattern by now, setup /usr/local/etc/dhcpd.conf (since dhcpd is not part of the base system, its config lives in /usr/local/etc) as follows:

# Add a suffix for local name queries
option domain-name "evilham.local";
# TODO: change to own DNS. Using quad9 for now
option domain-name-servers,;

# Default to a 1 day lease
default-lease-time 86400;
# Limit the lease to, say a month if someone
# requests a longer lease.
max-lease-time 2592000;

# 2^16 should be enough addresses for everyone (' ^.^)
option subnet-mask;

# This will have to be the internal IPv4 of the router
option routers;

subnet netmask {
  # I want to "reserve" some addresses for ${reasons}

And setup /etc/rc.conf:

# Modify this existing line to add the router's IPv4 and subnet
ifconfig_lair="inet addm igb1 addm wlan0"

# Start giving out IPv4 addresses
#   And explicitly do that only locally

Then start the dhcpd service:

service dhcpd start

Now any device connected to the bridge interface will get an IPv4 address \o/.

BUT! We have no NAT yet, so, things will get weird soon! (*)

(*): they got weirder than expected, ppp was doing NAT?

That doesn’t sound right :-D. Let’s trust pf more than ppp for that job.

Modified /etc/ppp/ppp.conf so that the exo profile also contains a line: nat enable no This should have been the default if I understood documentation properly.

TODO: Figure out why this was the default behaviour and fix documentation/open a bug

Besides talking to our local network, these addresses are not good for anything. Since they are not routable, any routers must refuse to forward the packets any further.

NAT with pf: forwarding packets properly

FreeBSD has two main firewalls, I like pf. It originated in OpenBSD like so many great things, and was ported to FreeBSD.

Kristof Provost’s talk on automated firewall testing, explains a bit how it came to be that OpenBSD‘s pf and FreeBSD‘s pf are actually different. The TL;DR is: nobody has done the work to upgrade it, it works mostly fine, and the diff is not as small as one would expect.

OpenBSD‘s pf has a bunch of goodies, like support for NAT64 :-). Hopefully things catch up soon.

Back to topic: man 4 pf, man 5 pf.conf and man 8 pfctl are the magical man pages.

And a super-tiny but permissive example on how to do NAT was already done by Kamila here, so go steal that into /etc/pf.conf.

With an important addition for dhcp6c:

# TODO: update with provisioning example
# In practise, this should be limited to link local addresses
pass in quick proto udp to port dhcpv6-client

Now we have to apply those rules, since I was already loading pf, albeit with an empty set of rules, this means running:

service pf reload

And boom, now everything in the network can talk to the outside over IPv4.

Remaining work

  • Finish securing the firewall with pf
  • Create and publish cdist types for all the things

Internal network bits

  • Monitoring (Prometheus)
  • DNS (unbound)
  • Traffic dashboards (taking into account the percentiles)