From b5c703e4e3dc28715a04fe5e43f85ecf4489ff90 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:48:08 +0200 Subject: [PATCH] Fix DNS Management: CAA management deletes too third-party CAA entries --- CHANGELOG.md | 1 + crates/services/src/task_manager/dns.rs | 53 ++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9339e13..d26fcd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If - VacationResponse: `isEnabled` reset to false whenever properties are changed. - Capabilities: Return RFC-3339-conformant UTCDate literals in capabilities: min `0001-01-01T00:00:00Z`, max `9999-12-31T23:59:59Z`. - Directory: Update local groups only when the external directory includes a group attribute. +- DNS Management: CAA management deletes too third-party CAA entries. - Reject invalid duration values (e.g. `1h30m`). - Branding: Custom logos for domains do not work. - Sieve: add `Received` headers to auto-generated messages and detect loops. diff --git a/crates/services/src/task_manager/dns.rs b/crates/services/src/task_manager/dns.rs index 49d3b13e..ab650477 100644 --- a/crates/services/src/task_manager/dns.rs +++ b/crates/services/src/task_manager/dns.rs @@ -6,7 +6,7 @@ use crate::task_manager::TaskResult; use common::Server; -use dns_update::{DnsRecord, DnsRecordType, Error as DnsUpdateError}; +use dns_update::{CAARecord, DnsRecord, DnsRecordType, Error as DnsUpdateError, KeyValue}; use registry::schema::structs::{ DnsManagement, Domain, Task, TaskDnsManagement, TaskDomainManagement, TaskStatus, }; @@ -73,11 +73,16 @@ async fn dns_management(server: &Server, task: &TaskDnsManagement) -> trc::Resul let mut errors = String::new(); for ((name, record_type), mut recs) in by_owner { - if matches!(record_type, DnsRecordType::TXT) && !is_owned_txt_name(&name) { + let preserve_unrelated = match record_type { + DnsRecordType::TXT => !is_owned_txt_name(&name), + DnsRecordType::CAA => true, + _ => false, + }; + if preserve_unrelated { match dns_updater.list_rrset(origin, &name, record_type).await { Ok(existing) => { for existing_rec in existing { - if !recs.iter().any(|new| same_txt_family(new, &existing_rec)) { + if !recs.iter().any(|new| same_rrset_family(new, &existing_rec)) { recs.push(existing_rec); } } @@ -138,6 +143,14 @@ async fn dns_management(server: &Server, task: &TaskDnsManagement) -> trc::Resul } } +fn same_rrset_family(a: &DnsRecord, b: &DnsRecord) -> bool { + match (a, b) { + (DnsRecord::TXT(_), DnsRecord::TXT(_)) => same_txt_family(a, b), + (DnsRecord::CAA(_), DnsRecord::CAA(_)) => same_caa_family(a, b), + _ => false, + } +} + fn same_txt_family(a: &DnsRecord, b: &DnsRecord) -> bool { match (a, b) { (DnsRecord::TXT(va), DnsRecord::TXT(vb)) => match (txt_family(va), txt_family(vb)) { @@ -148,10 +161,38 @@ fn same_txt_family(a: &DnsRecord, b: &DnsRecord) -> bool { } } +fn same_caa_family(a: &DnsRecord, b: &DnsRecord) -> bool { + match (a, b) { + (DnsRecord::CAA(ca), DnsRecord::CAA(cb)) => match (ca, cb) { + (CAARecord::Issue { options: oa, .. }, CAARecord::Issue { options: ob, .. }) + | ( + CAARecord::IssueWild { options: oa, .. }, + CAARecord::IssueWild { options: ob, .. }, + ) => match (caa_account_uri(oa), caa_account_uri(ob)) { + (Some(ua), Some(ub)) => ua.eq_ignore_ascii_case(ub), + _ => false, + }, + (CAARecord::Iodef { url: ua, .. }, CAARecord::Iodef { url: ub, .. }) => { + ua.eq_ignore_ascii_case(ub) + } + _ => false, + }, + _ => false, + } +} + +fn caa_account_uri(options: &[KeyValue]) -> Option<&str> { + options + .iter() + .find(|kv| kv.key.eq_ignore_ascii_case("accounturi")) + .map(|kv| kv.value.as_str()) +} + fn txt_family(value: &str) -> Option<&str> { - let rest = value.trim_start().strip_prefix("v=")?; - let end = rest.find([';', ' ']).unwrap_or(rest.len()); - Some(&rest[..end]) + value.trim_start().strip_prefix("v=").map(|rest| { + rest.split_once([';', ' ']) + .map_or(rest, |(family, _)| family) + }) } fn is_owned_txt_name(name: &str) -> bool {