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!
- 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
- 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
- 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.
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
- 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.
- Enable DNS Forwarder with an initial site configuration
- Define firewall rules for DHCPv4
- Migrate static mappings and disable DHCP Server
- Configure dnsmasq to serve DHCPv4
- Define Domain Overrides in DNS Resolver
- Test and troubleshoot
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
, anddhcp-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 addingno-negcache
and/oruse-stale-cache
.
- Make sure to update the
For more information about these directives and more, please reference the dnsmasq man page.
Save and apply.
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.
- 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.
- Action: Pass
- Disabled: [ ]
- Address Family: IPv4
- Protocol: UDP
- Source: LAN net, port 68
- Target: LAN address, port 67
- Description: DHCPREQUEST
Save and apply.
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).
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
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.
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).
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.
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.
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.
Functional Changes
2023-10-14
cache-size
directive to zero).2023-09-17
2023-09-16
2023-09-14