use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use cellos_core::{CellosError, DnsAuthority, DnsQueryType};
use hickory_proto::dnssec::Proof;
use hickory_resolver::config::{
ConnectionConfig, NameServerConfig, ProtocolConfig, ResolveHosts, ResolverConfig, ResolverOpts,
};
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use hickory_resolver::net::{DnsError, NetError};
use hickory_resolver::proto::op::ResponseCode;
use hickory_resolver::proto::rr::RecordType;
use hickory_resolver::Resolver;
use super::parser::parse_query;
use crate::resolver_refresh::hickory_resolve::{
extract_rrsig_metadata, proof_to_validation_result_with_rrsig,
};
use crate::resolver_refresh::DnssecValidationResult;
pub use crate::resolver_refresh::dnssec::TrustAnchors;
const DEFAULT_VALIDATION_TIMEOUT: Duration = Duration::from_millis(400);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataplaneDnssecOutcome {
Skip,
Validated,
Unsigned,
Failed {
reason: &'static str,
},
}
pub type DataplaneDnssecBackend =
dyn Fn(&str, u16) -> std::io::Result<DnssecValidationResult> + Send + Sync;
pub type DataplaneTypedDnssecBackend =
dyn Fn(&str, RecordType) -> std::io::Result<DnssecValidationResult> + Send + Sync;
pub struct DataplaneDnssecValidator {
fail_closed: bool,
trust_anchor_source: String,
backend: Arc<DataplaneDnssecBackend>,
typed_backend: Option<Arc<DataplaneTypedDnssecBackend>>,
}
impl std::fmt::Debug for DataplaneDnssecValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DataplaneDnssecValidator")
.field("fail_closed", &self.fail_closed)
.field("trust_anchor_source", &self.trust_anchor_source)
.field("backend", &"<Arc<dyn Fn>>")
.field(
"typed_backend",
&self.typed_backend.as_ref().map(|_| "<Arc<dyn Fn>>"),
)
.finish()
}
}
impl DataplaneDnssecValidator {
pub fn from_authority(auth: &DnsAuthority) -> Result<Option<Self>, CellosError> {
let Some(resolver) = auth.resolvers.first() else {
return Ok(None);
};
let Some(policy) = resolver.dnssec.as_ref() else {
return Ok(None);
};
if !policy.validate {
return Ok(None);
}
let anchors = TrustAnchors::load(policy.trust_anchors_path.as_deref())?;
let trust_anchor_source = anchors.source.clone();
let upstream_addr: SocketAddr = resolver.endpoint.parse().map_err(|e| {
CellosError::InvalidSpec(format!(
"dns_proxy::dnssec: cannot parse resolver.endpoint '{}' as SocketAddr: {e}",
resolver.endpoint
))
})?;
let anchors = Arc::new(anchors);
let backend =
build_hickory_backend(upstream_addr, DEFAULT_VALIDATION_TIMEOUT, anchors.clone());
let typed_backend =
build_hickory_typed_backend(upstream_addr, DEFAULT_VALIDATION_TIMEOUT, anchors);
Ok(Some(Self {
fail_closed: policy.fail_closed,
trust_anchor_source,
backend,
typed_backend: Some(typed_backend),
}))
}
#[doc(hidden)]
pub fn with_backend(
fail_closed: bool,
trust_anchor_source: String,
backend: Arc<DataplaneDnssecBackend>,
) -> Self {
Self {
fail_closed,
trust_anchor_source,
backend,
typed_backend: None,
}
}
#[doc(hidden)]
pub fn with_backends(
fail_closed: bool,
trust_anchor_source: String,
backend: Arc<DataplaneDnssecBackend>,
typed_backend: Arc<DataplaneTypedDnssecBackend>,
) -> Self {
Self {
fail_closed,
trust_anchor_source,
backend,
typed_backend: Some(typed_backend),
}
}
#[must_use]
pub fn is_require_mode(&self) -> bool {
self.fail_closed
}
#[must_use]
pub fn trust_anchor_source(&self) -> &str {
&self.trust_anchor_source
}
pub fn validate(&self, query: &[u8], _upstream_answer: &[u8]) -> DataplaneDnssecOutcome {
let view = match parse_query(query) {
Ok(v) => v,
Err(_) => return DataplaneDnssecOutcome::Skip,
};
let typed = cellos_core::qtype_to_dns_query_type(view.qtype);
let dispatch = match typed {
Some(DnsQueryType::A) | Some(DnsQueryType::AAAA) => Dispatch::Legacy,
Some(DnsQueryType::CNAME) => Dispatch::Typed(RecordType::CNAME),
Some(DnsQueryType::HTTPS) => Dispatch::Typed(RecordType::HTTPS),
Some(DnsQueryType::SVCB) => Dispatch::Typed(RecordType::SVCB),
Some(DnsQueryType::MX) => Dispatch::Typed(RecordType::MX),
Some(DnsQueryType::TXT) => Dispatch::Typed(RecordType::TXT),
_ => return DataplaneDnssecOutcome::Skip,
};
let result = match dispatch {
Dispatch::Legacy => (self.backend)(&view.qname, view.qtype),
Dispatch::Typed(rtype) => match self.typed_backend.as_ref() {
Some(typed_backend) => (typed_backend)(&view.qname, rtype),
None => return DataplaneDnssecOutcome::Skip,
},
};
match result {
Ok(DnssecValidationResult::Validated { .. }) => DataplaneDnssecOutcome::Validated,
Ok(DnssecValidationResult::Unsigned) => DataplaneDnssecOutcome::Unsigned,
Ok(DnssecValidationResult::Failed { .. }) => DataplaneDnssecOutcome::Failed {
reason: "validation_failed",
},
Err(_) => DataplaneDnssecOutcome::Failed {
reason: "validation_failed",
},
}
}
}
enum Dispatch {
Legacy,
Typed(RecordType),
}
fn build_hickory_backend(
upstream: SocketAddr,
timeout: Duration,
anchors: Arc<TrustAnchors>,
) -> Arc<DataplaneDnssecBackend> {
Arc::new(move |hostname: &str, _qtype: u16| {
let handle = tokio::runtime::Handle::try_current().map_err(|e| {
std::io::Error::other(format!(
"dns_proxy::dnssec backend: no tokio runtime in scope: {e}"
))
})?;
let anchors = anchors.clone();
let hostname = hostname.to_string();
let result = handle.block_on(async move {
crate::resolver_refresh::resolve_with_ttl_validated(
&hostname, upstream, timeout, &anchors,
)
.await
})?;
Ok(result.validation)
})
}
fn build_hickory_typed_backend(
upstream: SocketAddr,
timeout: Duration,
anchors: Arc<TrustAnchors>,
) -> Arc<DataplaneTypedDnssecBackend> {
Arc::new(move |hostname: &str, record_type: RecordType| {
let handle = tokio::runtime::Handle::try_current().map_err(|e| {
std::io::Error::other(format!(
"dns_proxy::dnssec typed_backend: no tokio runtime in scope: {e}"
))
})?;
let anchors = anchors.clone();
let hostname = hostname.to_string();
handle.block_on(async move {
resolve_typed_validated(&hostname, record_type, upstream, timeout, &anchors).await
})
})
}
async fn resolve_typed_validated(
hostname: &str,
record_type: RecordType,
upstream: SocketAddr,
timeout: Duration,
trust_anchors: &TrustAnchors,
) -> std::io::Result<DnssecValidationResult> {
let mut config = ResolverConfig::from_parts(None, Vec::new(), Vec::new());
config.add_name_server(build_typed_nameserver_config(upstream));
let mut opts = ResolverOpts::default();
opts.cache_size = 0;
opts.attempts = 1;
opts.timeout = timeout;
opts.use_hosts_file = ResolveHosts::Never;
opts.edns0 = false;
opts.validate = true;
if let Some(path) = trust_anchors.path() {
opts.trust_anchor = Some(PathBuf::from(path));
}
let mut builder =
Resolver::builder_with_config(config, TokioRuntimeProvider::default()).with_options(opts);
if let Some(path) = trust_anchors.path() {
match hickory_proto::dnssec::TrustAnchors::from_file(path) {
Ok(loaded) => {
builder = builder.with_trust_anchor(std::sync::Arc::new(loaded));
}
Err(e) => {
return Err(std::io::Error::other(format!(
"dns_proxy::dnssec typed_backend: trust anchor parse failed for {}: {e}",
path.display()
)));
}
}
}
let resolver = builder.build().map_err(|e| {
std::io::Error::other(format!(
"dns_proxy::dnssec typed_backend: hickory-resolver build (validating): {e}"
))
})?;
let work = async {
let lookup_result = resolver.lookup(hostname, record_type).await;
let lookup = match lookup_result {
Ok(l) => l,
Err(e) => {
if let Some(rc) = no_records_response_code(&e) {
if matches!(rc, ResponseCode::NXDomain | ResponseCode::NoError) {
return Ok(DnssecValidationResult::Failed {
reason: "validation_indeterminate".to_string(),
});
}
}
return Err(map_typed_net_error(e));
}
};
let answers = lookup.answers();
let proof = worst_proof_local(answers);
let rrsig_metadata = extract_rrsig_metadata(answers, &[record_type]);
Ok(proof_to_validation_result_with_rrsig(proof, rrsig_metadata))
};
match tokio::time::timeout(timeout, work).await {
Ok(inner) => inner,
Err(_) => Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"dns_proxy::dnssec typed_backend: hickory-resolver timed out after {timeout:?} for {hostname} {record_type:?}"
),
)),
}
}
fn build_typed_nameserver_config(upstream: SocketAddr) -> NameServerConfig {
let mut udp = ConnectionConfig::new(ProtocolConfig::Udp);
udp.port = upstream.port();
let mut tcp = ConnectionConfig::new(ProtocolConfig::Tcp);
tcp.port = upstream.port();
NameServerConfig::new(upstream.ip(), true, vec![udp, tcp])
}
fn worst_proof_local(records: &[hickory_resolver::proto::rr::Record]) -> Proof {
let mut have_any = false;
let mut all_secure = true;
let mut any_insecure = false;
let mut any_indeterminate = false;
for record in records {
have_any = true;
match record.proof {
Proof::Bogus => return Proof::Bogus,
Proof::Indeterminate => {
any_indeterminate = true;
all_secure = false;
}
Proof::Insecure => {
any_insecure = true;
all_secure = false;
}
Proof::Secure => {}
}
}
if !have_any {
return Proof::Indeterminate;
}
if all_secure {
Proof::Secure
} else if any_indeterminate {
Proof::Indeterminate
} else if any_insecure {
Proof::Insecure
} else {
Proof::Indeterminate
}
}
fn no_records_response_code(e: &NetError) -> Option<ResponseCode> {
match e {
NetError::Dns(DnsError::NoRecordsFound(no_records)) => Some(no_records.response_code),
_ => None,
}
}
fn map_typed_net_error(e: NetError) -> std::io::Error {
let kind = match &e {
NetError::Timeout => std::io::ErrorKind::TimedOut,
NetError::Io(io_err) => io_err.kind(),
_ => std::io::ErrorKind::Other,
};
std::io::Error::new(
kind,
format!("dns_proxy::dnssec typed_backend: hickory-resolver error: {e}"),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resolver_refresh::ENV_TRUST_ANCHORS_PATH;
use cellos_core::{DnsResolver, DnsResolverDnssecPolicy, DnsResolverProtocol};
use tempfile::tempdir;
struct EnvGuard {
prior: Option<String>,
}
impl EnvGuard {
fn new() -> Self {
let prior = std::env::var(ENV_TRUST_ANCHORS_PATH).ok();
std::env::remove_var(ENV_TRUST_ANCHORS_PATH);
Self { prior }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match self.prior.take() {
Some(v) => std::env::set_var(ENV_TRUST_ANCHORS_PATH, v),
None => std::env::remove_var(ENV_TRUST_ANCHORS_PATH),
}
}
}
fn authority_with_resolver(resolver: DnsResolver) -> DnsAuthority {
DnsAuthority {
resolvers: vec![resolver],
..Default::default()
}
}
fn make_resolver(dnssec: Option<DnsResolverDnssecPolicy>) -> DnsResolver {
DnsResolver {
resolver_id: "test-resolver".into(),
endpoint: "127.0.0.1:53".into(),
protocol: DnsResolverProtocol::Do53Udp,
trust_kid: None,
dnssec,
}
}
#[test]
fn from_authority_returns_none_when_dnssec_block_absent() {
let auth = authority_with_resolver(make_resolver(None));
let v = DataplaneDnssecValidator::from_authority(&auth).expect("ok");
assert!(
v.is_none(),
"mode=off (no dnssec block) MUST yield None so proxy hot path is unchanged"
);
}
#[test]
fn from_authority_returns_none_when_validate_false() {
let auth = authority_with_resolver(make_resolver(Some(DnsResolverDnssecPolicy {
validate: false,
fail_closed: false,
trust_anchors_path: None,
})));
let v = DataplaneDnssecValidator::from_authority(&auth).expect("ok");
assert!(
v.is_none(),
"validate=false MUST yield None — the block is observational only"
);
}
#[test]
fn from_authority_returns_none_when_no_resolvers() {
let auth = DnsAuthority::default();
let v = DataplaneDnssecValidator::from_authority(&auth).expect("ok");
assert!(v.is_none(), "no resolvers MUST yield None");
}
#[test]
fn from_authority_returns_some_for_require_with_iana_defaults() {
let _guard = EnvGuard::new();
let auth = authority_with_resolver(make_resolver(Some(DnsResolverDnssecPolicy {
validate: true,
fail_closed: true,
trust_anchors_path: None,
})));
let v = DataplaneDnssecValidator::from_authority(&auth)
.expect("ok")
.expect("Some");
assert!(v.is_require_mode(), "fail_closed=true → require");
assert_eq!(
v.trust_anchor_source(),
"iana-default",
"no path override → bundled IANA defaults"
);
}
#[test]
fn from_authority_returns_some_for_best_effort() {
let auth = authority_with_resolver(make_resolver(Some(DnsResolverDnssecPolicy {
validate: true,
fail_closed: false,
trust_anchors_path: None,
})));
let v = DataplaneDnssecValidator::from_authority(&auth)
.expect("ok")
.expect("Some");
assert!(!v.is_require_mode(), "fail_closed=false → best_effort");
}
#[test]
fn from_authority_rejects_missing_trust_anchor_path() {
let _guard = EnvGuard::new();
let dir = tempdir().expect("tempdir");
let bogus = dir.path().join("does-not-exist.bin");
let auth = authority_with_resolver(make_resolver(Some(DnsResolverDnssecPolicy {
validate: true,
fail_closed: true,
trust_anchors_path: Some(bogus.to_string_lossy().into_owned()),
})));
let err = DataplaneDnssecValidator::from_authority(&auth)
.expect_err("missing trust-anchor path MUST be rejected at activation");
let msg = format!("{err}");
assert!(
msg.contains("trust anchors") && msg.contains("does-not-exist.bin"),
"rejection must mention the path for operator triage; got {msg}"
);
}
#[cfg(unix)]
#[test]
fn from_authority_rejects_symlinked_trust_anchor_path() {
let _guard = EnvGuard::new();
let dir = tempdir().expect("tempdir");
let real = dir.path().join("real-anchor.bin");
let link = dir.path().join("symlinked-anchor.bin");
std::fs::write(&real, b"REAL-KEY-BYTES").expect("write real anchor");
std::os::unix::fs::symlink(&real, &link).expect("create symlink");
let auth = authority_with_resolver(make_resolver(Some(DnsResolverDnssecPolicy {
validate: true,
fail_closed: true,
trust_anchors_path: Some(link.to_string_lossy().into_owned()),
})));
let err = DataplaneDnssecValidator::from_authority(&auth)
.expect_err("symlinked trust-anchor path MUST be rejected by O_NOFOLLOW");
let msg = format!("{err}");
assert!(
msg.contains(link.to_str().unwrap()),
"rejection must include the symlinked path; got {msg}"
);
}
#[test]
fn from_authority_rejects_unparseable_endpoint() {
let mut resolver = make_resolver(Some(DnsResolverDnssecPolicy {
validate: true,
fail_closed: true,
trust_anchors_path: None,
}));
resolver.endpoint = "not-a-socket-addr".into();
let auth = authority_with_resolver(resolver);
let err = DataplaneDnssecValidator::from_authority(&auth)
.expect_err("unparseable endpoint MUST be rejected");
let msg = format!("{err}");
assert!(
msg.contains("resolver.endpoint"),
"rejection must reference the field for operator triage; got {msg}"
);
}
#[test]
fn validate_returns_skip_for_non_a_aaaa_qtype() {
let backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| panic!("backend MUST NOT be called for non-A/AAAA"));
let v = DataplaneDnssecValidator::with_backend(true, "iana-default".into(), backend);
let q = build_query("api.example.com", 16); let outcome = v.validate(&q, &[]);
assert!(
matches!(outcome, DataplaneDnssecOutcome::Skip),
"TXT must Skip; got {outcome:?}"
);
}
#[test]
fn validate_maps_validated_outcome() {
let backend: Arc<DataplaneDnssecBackend> = Arc::new(|_h, _t| {
Ok(DnssecValidationResult::Validated {
algorithm: "RSASHA256".into(),
key_tag: 12345,
})
});
let v = DataplaneDnssecValidator::with_backend(false, "iana-default".into(), backend);
let q = build_query("api.example.com", 1); assert!(matches!(
v.validate(&q, &[]),
DataplaneDnssecOutcome::Validated
));
}
#[test]
fn validate_maps_unsigned_outcome() {
let backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| Ok(DnssecValidationResult::Unsigned));
let v = DataplaneDnssecValidator::with_backend(false, "iana-default".into(), backend);
let q = build_query("api.example.com", 1);
assert!(matches!(
v.validate(&q, &[]),
DataplaneDnssecOutcome::Unsigned
));
}
#[test]
fn validate_maps_failed_outcome() {
let backend: Arc<DataplaneDnssecBackend> = Arc::new(|_h, _t| {
Ok(DnssecValidationResult::Failed {
reason: "synthetic".into(),
})
});
let v = DataplaneDnssecValidator::with_backend(true, "iana-default".into(), backend);
let q = build_query("api.example.com", 1);
let outcome = v.validate(&q, &[]);
assert!(
matches!(outcome, DataplaneDnssecOutcome::Failed { reason } if reason == "validation_failed"),
"Failed must map to validation_failed; got {outcome:?}"
);
}
#[test]
fn validate_fails_closed_on_backend_io_error() {
let backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| Err(std::io::Error::other("synthetic-transport-failure")));
let v = DataplaneDnssecValidator::with_backend(true, "iana-default".into(), backend);
let q = build_query("api.example.com", 1);
let outcome = v.validate(&q, &[]);
assert!(
matches!(outcome, DataplaneDnssecOutcome::Failed { .. }),
"I/O error MUST fail closed; got {outcome:?}"
);
}
#[test]
fn validate_returns_skip_for_malformed_query() {
let backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| panic!("backend MUST NOT be called for malformed query"));
let v = DataplaneDnssecValidator::with_backend(false, "iana-default".into(), backend);
let outcome = v.validate(&[0u8; 4], &[]); assert!(matches!(outcome, DataplaneDnssecOutcome::Skip));
}
fn build_query(qname: &str, qtype: u16) -> Vec<u8> {
let mut p = Vec::new();
p.extend_from_slice(&[
0xab, 0xcd, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]);
for label in qname.split('.') {
p.push(label.len() as u8);
p.extend_from_slice(label.as_bytes());
}
p.push(0);
p.extend_from_slice(&qtype.to_be_bytes());
p.extend_from_slice(&[0x00, 0x01]);
p
}
#[test]
fn validate_routes_cname_to_typed_backend() {
let aaaa_backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| panic!("A/AAAA backend MUST NOT be called for CNAME"));
let typed_backend: Arc<DataplaneTypedDnssecBackend> = Arc::new(|_h, rt| {
assert_eq!(rt, RecordType::CNAME, "typed backend MUST receive CNAME");
Ok(DnssecValidationResult::Validated {
algorithm: "RSASHA256".into(),
key_tag: 12345,
})
});
let v = DataplaneDnssecValidator::with_backends(
true,
"iana-default".into(),
aaaa_backend,
typed_backend,
);
let q = build_query("api.example.com", 5); assert!(matches!(
v.validate(&q, &[]),
DataplaneDnssecOutcome::Validated
));
}
#[test]
fn validate_routes_https_to_typed_backend_unsigned() {
let aaaa_backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| panic!("A/AAAA backend MUST NOT be called for HTTPS"));
let typed_backend: Arc<DataplaneTypedDnssecBackend> = Arc::new(|_h, rt| {
assert_eq!(rt, RecordType::HTTPS, "typed backend MUST receive HTTPS");
Ok(DnssecValidationResult::Unsigned)
});
let v = DataplaneDnssecValidator::with_backends(
false,
"iana-default".into(),
aaaa_backend,
typed_backend,
);
let q = build_query("api.example.com", 65); assert!(matches!(
v.validate(&q, &[]),
DataplaneDnssecOutcome::Unsigned
));
}
#[test]
fn validate_routes_svcb_to_typed_backend_failed() {
let aaaa_backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| panic!("A/AAAA backend MUST NOT be called for SVCB"));
let typed_backend: Arc<DataplaneTypedDnssecBackend> = Arc::new(|_h, rt| {
assert_eq!(rt, RecordType::SVCB, "typed backend MUST receive SVCB");
Ok(DnssecValidationResult::Failed {
reason: "synthetic-bogus".into(),
})
});
let v = DataplaneDnssecValidator::with_backends(
true,
"iana-default".into(),
aaaa_backend,
typed_backend,
);
let q = build_query("api.example.com", 64); let outcome = v.validate(&q, &[]);
assert!(
matches!(outcome, DataplaneDnssecOutcome::Failed { reason } if reason == "validation_failed"),
"SVCB Failed → validation_failed; got {outcome:?}"
);
}
#[test]
fn validate_routes_mx_to_typed_backend_io_error_fails_closed() {
let aaaa_backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| panic!("A/AAAA backend MUST NOT be called for MX"));
let typed_backend: Arc<DataplaneTypedDnssecBackend> = Arc::new(|_h, rt| {
assert_eq!(rt, RecordType::MX, "typed backend MUST receive MX");
Err(std::io::Error::other("synthetic-transport-failure"))
});
let v = DataplaneDnssecValidator::with_backends(
true,
"iana-default".into(),
aaaa_backend,
typed_backend,
);
let q = build_query("mail.example.com", 15); let outcome = v.validate(&q, &[]);
assert!(
matches!(outcome, DataplaneDnssecOutcome::Failed { reason } if reason == "validation_failed"),
"MX I/O error MUST fail closed; got {outcome:?}"
);
}
#[test]
fn validate_routes_txt_to_typed_backend_validated() {
let aaaa_backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| panic!("A/AAAA backend MUST NOT be called for TXT"));
let typed_backend: Arc<DataplaneTypedDnssecBackend> = Arc::new(|_h, rt| {
assert_eq!(rt, RecordType::TXT, "typed backend MUST receive TXT");
Ok(DnssecValidationResult::Validated {
algorithm: "ED25519".into(),
key_tag: 60_999,
})
});
let v = DataplaneDnssecValidator::with_backends(
true,
"iana-default".into(),
aaaa_backend,
typed_backend,
);
let q = build_query("api.example.com", 16); assert!(matches!(
v.validate(&q, &[]),
DataplaneDnssecOutcome::Validated
));
}
#[test]
fn validate_skips_residual_types_even_with_typed_backend() {
let aaaa_backend: Arc<DataplaneDnssecBackend> =
Arc::new(|_h, _t| panic!("A/AAAA backend MUST NOT be called for NS"));
let typed_backend: Arc<DataplaneTypedDnssecBackend> =
Arc::new(|_h, _rt| panic!("typed backend MUST NOT be called for NS"));
let v = DataplaneDnssecValidator::with_backends(
true,
"iana-default".into(),
aaaa_backend,
typed_backend,
);
let q = build_query("ns1.example.com", 2); assert!(
matches!(v.validate(&q, &[]), DataplaneDnssecOutcome::Skip),
"NS (qtype=2) MUST Skip even when typed backend is wired"
);
}
#[test]
fn from_authority_populates_typed_backend_in_require_mode() {
let auth = authority_with_resolver(make_resolver(Some(DnsResolverDnssecPolicy {
validate: true,
fail_closed: true,
trust_anchors_path: None,
})));
let v = DataplaneDnssecValidator::from_authority(&auth)
.expect("ok")
.expect("Some");
let dbg = format!("{v:?}");
assert!(
dbg.contains("typed_backend: Some"),
"from_authority MUST populate typed_backend so post-A2 validators \
validate CNAME/HTTPS/SVCB/MX/TXT end-to-end; got {dbg}"
);
}
}