Compare commits
10 Commits
522220321f
...
66dbc73d2b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
66dbc73d2b | ||
![]() |
b9009e770c | ||
![]() |
02a8668fd5 | ||
![]() |
1ebc11c1d9 | ||
![]() |
b7e4f9a984 | ||
![]() |
9f8c19cb71 | ||
![]() |
5d078ab765 | ||
![]() |
da5423cb06 | ||
![]() |
198128ca48 | ||
![]() |
3129a31386 |
|
@ -17,6 +17,8 @@ All notes should be prepended with the location of the change, e.g. `(proto)` or
|
|||
- (all) Update mio to 0.8.11 to fix RUSTSEC-2024-0019 #2166 by marcus0x62
|
||||
- (proto) Fix formatting issue in crates/proto/src/op/message.rs #2165 by marcus0x62
|
||||
- (proto) fix internal representation of OPT #2151 by esensar
|
||||
- (proto) ECH service parameter key corrected from "echconfig" to "ech" #2183 by cpu
|
||||
- (proto) SVCB/HTTPS record parsing fixes (quoted values, arbitrary numeric keys, lists containing delim) #2183 by cpu
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -45,6 +47,8 @@ All notes should be prepended with the location of the change, e.g. `(proto)` or
|
|||
- (all) get(0) to first() and zerocopy package updates to fix clippy and cargo audit errors #2121 by marcus0x62
|
||||
- (resolver) Add getters for resolver config and options #2093 by hoxxep
|
||||
- (client) updated h2_client_connection and web-pki-roots config #2088 by marcbrevoort-cyberhive
|
||||
- (proto) EchConfig renamed to EchConfigList to match content #2183 by cpu
|
||||
- (proto) EchConfigList updated to wrap TLS presentation language encoding of content #2183 by cpu
|
||||
|
||||
### Added
|
||||
|
||||
|
|
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -283,7 +283,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-std-resolver"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
|
@ -894,7 +894,7 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
|||
|
||||
[[package]]
|
||||
name = "hickory-client"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
|
@ -917,7 +917,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hickory-compatibility"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"futures",
|
||||
|
@ -929,7 +929,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hickory-dns"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"futures-util",
|
||||
|
@ -949,7 +949,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hickory-integration"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
|
@ -971,7 +971,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
|
@ -1018,7 +1018,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hickory-recursor"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
|
@ -1039,7 +1039,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-executor",
|
||||
|
@ -1067,7 +1067,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hickory-server"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"basic-toml",
|
||||
|
@ -1101,7 +1101,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hickory-util"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0-alpha.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"console",
|
||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -28,11 +28,11 @@ license = "MIT OR Apache-2.0"
|
|||
|
||||
[workspace.dependencies]
|
||||
# hickory
|
||||
hickory-client = { version = "0.24.0", path = "crates/client", default-features = false }
|
||||
hickory-recursor = { version = "0.24.0", path = "crates/recursor", default-features = false }
|
||||
hickory-resolver = { version = "0.24.0", path = "crates/resolver", default-features = false }
|
||||
hickory-server = { version = "0.24.0", path = "crates/server", default-features = false }
|
||||
hickory-proto = { version = "0.24.0", path = "crates/proto", default-features = false }
|
||||
hickory-client = { version = "0.25.0-alpha.1", path = "crates/client", default-features = false }
|
||||
hickory-recursor = { version = "0.25.0-alpha.1", path = "crates/recursor", default-features = false }
|
||||
hickory-resolver = { version = "0.25.0-alpha.1", path = "crates/resolver", default-features = false }
|
||||
hickory-server = { version = "0.25.0-alpha.1", path = "crates/server", default-features = false }
|
||||
hickory-proto = { version = "0.25.0-alpha.1", path = "crates/proto", default-features = false }
|
||||
|
||||
|
||||
# logging
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
mod rfc4035;
|
||||
mod rfc5155;
|
||||
mod scenarios;
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use dns_test::client::{Client, DigSettings, DigStatus};
|
||||
use dns_test::name_server::NameServer;
|
||||
use dns_test::nsec3::NSEC3Records;
|
||||
use dns_test::record::{Record, RecordType, NSEC3};
|
||||
use dns_test::{Network, Result, FQDN};
|
||||
|
||||
const TLD_FQDN: &str = "alice.com.";
|
||||
const NON_EXISTENT_FQDN: &str = "charlie.alice.com.";
|
||||
const WILDCARD_FQDN: &str = "*.alice.com.";
|
||||
|
||||
// These hashes are computed with 1 iteration of SHA-1 without salt and must be recomputed if
|
||||
// those parameters were to change.
|
||||
const TLD_HASH: &str = "LLKH4L6I60VHAPP6VRM3DFR9RI8AK9I0"; /* h(alice.com.) */
|
||||
const NON_EXISTENT_HASH: &str = "99P1CCPQ2N64LIRMT2838O4HK0QFA51B"; /* h(charlie.alice.com.) */
|
||||
const WILDCARD_HASH: &str = "19GBV5V1BO0P51H34JQDH1C8CIAA5RAQ"; /* h(*.alice.com.) */
|
||||
|
||||
// This test checks that name servers produce a name error response compliant with section 7.2.2.
|
||||
// of RFC5155.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn name_error_response() -> Result<()> {
|
||||
let alice_fqdn = FQDN(TLD_FQDN)?;
|
||||
// The queried name
|
||||
let qname = FQDN(NON_EXISTENT_FQDN)?;
|
||||
|
||||
let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
|
||||
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
|
||||
&qname,
|
||||
RecordType::A,
|
||||
)?;
|
||||
|
||||
assert!(status.is_nxdomain());
|
||||
|
||||
// Closest Encloser Proof
|
||||
//
|
||||
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
|
||||
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
|
||||
// existing RR.
|
||||
//
|
||||
// The next closer name of a name is the name one label longer than its closest encloser. In
|
||||
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
|
||||
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_HASH)
|
||||
.expect("Cannot find a closest encloser proof in the zonefile");
|
||||
|
||||
// Wildcard at the closet encloser RR: Must cover the wildcard at the closest encloser of
|
||||
// QNAME.
|
||||
//
|
||||
// In this scenario, the closest encloser is `alice.com.`, so the wildcard at the closer
|
||||
// encloser is `*.alice.com.`.
|
||||
//
|
||||
// This NSEC3 RR must cover the hash of the wildcard at the closests encloser.
|
||||
|
||||
// if this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let wildcard_rr = nsec3_rrs
|
||||
.find_cover(WILDCARD_HASH)
|
||||
.expect("No RR in the zonefile covers the wildcard");
|
||||
|
||||
// Now we check that the response has the three NSEC3 RRs.
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
[
|
||||
(
|
||||
closest_encloser_rr,
|
||||
"No RR in the response matches the closest encloser",
|
||||
),
|
||||
(
|
||||
next_closer_name_rr,
|
||||
"No RR in the response covers the next closer name",
|
||||
),
|
||||
(wildcard_rr, "No RR in the response covers the wildcard"),
|
||||
],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This test checks that name servers produce a no data response compliant with section 7.2.3.
|
||||
// of RFC5155 when the query type is not DS.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn no_data_response_not_ds() -> Result<()> {
|
||||
let alice_fqdn = FQDN(TLD_FQDN)?;
|
||||
// The queried name
|
||||
let qname = alice_fqdn.clone();
|
||||
|
||||
let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
|
||||
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
|
||||
&qname,
|
||||
RecordType::MX,
|
||||
)?;
|
||||
|
||||
// The server MUST include the NSEC3 RR that matches QNAME.
|
||||
|
||||
// if this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let qname_rr = nsec3_rrs
|
||||
.find_match(TLD_HASH)
|
||||
.expect("No RR in the zonefile matches QNAME");
|
||||
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
[(qname_rr, "No RR in the response matches QNAME")],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This test checks that name servers produce a no data response compliant with section 7.2.4.
|
||||
// of RFC5155 when the query type is DS and there is an NSEC3 RR that matches the queried name.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn no_data_response_ds_match() -> Result<()> {
|
||||
let alice_fqdn = FQDN(TLD_FQDN)?;
|
||||
// The queried name
|
||||
let qname = alice_fqdn.clone();
|
||||
|
||||
let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
|
||||
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
|
||||
&qname,
|
||||
RecordType::DS,
|
||||
)?;
|
||||
|
||||
// If there is an NSEC3 RR that matches QNAME, the server MUST return it in the response.
|
||||
|
||||
// if this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let qname_rr = nsec3_rrs
|
||||
.find_match(TLD_HASH)
|
||||
.expect("No RR in the zonefile matches QNAME");
|
||||
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
[(qname_rr, "No RR in the response matches QNAME")],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This test checks that name servers produce a no data response compliant with section 7.2.4.
|
||||
// of RFC5155 when the query type is DS and no NSEC3 RR matches the queried name.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn no_data_response_ds_no_match() -> Result<()> {
|
||||
let alice_fqdn = FQDN(TLD_FQDN)?;
|
||||
// The queried name
|
||||
let qname = FQDN(NON_EXISTENT_FQDN)?;
|
||||
|
||||
let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
|
||||
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
|
||||
&qname,
|
||||
RecordType::DS,
|
||||
)?;
|
||||
|
||||
// If no NSEC3 RR matches QNAME, the server MUST return a closest provable encloser proof for
|
||||
// QNAME.
|
||||
|
||||
// Closest Encloser Proof
|
||||
//
|
||||
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
|
||||
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
|
||||
// existing RR.
|
||||
//
|
||||
// The next closer name of a name is the name one label longer than its closest encloser. In
|
||||
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
|
||||
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_HASH)
|
||||
.expect("Cannot find a closest encloser proof in the zonefile");
|
||||
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
[
|
||||
(
|
||||
closest_encloser_rr,
|
||||
"No RR in the response matches the closest encloser",
|
||||
),
|
||||
(
|
||||
next_closer_name_rr,
|
||||
"No RR in the response covers the next closer name",
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This test checks that name servers produce a wildcard no data response compliant with section 7.2.5.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn wildcard_no_data_response() -> Result<()> {
|
||||
let wildcard_fqdn = FQDN(WILDCARD_FQDN)?;
|
||||
// The queried name
|
||||
let qname = FQDN(NON_EXISTENT_FQDN)?;
|
||||
|
||||
let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
|
||||
[Record::a(wildcard_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
|
||||
&qname,
|
||||
RecordType::MX,
|
||||
)?;
|
||||
|
||||
// If there is a wildcard match for QNAME, but QTYPE is not present at that name, the response MUST
|
||||
// include a closest encloser proof for QNAME and MUST include the NSEC3 RR that matches the
|
||||
// wildcard.
|
||||
|
||||
// Closest Encloser Proof
|
||||
//
|
||||
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
|
||||
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
|
||||
// existing RR.
|
||||
//
|
||||
// The next closer name of a name is the name one label longer than its closest encloser. In
|
||||
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
|
||||
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_HASH)
|
||||
.expect("Cannot find a closest encloser proof in the zonefile");
|
||||
|
||||
// Wildcard RR: This NSEC3 RR must match `*.alice.com`.
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let wildcard_rr = nsec3_rrs
|
||||
.find_match(WILDCARD_HASH)
|
||||
.expect("No RR in the zonefile matches the wildcard");
|
||||
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
[
|
||||
(
|
||||
closest_encloser_rr,
|
||||
"No RR in the response matches the closest encloser",
|
||||
),
|
||||
(
|
||||
next_closer_name_rr,
|
||||
"No RR in the response covers the next closer name",
|
||||
),
|
||||
(wildcard_rr, "No RR in the response covers the wildcard"),
|
||||
],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This test checks that name servers produce a wildcard answer response compliant with section 7.2.6.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn wildcard_answer_response() -> Result<()> {
|
||||
let wildcard_fqdn = FQDN(WILDCARD_FQDN)?;
|
||||
// The queried name
|
||||
let qname = FQDN(NON_EXISTENT_FQDN)?;
|
||||
|
||||
let (nsec3_rrs, _status, nsec3_rrs_response) = query_nameserver(
|
||||
[Record::a(wildcard_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
|
||||
&qname,
|
||||
RecordType::A,
|
||||
)?;
|
||||
|
||||
// If there is a wildcard match for QNAME and QTYPE, then, in addition to the expanded wildcard
|
||||
// RRSet returned in the answer section of the response, proof that the wildcard match was
|
||||
// valid must be returned. ... To this end, the NSEC3 RR that covers the "next closer" name of the
|
||||
// immediate ancestor of the wildcard MUST be returned.
|
||||
|
||||
// The next closer name of a name is the name one label longer than its closest encloser. In
|
||||
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
|
||||
|
||||
// If this panics, it probably means that the precomputed hashes must be recomputed.
|
||||
let next_closer_name_rr = nsec3_rrs
|
||||
.find_cover(NON_EXISTENT_HASH)
|
||||
.expect("No RR in the zonefile covers the next closer name");
|
||||
|
||||
find_records(
|
||||
&nsec3_rrs_response,
|
||||
[(
|
||||
next_closer_name_rr,
|
||||
"No RR in the response covers the next closer name",
|
||||
)],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn query_nameserver(
|
||||
records: impl IntoIterator<Item = Record>,
|
||||
qname: &FQDN,
|
||||
qtype: RecordType,
|
||||
) -> Result<(NSEC3Records, DigStatus, Vec<NSEC3>)> {
|
||||
let network = Network::new()?;
|
||||
let mut ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)?;
|
||||
|
||||
for record in records {
|
||||
ns.add(record);
|
||||
}
|
||||
|
||||
let ns = ns.sign()?;
|
||||
|
||||
let nsec3_rrs = NSEC3Records::new(ns.signed_zone_file());
|
||||
|
||||
let ns = ns.start()?;
|
||||
|
||||
let client = Client::new(&network)?;
|
||||
let output = client.dig(
|
||||
*DigSettings::default().dnssec().authentic_data(),
|
||||
ns.ipv4_addr(),
|
||||
qtype,
|
||||
qname,
|
||||
)?;
|
||||
|
||||
let nsec3_rrs_response = output
|
||||
.authority
|
||||
.into_iter()
|
||||
.filter_map(|rr| rr.try_into_nsec3().ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok((nsec3_rrs, output.status, nsec3_rrs_response))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn find_records<'a>(
|
||||
records: &[NSEC3],
|
||||
records_and_err_msgs: impl IntoIterator<Item = (&'a NSEC3, &'a str)>,
|
||||
) {
|
||||
for (record, err_msg) in records_and_err_msgs {
|
||||
records.iter().find(|&rr| rr == record).expect(err_msg);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ pub struct FQDN {
|
|||
#[allow(non_snake_case)]
|
||||
pub fn FQDN(input: impl Into<Cow<'static, str>>) -> Result<FQDN> {
|
||||
let input = input.into();
|
||||
|
||||
if !input.ends_with('.') {
|
||||
return Err("FQDN must end with a `.`".into());
|
||||
}
|
||||
|
@ -77,6 +78,10 @@ impl FQDN {
|
|||
.filter(|label| !label.is_empty())
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn last_label(&self) -> &str {
|
||||
self.inner.split_once('.').map(|(label, _)| label).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FQDN {
|
||||
|
|
|
@ -15,6 +15,7 @@ mod container;
|
|||
mod fqdn;
|
||||
mod implementation;
|
||||
pub mod name_server;
|
||||
pub mod nsec3;
|
||||
pub mod record;
|
||||
mod resolver;
|
||||
mod trust_anchor;
|
||||
|
|
66
conformance/packages/dns-test/src/nsec3.rs
Normal file
66
conformance/packages/dns-test/src/nsec3.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{record::NSEC3, zone_file::ZoneFile};
|
||||
|
||||
pub struct NSEC3Records {
|
||||
records: BTreeMap<String, NSEC3>,
|
||||
}
|
||||
|
||||
impl NSEC3Records {
|
||||
/// Extract the NSEC3 RRs from the signed zonefile and sort them by the hash embedded in the
|
||||
/// last label of each record's owner.
|
||||
pub fn new(signed_zf: &ZoneFile) -> Self {
|
||||
Self {
|
||||
records: signed_zf
|
||||
.records
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(|rr| {
|
||||
let mut nsec3_rr = rr.try_into_nsec3().ok()?;
|
||||
nsec3_rr.next_hashed_owner_name =
|
||||
nsec3_rr.next_hashed_owner_name.to_uppercase();
|
||||
Some((nsec3_rr.fqdn.last_label().to_uppercase(), nsec3_rr))
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// An NSEC3 RR is said to "match" a name if the owner name of the NSEC3 RR is the same as the
|
||||
/// hashed owner name of that name.
|
||||
pub fn find_match<'a>(&'a self, name_hash: &str) -> Option<&'a NSEC3> {
|
||||
self.records.get(name_hash)
|
||||
}
|
||||
|
||||
/// An NSEC3 RR is said to cover a name if the hash of the name or "next closer" name falls between
|
||||
/// the owner name and the next hashed owner name of the NSEC3. In other words, if it proves the
|
||||
/// nonexistence of the name, either directly or by proving the nonexistence of an ancestor of the
|
||||
/// name.
|
||||
pub fn find_cover<'a>(&'a self, name_hash: &str) -> Option<&'a NSEC3> {
|
||||
let (hash, candidate) = self
|
||||
.records
|
||||
// Find the greater hash that is less or equal than the name's hash.
|
||||
.range(..=name_hash.to_owned())
|
||||
.last()
|
||||
// If no value is less or equal than the name's hash, it means that the name's hash is out
|
||||
// of range and the last record covers it.
|
||||
.or_else(|| self.records.last_key_value())?;
|
||||
|
||||
// If the found hash is exactly the name's hash, return None as it wouldn't be proving its
|
||||
// nonexistence. Otherwise return the RR with that hash.
|
||||
(hash != name_hash).then_some(candidate)
|
||||
}
|
||||
|
||||
/// This proof consists of (up to) two different NSEC3 RRs:
|
||||
/// - An NSEC3 RR that matches the closest (provable) encloser.
|
||||
/// - An NSEC3 RR that covers the "next closer" name to the closest encloser.
|
||||
pub fn closest_encloser_proof<'a>(
|
||||
&'a self,
|
||||
closest_encloser_hash: &str,
|
||||
next_closer_name_hash: &str,
|
||||
) -> Option<(&'a NSEC3, &'a NSEC3)> {
|
||||
Some((
|
||||
self.find_match(closest_encloser_hash)?,
|
||||
self.find_cover(next_closer_name_hash)?,
|
||||
))
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ const CLASS: &str = "IN"; // "internet"
|
|||
macro_rules! record_types {
|
||||
($($variant:ident),*) => {
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum RecordType {
|
||||
$($variant),*
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ macro_rules! record_types {
|
|||
|
||||
record_types!(A, AAAA, DNSKEY, DS, MX, NS, NSEC3, NSEC3PARAM, RRSIG, SOA, TXT);
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum Record {
|
||||
A(A),
|
||||
|
@ -150,6 +150,14 @@ impl Record {
|
|||
Err(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_into_nsec3(self) -> CoreResult<NSEC3, Self> {
|
||||
if let Self::NSEC3(v) = self {
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Record {
|
||||
|
@ -192,7 +200,7 @@ impl fmt::Display for Record {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct A {
|
||||
pub fqdn: FQDN,
|
||||
pub ttl: u32,
|
||||
|
@ -395,7 +403,7 @@ impl fmt::Display for DS {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NS {
|
||||
pub zone: FQDN,
|
||||
pub ttl: u32,
|
||||
|
@ -439,7 +447,7 @@ impl FromStr for NS {
|
|||
}
|
||||
|
||||
// integer types chosen based on bit sizes in section 3.2 of RFC5155
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NSEC3 {
|
||||
pub fqdn: FQDN,
|
||||
pub ttl: u32,
|
||||
|
@ -509,7 +517,7 @@ impl fmt::Display for NSEC3 {
|
|||
}
|
||||
|
||||
// integer types chosen based on bit sizes in section 4.2 of RFC5155
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NSEC3PARAM {
|
||||
pub zone: FQDN,
|
||||
pub ttl: u32,
|
||||
|
@ -567,7 +575,7 @@ impl fmt::Display for NSEC3PARAM {
|
|||
|
||||
// integer types chosen based on bit sizes in section 3.1 of RFC4034
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RRSIG {
|
||||
pub fqdn: FQDN,
|
||||
pub ttl: u32,
|
||||
|
@ -646,7 +654,7 @@ impl fmt::Display for RRSIG {
|
|||
}
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SOA {
|
||||
pub zone: FQDN,
|
||||
pub ttl: u32,
|
||||
|
@ -704,7 +712,7 @@ impl fmt::Display for SOA {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SoaSettings {
|
||||
pub serial: u32,
|
||||
pub refresh: u32,
|
||||
|
|
|
@ -12,6 +12,7 @@ use std::str::FromStr;
|
|||
use crate::record::{self, Record, SOA};
|
||||
use crate::{Error, Result, DEFAULT_TTL, FQDN};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZoneFile {
|
||||
origin: FQDN,
|
||||
pub soa: SOA,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// https://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc, time::Instant};
|
||||
use std::{net::SocketAddr, time::Instant};
|
||||
|
||||
use async_recursion::async_recursion;
|
||||
use futures_util::{future::select_all, FutureExt};
|
||||
|
@ -353,15 +353,27 @@ impl Recursor {
|
|||
let ns = ns.ok_or_else(|| Error::from(format!("no nameserver found for {zone}")))?;
|
||||
debug!("found zone {} for {}", ns.zone(), query);
|
||||
|
||||
let dnssec = if self.security_aware {
|
||||
Dnssec::Aware {
|
||||
query_has_dnssec_ok,
|
||||
}
|
||||
let response = self.lookup(query.clone(), ns, request_time).await?;
|
||||
|
||||
// RFC 4035 section 3.2.1 if DO bit not set, strip DNSSEC records unless
|
||||
// explicitly requested
|
||||
let lookup = if !query_has_dnssec_ok {
|
||||
let records = response
|
||||
.records()
|
||||
.iter()
|
||||
.filter(|rrset| {
|
||||
let record_type = rrset.record_type();
|
||||
record_type == query.query_type() || !record_type.is_dnssec()
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Lookup::new_with_deadline(query, records, response.valid_until())
|
||||
} else {
|
||||
Dnssec::Unaware
|
||||
response
|
||||
};
|
||||
let response = self.lookup(query, ns, request_time, dnssec).await?;
|
||||
Ok(response)
|
||||
|
||||
Ok(lookup)
|
||||
}
|
||||
|
||||
async fn lookup(
|
||||
|
@ -369,13 +381,10 @@ impl Recursor {
|
|||
query: Query,
|
||||
ns: RecursorPool<TokioRuntimeProvider>,
|
||||
now: Instant,
|
||||
dnssec: Dnssec,
|
||||
) -> Result<Lookup, Error> {
|
||||
if !dnssec.is_security_aware() {
|
||||
if let Some(lookup) = self.record_cache.get(&query, now) {
|
||||
debug!("cached data {lookup:?}");
|
||||
return lookup.map_err(Into::into);
|
||||
}
|
||||
if let Some(lookup) = self.record_cache.get(&query, now) {
|
||||
debug!("cached data {lookup:?}");
|
||||
return lookup.map_err(Into::into);
|
||||
}
|
||||
|
||||
let response = ns.lookup(query.clone(), self.security_aware);
|
||||
|
@ -388,44 +397,26 @@ impl Recursor {
|
|||
let mut r = r.into_message();
|
||||
info!("response: {}", r.header());
|
||||
|
||||
if let Dnssec::Aware {
|
||||
query_has_dnssec_ok,
|
||||
} = dnssec
|
||||
{
|
||||
// TODO: validation must be performed if the CD (Checking Disabled) is not set
|
||||
let mut answers = r.take_answers();
|
||||
if !query_has_dnssec_ok {
|
||||
answers.retain(|rrset| {
|
||||
// RFC 4035 section 3.2.1 if DO bit not set, strip DNSSEC records
|
||||
// unless explicitly requested
|
||||
let record_type = rrset.record_type();
|
||||
record_type == query.query_type() || !record_type.is_dnssec()
|
||||
});
|
||||
}
|
||||
let records = r
|
||||
.take_answers()
|
||||
.into_iter()
|
||||
.chain(r.take_name_servers())
|
||||
.chain(r.take_additionals())
|
||||
.filter(|x| {
|
||||
if !is_subzone(ns.zone().clone(), x.name().clone()) {
|
||||
warn!(
|
||||
"Dropping out of bailiwick record {x} for zone {}",
|
||||
ns.zone().clone()
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Lookup::new_with_max_ttl(query, Arc::from(answers)))
|
||||
} else {
|
||||
let records = r
|
||||
.take_answers()
|
||||
.into_iter()
|
||||
.chain(r.take_name_servers())
|
||||
.chain(r.take_additionals())
|
||||
.filter(|x| {
|
||||
if !is_subzone(ns.zone().clone(), x.name().clone()) {
|
||||
warn!(
|
||||
"Dropping out of bailiwick record {x} for zone {}",
|
||||
ns.zone().clone()
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let lookup = self.record_cache.insert_records(query, records, now);
|
||||
|
||||
let lookup = self.record_cache.insert_records(query, records, now);
|
||||
|
||||
lookup.ok_or_else(|| Error::from("no records found"))
|
||||
}
|
||||
lookup.ok_or_else(|| Error::from("no records found"))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("lookup error: {e}");
|
||||
|
@ -458,12 +449,7 @@ impl Recursor {
|
|||
|
||||
let lookup = Query::query(zone.clone(), RecordType::NS);
|
||||
let response = self
|
||||
.lookup(
|
||||
lookup.clone(),
|
||||
nameserver_pool.clone(),
|
||||
request_time,
|
||||
Dnssec::Unaware,
|
||||
)
|
||||
.lookup(lookup.clone(), nameserver_pool.clone(), request_time)
|
||||
.await?;
|
||||
|
||||
// let zone_nameservers = response.name_servers();
|
||||
|
@ -586,18 +572,6 @@ impl Recursor {
|
|||
}
|
||||
}
|
||||
|
||||
enum Dnssec {
|
||||
Unaware,
|
||||
Aware { query_has_dnssec_ok: bool },
|
||||
}
|
||||
|
||||
impl Dnssec {
|
||||
#[must_use]
|
||||
fn is_security_aware(&self) -> bool {
|
||||
matches!(self, Self::Aware { .. })
|
||||
}
|
||||
}
|
||||
|
||||
fn recursor_opts() -> ResolverOpts {
|
||||
let mut options = ResolverOpts::default();
|
||||
options.ndots = 0;
|
||||
|
|
|
@ -12,11 +12,15 @@ use std::sync::Arc;
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use hickory_proto::error::{ProtoError, ProtoErrorKind};
|
||||
#[cfg(feature = "dnssec")]
|
||||
use hickory_proto::rr::dnssec::rdata::RRSIG;
|
||||
use lru_cache::LruCache;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use proto::op::Query;
|
||||
use proto::rr::Record;
|
||||
#[cfg(feature = "dnssec")]
|
||||
use proto::rr::RecordData;
|
||||
|
||||
use crate::config;
|
||||
use crate::lookup::Lookup;
|
||||
|
@ -242,7 +246,44 @@ impl DnsLru {
|
|||
let records = records.fold(
|
||||
HashMap::<Query, Vec<(Record, u32)>>::new(),
|
||||
|mut map, record| {
|
||||
let mut query = Query::query(record.name().clone(), record.record_type());
|
||||
// it's not useful to cache RRSIGs on their own using `name()` as a key because
|
||||
// there can be multiple RRSIG associated to the same domain name where each
|
||||
// RRSIG is *covering* a different record type
|
||||
//
|
||||
// an example of this is shown below
|
||||
//
|
||||
// ``` console
|
||||
// $ dig @a.iana-servers.net. +norecurse +dnssec A example.com.
|
||||
// example.com. 3600 IN A 93.184.215.14
|
||||
// example.com. 3600 IN RRSIG A 13 2 3600 20240705065834 (..)
|
||||
//
|
||||
// $ dig @a.iana-servers.net. +norecurse +dnssec A example.com.
|
||||
// example.com. 86400 IN NS a.iana-servers.net.
|
||||
// example.com. 86400 IN NS b.iana-servers.net.
|
||||
// example.com. 86400 IN RRSIG NS 13 2 86400 20240705060635 (..)
|
||||
// ```
|
||||
//
|
||||
// note that there are two RRSIG records associated to `example.com.` but they are
|
||||
// covering different record types. the first RRSIG covers the
|
||||
// `A example.com.` record. the second RRSIG covers two `NS example.com.` records
|
||||
//
|
||||
// if we use ("example.com.", RecordType::RRSIG) as a key in our cache these two
|
||||
// consecutive queries will cause the entry to be overwriten, losing the RRSIG
|
||||
// covering the A record
|
||||
//
|
||||
// to avoid this problem, we'll cache the RRSIG along the record it covers using
|
||||
// the record's type along the record's `name()` as the key in the cache
|
||||
|
||||
#[cfg(feature = "dnssec")]
|
||||
let rtype = if let Some(rrsig) = RRSIG::try_borrow(record.data()) {
|
||||
rrsig.type_covered()
|
||||
} else {
|
||||
record.record_type()
|
||||
};
|
||||
#[cfg(not(feature = "dnssec"))]
|
||||
let rtype = record.record_type();
|
||||
|
||||
let mut query = Query::query(record.name().clone(), rtype);
|
||||
query.set_query_class(record.dns_class());
|
||||
|
||||
let ttl = record.ttl();
|
||||
|
|
34
justfile
34
justfile
|
@ -103,18 +103,18 @@ cleanliness: clippy fmt audit
|
|||
coverage: init-llvm-cov
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
|
||||
export RUSTFLAGS="{{COV_RUSTFLAGS}}"
|
||||
export CARGO_LLVM_COV={{COV_CARGO_LLVM_COV}}
|
||||
export CARGO_LLVM_COV_TARGET_DIR={{COV_CARGO_LLVM_COV_TARGET_DIR}}
|
||||
export LLVM_PROFILE_FILE={{COV_LLVM_PROFILE_FILE}}
|
||||
|
||||
echo $RUSTFLAGS
|
||||
|
||||
|
||||
cargo +nightly llvm-cov clean
|
||||
mkdir -p {{COV_CARGO_LLVM_COV_TARGET_DIR}}
|
||||
|
||||
cargo +nightly build --workspace --all-targets --all-features
|
||||
|
||||
cargo +nightly build --workspace --all-targets --all-features
|
||||
cargo +nightly llvm-cov test --workspace --no-report --all-targets --benches --examples --bins --tests --all-features
|
||||
cargo +nightly llvm-cov test --workspace --no-report --doc --doctests --all-features
|
||||
cargo +nightly llvm-cov report --codecov --output-path {{join(COV_CARGO_LLVM_COV_TARGET_DIR, "hickory-dns-coverage.json")}}
|
||||
|
@ -123,7 +123,7 @@ coverage: init-llvm-cov
|
|||
coverage-html: coverage
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
|
||||
export RUSTFLAGS="{{COV_RUSTFLAGS}}"
|
||||
export CARGO_LLVM_COV={{COV_CARGO_LLVM_COV}}
|
||||
export CARGO_LLVM_COV_TARGET_DIR={{COV_CARGO_LLVM_COV_TARGET_DIR}}
|
||||
|
@ -193,44 +193,44 @@ init-openssl:
|
|||
[macos]
|
||||
init-bind9-deps: init-openssl
|
||||
pip install ply
|
||||
brew install wget
|
||||
brew install wget libuv userspace-rcu openssl
|
||||
|
||||
[private]
|
||||
[linux]
|
||||
init-bind9-deps:
|
||||
if apt-get --version ; then sudo apt-get install -y python3-ply ; fi
|
||||
if apt-get --version ; then sudo apt-get install -y python3-ply libuv1-dev liburcu-dev libssl-dev libcap-dev ; fi
|
||||
|
||||
# Install BIND9, needed for compatability tests
|
||||
[unix]
|
||||
init-bind9:
|
||||
init-bind9:
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
|
||||
if {{TDNS_BIND_PATH}}/sbin/named -v ; then exit 0 ; fi
|
||||
|
||||
|
||||
just init-bind9-deps
|
||||
|
||||
## This must run after OpenSSL installation
|
||||
## This must run after OpenSSL installation
|
||||
if openssl version ; then WITH_OPENSSL="--with-openssl=$(dirname $(dirname $(which openssl)))" ; fi
|
||||
|
||||
|
||||
mkdir -p {{TARGET_DIR}}
|
||||
|
||||
|
||||
echo "----> downloading bind"
|
||||
rm -rf {{TARGET_DIR}}/bind-{{BIND_VER}}
|
||||
wget -O {{TARGET_DIR}}/bind-{{BIND_VER}}.tar.xz https://downloads.isc.org/isc/bind9/{{BIND_VER}}/bind-{{BIND_VER}}.tar.xz
|
||||
|
||||
ls -la {{TARGET_DIR}}/bind-{{BIND_VER}}.tar.xz
|
||||
tar -xJf {{TARGET_DIR}}/bind-{{BIND_VER}}.tar.xz -C {{TARGET_DIR}}
|
||||
|
||||
|
||||
echo "----> compiling bind"
|
||||
cd {{TARGET_DIR}}/bind-{{BIND_VER}}
|
||||
|
||||
|
||||
./configure --prefix {{TDNS_BIND_PATH}} ${WITH_OPENSSL}
|
||||
make install
|
||||
cd -
|
||||
|
||||
|
||||
{{TDNS_BIND_PATH}}/sbin/named -v
|
||||
|
||||
|
||||
rm {{TARGET_DIR}}/bind-{{BIND_VER}}.tar.xz
|
||||
rm -rf {{TARGET_DIR}}/bind-{{BIND_VER}}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user