Skip to content

Instantly share code, notes, and snippets.

@mwpastore
Last active October 15, 2023 01:01
Show Gist options
  • Save mwpastore/9b47ba0fd9d07b93dbf795c4ee33dead to your computer and use it in GitHub Desktop.
Save mwpastore/9b47ba0fd9d07b93dbf795c4ee33dead to your computer and use it in GitHub Desktop.
pfSense unbound+dnsmasq

What is this?

This guide shows how to replace the DHCP Server function in pfSense with dnsmasq. You can do this for some or all of your subnets. This addresses a 12+-years old bug in pfSense.

While it is possible to replace the DHCPv6 Server & RA and/or DNS Resolver functions in pfSense with dnsmasq as well, these are out of scope for this guide. Unbound can run in recursive mode and it has some performance advantages over dnsmasq; generally speaking pfSense seems "happier" with it enabled.

DISCLAIMER: While I have done profesionally network administration in the past, I am not per se a professional network administrator. I'm just a guy with an overly-complicated homelab who's been using dnsmasq for many years. Follow this guide at your own risk—and please take a backup before you start!

Pros

  • Per-interface domain name assignments for DHCP clients
  • Migrate away from the now-deprecated ISC DHCP
  • Gain powerful dnsmasq features such as more expressive DHCP static mappings and DHCP vendor options
  • Unbound doesn't have to restart each time a new DHCP lease is issued

Cons

  • More complicated, more to manage
  • More services running and consuming resources
  • One more hop for local domain name resolution
  • You will be editing files to configure DHCP and static mappings instead of using the GUI

Other Notes

  • Existing Host Overrides in DNS Resolver are not affected. They can optionally be brought into dnsmasq but it works better (IME) and it's easier to manage if you don't.
  • Existing Domain Overrides in DNS Resolver are not affected. They can optionally be brought into dnsmasq but it works better (IME) and it's easier to manage if you don't.
  • In general, your pfSense configuration won't change much. We don't even have to install any additional packages.

Example use case

Given:

  • domain "mylab", subnet 192.168.0.0/24, interface LAN (ix0.10), do not forward requests upstream
  • domain "example.net", subnet 10.0.0.0/24, interface OPT1 (ix0.20), forward requests upstream

We want to achieve:

  • DHCP client with ID "foo" on LAN (ix0.10) will get an address in the range 192.168.0.100–150 and the locally-resolvable domain name "foo.mylab"

  • DHCP client with MAC address de:ad:co:de:ca:fe on LAN (ix0.10) will get the address 192.168.0.3 and the locally-resolvable domain name "bar.mylab"

  • DHCP client with ID "gorp" on OPT1 (ix0.20) will get an address in the range 10.0.0.20–30 and the locally-resolvable domain name "gorp.example.net"

  • DHCP client with ID "myxy-beefcacababe" on OPT1 (ix0.20) will get an address in the range 10.0.0.20–30 and the locally-resolvable domain name "frob.example.net"

Additionally:

  • DNS lookups for "foo.mylab", "bar.mylab", "gorp.example.net", "frob.example.net" will return the expected results
  • DNS lookups for "foo" or "gorp" (or "bar.example.net" or "frob.mylab") will return NXDOMAIN

HOWTO

Pre-requisites

  • Decide which of your interfaces, domains, and subnets you want to switch over to dnsmasq. I will be referring to these as:
    • your "candidate" interfaces (LAN and OPT1 in the example use case),
    • your "candidate" domains (mylab and example.net in the example use case), and
    • your "candidate" subnets (192.168.0.0/24 and 10.0.0.0/24 in the example use case).
  • You will need some way to edit files in the pfSense installation. If you're comfortable with ssh and vi that's probably the easiest option, or you can use one of the file management packages.
    • The site configuration can go in custom options on the DNS Forwarder page, but we'll break out the DHCP static mappings and per-interface configuration into separate files on disk.
    • I've chosen to locate everything under /conf/dnsmasq/ to make it easier to find and keep backed up but feel free to blaze your own trail here. Log in and create this directory now.
  • You will need to pick an unused, non-standard listening port for dnsmasq so it doesn't conflict with Unbound.
    • I've chosen port 10053 but feel free to blaze your own trail here.

Overview

  1. Enable DNS Forwarder with an initial site configuration
  2. Define firewall rules for DHCPv4
  3. Migrate static mappings and disable DHCP Server
  4. Configure dnsmasq to serve DHCPv4
  5. Define Domain Overrides in DNS Resolver
  6. Test and troubleshoot

Step 1: Enable DNS Forwarder

Navigate to Services > DNS Forwarder.

  • Enable: [x]
  • DHCP Registration: [ ]
  • Static DHCP: [ ]
  • Prefer DHCP: [ ]
  • Query DNS servers sequentially: [ ] (recommended)
  • Require domain: [x] (recommended)
  • Do not forward private reverse lookups: [ ] (we'll use the bogus-priv directive instead)
  • Listen Port: 10053
  • Interfaces: Localhost
  • Strict binding: [x] (recommended)
  • Custom options:
    bogus-priv
    dhcp-authoritative
    dhcp-fqdn
    domain=
    no-hosts
    
    conf-dir=/conf/dnsmasq/conf.d,*.conf
    dhcp-hostsfile=/conf/dnsmasq/dhcp-hosts
    dhcp-leasefile=/conf/dnsmasq/leases
    
    • Make sure to update the conf-dir, dhcp-hostsfile, and dhcp-leasefile directives if you have decided to use different file paths.
    • If you have older, buggier DHCP clients on your network, consider adding the dhcp-no-override directive.
    • Unbound will not cache most of the forwarded lookup results because pfSense sets dnsmasq's local-ttl to one second. You can tune dnsmasq's own cache by increasing the cache-size directive from the default value of 150 entries and by optionally adding no-negcache and/or use-stale-cache.

For more information about these directives and more, please reference the dnsmasq man page.

Save and apply.

Step 2: Define firewall rules

When you enable DHCP Server in pfSense it auto-generates ingress firewall rules DHCPOFFER and DHCPREQUEST packets. Since we will be disabling DHCP Server in Step 3, we have to create our own firewall rules to serve this purpose.

Add a new floating rule for your candidate interfaces (LAN and OPT1 in the example use case):

  • Action: Pass
  • Disabled: [ ]
  • Quick: [x]
  • Direction: in
  • Address Family: IPv4
  • Protocol: UDP
  • Source: Single host or alias, 0.0.0.0, port 68
  • Target: Single host or alias, 255.255.255.255, port 67
  • Description: DHCPDISCOVER

Save.

Add a new rule for each candidate interface (one for LAN and one for OPT1 in the example use case):

  • Action: Pass
  • Disabled: [ ]
  • Address Family: IPv4
  • Protocol: UDP
  • Source: LAN net, port 68
  • Target: LAN address, port 67
  • Description: DHCPREQUEST

Save and apply.

Step 3: Migrate static mappings and disable DHCP Server

Navigate to Services > DHCP Server and click the tab for your first candidate interface (LAN in the example use case). Repeat this step as necessary for any other candidate interfaces (OPT1 in the example use case).

Migrate static mappings

If you have any DHCP Static Mappings for this interface, we'll need to copy and translate them into our new /conf/dnsmasq/dhcp-hosts file. The syntax is as follows:

# Match on $client-id and $interface, use $ip-address as IP address, use $hostname as hostname.
id:<client-id>,tag:<interface>,<ip-address>,<hostname>

# Match on $client-id and $interface, use $ip-address as IP address, keep $client-id as hostname.
id:<client-id>,tag:<interface>,<ip-address>

# Match on $client-id and $interface, assign dynamic IP address, use $hostname as hostname.
id:<client-id>,tag:<interface>,<hostname>

# Match on $mac-address and $interface, use $ip-address as IP address, use $hostname as hostname.
<mac-address>,tag:<interface>,<ip-address>,<hostname>

# Match on $mac-address and $interface, use $ip-address as IP address, keep $client-id as hostname.
<mac-address>,tag:<interface>,<ip-address>

# Match on $mac-address and $interface, assign dynamic IP address, use $hostname as hostname.
<mac-address>,tag:<interface>,<hostname>

So, very flexible, and there are other matchers that I won't cover here. Matching on interface in all cases lets us define multiple DHCP static mappings for a single machine on multiple subnets. For more information about this feature, please reference the dnsmasq man page, under the dhcp-host directive.

Here are the static mappings for the example use case:

  • DHCP client with MAC address de:ad:co:de:ca:fe on LAN (ix0.10) will get the address 192.168.0.3 and the locally-resolvable domain name "bar.mylab"
de:ad:co:de:ca:fe,tag:ix0.10,192.168.0.3,bar
  • DHCP client with ID "myxy-beefcacababe" on OPT1 (ix0.20) will get an address in the range 10.0.0.20–30 and the locally-resolvable domain name "frob.example.net"
myxy-beefcacababe,tag:ix0.20,frob

Note that we didn't define static mappings for "foo" or "gorp" because the default behavior covers those cases.

To apply any pending changes made to this file, SIGHUP the dnsmasq process: killall -HUP dnsmasq

Disable DHCP Server

Once our static mappings have been migrated for an interface, we can disable DHCP Server for that interface. Uncheck "Enable". N.B. DHCP clients will be unable to acquire or renew a lease on this interface until the next step is completed. Save and apply.

Step 4: Configure dnsmasq to serve DHCPv4

Next, we'll need to define a configuration file in /conf/dnsmasq/conf.d/ for each candidate interface to enable dnsmasq DHCP on that interface. I recommend naming each file after the interface name with a .conf extension.

Here are the configuration files for the example use case:

  • domain "mylab", subnet 192.168.0.0/24, interface LAN (ix0.10), do not forward requests upstream
### /conf/dnsmasq/conf.d/LAN.conf

# serve DHCP on this interface
interface=ix0.10

# perform DNS lookups for DHCP client names in this domain
# "local" means it will never forward queries for this domain upstream
domain=mylab,192.168.0.0/24,local

# define the block of dynamic addresses for this interface, as well as the default lease time
dhcp-range=tag:ix0.10,192.168.0.100,192.168.0.150,1h

# the special "0.0.0.0" address means "send the primary address for this interface"
# remember that unbound is also listening on this address, port 53!
dhcp-option=tag:ix0.10,option:router,0.0.0.0
dhcp-option=tag:ix0.10,option:dns-server,0.0.0.0
  • domain "example.net", subnet 10.0.0.0/24, interface OPT1 (ix0.20), forward requests upstream
### /conf/dnsmasq/conf.d/OPT1.conf

# serve DHCP on this interface
interface=ix0.20

# perform DNS lookups for DHCP client names in this domain
# it will forward queries for this domain upstream, i.e. to the DNS Servers listed in System > General Setup
domain=mylab,10.0.0.0/24

# define the block of dynamic addresses for this interface, as well as the default lease time
dhcp-range=tag:ix0.20,10.0.0.20,10.0.0.30,1w

# the special "0.0.0.0" address means "send the primary address for this interface"
# remember that unbound is also listening on this address, port 53!
dhcp-option=tag:ix0.20,option:router,0.0.0.0
dhcp-option=tag:ix0.20,option:dns-server,0.0.0.0

And, of course, there are lots of other different DHCP options you can send, as desired, each optionally tagged by interface. Untagged options will be sent regardless of the interface the DHCP request comes in on. For example:

dhcp-option=tag:ix0.10,option:domain-search,mylab,example.net
dhcp-option=tag:ix0.20,option:domain-search,example.net,mylab
dhcp-option=option:ntp-server,time.nist.gov

You can also only send certain options to certain clients using matchers. For more information about DHCP options, please reference the dnsmasq man page, under the dhcp-option directive.

To apply any pending changes made to this file, restart the DNS Forwarder service.

Monitor System Logs for errors; look under both System > General (dnsmasq-dhcp process) and System > DNS Resolver (dnsmasq process).

Step 5: Define Domain Overrides in DNS Resolver

Lastly, we have to tell Unbound to forward queries to dnsmasq. To do that, navigate to Services > DNS Resolver. Add this stanza under custom options:

server:
    do-not-query-localhost: no

Save.

Next, add a Domain Override for each candidate domain as follows:

  • Domain: your candidate domain (mylab or example.net in the example use case)
  • IP Address: 127.0.0.1@10053

We also have to forward reverse queries for each candidate subnet:

  • Domain: the in-addr.arpa domain for your candidate subnet (0.168.192.in-addr.arpa or 0.0.10.in-addr.arpa in the example use case)
  • IP Address: 127.0.0.1@10053

Save and apply.

Testing & Troubleshooting

Bonus Points

Unifi

If you have Unifi gear on your network, you can now easily send a DHCP vendor option with the address of your inform host. For example, if your Unifi devices connect via LAN (ix0.10) and you're running the controller at 192.168.0.44:

dhcp-option=tag:ix0.10,vendor:ubnt,1,192.168.0.44

Adjust the interface tag and IP address appropriately.

Alternate Host

Of course you can always choose to run dnsmasq on a different host. More features are enabled on a Linux platform, for example. If you go this route you'll want to add these directives in addition to the site configuration provided above:

# these are enabled by default in pfSense DNS Forwarder
stop-dns-rebind
# might be required for Pi-hole or any other content-filtering upstream server
rebind-localhost-ok

# same as un-checking the "Query DNS servers sequentially" option in pfSense DNS Forwarder
all-servers
# same as checking the "Strict binding" option in pfSense DNS Forwarder
bind-interfaces # or bind-dynamic on Linux
# same as checking the "Require domain" option in pfSense DNS Forwarder
domain-needed

# don't read upstream servers from resolv.conf to prevent a loop
no-resolv
# use explicitly-listed servers instead (whatever you want)
server=9.9.9.9
server=149.112.112.112
# these can also be specified per domain (Domain Override)
#server=/example.net/a.iana-servers.net
#server=/example.net/b.iana-servers.net

# if you have enabled DNSSEC support in pfSense DNS Resolver,
# it might make sense to enable this here (Linux only)
proxy-dnssec

Additionally, if you used the special "0.0.0.0" address in any dhcp-option directives, you'll probably want to change those to the actual address of the pfSense interface on that subnet.

@mwpastore
Copy link
Author

mwpastore commented Sep 16, 2023

Functional Changes

2023-10-14

  • Update dnsmasq caching recommendations to cache by default (i.e. don't set the cache-size directive to zero).

2023-09-17

  • Remove superfluous home.arpa default domain.

2023-09-16

  • Use localhost in Domain Overrides instead of per-interface addresses. This requires DNS Resolver custom options.

2023-09-14

  • Initial document.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment