Check TLSA lookups for false bogus verdicts too
Mail to chuckmckinnon.com sat in the queue for days with "Error fetching TLSA record: DNSSEC validation failed". Its MX, mail.usefulinsight.com, is on Cloudflare, and behind Hetzner's resolvers _25._tcp.mail.usefulinsight.com answers TLSA with a signed CNAME to the zone apex, which has no TLSA record. That is the second hickory 0.26.3 bug #72 works around: it checks the denial against the name first asked for, not the CNAME's target, and calls a valid answer bogus. #72 put MX and address lookups through validated_lookup but left the TLSA lookup calling hickory directly. It goes through validated_lookup now: a signed CNAME is followed, the denial at the target validates, and the result is "no TLSA record", so delivery goes ahead without DANE as it should. A TLSA record that rechecks as insecure is treated as no policy, since DANE needs a signed one. Cloudflare's own resolver answers that name with a compact denial at the name itself, which hickory already accepts, so the new ignored test takes a resolver from INBUXA_TEST_DNS_TCP. Run against 185.12.64.2 over an SSH bridge from host1, hickory alone fails with "DNSSEC validation failed", as in production, and validated_lookup returns a non-bogus denial. smtp lib tests pass; check --all-targets is clean.
This commit is contained in:
1 parent
f59a9de4dc
commit
c4a6e4d117
1 file changed
+70
-10
@@ -176,16 +176,23 @@ impl TlsaLookup for Server {
|
|||||||
return mail_auth::common::resolver::mock_resolve(key.as_ref());
|
return mail_auth::common::resolver::mock_resolve(key.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
let tlsa_lookup = match self
|
// Through `validated_lookup`, like the MX and address lookups: a TLSA
|
||||||
.core
|
// name that is a signed CNAME to a name with no TLSA record (seen at
|
||||||
.smtp
|
// `_25._tcp.mail.usefulinsight.com`, behind Hetzner's resolvers) is
|
||||||
.resolvers
|
// otherwise called bogus, and the message waits on it until it
|
||||||
.dnssec
|
// expires.
|
||||||
.resolver
|
let tlsa_lookup = match validated_lookup(
|
||||||
.tlsa_lookup(Name::from_str_relaxed(key.as_ref())?)
|
&self.core.smtp.resolvers.dnssec.resolver,
|
||||||
.await
|
self.core.smtp.resolvers.dns.resolver(),
|
||||||
|
Name::from_str_relaxed(key.as_ref())?,
|
||||||
|
RecordType::TLSA,
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(tlsa_lookup) => tlsa_lookup,
|
// A TLSA record proved to sit in an unsigned zone is no DANE
|
||||||
|
// policy at all.
|
||||||
|
Ok(validated) if validated.insecure => return Ok(TlsaResult::Missing),
|
||||||
|
Ok(validated) => validated.lookup,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if let Some(denial) = NegativeAnswer::from_error(&err) {
|
if let Some(denial) = NegativeAnswer::from_error(&err) {
|
||||||
return Ok(if denial.dnssec_status == DnssecStatus::Bogus {
|
return Ok(if denial.dnssec_status == DnssecStatus::Bogus {
|
||||||
@@ -436,7 +443,8 @@ impl TlsaLookup for Server {
|
|||||||
// record in the DS reply, and public resolvers often send none.
|
// record in the DS reply, and public resolvers often send none.
|
||||||
// - A signed CNAME to a signed name without the record type queried. Hickory
|
// - A signed CNAME to a signed name without the record type queried. Hickory
|
||||||
// checks the denial of existence against the name first asked for, not the
|
// checks the denial of existence against the name first asked for, not the
|
||||||
// target's, and rejects it.
|
// target's, and rejects it. TLSA lookups hit this too: a TLSA name that is
|
||||||
|
// a CNAME to the zone apex, with no TLSA there, held mail to it for a week.
|
||||||
//
|
//
|
||||||
// When hickory says bogus, check the answer again with lookups it gets right.
|
// When hickory says bogus, check the answer again with lookups it gets right.
|
||||||
// A signed CNAME is followed and the lookup repeated at its target. Otherwise
|
// A signed CNAME is followed and the lookup repeated at its target. Otherwise
|
||||||
@@ -869,4 +877,56 @@ mod tests {
|
|||||||
assert!(validated.insecure);
|
assert!(validated.insecure);
|
||||||
assert!(!validated.lookup.answers().is_empty());
|
assert!(!validated.lookup.answers().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Needs the network: a TLSA name that is a signed CNAME to the zone apex,
|
||||||
|
// which has no TLSA record. Cloudflare's resolver answers with a compact
|
||||||
|
// denial at the name itself; Hetzner's (and others) follow the CNAME, and
|
||||||
|
// hickory then calls the answer bogus. Point the lookup at a resolver that
|
||||||
|
// follows it with INBUXA_TEST_DNS_TCP=<ip:port> (TCP), for instance over
|
||||||
|
// an SSH tunnel to 185.12.64.2:53 from a Hetzner host.
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn validated_lookup_follows_signed_cname_for_tlsa() {
|
||||||
|
use mail_auth::hickory_resolver::{
|
||||||
|
config::{CLOUDFLARE, ConnectionConfig, NameServerConfig, ResolverConfig, ResolverOpts},
|
||||||
|
net::runtime::TokioRuntimeProvider,
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = match std::env::var("INBUXA_TEST_DNS_TCP") {
|
||||||
|
Ok(addr) => {
|
||||||
|
let addr: std::net::SocketAddr = addr.parse().unwrap();
|
||||||
|
let mut ns = NameServerConfig::new(addr.ip(), true, vec![ConnectionConfig::tcp()]);
|
||||||
|
if let Some(c) = ns.connections.first_mut() {
|
||||||
|
c.port = addr.port();
|
||||||
|
}
|
||||||
|
ResolverConfig::from_parts(None, vec![], vec![ns])
|
||||||
|
}
|
||||||
|
Err(_) => ResolverConfig::udp_and_tcp(&CLOUDFLARE),
|
||||||
|
};
|
||||||
|
let build = |validate: bool| {
|
||||||
|
let mut opts = ResolverOpts::default();
|
||||||
|
opts.validate = validate;
|
||||||
|
opts.num_concurrent_reqs = 1;
|
||||||
|
opts.cache_size = 0;
|
||||||
|
TokioResolver::builder_with_config(config.clone(), TokioRuntimeProvider::default())
|
||||||
|
.with_options(opts)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let (dnssec, plain) = (build(true), build(false));
|
||||||
|
let query = name("_25._tcp.mail.usefulinsight.com.");
|
||||||
|
|
||||||
|
let direct = dnssec.lookup(query.clone(), RecordType::TLSA).await;
|
||||||
|
eprintln!("hickory alone: {:?}", direct.as_ref().err().map(|e| e.to_string()));
|
||||||
|
|
||||||
|
let err = match validated_lookup(&dnssec, &plain, query, RecordType::TLSA).await {
|
||||||
|
Ok(validated) => panic!("expected no TLSA record, got {:?}", validated.lookup.answers()),
|
||||||
|
Err(err) => err,
|
||||||
|
};
|
||||||
|
let denial = NegativeAnswer::from_error(&err).expect("a denial of existence");
|
||||||
|
assert_eq!(denial.response_code, ResponseCode::NoError);
|
||||||
|
assert_ne!(denial.dnssec_status, DnssecStatus::Bogus);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user