[GH-ISSUE #2237] what's the use case for Recursor::resolve(Query::query(not_fully_qualified_name, ..), ..)? / bug in Recursor::resolve with security_aware = false? #932

Closed
opened 2026-03-16 01:00:43 +03:00 by kerem · 4 comments
Owner

Originally created by @japaric on GitHub (Jun 12, 2024).
Original GitHub issue: https://github.com/hickory-dns/hickory-dns/issues/2237

as far as I understand the DNS RFCs, specifically RFC1035, the NAME field of a resource record is always considered to be fully qualified. that can be confirmed with this hickory-dns code

let nfqdn = Name::from_ascii("example.com")?; // no DOT at the end
assert!(!nfqdn.is_fqdn());

let fqdn = Name::from_ascii("example.com.")?;
assert!(fqdn.is_fqdn());

let nfqdn_bytes = nfqdn.to_bytes()?;
let fqdn_bytes = fqdn.to_bytes()?;

// same representation on the wire
assert_eq!(nfqdn_bytes, fqdn_bytes); // [7, b"example", 3, b"com"]

let parsed = Name::read(&mut BinDecoder::new(&fqdn_bytes))?;

// fully qualified after decoding
assert!(parsed.is_fqdn());

and I believe that the main use case of Recursor is as a recursive resolver authority (in hickory-server) which answers requests (records) that have been decoded from some network source (e.g. UDP). given how Name::read behaves as shown above, it seems to me that Recursor::resolve will always observe FQDN (fully qualified domain names) when operating as a recursive resolver server / authority.

however, Recursor::resolve currently accepts not fully-qualified domain names. what it does in that case is that it performs the query as if the given Name was fully qualified. tracing the messages Recursor exchanges, I can see that in both cases (e.g. example.com & example.com.) Recursor gets the correct answer records but in the case the input Name was not fully qualified it discards the records using the is_subzone function and returns an error of the kind "no records found".

however, that's only the behavior when Recursor is configured as not being security_aware. when Recursor's security_aware setting is true then both example.com and example.com. resolve correctly

that behavior can be checked with the following program ( and revision ed192864f3 )

use std::{
    error::Error,
    net::{Ipv4Addr, SocketAddrV4},
    time::Instant,
};

use hickory_recursor::{
    proto::{op::Query, rr::RecordType},
    resolver::{
        config::{NameServerConfigGroup, Protocol},
        Name,
    },
    NameServerConfig, Recursor,
};
use tracing_subscriber::filter;
use tracing_subscriber::prelude::*;

fn main() -> Result<(), Box<dyn Error>> {
    let stdout_log = tracing_subscriber::fmt::layer().pretty();
    tracing_subscriber::registry()
        .with(stdout_log.with_filter(filter::LevelFilter::TRACE))
        .init();

    let mut config = NameServerConfigGroup::default();
    config.push(NameServerConfig::new(
        SocketAddrV4::new(Ipv4Addr::new(198, 41, 0, 4), 53).into(), // a.root-servers.net.
        Protocol::Udp,
    ));

    let recursor = Recursor::builder()
        // .security_aware(true) // works fine with both `example.com` and `example.com.`
        .security_aware(false) // = "no records found" error with `example.com`
        .build(config)?;

    let lookup = tokio_test::block_on(recursor.resolve(
        Query::query(Name::from_ascii("example.com")?, RecordType::A),
        Instant::now(),
        false,
    ))?;

    println!("{lookup:#?}");
}

my question is should Recursor::resolve reject (i.e. fail early) the query argument when it's not a fully qualified domain name?

if not, then I think Recursor::resolve has a bug since it behaves differently with not-fully-qualified domain names depending on the security_aware setting as explained above even though it correctly gets the answer from the name servers it contacts (i.e. resolution succeeds)

Originally created by @japaric on GitHub (Jun 12, 2024). Original GitHub issue: https://github.com/hickory-dns/hickory-dns/issues/2237 as far as I understand the DNS RFCs, specifically RFC1035, the NAME field of a resource record is always considered to be fully qualified. that can be confirmed with this `hickory-dns` code ``` rust let nfqdn = Name::from_ascii("example.com")?; // no DOT at the end assert!(!nfqdn.is_fqdn()); let fqdn = Name::from_ascii("example.com.")?; assert!(fqdn.is_fqdn()); let nfqdn_bytes = nfqdn.to_bytes()?; let fqdn_bytes = fqdn.to_bytes()?; // same representation on the wire assert_eq!(nfqdn_bytes, fqdn_bytes); // [7, b"example", 3, b"com"] let parsed = Name::read(&mut BinDecoder::new(&fqdn_bytes))?; // fully qualified after decoding assert!(parsed.is_fqdn()); ``` and I believe that the main use case of `Recursor` is as a recursive resolver authority (in `hickory-server`) which answers requests (records) that have been decoded from some network source (e.g. UDP). given how `Name::read` behaves as shown above, it seems to me that `Recursor::resolve` will always observe FQDN (fully qualified domain names) when operating as a recursive resolver server / authority. however, `Recursor::resolve` currently accepts not fully-qualified domain names. what it does in that case is that it performs the query as if the given `Name` *was fully qualified*. tracing the messages `Recursor` exchanges, I can see that in both cases (e.g. `example.com` & `example.com.`) `Recursor` gets the correct answer records but in the case the input `Name` was *not* fully qualified it discards the records using the `is_subzone` function and returns an error of the kind "no records found". however, that's only the behavior when `Recursor` is configured as *not* being `security_aware`. when `Recursor`'s `security_aware` setting is `true` then both `example.com` and `example.com.` resolve correctly that behavior can be checked with the following program ( and revision ed192864f3e1143bafd0e577baf9bee7e3b34a3c ) ``` rust use std::{ error::Error, net::{Ipv4Addr, SocketAddrV4}, time::Instant, }; use hickory_recursor::{ proto::{op::Query, rr::RecordType}, resolver::{ config::{NameServerConfigGroup, Protocol}, Name, }, NameServerConfig, Recursor, }; use tracing_subscriber::filter; use tracing_subscriber::prelude::*; fn main() -> Result<(), Box<dyn Error>> { let stdout_log = tracing_subscriber::fmt::layer().pretty(); tracing_subscriber::registry() .with(stdout_log.with_filter(filter::LevelFilter::TRACE)) .init(); let mut config = NameServerConfigGroup::default(); config.push(NameServerConfig::new( SocketAddrV4::new(Ipv4Addr::new(198, 41, 0, 4), 53).into(), // a.root-servers.net. Protocol::Udp, )); let recursor = Recursor::builder() // .security_aware(true) // works fine with both `example.com` and `example.com.` .security_aware(false) // = "no records found" error with `example.com` .build(config)?; let lookup = tokio_test::block_on(recursor.resolve( Query::query(Name::from_ascii("example.com")?, RecordType::A), Instant::now(), false, ))?; println!("{lookup:#?}"); } ``` my question is should `Recursor::resolve` reject (i.e. fail early) the query argument when it's *not* a fully qualified domain name? if not, then I think `Recursor::resolve` has a bug since it behaves differently with not-fully-qualified domain names depending on the `security_aware` setting as explained above even though it correctly gets the answer from the name servers it contacts (i.e. resolution succeeds)
kerem 2026-03-16 01:00:43 +03:00
Author
Owner

@djc commented on GitHub (Jun 13, 2024):

my question is should Recursor::resolve reject (i.e. fail early) the query argument when it's not a fully qualified domain name?

I don't have a great amount of context here but you've convinced me that it probably should. Have you tested the behavior of other implementations in this regard, for example in your conformance test suite?

<!-- gh-comment-id:2165092974 --> @djc commented on GitHub (Jun 13, 2024): > my question is should `Recursor::resolve` reject (i.e. fail early) the query argument when it's _not_ a fully qualified domain name? I don't have a great amount of context here but you've convinced me that it probably should. Have you tested the behavior of other implementations in this regard, for example in your conformance test suite?
Author
Owner

@japaric commented on GitHub (Jun 13, 2024):

Have you tested the behavior of other implementations in this regard, for example in your conformance test suite?

the conformance tests interact with name servers and resolvers over the network and over the network all the domain names in the queries are treated as being fully qualified so through those tests I can't test the equivalent of Recursor::resolve("example.com"). IOW, the BIND / unbound / hickory-dns binaries are not going to exercise that code path, I think.

I think the only way to test / trigger recursive resolution of non-fully qualified domain names is via library API and I don't know if BIND / unbound offer such API or even libraries for development since they are mainly use as server binaries.

<!-- gh-comment-id:2165671443 --> @japaric commented on GitHub (Jun 13, 2024): > Have you tested the behavior of other implementations in this regard, for example in your conformance test suite? the conformance tests interact with name servers and resolvers over the network and over the network all the domain names in the queries are treated as being fully qualified so through those tests I can't test the equivalent of `Recursor::resolve("example.com")`. IOW, the BIND / unbound / hickory-dns *binaries* are not going to exercise that code path, I think. I think the only way to test / trigger recursive resolution of *non*-fully qualified domain names is via library API and I don't know if BIND / unbound offer such API or even libraries for development since they are mainly use as server binaries.
Author
Owner

@djc commented on GitHub (Jun 13, 2024):

By itself that seems like enough reason to disallow it for now, can always adjust course if we get feedback about it later? (I don't think many people are relying on the Recursor library API, though...)

<!-- gh-comment-id:2165687985 --> @djc commented on GitHub (Jun 13, 2024): By itself that seems like enough reason to disallow it for now, can always adjust course if we get feedback about it later? (I don't think many people are relying on the `Recursor` library API, though...)
Author
Owner

@bluejekyll commented on GitHub (Jun 16, 2024):

For the recursor, I agree, this makes sense to disallow. I think I was basing a lot of this implementation initially off the resolver interface, I make not have considered this in enough detail in the current implementation.

<!-- gh-comment-id:2170990175 --> @bluejekyll commented on GitHub (Jun 16, 2024): For the recursor, I agree, this makes sense to disallow. I think I was basing a lot of this implementation initially off the resolver interface, I make not have considered this in enough detail in the current implementation.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/hickory-dns#932
No description provided.