Unbound + odhcpd warnings duplicate local-zone + serving local hostnames

Hei!

Time for some experiments: i changed my router from dnsmasq + https dns proxy to unbound + odhcpd. I checked the readme etc. and it seems to work for now. But now i have a lot of warnings in my log on every restart
[16.01.2026, 20:05:13 MEZ] daemon.warn: unbound: [21208:0] warning: duplicate local-zone joek.eu.
[16.01.2026, 20:05:13 MEZ] daemon.warn: unbound: [21208:0] warning: duplicate local-zone joek.eu.
[16.01.2026, 20:05:13 MEZ] daemon.warn: unbound: [21208:0] warning: duplicate local-zone joek.eu.
[16.01.2026, 20:05:13 MEZ] daemon.warn: unbound: [21208:0] warning: duplicate local-zone joek.eu.
[16.01.2026, 20:05:13 MEZ] daemon.warn: unbound: [21208:0] warning: duplicate local-zone joek.eu.
[16.01.2026, 20:05:13 MEZ] daemon.warn: unbound: [21208:0] warning: duplicate local-zone joek.eu.
[16.01.2026, 20:05:13 MEZ] daemon.warn: unbound: [21208:0] warning: duplicate local-zone joek.eu.

The list is even longer than this. What i‘m doing wrong here? I have a lot of static leases configured, and also some hostnames and cname defined.

Second thing: is it possible to configure unbound so it answers to „nslookup myhostname“ like „nslookup myhostname.joek.eu“. I find it funny, on the router itself it works, but clients don‘t get an answer.

Thanks

Which adblocker does not eliminate duplicate DNS names from 20k+ long list?

See what’s duplicated in /var/lib/unbound/unbound.conf.

grep local-zone: /var/lib/unbound/unbound.conf

up to the the line number 21208

That’s the PID.

1 Like

there it is:

root@block:~#grep local-zone: /var/lib/unbound/unbound.conf
  local-zone: joek.eu static
  local-zone: local always_nxdomain
  local-zone: block static
  local-zone: 247.128.51.185.in-addr.arpa transparent
  local-zone: 2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.a.1.4.0.a.1.f.1.0.7.4.0.1.0.0.2.ip6.arpa transparent
  local-zone: 11.168.192.in-addr.arpa static
  local-zone: f.e.e.b.e.9.0.2.0.7.4.0.1.0.0.2.ip6.arpa static
  local-zone: f.e.e.b.2.0.5.0.a.d.8.b.0.9.d.f.ip6.arpa static
  local-zone: 22.168.192.in-addr.arpa static
  local-zone: 11.168.192.in-addr.arpa static
  local-zone: f.e.e.b.e.9.0.2.0.7.4.0.1.0.0.2.ip6.arpa static
  local-zone: f.e.e.b.2.0.5.0.a.d.8.b.0.9.d.f.ip6.arpa static
  local-zone: 22.168.192.in-addr.arpa static
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent

looking into the unbound.conf there a comment before all those duplicates:

…
# /var/lib/unbound/dnsmasq_srv.conf.tmp generated by UCI
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
  local-zone: joek.eu transparent
…

dnsmasq is still installed but disabled …

Do you have dhcp hosts defined in /etc/config/dhcp in a similar quantity to these transparent local-zone: entries?

the numbers don‘t match …

root@block:~#grep -c "local-zone: joek.eu" /var/lib/unbound
/unbound.conf
47
root@block:~#grep -c "config cname" /etc/config/dhcp
5
root@block:~#grep -c "config domain" /etc/config/dhcp
6
root@block:~#grep -c "config domain" /etc/config/dhcp
6
root@block:~#grep -c "config host" /etc/config/dhcp
63

Will you share uci export unbound also?

root@block:~#uci export unbound
package unbound

config unbound 'ub_main'
        option add_extra_dns '3'
        option add_local_fqdn '4'
        option add_wan_fqdn '0'
        option dhcp_link 'odhcpd'
        option dns64 '0'
        option domain_type 'static'
        option edns_size '1232'
        option extended_stats '1'
        option hide_binddata '1'
        option interface_auto '1'
        option listen_port '53'
        option localservice '1'
        option manual_conf '0'
        option num_threads '1'
        option protocol 'default'
        option rate_limit '0'
        option rebind_localhost '0'
        option rebind_protection '0'
        option recursion 'aggressive'
        option resource 'large'
        option root_age '9'
        option ttl_min '120'
        option ttl_neg_max '1000'
        option unbound_control '1'
        option validator '1'
        option verbosity '1'
        list iface_trig 'lan'
        list iface_trig 'wan'
        option enabled '1'
        list iface_lan 'lan'
        list iface_lan 'wg_vpn'
        list iface_wan 'wan'
        list iface_wan 'wan6'
        option domain 'joek.eu'
        option dnsmasq_conf '0'

config zone 'auth_icann'
        option enabled '0'
        option fallback '1'
        option url_dir 'https://www.internic.net/domain/'
        option zone_type 'auth_zone'
        list server 'lax.xfr.dns.icann.org'
        list server 'iad.xfr.dns.icann.org'
        list zone_name '.'
        list zone_name 'arpa.'
        list zone_name 'in-addr.arpa.'
        list zone_name 'ip6.arpa.'

config zone 'fwd_isp'
        option enabled '0'
        option fallback '1'
        option resolv_conf '1'
        option zone_type 'forward_zone'
        list zone_name 'isp-bill.example.com.'
        list zone_name 'isp-mail.example.net.'

config zone 'fwd_google'
        option enabled '0'
        option fallback '1'
        option tls_index 'dns.google'
        option tls_upstream '1'
        option zone_type 'forward_zone'
        list server '8.8.4.4'
        list server '8.8.8.8'
        list server '2001:4860:4860::8844'
        list server '2001:4860:4860::8888'
        list zone_name '.'

config zone 'fwd_cloudflare'
        option enabled '1'
        option fallback '1'
        option tls_index 'cloudflare-dns.com'
        option tls_upstream '1'
        option zone_type 'forward_zone'
        list server '1.1.1.1'
        list server '1.0.0.1'
        list server '2606:4700:4700::1111'
        list server '2606:4700:4700::1001'
        list zone_name '.'
        option dns_assist 'none'

Crossing google and cloudflare in same place is unnecessary, youd want one single provider, like to have consistent ipsec and extension handling.

what do you mean crossing google and cloudflare?

those forward zones are oob and only cloudflare is enabled.

Do your dhcp config host entries have joek.eu appended to the name or are they just the short name?

It’s hard to follow the Unbound dnsmasq script (it reads the dhcp config entries even when dnsmasq is inactive).

dhcp.host.name are always only the name;

dhcp.cname.name are the name+joek.eu same as dhcp.domain.name entries

I think there’s a flaw in this function where it doesn’t fully recognize that it’s already seen your joek.eu domain already and keeps adding it multiple times. target has the entire FQDN, but in practice, it should only contain the domain part.

yeah, definitely this function does not work as expected.
i put some logs in the script: target has the fqdn so the case never matches …

also interesting: DM_LIST_KNOWN_ZONES is never read

What if you move the partial value earlier before the case to only include the last 2 parts of the domain? And use partial instead of target in the case?

create_local_zone() {
  local target="$1"
  local partial domain found

  partial=$( echo "$target" | awk -F. '{ j=NF ; i=j-1; print $i"."$j }' )


  case $DM_LIST_TRN_ZONES in
    *" ${partial} "*)
      found=1
      ;;

    *)
      case $target in
        [A-Za-z0-9]*.[A-Za-z0-9]*)
          found=0
          ;;

        *) # no dots
          found=1
          ;;
      esac
  esac

  if [ $found -eq 0 ] ; then
    # New Zone! Bundle local-zones: by first two name tiers "abcd.tld."
    DM_LIST_TRN_ZONES="$DM_LIST_TRN_ZONES $partial "
    DM_LIST_KNOWN_ZONES="$DM_LIST_KNOWN_ZONES $partial "
  fi
}

yes. moved the partial value, changed the case match and added a trailing space in the list. so the matching pattern is blankDomain.tldblank
no duplicates anymore. still warnings because joek.eu is included one time static and one time transparent

1 Like

Second version, so if the domain is already known and static it won’t be added at all:

create_local_zone() {
  local target="$1"
  local partial domain found

  partial=$( echo "$target" | awk -F. '{ j=NF ; i=j-1; print $i"."$j }' )

  # Zone ist bereits bekannt (z. B. Router-Domain)
  case " $DM_LIST_KNOWN_ZONES " in
    *" $partial "*) return ;;
  esac


  case " $DM_LIST_TRN_ZONES " in
    *" ${partial} "*)
      found=1
      ;;

    *)
      case $target in
        [A-Za-z0-9]*.[A-Za-z0-9]*)
          found=0
          ;;

        *) # no dots
          found=1
          ;;
      esac
  esac

  if [ $found -eq 0 ] ; then
    # New Zone! Bundle local-zones: by first two name tiers "abcd.tld."
    DM_LIST_TRN_ZONES="$DM_LIST_TRN_ZONES $partial "
    DM_LIST_KNOWN_ZONES="$DM_LIST_KNOWN_ZONES $partial "
  fi
}