# How to Set Up Unbound DNS Resolver on FreeBSD
Every DNS query your server makes travels to a third-party resolver by default. That resolver sees every domain you visit, every API you call, and every service you depend on. Running your own recursive DNS resolver with Unbound on FreeBSD eliminates that dependency. You get faster lookups from local caching, DNSSEC validation to prevent spoofing, DNS-over-TLS encryption for forwarded queries, and the ability to block ad and tracking domains at the network level.
Unbound ships in the FreeBSD base system. There is nothing to install. This guide covers the full setup from enabling the service to production-grade tuning.
Why Run Your Own DNS Resolver
There are four practical reasons to operate a local DNS resolver instead of relying on your ISP or a public resolver like Google (8.8.8.8) or Cloudflare (1.1.1.1).
**Privacy.** A third-party resolver sees every domain you resolve. Your ISP resolver may log and sell that data. Even privacy-focused public resolvers require trust in their logging policies. A local resolver queries authoritative nameservers directly or forwards over encrypted channels you control. No third party builds a profile of your DNS traffic.
**Speed.** A local caching resolver stores answers for their full TTL. The second query for the same domain returns in under a millisecond instead of the 10-50ms round trip to an upstream resolver. On a busy server or network, this compounds into measurable latency reduction for web applications, mail delivery, and package management.
**DNSSEC validation.** Unbound validates DNSSEC signatures by default once configured. This prevents cache poisoning and man-in-the-middle attacks on DNS responses. Most stub resolvers in /etc/resolv.conf perform no validation at all.
**Ad and tracker blocking.** A local resolver can return NXDOMAIN or a null address for known ad-serving and tracking domains. This blocks ads and telemetry for every device on the network without browser extensions or per-device configuration.
If you are building a [FreeBSD router](/blog/freebsd-router-gateway/) or running a [DHCP server](/blog/dhcp-server-freebsd/) for a local network, Unbound is the natural companion service.
Unbound in the FreeBSD Base System
Unbound has been part of the FreeBSD base system since FreeBSD 10.0. It lives under /var/unbound/ with its configuration, root hints, and trust anchor files. You do not need to install anything from pkg or the ports tree.
The base system includes:
- /usr/sbin/unbound -- the resolver daemon
- /usr/sbin/unbound-control -- the management and statistics utility
- /usr/sbin/unbound-anchor -- the DNSSEC trust anchor updater
- /usr/sbin/unbound-checkconf -- the configuration file validator
- /var/unbound/unbound.conf -- the main configuration file
- /var/unbound/root.hints -- the root nameserver list
- /var/unbound/root.key -- the DNSSEC root trust anchor
Verify Unbound is present on your system:
sh
unbound -V
This prints the version and compile-time options. You should see support for libevent, subnet, and tls-port among the features listed.
Basic Configuration
The default configuration file at /var/unbound/unbound.conf is minimal. Below is a complete working configuration that covers recursive resolution, DNSSEC, access control, and sensible caching defaults. Start from this template and adjust for your environment.
yaml
# /var/unbound/unbound.conf
server:
# Network interface to listen on
interface: 0.0.0.0
interface: ::0
# Port (default 53)
port: 53
# Protocols
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
# Access control -- adjust to your network
access-control: 127.0.0.0/8 allow
access-control: 10.0.0.0/8 allow
access-control: 172.16.0.0/12 allow
access-control: 192.168.0.0/16 allow
access-control: ::1/128 allow
access-control: fd00::/8 allow
access-control: 0.0.0.0/0 refuse
access-control: ::0/0 refuse
# Run as unbound user
username: "unbound"
directory: "/var/unbound"
chroot: "/var/unbound"
# Logging
verbosity: 1
use-syslog: yes
log-queries: no
log-replies: no
# Privacy and security
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
harden-algo-downgrade: yes
use-caps-for-id: yes
qname-minimisation: yes
# DNSSEC trust anchor
auto-trust-anchor-file: "/var/unbound/root.key"
# Root hints
root-hints: "/var/unbound/root.hints"
# Cache sizing
msg-cache-size: 64m
rrset-cache-size: 128m
key-cache-size: 32m
neg-cache-size: 16m
# Cache behavior
prefetch: yes
prefetch-key: yes
serve-expired: yes
serve-expired-ttl: 86400
# Performance
num-threads: 2
msg-cache-slabs: 4
rrset-cache-slabs: 4
infra-cache-slabs: 4
key-cache-slabs: 4
so-reuseport: yes
so-rcvbuf: 4m
so-sndbuf: 4m
outgoing-range: 8192
num-queries-per-thread: 4096
# Minimize unnecessary records
minimal-responses: yes
# Private address ranges -- block rebinding attacks
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: fd00::/8
private-address: fe80::/10
# Include additional configuration files
include: "/var/unbound/conf.d/*.conf"
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
server-key-file: "/var/unbound/unbound_server.key"
server-cert-file: "/var/unbound/unbound_server.pem"
control-key-file: "/var/unbound/unbound_control.key"
control-cert-file: "/var/unbound/unbound_control.pem"
Create the include directory for modular configuration:
sh
mkdir -p /var/unbound/conf.d
Validate the configuration before starting:
sh
unbound-checkconf /var/unbound/unbound.conf
If the output says no errors in /var/unbound/unbound.conf, the configuration is valid.
Enabling and Starting Unbound
Add Unbound to /etc/rc.conf so it starts at boot:
sh
sysrc unbound_enable="YES"
Generate the control keys for unbound-control:
sh
unbound-control-setup
This creates the TLS certificates in /var/unbound/ that allow unbound-control to communicate with the running daemon.
Fetch or update the root trust anchor for DNSSEC:
sh
unbound-anchor -a /var/unbound/root.key
Update the root hints file (the list of root nameservers):
sh
fetch -o /var/unbound/root.hints https://www.internic.net/domain/named.root
Start the service:
sh
service local_unbound start
Note: FreeBSD uses local_unbound as the service name in the base system, not unbound. If you installed Unbound from packages instead of using the base system version, the service name is unbound.
Now point your server at its own resolver by editing /etc/resolv.conf:
nameserver 127.0.0.1
To prevent dhclient or resolvconf from overwriting this file, add to /etc/dhclient.conf:
supersede domain-name-servers 127.0.0.1;
Or make resolv.conf immutable:
sh
chflags schg /etc/resolv.conf
Test that resolution works:
sh
drill freebsd.org @127.0.0.1
You should get an A record in the answer section with a status of NOERROR.
DNSSEC Validation
The configuration above enables DNSSEC through the auto-trust-anchor-file directive. Unbound automatically manages the root zone trust anchor using RFC 5011 automated updates.
Test DNSSEC validation with a known signed domain:
sh
drill -D freebsd.org @127.0.0.1
The -D flag requests DNSSEC records. You should see RRSIG records in the response and the ad (Authenticated Data) flag set in the header.
Test that DNSSEC validation rejects bad signatures using a deliberately misconfigured domain:
sh
drill dnssec-failed.org @127.0.0.1
This domain has intentionally broken DNSSEC. Unbound should return SERVFAIL instead of an answer, proving that validation is working.
You can also verify with dig if you have it installed (pkg install bind-tools):
sh
dig +dnssec freebsd.org @127.0.0.1
Look for the ad flag in the response header flags line.
To keep the root trust anchor current, add a periodic task. Create /etc/periodic/weekly/600.unbound-anchor:
sh
#!/bin/sh
/usr/sbin/unbound-anchor -a /var/unbound/root.key
Make it executable:
sh
chmod +x /etc/periodic/weekly/600.unbound-anchor
DNS-over-TLS Forwarding
By default, Unbound resolves queries recursively by querying root servers, TLD servers, and authoritative servers directly. This is the most private option because no single upstream sees all your queries. However, some environments require forwarding through an upstream resolver -- corporate networks, networks that block port 53 outbound, or situations where you want the upstream's anycast performance.
DNS-over-TLS (DoT) encrypts the forwarding connection so intermediate networks cannot snoop on or tamper with your DNS traffic. Add a forwarding configuration in /var/unbound/conf.d/forward.conf:
Forwarding to Cloudflare
yaml
# /var/unbound/conf.d/forward-cloudflare.conf
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 1.1.1.1@853#cloudflare-dns.com
forward-addr: 1.0.0.1@853#cloudflare-dns.com
forward-addr: 2606:4700:4700::1111@853#cloudflare-dns.com
forward-addr: 2606:4700:4700::1001@853#cloudflare-dns.com
Forwarding to Quad9
yaml
# /var/unbound/conf.d/forward-quad9.conf
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-addr: 149.112.112.112@853#dns.quad9.net
forward-addr: 2620:fe::fe@853#dns.quad9.net
forward-addr: 2620:fe::9@853#dns.quad9.net
Forwarding to Google
yaml
# /var/unbound/conf.d/forward-google.conf
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 8.8.8.8@853#dns.google
forward-addr: 8.8.4.4@853#dns.google
forward-addr: 2001:4860:4860::8888@853#dns.google
forward-addr: 2001:4860:4860::8844@853#dns.google
Only use one of these files at a time. The @853 specifies the TLS port, and the # prefix identifies the server's TLS authentication name.
After adding the file, check and reload:
sh
unbound-checkconf
service local_unbound reload
Verify TLS forwarding is active by temporarily increasing verbosity or checking that queries resolve through the upstream:
sh
drill google.com @127.0.0.1
Note: When forwarding, DNSSEC validation still happens locally. Unbound validates the signatures it receives from the upstream forwarder, so you retain protection against spoofed responses even though you are not querying authoritative servers directly.
Local Zones and Overrides
Local zones let you define DNS records for internal hostnames, override public DNS for split-horizon setups, or redirect domains to specific addresses. This is essential if you run services on your LAN that need proper DNS names.
Create /var/unbound/conf.d/local-zones.conf:
yaml
# /var/unbound/conf.d/local-zones.conf
# Internal domain for LAN services
server:
# Define a local zone for your internal domain
local-zone: "home.lab." static
# Internal hosts
local-data: "router.home.lab. A 10.0.0.1"
local-data: "nas.home.lab. A 10.0.0.10"
local-data: "web.home.lab. A 10.0.0.20"
local-data: "db.home.lab. A 10.0.0.30"
local-data: "mail.home.lab. A 10.0.0.40"
# PTR records for reverse DNS
local-data-ptr: "10.0.0.1 router.home.lab."
local-data-ptr: "10.0.0.10 nas.home.lab."
local-data-ptr: "10.0.0.20 web.home.lab."
local-data-ptr: "10.0.0.30 db.home.lab."
local-data-ptr: "10.0.0.40 mail.home.lab."
# Override a public domain for split DNS
# (e.g., internal app resolves to LAN IP instead of public IP)
local-zone: "app.example.com." redirect
local-data: "app.example.com. A 10.0.0.20"
The static zone type means only the records you define are returned; queries for names not listed get NXDOMAIN. Other useful zone types:
- transparent -- serves local data if defined, otherwise resolves normally
- redirect -- redirects all names in the zone to the defined data
- deny -- silently drops queries for names in the zone
- refuse -- returns REFUSED for queries in the zone
- inform -- like transparent, but logs queries to this zone
Reload after changes:
sh
service local_unbound reload
Test:
sh
drill nas.home.lab @127.0.0.1
drill -x 10.0.0.10 @127.0.0.1
Access Control
The access-control directives in the main configuration determine which clients can send queries to your resolver. This is critical for any resolver listening on a public interface. An open resolver will be abused for DNS amplification attacks within hours.
The configuration in the template above allows queries from localhost and RFC 1918 private ranges, and refuses everything else. Adjust based on your network:
yaml
server:
# Allow only your specific subnet
access-control: 10.0.0.0/24 allow
# Allow a trusted remote subnet (e.g., VPN clients)
access-control: 10.8.0.0/24 allow
# Refuse everything else
access-control: 0.0.0.0/0 refuse
access-control: ::0/0 refuse
Access control values:
- allow -- allow queries, apply DNSSEC validation
- deny -- silently drop the query
- refuse -- return REFUSED error
- allow_snoop -- allow queries including cache snooping (only for trusted debugging hosts)
- allow_setrd -- allow queries with the RD (recursion desired) bit set
For a server acting as a resolver for a local network behind a [FreeBSD router](/blog/freebsd-router-gateway/), allow for your LAN subnet and refuse for everything else is the correct policy.
If your server is also a [hardened FreeBSD system](/blog/hardening-freebsd-server/), combine Unbound access control with PF firewall rules that restrict port 53 access to your LAN:
# In /etc/pf.conf
pass in on $lan_if proto { tcp, udp } from $lan_net to self port 53
block in on $ext_if proto { tcp, udp } to self port 53
DNS-Based Ad Blocking
Unbound can block ads, trackers, and malware domains by returning NXDOMAIN or 0.0.0.0 for known bad domains. This works at the network level -- every device using your resolver gets ad blocking without installing anything.
Create a blocklist configuration file:
sh
touch /var/unbound/conf.d/blocklist.conf
Write a script that downloads and formats a blocklist. Create /var/unbound/update-blocklist.sh:
sh
#!/bin/sh
# /var/unbound/update-blocklist.sh
# Downloads ad/tracker blocklists and formats them for Unbound
BLOCKLIST_URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
TMPFILE="/tmp/unbound-blocklist.tmp"
OUTFILE="/var/unbound/conf.d/blocklist.conf"
# Download the hosts file
fetch -q -o "$TMPFILE" "$BLOCKLIST_URL"
if [ $? -ne 0 ]; then
echo "Failed to download blocklist"
exit 1
fi
# Convert hosts file format to Unbound local-zone format
echo "server:" > "$OUTFILE"
grep '^0\.0\.0\.0' "$TMPFILE" | \
awk '{print $2}' | \
grep -v '0.0.0.0' | \
sort -u | \
while read domain; do
echo " local-zone: \"${domain}.\" always_nxdomain" >> "$OUTFILE"
done
rm -f "$TMPFILE"
# Validate configuration
/usr/sbin/unbound-checkconf > /dev/null 2>&1
if [ $? -eq 0 ]; then
# Reload Unbound to apply the new blocklist
/usr/sbin/unbound-control reload > /dev/null 2>&1
BLOCKED=$(grep -c 'local-zone' "$OUTFILE")
echo "Blocklist updated: ${BLOCKED} domains blocked"
else
echo "Configuration error after blocklist update"
echo "server:" > "$OUTFILE"
/usr/sbin/unbound-control reload > /dev/null 2>&1
exit 1
fi
Make the script executable and run it:
sh
chmod +x /var/unbound/update-blocklist.sh
/var/unbound/update-blocklist.sh
The StevenBlack consolidated hosts list blocks approximately 80,000-130,000 domains covering ads, trackers, malware, and fakenews sources. You can add additional lists by appending more fetch and processing steps.
Schedule weekly updates with a cron job:
sh
crontab -e
Add:
0 4 * * 0 /var/unbound/update-blocklist.sh >> /var/log/unbound-blocklist.log 2>&1
This updates the blocklist every Sunday at 4 AM.
To whitelist a domain that the blocklist incorrectly blocks, add it before the blocklist include in your main configuration or in a separate file:
yaml
# /var/unbound/conf.d/whitelist.conf
server:
local-zone: "example-allowed-domain.com." transparent
Whitelist entries loaded before the blocklist override the always_nxdomain directive.
Caching Tuning
DNS caching is where Unbound delivers its biggest performance win. Every cached response avoids a network round trip. The default cache sizes are conservative. On a server with available RAM, increase them.
Key cache parameters:
yaml
server:
# Message cache -- stores DNS response messages
msg-cache-size: 64m
# RRset cache -- stores individual resource record sets
# Should be roughly 2x msg-cache-size
rrset-cache-size: 128m
# Key cache -- stores DNSSEC keys
key-cache-size: 32m
# Negative cache -- stores NXDOMAIN and NODATA answers
neg-cache-size: 16m
# Prefetch almost-expired records in the background
prefetch: yes
# Prefetch DNSSEC keys ahead of expiry
prefetch-key: yes
# Serve stale cache entries while refreshing in the background
serve-expired: yes
serve-expired-ttl: 86400
# Minimum TTL -- never cache for less than this (seconds)
cache-min-ttl: 300
# Maximum TTL -- never cache for more than this (seconds)
cache-max-ttl: 86400
The prefetch option is particularly valuable. When a cached entry reaches 10% of its remaining TTL, Unbound proactively refreshes it in the background. The next client to request that domain gets an instant cache hit with fresh data instead of waiting for a full resolution.
The serve-expired option ensures that even if an upstream nameserver is temporarily unreachable, clients still get answers from cache (marked as stale). This dramatically improves reliability during upstream outages.
**Sizing guide:**
| Network size | msg-cache-size | rrset-cache-size | key-cache-size |
|---|---|---|---|
| Single server | 16m | 32m | 8m |
| Small LAN (1-10 devices) | 32m | 64m | 16m |
| Medium LAN (10-50 devices) | 64m | 128m | 32m |
| Large network (50+ devices) | 128m | 256m | 64m |
For the threading and slab parameters, set num-threads to match your CPU core count, and set all slab counts to a power of 2 close to num-threads:
yaml
server:
num-threads: 4
msg-cache-slabs: 4
rrset-cache-slabs: 4
infra-cache-slabs: 4
key-cache-slabs: 4
Integration with DHCP
If your FreeBSD machine serves as a router and [DHCP server](/blog/dhcp-server-freebsd/), configure DHCP to push the Unbound resolver address to all clients automatically.
For ISC DHCP (isc-dhcp44-server), add to /usr/local/etc/dhcpd.conf:
option domain-name-servers 10.0.0.1;
option domain-name "home.lab";
Replace 10.0.0.1 with the LAN IP of your FreeBSD resolver.
For dnsmasq setups, point dnsmasq at Unbound as the upstream:
server=127.0.0.1#53
For networks using DHCPv6, set the DNS server option in your RA (Router Advertisement) daemon or DHCPv6 server configuration to point at the Unbound host's IPv6 address.
After updating DHCP configuration, clients will receive your Unbound resolver on their next lease renewal. Force an immediate renewal on a test client:
sh
# On a FreeBSD client
dhclient -r em0 && dhclient em0
cat /etc/resolv.conf
Confirm the nameserver line points to your Unbound host.
Logging and Monitoring
Unbound provides detailed statistics through unbound-control. This requires the remote-control section to be enabled (included in the template above).
View current statistics:
sh
unbound-control stats_noreset
Key metrics to monitor:
- total.num.queries -- total queries received
- total.num.cachehits -- queries answered from cache
- total.num.cachemiss -- queries requiring upstream resolution
- total.num.recursivereplies -- completed recursive lookups
- total.requestlist.avg -- average outstanding queries (should be low)
- num.answer.rcode.NOERROR -- successful responses
- num.answer.rcode.SERVFAIL -- failures (watch for spikes)
- num.answer.rcode.NXDOMAIN -- non-existent domain responses
Calculate your cache hit ratio:
sh
unbound-control stats_noreset | grep 'total.num.cachehits\|total.num.cachemiss'
A healthy resolver should maintain a cache hit ratio above 70%. If it is lower, increase cache sizes or enable prefetch.
For temporary debugging, increase verbosity:
sh
unbound-control verbosity 3
Verbosity levels:
- 0 -- errors only
- 1 -- operational information (default)
- 2 -- detailed operational information
- 3 -- query-level information
- 4 -- algorithm-level information
- 5 -- cache miss information
Reset to normal after debugging:
sh
unbound-control verbosity 1
To log all queries permanently (generates significant I/O):
yaml
server:
log-queries: yes
log-replies: yes
log-tag-queryreply: yes
Logs go to syslog by default. View them with:
sh
grep unbound /var/log/messages | tail -50
For dedicated log files, configure syslog to route daemon.info to a separate file in /etc/syslog.conf.
Troubleshooting
Unbound Fails to Start
Check configuration syntax first:
sh
unbound-checkconf /var/unbound/unbound.conf
If this passes, check for port conflicts. Another DNS service may already be using port 53:
sh
sockstat -l -4 -6 | grep ':53 '
Common culprits: local_unbound (the default FreeBSD stub resolver), dnsmasq, or named. Stop the conflicting service before starting Unbound.
Queries Return SERVFAIL
SERVFAIL usually indicates a DNSSEC validation failure. Test without DNSSEC to confirm:
sh
drill -o CD freebsd.org @127.0.0.1
The -o CD flag sets the Checking Disabled bit. If this succeeds but a normal query fails, the issue is DNSSEC validation. Common causes:
- Stale root trust anchor. Run unbound-anchor -a /var/unbound/root.key and reload.
- System clock is wrong. DNSSEC signatures have validity periods. Run ntpdate pool.ntp.org or ensure ntpd is running.
- Upstream network blocks DNSSEC responses (rare but possible on some ISPs).
Slow Resolution
If initial queries are slow (but cached queries are fast), check connectivity to root servers:
sh
drill . NS @198.41.0.4
If this times out, your network may be blocking outbound port 53. Switch to forwarding mode with DNS-over-TLS (port 853) as described above.
Check Unbound statistics for abnormal query times:
sh
unbound-control stats_noreset | grep 'total.recursion.time'
Average recursion time should be under 100ms. If consistently higher, consider enabling forwarding to a nearby upstream resolver.
Testing DNSSEC End to End
Use these test domains to verify DNSSEC validation:
sh
# Should succeed (valid DNSSEC)
drill sigok.verteiltesysteme.net @127.0.0.1
# Should fail with SERVFAIL (invalid DNSSEC)
drill sigfail.verteiltesysteme.net @127.0.0.1
Checking Cache Contents
Dump the current cache for inspection:
sh
unbound-control dump_cache > /tmp/unbound-cache.txt
Search for a specific domain:
sh
grep "example.com" /tmp/unbound-cache.txt
Flush a single domain from cache:
sh
unbound-control flush example.com
Flush an entire zone:
sh
unbound-control flush_zone example.com
Complete Configuration Reference
For convenience, here is the full /var/unbound/unbound.conf from this guide as a single file ready to deploy:
yaml
server:
interface: 0.0.0.0
interface: ::0
port: 53
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
access-control: 127.0.0.0/8 allow
access-control: 10.0.0.0/8 allow
access-control: 172.16.0.0/12 allow
access-control: 192.168.0.0/16 allow
access-control: ::1/128 allow
access-control: fd00::/8 allow
access-control: 0.0.0.0/0 refuse
access-control: ::0/0 refuse
username: "unbound"
directory: "/var/unbound"
chroot: "/var/unbound"
verbosity: 1
use-syslog: yes
log-queries: no
log-replies: no
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
harden-algo-downgrade: yes
use-caps-for-id: yes
qname-minimisation: yes
auto-trust-anchor-file: "/var/unbound/root.key"
root-hints: "/var/unbound/root.hints"
msg-cache-size: 64m
rrset-cache-size: 128m
key-cache-size: 32m
neg-cache-size: 16m
prefetch: yes
prefetch-key: yes
serve-expired: yes
serve-expired-ttl: 86400
cache-min-ttl: 300
cache-max-ttl: 86400
num-threads: 2
msg-cache-slabs: 4
rrset-cache-slabs: 4
infra-cache-slabs: 4
key-cache-slabs: 4
so-reuseport: yes
so-rcvbuf: 4m
so-sndbuf: 4m
outgoing-range: 8192
num-queries-per-thread: 4096
minimal-responses: yes
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: fd00::/8
private-address: fe80::/10
include: "/var/unbound/conf.d/*.conf"
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
server-key-file: "/var/unbound/unbound_server.key"
server-cert-file: "/var/unbound/unbound_server.pem"
control-key-file: "/var/unbound/unbound_control.key"
control-cert-file: "/var/unbound/unbound_control.pem"
Frequently Asked Questions
Should I use recursive resolution or forwarding mode?
Use recursive resolution (the default) when your server has unrestricted outbound access on port 53. This is the most private option because no single upstream sees all your queries. Use forwarding with DNS-over-TLS when your network blocks outbound port 53, when you want the performance benefits of a major resolver's anycast network, or when you are behind a restrictive corporate firewall.
Does Unbound replace local_unbound on FreeBSD?
FreeBSD ships local_unbound as a lightweight stub resolver for the system itself. The local_unbound service uses the same Unbound binary but with an auto-generated minimal configuration. If you configure Unbound as a full resolver using this guide, disable local_unbound to avoid port conflicts:
sh
sysrc local_unbound_enable="NO"
service local_unbound stop
sysrc unbound_enable="YES"
service unbound start
Alternatively, you can use the local_unbound service name and simply replace its configuration with the full configuration from this guide. The binary is the same.
How much RAM does Unbound use?
With the cache sizes in this guide (64m message cache, 128m RRset cache, 32m key cache), Unbound typically uses 250-400 MB of RAM under load. On a minimal configuration with default cache sizes, it uses under 50 MB. Adjust cache sizes based on your available memory. Even 16m/32m provides significant caching benefit for a single server.
Can Unbound and PF work together for DNS filtering?
Yes. Use PF to enforce that all LAN clients use your resolver by redirecting any DNS traffic that tries to bypass it:
# In /etc/pf.conf -- redirect all DNS to local Unbound
rdr on $lan_if proto { tcp, udp } from $lan_net to !$self port 53 -> 127.0.0.1 port 53
This intercepts DNS queries from devices that have hardcoded DNS servers (like Google Home devices using 8.8.8.8) and forces them through your Unbound instance with its ad blocking and DNSSEC validation.
How do I update the root hints file?
The root hints file (/var/unbound/root.hints) lists the IP addresses of the 13 root nameservers. It changes rarely, but should be updated every six months:
sh
fetch -o /var/unbound/root.hints https://www.internic.net/domain/named.root
service local_unbound reload
Add this to a periodic task or cron job for automatic updates.
Can I use Unbound with jails?
Yes. Run Unbound on the host and configure jails to use the host's IP as their DNS resolver. In the jail configuration or within each jail's /etc/resolv.conf, set the nameserver to the host's loopback or bridge IP. Ensure Unbound's access-control permits queries from the jail subnet.
How do I monitor blocked domains from the ad blocklist?
Enable query logging temporarily to see what is being blocked:
sh
unbound-control verbosity 3
Then watch the logs:
sh
tail -f /var/log/messages | grep unbound
Blocked domains will show responses with NXDOMAIN. Reset verbosity after debugging to avoid excessive logging.
Summary
Unbound on FreeBSD gives you a private, validated, cached DNS resolver with zero package installations. The base system includes everything needed. Enable it in rc.conf, configure access control for your network, turn on DNSSEC validation, and optionally add DNS-over-TLS forwarding and ad blocking.
For a complete network stack, combine Unbound with a [FreeBSD router](/blog/freebsd-router-gateway/) for NAT and packet filtering, a [DHCP server](/blog/dhcp-server-freebsd/) to push DNS settings to clients automatically, and [server hardening](/blog/hardening-freebsd-server/) to lock down the host itself. This gives every device on your network fast, validated, ad-free DNS resolution controlled entirely by your own infrastructure.