Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

MichaIng
Copy link
Owner

@MichaIng MichaIng commented Jul 24, 2025

Test builds: https://github.com/MichaIng/DietPi/actions/runs/16510133216

Our package has a few differences compared to the Debian package:

  • It does not have the Python module option enabled, which is using python keyword in module-config: setting, and defining a python-script: to be called along the processing chain. If anyone is actually making use of this, we can compile it in, but calling Python scripts along DNS query processing sounds like a very inefficient and slowing-down procedure, so I would be very interested of the use case then. Not compiling it in gets rid of Python dependency of our package.
  • It does not have the AppArmor profile from the Debian package included. If someone uses AppArmor and finds that valuable enough, we can add it, but it would prefer to instead add sandboxing via systemd unit, which is done upstream, but not by Debian. The chroot could also be enabled OOTB, like done upstream, and it should be additionally possible to run the service itself as unbound user, instead of relying on Unbound dropping privileges, adding port 53 permissions via privileges instead.
  • It does not include the resolvconf handling done by the Debian package, which IMO causes more issues than it solves.
  • It does not include the helper script from the Debian package, which does 3 things:
    1. Providing functions for the Debian-only unbound-resolvconf.service, which we do not ship as of above.
    2. Copy root trust anchor from dns-root-data package in place, which we skip, letting Unbound prime it from builtin data instead, if missing on first start. Unbound keeps these updated automatically, so it is relevant only on very first start. The only situation it could break is if the system stays offline for a very long time, so that both contained keys become invalid, making it impossible for Unbound to use them as basis for key updates. However, this really happens only every several years, and probably Unbound is then able (after upgrading the package) to replace the outdated keys with builtin ones, like if the file was missing. In any case simply removing the anchor file and upgrading the Unbound package (for up-to-date builtin keys) would solve it.
    3. Setting up a chroot if configured, which is not enabled by default with the Debian package. If it is wanted, we would set it up via systemd unit BindPaths instead.
  • It ships without init.d service. Somehow, when upgrading the package, all config files of the old package, including /etc/init.d/unbound are appended to the unbound.list (static files) of the new package, and hence preserved until probably/hopefully only next package upgrade. I do not know the reason behind it. However, this means that additional processing is done when enabling/disabling the service after the upgrade, including two additional systemd system-reload steps etc, and the old service depends on the helper script, which is not available anymore. Hence our package manually removes that init.d service via postinst script.
  • It ships without Debian's /etc/unbound/unbound.conf.d/remote-control.conf, hence remote control needs to be enabled manually if needed, i.e. if unbound-control is used. Though, since it does not use a TCP port, but a UNIX socket, it is probably worth it to keep that one? EDIT: Added as of user request.
    /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf for DNSSEC is included, of course.

I am happy to add sandboxing via systemd unit and/or chroot by default, the way the upstream systemd unit does, and hence thankful for everyone who can help testing those directives.

To test

Not necessarily needed, but at best update to DietPi v9.15. Then:

cd /tmp
wget "https://dietpi.com/downloads/binaries/$G_DISTRO_NAME/unbound_$G_HW_ARCH_NAME.deb"
apt install ./"unbound_$G_HW_ARCH_NAME.deb"

There are some other changes, unrelated to the own builds:

  • There is not much point to download root hints and update them via cron job. We could use the dns-root-data package, which ships and keeps them updated. However, root servers change only every several years, last update was one server ~3 years ago, And Unbound itself keeps them updated in their embedded list as well. For simplicity, since we aim to distribute our own package with ~instant updates on upstream release, and to align with upstream and Debian package defaults as well, we drop the external root hints usage entirely and rely on Unbound builtin data, getting also rid of one potential attack vector.
  • Whenever we touch /etc/unbound/unbound.conf.d/dietpi.conf, potentially newly added settings (some just in case they were manually removed previously) are assured to be added within the server: block.
  • The unbound-resolvconf.service is now not disabled after package installation, but (in case disabled and) masked before the package installation, so it really never starts when installed via DietPi. Our own package won't ship it at all, it is a Debian-only maintainer implementation, not upstream, hence the mask is relevant for the Debian package, and in case someone decides to downgrade from our package to the original Debian package for some reason. In any case, we aim to be compatible with both for now.

@MichaIng MichaIng added this to the v9.15 milestone Jul 24, 2025
@MichaIng MichaIng linked an issue Jul 24, 2025 that may be closed by this pull request
@MichaIng MichaIng force-pushed the unbound branch 14 times, most recently from 32f7414 to df9851c Compare July 26, 2025 14:47
@MichaIng MichaIng force-pushed the unbound branch 7 times, most recently from ba82fbc to e784d55 Compare July 26, 2025 18:02
@MichaIng MichaIng merged commit b02213d into dev Jul 26, 2025
2 checks passed
@MichaIng
Copy link
Owner Author

MichaIng commented Jul 31, 2025

@ciro-mota

I changed the interface to 127.0.0.1 and the port to 5335.

Why did you do that? Do you use it with Pi-hole or AdGuard Home?

I added root-hints: "/usr/share/dns/root.hints".

While this should not cause issues, it is not needed, since the Unbound-embedded root hints are recent enough. The last time root servers actually changed was over 2 years ago, before that 6 years ago. Even the Debian package does not set root-hints: by default, and there it would make sense, since Debian never updates their Unbound version within a Debian release.

If I add:

Why do you define a forward zone? This effectively disables Unbounds recursive and validating DNS resolving, also rendering root-hints: unused.

Did you change settings in /etc/unbound/unbound.conf.d/dietpi.conf? As long as nothing else is listening on port 53, there should be no changes required at all, but it should work OOTB. If port 53 is used, only that one would need to be adjusted in /etc/unbound/unbound.conf.d/dietpi.conf, and the interface: 127.0.0.1 optionally, if it is not directly used by other clients in the LAN. But both is done by dietpi-software automatically, when Pi-hole or AdGuard Home is or was installed.

If the issue persists, please raise verbosity: 2 or higher, do a query, and paste the journalctl -u unbound output, which should contain details to that query then. The Debian package does no config hardening. So theoretically possible that one of the hardenings causes some network-related issues in your case. So commenting some of the related settings in /etc/unbound/unbound.conf.d/dietpi.conf could be tested.

@Dynamic5912
Copy link

Nearly all of Unbound users will use serve-expired with some level of caching, since it's used as a recursive DNS server - that's the unique selling point of unbound.

@ciro-mota
Copy link

@MichaIng

Why did you do that? Do you use it with Pi-hole or AdGuard Home?

Yes, it's for AdGuard Home.

Why do you define a forward zone? This effectively disables Unbounds recursive and validating DNS resolving, also rendering root-hints: unused.

It is for use in conjunction with dnscrypt-proxy, as per this guide.

If the issue persists, please raise verbosity: 2 or higher, do a query, and paste the journalctl -u unbound output, which should contain details to that query then. The Debian package does no config hardening. So theoretically possible that one of the hardenings causes some network-related issues in your case. So commenting some of the related settings in /etc/unbound/unbound.conf.d/dietpi.conf could be tested.

I tested this on a clean VM before migrating my current configuration, and with the default settings and adding the forward-zone: line for dnscrypt-proxy, I got SERVERFAIL. With the default settings for each, both AdGuard Home, unbound, and dnscrypt-proxy, they all respond to queries normally. In this case, what additional line will be needed in dietpi.conf for forward-zone: to work?

Aug 01 19:21:14 DietPi unbound[22484]: [1754076074] unbound[22484:0] debug: module config: "validator iterator"
Aug 01 19:21:14 DietPi unbound[22484]: [1754076074] unbound[22484:0] debug: switching log to syslog
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: chdir to /etc/unbound
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: drop user privileges, run as unbound
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: Forward zone server list:
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] info: DelegationPoint<.>: 0 names (0 missing), 2 addrs (0 result, 2 avail) parentNS
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] notice: init module 0: validator
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] notice: init module 1: iterator
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: target fetch policy for level 0 is 3
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: target fetch policy for level 1 is 2
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: target fetch policy for level 2 is 1
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: target fetch policy for level 3 is 0
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: target fetch policy for level 4 is 0
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] debug: cache memory msg=66104 rrset=66104 infra=7952 val=66384 subnet=0
Aug 01 19:21:14 DietPi unbound[22484]: [22484:0] info: start of service (unbound 1.23.1).
Aug 01 19:21:14 DietPi systemd[1]: Started unbound.service - Unbound DNS server.
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] debug: validator[module 0] operate: extstate:module_state_initial event:module_event_new
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] info: validator operate: query google.com. A IN
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] debug: iterator[module 1] operate: extstate:module_state_initial event:module_event_pass
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] info: resolving google.com. A IN
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] info: processQueryTargets: google.com. A IN
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] debug: configured stub or forward servers failed -- returning SERVFAIL
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] debug: return error response SERVFAIL
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] debug: validator[module 0] operate: extstate:module_wait_module event:module_event_moddone
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] info: validator operate: query google.com. A IN
Aug 01 19:22:56 DietPi unbound[22484]: [22484:0] debug: cache memory msg=66348 rrset=66104 infra=8224 val=66384 subnet=0

I'm guessing it's thanks to "module-config: "validator iterator"" but commenting out this line still gets SERVERFAIL.

Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: chdir to /etc/unbound
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: drop user privileges, run as unbound
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: Forward zone server list:
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] info: DelegationPoint<.>: 0 names (0 missing), 2 addrs (0 result, 2 avail) parentNS
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] notice: init module 0: validator
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] notice: init module 1: iterator
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: target fetch policy for level 0 is 3
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: target fetch policy for level 1 is 2
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: target fetch policy for level 2 is 1
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: target fetch policy for level 3 is 0
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: target fetch policy for level 4 is 0
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] debug: cache memory msg=66104 rrset=66104 infra=7952 val=66384 subnet=0
Aug 01 19:34:46 DietPi unbound[22856]: [22856:0] info: start of service (unbound 1.23.1).
Aug 01 19:34:46 DietPi systemd[1]: Started unbound.service - Unbound DNS server.
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] debug: validator[module 0] operate: extstate:module_state_initial event:module_event_new
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] info: validator operate: query google.com. A IN
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] debug: iterator[module 1] operate: extstate:module_state_initial event:module_event_pass
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] info: resolving google.com. A IN
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] info: processQueryTargets: google.com. A IN
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] debug: configured stub or forward servers failed -- returning SERVFAIL
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] debug: return error response SERVFAIL
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] debug: validator[module 0] operate: extstate:module_wait_module event:module_event_moddone
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] info: validator operate: query google.com. A IN
Aug 01 19:34:50 DietPi unbound[22856]: [22856:0] debug: cache memory msg=66348 rrset=66104 infra=8224 val=66384 subnet=0

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 2, 2025

Nearly all of Unbound users will use serve-expired with some level of caching, since it's used as a recursive DNS server - that's the unique selling point of unbound.

Sure, we also enabled it OOTB, as well as prefetch: and raise cache-min-ttl: to 5 minutes: https://github.com/MichaIng/DietPi/blob/master/.conf/dps_182/unbound.conf#L83

But this has nothing to do with external caching, or whether a persistent cache makes sense or not. We are talking about minutes here, and these features are more meant to be fallbacks in case of failing upstream queries, and Unbound recently hardened defaults to try harder with upstream queries, instead of using expired cache entries overly long.

The selling point of Unbound, if you have Pi-hole or AGH with their own DNS caches, is IMO not so much the cache, but the privacy aspect, either skipping 3rd party DNS providers, or alternatively encrypting queries to lock out LAN and ISP from the content. If you use Unbound only, then the cache of course is valuable. But an external potentially persistent caching backend, IMO not so much, based on what we discussed so far.

It is for use in conjunction with dnscrypt-proxy, as per this guide.

Okay, so first of all good to know that you did not install Unbound with dietpi-software, right? Then it makes sense that you face a difference with our own package, which includes /etc/unbound/unbound.conf.d/dietpi.conf, which is otherwise manually installed only via dietpi-software.

So you installed dnscrypt-proxy and want to use it as backend for Unbound. Whether this makes is another question. But:

  • AFAIK Unbound cannot do DNSSEC in this case, but it is skipped automatically for anything that matches the forward zone, all queries in you case. Hence root-hints: remains unused, but it shouldn't cause any issues to set it up either.
  • forward-addr: 1.1.1.1 however does not use dnscrypt-proxy, but queries the Cloudflare DNS directly. In such case, there is no point to use Unbound at all, but you would instead configure AdGuard Home to use 1.1.1.1 directly, skipping all additional software. If you want to use dnscrypt-proxy with Unbound, you'd need to set forward-addr: 127.0.0.1@5353. I assume that dnscrypt-proxy uses port 5353, since the guide configures it that way. In case check via ss -tulpn which process listens on which port.
  • Since AdGuard Home needs to listen on port 53, Unbound needs to be configured to listen on another port.
  • One problem now is that the guide is installing a redundant Unbound config: https://github.com/trinib/AdGuard-WireGuard-Unbound-DNScrypt/blob/main/unbound.conf
    This is moreless an outdated duplicate of /etc/unbound/unbound.conf.d/dietpi.conf. Skip it and, if any adjustments are needed, do them in /etc/unbound/unbound.conf.d/dietpi.conf instead:
    sudo rm /etc/unbound/unbound.conf.d/unbound.conf
    Adjust port: 5335 and interface: 127.0.0.1 in /etc/unbound/unbound.conf.d/dietpi.conf. Add the forward zone better as separate config:
    cat << '_EOF_' | sudo tee /etc/unbound/unbound.conf.d/dnscrypt-proxy.conf
    forward-zone:
      name: "."
      forward-addr: 127.0.0.1@5353
    _EOF_
    sudo systemctl restart unbound
  • Check back with ss -tulpn that adguardhome listens on port 53, unbound on port 5335, and dnscrypt-proxy on port 5353. This matches the setup intended by the guide: https://github.com/trinib/AdGuard-WireGuard-Unbound-DNScrypt/wiki/Install-DNScrypt-proxy-(DoH)(oDoH)(Anonymized-DNS)

But whether adding dnscrypt-proxy and Unbound in combination makes sense is another question.

  • dnscrypt-proxy supports oDoH, which Unbound does not. Compared to DoT (which Unbound supports), DoH makes DNS packets harder to detect, since they are sent via HTTPS on port 443 together with all other HTTPS requests from the network. And oDoH additionally prevents one upstream DNS provider from knowing your IP and the DNS query contents both: an additional proxy is used between client and upstream resolver, hence all together it can be compared to a VPN for DNS.
  • But when using dnscrypt-proxy anyway, Unbound has no real purpose in between. So in that case, I would configure AdGuard Home to use dnscrypt-proxy directly, hence adjust its upstream DNS to use port 5353 instead of 5335, and, as fast as this has been proven to work, uninstall Unbound.
  • If you need DNS encryption mainly to prevent your LAN and/or ISP from being able to read the queries, DoT would however be enough. In that case dnscrypt-proxy would not need to be required (sudo rm /etc/unbound/unbound.conf.d/dnscrypt-proxy.conf), and you can instead enable DoT in Unbound that way: https://dietpi.com/docs/software/dns_servers/#unbound-activating-dns-over-tls-dot

Btw, to everyone here: We were always talking about Unbound having DoH support in newer versions. However, I could actually only find downstream DoH, also downstream DoQ and even downstream DNScrypt (protocol, not to confuse with dnscrypt-proxy), but neither of this for upstream queries. I actually wanted to add upstream DoH setup to our Unbound docs, now that we offer an up-to-date Unbound package for older Debian versions, and am somewhat disappointed now πŸ˜…. Am I missing something, or does Unbound really only support DoT as upstream encryption protocol, nothing else?

@Dynamic5912
Copy link

serve-expired-reply-ttl is the ttl to which a cached response is served back to the client - it's not the lifetime of the entry in the cache.

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 4, 2025

True, but it first tries to resolve upstream:

By default the expired answer will be used after a resolution attempt errored out or is taking more than serve-expired-client-timeout: to resolve.

But you are right, I misinterpreted it earlier. So with serve-expired: enabled, if a matching cache entry is expired:

  • The query is first tried to be resolved upstream.
  • If that fails or takes longer than serve-expired-client-timeout: (default: 1.8s), and the cache entry is no more than serve-expired-ttl: (default: 1 day) above its TTL, it is served with serve-expired-reply-ttl: TTL (default: 30s) to the client.

In older Unbound versions, serve-expired-client-timeout: was 0 by default, in which case the cache entry is served right away, and instead tried to be updated afterwards. Also serve-expired-ttl: was 0 so that a matching cache entry of any age was served. Or at least this is how those values would behave now. So reverting to those would theoretically speed up client requests for expired cache entries, and in combination with a persistent cache would also speed up the very first request after Unbound (re)start, with the implied risk that the returned IP is not valid (for that hostname) anymore.

I guess for our default configuration, we could set serve-expired-client-timeout: 0 but reduce serve-expired-ttl: to something lower, to not serve a cache entry that has expired for 1 day for a DNS record that has a TTL of 1 minute only ... Would be actually nice to have this relative to the original TTL, like serving records from cache which are up to 3x above original TTL or something like that. Since usually one would expect a record with short TTL to change more frequently than one with a long TTL. Only the combination of both values at 0, serving cache entries of any age directly to clients before trying to update them, is IMO too much risk, not worth for a single little performance gain in a long range of time.

@Dynamic5912
Copy link

My current Unbound config uses the following and it's been flawless for me in terms of performance and security for the last 3+ years:

cache-min-ttl: 3600
cache-max-ttl: 86400
serve-expired: yes
serve-expired-ttl: 86400
serve-expired-ttl-reset: yes
serve-expired-reply-ttl: 30
prefetch: yes
prefetch-key: yes
msg-cache-size: 128m
rrset-cache-size: 256m
key-cache-size: 8m
neg-cache-size: 8m

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 4, 2025

As a default, I would probably not have a minimum cache TTL of 1 hour, and not reset the cache entry TTL in case of every failed upstream query (serve-expired-ttl-reset: yes). But looking at our current default config, seems a consequent adjustment to reduce upstream queries and raise the cache hit rate with minimum TTL, and the cache size accordingly. Did you ever notice an invalid IP, like browser saying website is not reachable, but then accessing it successfully on auto-reload a second later?

But regarding the original topic of persistent Redis cache: with serve-expired-reply-ttl: anything different than 0, a persistent cache seems to be able to make any difference only in very constructed scenarios.

@Dynamic5912
Copy link

As a default, I would probably not have a minimum cache TTL of 1 hour, and not reset the cache entry TTL in case of every failed upstream query (serve-expired-ttl-reset: yes). But looking at our current default config, seems a consequent adjustment to reduce upstream queries and raise the cache hit rate with minimum TTL, and the cache size accordingly. Did you ever notice an invalid IP, like browser saying website is not reachable, but then accessing it successfully on auto-reload a second later?

But regarding the original topic of persistent Redis cache: with serve-expired-reply-ttl: anything different than 0, a persistent cache seems to be able to make any difference only in very constructed scenarios.

I've not noticed invalid IP's or otherwise - when I previously had my expired TTL's set to 0 (i.e the previous unbound default of "infinite") I did see it occasionally (and/or certificate errors for websites).

This all went away when using the config above and have found it to be a good balance between performance and security/privacy.

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 4, 2025

I guess cache-min-ttl: generally has the largest impact, reducing also upstream queries. I'll probably raise our default as well, however, maybe more gradually. And cache sizes to the next power of 2.

Regarding serve-expired-ttl-reset: yes, I guess NXDOMAIN or NODATA is not meant with a resolution error, hence this only makes a difference if there is an actual network connection issue between Unbound and upstream servers. So this can make sense if Internet (from that device) is known to be flaky, like a mobile instance, to keep up DNS for clients. But if Internet is stable, and otherwise down for all clients as well, it doesn't make a difference.

@ciro-mota
Copy link

@MichaIng

Okay, so first of all good to know that you did not install Unbound with dietpi-software, right? Then it makes sense that you face a difference with our own package, which includes /etc/unbound/unbound.conf.d/dietpi.conf, which is otherwise manually installed only via dietpi-software.

I'm not directly installing via dietpi-software but using the method you indicate for testing in the first post of this PR.

* `forward-addr: 1.1.1.1` however does not use `dnscrypt-proxy`, but queries the Cloudflare DNS directly. In such case, there is no point to use Unbound at all, but you would instead configure AdGuard Home to use `1.1.1.1` directly, skipping all additional software. If you want to use `dnscrypt-proxy` with Unbound, you'd need to set `forward-addr: 127.0.0.1@5353`. I assume that `dnscrypt-proxy` uses port 5353, since the guide configures it that way. In case check via `ss -tulpn` which process listens on which port.

forward-addr: 1.1.1.1 it was just a specific test to see if a configuration pointing to a public DNS would make Unbound respond. My current configuration is forward-addr: 127.0.0.1@5353 where dnscrypt-proxy via its own query dig @127.0.0.1 -p 5353 google.com works, but starting from Unbound dig @127.0.0.1 -p 5335 google.com I get SERVERFAIL with the default settings.

ss -tulpn points to all of them listening at their respective doors.

  Adjust `port: 5335` and `interface: 127.0.0.1` in `/etc/unbound/unbound.conf.d/dietpi.conf`. Add the forward zone better as separate config:
  ```shell
  cat << '_EOF_' | sudo tee /etc/unbound/unbound.conf.d/dnscrypt-proxy.conf
  forward-zone:
    name: "."
    forward-addr: 127.0.0.1@5353
  _EOF_
  sudo systemctl restart unbound
  ```

* Check back with `ss -tulpn` that `adguardhome` listens on port 53, `unbound` on port 5335, and `dnscrypt-proxy` on port 5353. This matches the setup intended by the guide: [Wiki: Install DNScrypt proxy (DoH)(oDoH)(Anonymized DNS) (trinib/AdGuard-WireGuard-Unbound-DNScrypt)](https://github.com/trinib/AdGuard-WireGuard-Unbound-DNScrypt/wiki/Install-DNScrypt-proxy-(DoH)(oDoH)(Anonymized-DNS))

I keep getting SERVERFAIL with these settings:

Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] info: start of service (unbound 1.23.1).
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] debug: validator[module 0] operate: extstate:module_state_initial event:module_event_new
Aug 04 17:27:49 DietPi systemd[1]: Started unbound.service - Unbound DNS server.
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] info: validator operate: query . DNSKEY IN
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] debug: iterator[module 1] operate: extstate:module_state_initial event:module_event_pass
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] info: resolving . DNSKEY IN
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] info: processQueryTargets: . DNSKEY IN
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] debug: configured stub or forward servers failed -- returning SERVFAIL
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] debug: return error response SERVFAIL
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] debug: validator[module 0] operate: extstate:module_wait_module event:module_event_moddone
Aug 04 17:27:49 DietPi unbound[2254]: [2254:0] info: validator operate: query . DNSKEY IN
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] debug: validator[module 0] operate: extstate:module_state_initial event:module_event_new
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] info: validator operate: query google.com. A IN
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] debug: iterator[module 1] operate: extstate:module_state_initial event:module_event_pass
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] info: resolving google.com. A IN
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] info: processQueryTargets: google.com. A IN
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] debug: configured stub or forward servers failed -- returning SERVFAIL
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] debug: return error response SERVFAIL
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] debug: validator[module 0] operate: extstate:module_wait_module event:module_event_moddone
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] info: validator operate: query google.com. A IN
Aug 04 17:27:59 DietPi unbound[2254]: [2254:0] debug: cache memory msg=66581 rrset=66104 infra=8224 val=66384 subnet=0

But whether adding dnscrypt-proxy and Unbound in combination makes sense is another question.

So would this guide be conceptually incorrect or not necessary for today's times?

* `dnscrypt-proxy` supports oDoH, which Unbound does not. Compared to DoT (which Unbound supports), DoH makes DNS packets harder to detect, since they are sent via HTTPS on port 443 together with all other HTTPS requests from the network. And oDoH additionally prevents one upstream DNS provider from knowing your IP and the DNS query contents both: an additional proxy is used between client and upstream resolver, hence all together it can be compared to a VPN for DNS.

* But when using `dnscrypt-proxy` anyway, Unbound has no real purpose in between. So in that case, I would configure AdGuard Home to use `dnscrypt-proxy` directly, hence adjust its upstream DNS to use port 5353 instead of 5335, and, as fast as this has been proven to work, uninstall Unbound.

But what if Unbound doesn't offer DoH, wouldn't this implementation with dnscrypt-proxy "fix" that?

* If you need DNS encryption mainly to prevent your LAN and/or ISP from being able to read the queries, DoT would however be enough. In that case `dnscrypt-proxy` would not need to be required (`sudo rm /etc/unbound/unbound.conf.d/dnscrypt-proxy.conf`), and you can instead enable DoT in Unbound that way: [dietpi.com/docs/software/dns_servers#unbound-activating-dns-over-tls-dot](https://dietpi.com/docs/software/dns_servers/#unbound-activating-dns-over-tls-dot)

The goal would be privacy with performance, as you mention, making packets harder to detect, both by IPS and upstream DNS, hence the preference for DoH.

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 4, 2025

Okay got it.

debug: configured stub or forward servers failed -- returning SERVFAIL

So Unbound fails to connect to (or get a proper response from) the dnscrypt-proxy. I found an identical issue where: https://forum.opnsense.org/index.php?topic=34946.0
They used dnscrypt-proxy for DoT, probably with an Unbound version that did not support DoT yet, but otherwise it is the same.

So previously you used /etc/unbound/unbound.conf.d/unbound.conf from the guide, right? I compared it again, and it is practically identical to ours, aside of the forward zone at the bottom, which has upstream DoT enabled, but without and upstream resolver uncommented: https://github.com/trinib/AdGuard-WireGuard-Unbound-DNScrypt/blob/main/unbound.conf
Now the dnscrypt-proxy section uncomments the two related forward addresses, compared to what we tried above, also IPv6. And, more importantly, it leaves upstream DoT enabled πŸ€”. Does this work?

cat << '_EOF_' | sudo tee /etc/unbound/unbound.conf.d/dnscrypt-proxy.conf
server:
  tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt
forward-zone:
  name: "."
  forward-tls-upstream: yes

  forward-addr: 127.0.0.1@5353
  forward-addr: ::1@5353
_EOF_
sudo systemctl restart unbound

While the option to connect via IPv6 doesn't hurt, it should work via IPv4 as well, of course, and does so with dig. And regarding DoT: I do not see dnscrypt-proxy being configured for downstream DoT here, and it should require a hostname in the forward address to use it, so I guess DoT is just skipped by Unbound in this case, not enforced. But let's see whether it makes a difference.

I'll try this locally as well later tonight.

So would this guide be conceptually incorrect or not necessary for today's times?

Not directly "incorrect", but adds one component that IMO does not serve any purpose.

But what if Unbound doesn't offer DoH, wouldn't this implementation with dnscrypt-proxy "fix" that?

Yes, but why using Unbound at all then? If you want to use DoH or oDoH, use dnscrypt-proxy. If DoT or direct recursive validating resolution is wanted, use Unbound. But Unbound as nothing but a forwarder between AGH and dnscrypt-proxy seems unnecessary complexity and overhead to me.

Unbound adds another cache. The guide hence disables the cache in dnscrypt-proxy.toml with cache = false. It could be enabled. But AGH should however have a DNS cache as well, like Pi-hole does. So in any case Unbound seems to not add any value.

@ciro-mota
Copy link

I found the problem:

    # Unbound local queries needs to be off if using stubby or dnscrypt
    do-not-query-localhost: no

These settings aren't in dietpi.conf, but they are in the redundant unbound.conf. Adding these line to dietpi.conf resolved the issue.

Adding this new set of lines that indicates, returns SERVERFAIL.

Would it be a problem to include do-not-query-localhost: no them in a future release of this file?

Unbound adds another cache. The guide hence disables the cache in dnscrypt-proxy.toml with cache = false. It could be enabled. But AGH should however have a DNS cache as well, like Pi-hole does. So in any case Unbound seems to not add any value.

Since AGH has the DNS cache option, would enabling it there bypass the system's use of the cache similar to what Unbound would do?

Thank you very much for the answers.

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 4, 2025

Great, I missed that indeed, respectively did not think of it being relevant in this case, falsely interpreted it as "do not try to resolve 'localhost' upstream". Makes sense, so this setting is enabled by default, most likely to prevent DNS loops, and hence needs to be disabled to allow Unbound using another local resolver as upstream: https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#unbound-conf-do-not-query-localhost

Looks like something we should add to our docs, though the question remains about the purpose of Unbound in such case.

@Dynamic5912
Copy link

So just checking this build is ok to install or are further changes/fixes/etc. required?

What happens with my existing installation of Unbound that was made via DietPi Software?

@Joulinar
Copy link
Collaborator

Unbound as an application will be overwritten and updated to a newer version. The configuration should remain intact. It is better to make a backup beforehand.

@Dynamic5912
Copy link

Installed. Everything seems to be good!

@MichaIng
Copy link
Owner Author

Yes, config file setup has been aligned with Debian's package, so all modified config files remain untouched (new ones will be stored next to them with .dpkg-dist suffix), only config files from the old package which were never edited (dpkg compares hashes to know that) can be replaced by a package upgrade.

So it is a drop-in replacement as much as possible.

@Dynamic5912
Copy link

Worked a treat!

Unbound-control still works - presumably this was because it was there previously?

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 12, 2025

unbound-control is part of the unbound package, same upstream and on Debian. Debian splits off only unbound-anchor and unbound-host, and we do the same, but not provide those packages at all. They both use the library libunbound8 which is another dedicated package, and all of them are hence independent of the Unbound core/daemon package. They hence remain compatible even when installing our unbound package.

To see package content:

dpkg -L unbound

@Dynamic5912
Copy link

Is unbound-anchor not needed for DNSSEC validation and then the updating of the root trust file thereafter?

Along with the corresponding line in the config to point to the root.key file?

https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound-anchor.html

@MichaIng
Copy link
Owner Author

Is unbound-anchor not needed for DNSSEC validation and then the updating of the root trust file thereafter?

No it is not. This tool allows to basically copy the DNSSEC keys from somewhere else to the path where Unbound is reading them, assuring correct permissions, and probably doing some validation. However, Debian provides the keys via dns-root-data package where no validation is needed, and copying them from one place to another is not exactly a task we need a dedicated tool for. Debian thought the same, and hence split that tool into its own package, instead of keeping it as part of the unbound package like upstream builds do. Debian copies the keys from dns-root-data package into Unbound path on service start, if missing or newer, we do that on package install.

The setting with the path is in /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf.

@Dynamic5912
Copy link

Got it. Thanks.

By default, Unbound stores the root.hints and root.key files into /usr/share/dns/ but all the config files that DietPi provides point to /var/lib/unbound/

Presumably the files are copied from usr/share/dns to /var/lib/unbound whenever Unbound (or the files?) are updated to ensure that recent and current files are used?

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 25, 2025

By default, Unbound stores the root.hints and root.key files into /usr/share/dns/ but all the config files that DietPi provides point to /var/lib/unbound/

Both settings have no default at all:

Not sure where you got /usr/share/dns from, but /usr/share generally is reserved for distribution package files, not intended to be written to by software or even admins. Software is intended to write data to /var/lib, which is why Debian chose this directory for root trust anchors as well.

/usr/share/dns is where the dns-root-data package contains its files. But this is not intended to be used by Unbound directly. For root hints, this would be possible, but is unnecessary as of Unbound's internal root hints. For the trust anchors, it requires write access to the file, which is not and must not be permitted to /usr/share/dns. This is why copying them from /usr/share/dns to /var/lib/unbound is done in the first place.

all the config files that DietPi provides

This config file is part of the Debian package, so we chose to keep it that way, staying compatible with the Debian package also regarding config files. We would have stored that setting with the same value in the dietpi.conf instead. But as said, the idea here was to change as little as possible, allowing to switch back and forth between the Debian package and ours, without breaking the configs.

@Dynamic5912
Copy link

So there's a script that copies them to /var/lib...etc. from /usr/share/dns?

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 25, 2025

So there's a script that copies them to /var/lib...etc. from /usr/share/dns?

Yes, as said, Debian does this as part of the service start. Check the ExecStartPre in its unbound.service. This runs a dedicated script which supports some other stuff that I find unnecessary or even harmful (see OP message). Hence we do not ship that helper script. Also I find it unnecessary to run the copy on every service start, hence we do this once on package install (or upgrade). See the package maintainer script /var/lib/dpkg/info/unbound.postinst.

@Dynamic5912
Copy link

Presumably the file copy is here - carried out in the build.bash script for Unbound (correct me if I'm also wrong that this only copies the file if it doesn't already exist - it doesn't overwrite it with a newer version if applicable):

** if [ ! -f '/var/lib/$NAME/root.key' ]
then
echo 'Bootstrapping root trust anchors /var/lib/$NAME/root.key from /usr/share/dns/root.key ...'
setpriv --reuid=$NAME --regid=$NAME --clear-groups cp -v /usr/share/dns/root.key /var/lib/$NAME/
fi**

So given that it copies the files from /usr/share/dns/ which is created by dns-root-data, how, for example, do you ensure the root.key file/s are kept up-to-date - since the latest installed version of dns-root-data is from 2024 (and therefore the files that reside therein are from that time as well??) - without unbound-anchor installed (which runs automated checks to update the root.key files)?

Or does Unbound keep the files updated regardless?

Also regarding root.hints - even the unbound devs (and documentation) recommend using a root.hints file and not relying on the built-in hints.

@MichaIng
Copy link
Owner Author

MichaIng commented Aug 25, 2025

it doesn't overwrite it with a newer version

These keys are only needed a single time as anchor. From that point on, Unbound updates them by itself, which is why the file needs to be writable. You can compare its contents on original and Unbound paths, where you see that it validates them regularly, and updates them in case. So updating it with a "new version" is not needed, it could even make the keys older. And since Unbound writes the files regularly, the mtime is moreless always newer anyway.

The only reason why copying on service start could make sense, is if you do not run unbound for some years, so that the keys are too old to serve as anchor for new ones. For now, I think this scenario is not so relevant. But I guess a cp -u wouldn't hurt on every package upgrade πŸ€”.

Also regarding root.hints - even the unbound devs (and documentation) recommend using a root.hints

Where do they recommend it? It makes sense for Debian, since they their Unbound version is frozen within a Debian release. We however update it on every Unbound release, which makes it unnecessary. Note that DNS root server IPs change only every several years. The last change was an addition some years ago. So even if that was missed, there wouldn't have been any outdated entry in the embedded data. Read the first post, where I discussed all decisions πŸ˜‰.

EDIT: Funnily, Debian does not set root-hints either, even that for them it would make quite some sense.

@Dynamic5912
Copy link

Where do they recommend it?

In the docs - by recommend I mean good/best practice:

image

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

Successfully merging this pull request may close these issues.

DietPi-Software | Unbound: Provide own newer builds
6 participants