NLnetLabs / unbound

Unbound is a validating, recursive, and caching DNS resolver.
https://nlnetlabs.nl/unbound
BSD 3-Clause "New" or "Revised" License
3.13k stars 359 forks source link

CNAME lookups not recursing as expected #747

Open bedge opened 2 years ago

bedge commented 2 years ago

Unbound does not chain lookups for CNAMEs from configured stub or forwarding zones.

Example: Local zone: example.com; contains numerous A records injected by DHCP server.

host.example.com A 10.1.1.100

Stub zone in unbound.conf

stub-zone:
    name: "example.com"
    stub-host: ad_domain_controller.example.com
    stub-first: no
    stub-no-cache: yes

referencing hosts that exist in the local zone. ie: ad_domain_controller.example.com contains:

alias.example.com CNAME host.example.com

Observed behavior: A DNS lookup against Unbound for alias.example.com returns the CNAME (retrieved from stub server) to host.example.com but does not return an A record or IP address (even though that record exists in the local zone) unless that A record exists in the stub DNS server.

Expected behavior: Unbound first checks local zone and cache, then queries stub zone if no match found. Unbound receives CNAME from stub server, then initiates a new lookup starting with the local zone, returning the local-zone A record. Final result of the lookup should be the IP addr 10.1.1.100 rather than the CNAME alias.example.com

Context: running Unbound 1.13.2 on pfSense. Reviewed release notes through current version and none seem relevant to this issue. I have an AD domain controller (which is an authoritative DNS server) which I'm using to host some CNAME records in my local domain. I don't want to use the AD DHCP server, or its DNS server exclusively, because I run pfBlocker and want its logs to show me which host made each blocked request.

wcawijngaards commented 2 years ago

Unbound does not inspect the local zone data for recursively looked up CNAME targets. If you want that to be changed, there are some options. The respip module can trigger on the response IP address and it also works for recursively looked up names after a CNAME, and that could be something if you want to trigger that by IP address instead of by name. By name is also possible, the RPZ zones are inspected also for recursively looked up content. In the man page 'Response Policy Zone Options', there it also explains the syntax by which the specific name can get an override, to another IP address, or to block it. Then load the respip module in module-config and the rpz zone with that configuration.

bedge commented 2 years ago

Thanks @wcawijngaards, the pointers are much appreciated. So is your patience with the uneducated (me). Reading the docs I feel I'm looking for a much simpler case than for what rpz is targeting.

Here's the config I added for rpz:

module-config: "respip validator iterator". <-- I just added respip

rpz:
        name: "rpz.example.com"                  <-- I'm assuming I need a prefix on `example.com`?
        zonefile: /var/unbound/rpz.example.com
        primary: 10.1.1.1                        <-- this is the host running unbound. 
        rpz-log: yes
        rpz-log-name: rpz.example.com

I'm having trouble constructing the minimum zonefile for rpz, eg: rpz.example.com.

Are there any examples for this use case? ie: falling back to a local lookup for when a DNS query results in a CNAME?

Ideally, I want unbound to lookup the returned CNAME locally if the returned CNAME matches *.sample.com, but if that's not viable, I could construct a zone file with a set of A records to provide the IPs. Just looking for some pointers on the format.

I already have local-data statements in my unbound cfg, which are able to resolve the CNAMEs coming back from the stub-zone, eg:

# Static host entries
include: /var/unbound/host_entries.conf

and, in /var/unbound/host_entries.conf:

local-data-ptr: "10.1.1.100 host.sample.com"
local-data: "host.sample.com. A 10.1.1.100"

Can I reference these, or must they be defined anew in the rpz zonefile?

wcawijngaards commented 2 years ago

The rpz file only needs a line like host.sample.com A 10.1.1.100 for the A records. For the PTR records, you could put in the PTR records for the reverse, in reverse notation. Note that it is without a trailing dot '.' after the sample.com so it gets extended by the rpz zone name.

redge commented 2 years ago

Backing up a level of abstraction: the ideal solution would be one that causes Unbound to first check the local zone, then the stub zone, for both initial lookups and any recursion. Is there a way to write (and perhaps an example of) and RPZ zonefile that would deliver that behavior generically for a given domain and any children, rather than having to programmatically generate a zonefile explicitly specifying each host?

wcawijngaards commented 2 years ago

I think the RPZ syntax may support the wildcard syntax, *.sample.com A 192.0.2.1

RPZ is also applied, I believe for incoming traffic, not just when recursing, so the local zone is not needed, it checks the RPZ zone.

redge commented 2 years ago

We'll test the wildcard syntax and report back.

But I think we have to keep the local zone, as that's where pfSense inserts the host entries for the DHCP leases; the RPZ is just the mechanism through which we can have CNAMEs from an authoritative server recurse to that local zone.

bedge commented 2 years ago

I think the RPZ syntax may support the wildcard syntax, *.sample.com A 192.0.2.1

While I am after a solution that recurses the lookup locally when a *.sample.com CNAME is returned, I can't specify an A record as it will differ based on the CNAME returned.

eg: There's host1.sample.com and host1.sample.com that are known locally, and both have CNAMEs in the stub zone, eg:

alias1.sample.com = host1.sample.com alias2.sample.com = host2.sample.com

So, a wildcard QNAME can't specify a single A record, but rather needs to indicate that the unbound resolve the returned CNAME locally.

The path being something like: (forgive my terminology assassination)

request: alias1.sample.com -> stub zone resolves alias1.sample.com -> returns CNAME: host1.sample.com -> unbound recurses with a local lookup because the stub zone response matches a QNAME trigger in the rpz: *.sample.com.sample.com -> some as yet undiscovered action that looks up the returned CNAME, eg: -> host1.sample.com -> 10.1.1.100

wcawijngaards commented 2 years ago

No, it is not that the RPZ causes it to perform local zone lookup. With the RPZ it can do an RPZ lookup. The A records can be specified in the RPZ zone. For the individual hosts.

bedge commented 2 years ago

OK, gotcha. Then this is not the appropriate solution, unless I figure something else out to dump the internal resolver state to a zone file. Which may not be the worst thing, but that feels like a band-aid fix.

Is there some other mechanism that does perform a local lookup of a CNAME returned from a stub zone?

wcawijngaards commented 2 years ago

If you do not like this. Then I do not know. You could maybe write a module in code, with dynlib, or in python, with pythonmod, and load that and make it look into the local zone information for such CNAMEs.

Or run two unbound instances, and one sends to another. That increases the configuration flexibility.

bedge commented 2 years ago

Understood. I can script the creation of an RPZ zone file containing all my local CNAMEs resolved to A records. Just feels like this should not be necessary given unbound's "recursive" claims.

In pseudocode:

If returned lookup is a CNAME AND domain == sample.com.  {
     lookup the returned CNAME
}

IOW: Why must I "hand feed" unbound with an RPZ zone file containing the CNAMES it already knows, given the "recursive" claim?

Honestly, feels like we're still missing something and I'm too much of a noob to know what it is.

Regardless, appreciate all the suggestions and engagement, thanks.

wcawijngaards commented 2 years ago

But you say the CNAME can be retrieved from the stub upstream? If so, it need not be in the RPZ file. But the A record that you want to have it resolve it, that is not the A record that would otherwise get loaded, that A record is then what is put in the RPZ file. And then unbound uses that instead of the information it would otherwise retrieve.

bedge commented 2 years ago

I get that it's not the simplest config. Apologies if I'm exacerbating the problem with poor descriptions.

But you say the CNAME can be retrieved from the stub upstream? If so, it need not be in the RPZ file.

No, the stub zone dereferences one CNAME into another CNAME and still returns a CNAME, that the localhost can resolve: (from https://github.com/NLnetLabs/unbound/issues/747#issuecomment-1240116373) The stub zone does this:

alias1.sample.com = host1.sample.com
alias2.sample.com = host2.sample.com

The localhost running unbound has this:

local-data-ptr: "10.1.1.100 host1.sample.com"
local-data: "host1.sample.com. A 10.1.1.100"

in host_entries.conf, as populated by the DHCP server reserved MAC mappings. This is why the A records cannot exist in the stub zone DNS server.

So the unbound host is capable of resolving host1.sample.com to an IP already, but it doesn't.

Given unbound gets a CNAME that it can resolve locally back from the stub zone, it should then map this to an A record. Rather it just returns the CNAME without attempting to resolve via the DHCP IP mappings.

Maybe a better way to phrase this is - How do I get unbound to consult the DHCP server mapped MAC->IP reservations?

redge commented 2 years ago

So it turns out we all overlooked a longer-standing issue from #132, and this is actually a deliberate "feature" of Unbound. The resolver has a baked-in check for CNAMEs referencing the local zone, and stops chaining lookups at the last CNAME in the original lookup zone. Ultimately, the cross-domain CNAMEing we've been trying to do appears to be impossible with the current version of Unbound.

rozhao2 commented 1 year ago

my unbound version is 1.16.0

# unbound -V
Version 1.16.0

Configure line: --build=x86_64-redhat-linux-gnu --host=x86_64-redhat-linux-gnu --program-prefix= --disable-dependency-tracking --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin --sbindir=/usr/sbin --sysconfdir=/etc --datadir=/usr/share --includedir=/usr/include --libdir=/usr/lib64 --libexecdir=/usr/libexec --localstatedir=/var --sharedstatedir=/var/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-conf-file=/var/unbound/unbound.conf --disable-rpath
Linked libs: mini-event internal (it uses select), OpenSSL 1.0.2k-fips  26 Jan 2017
Linked modules: dns64 respip validator iterator

BSD licensed, see LICENSE in source package for details.
Report bugs to unbound-bugs@nlnetlabs.nl or https://github.com/NLnetLabs/unbound/issues

Here is my configuration:

  1. unbound.conf

    server:                        
    module-config: "respip validator iterator"
    
    .... old lines...
    rpz:
    name: rpz.localhost
    zonefile: /etc/unbound/rpz.localhost
    
    ... old lines...
    1. /etc/unbound/rpz.localhost
      
      $ORIGIN rpz.localhost.

www.google.com IN CNAME www.facebook.com.


3. test result

host www.google.com

www.google.com is an alias for www.facebook.com. www.facebook.com has address 128.242.245.180

host www.facebook.com

www.facebook.com has address 128.242.245.180

host www.oracle.com

www.oracle.com is an alias for ds-www.oracle.com.edgekey.net. ds-www.oracle.com.edgekey.net is an alias for e2581.dscx.akamaiedge.net. e2581.dscx.akamaiedge.net has address 23.43.230.57



We can see www.google.com is overrided to facebook, other domain names are not impacted.

Post here as a reference, not sure if it can help others.