/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::{Core, Server}; use dns_update::{ Algorithm, DnsRecord, DnsRecordType, TsigAlgorithm, dnssec::{ self, SigningKey, crypto::{EcdsaSigningKey, Ed25519SigningKey}, }, providers::{ovh::OvhEndpoint, rfc2136::DnsAddress}, }; use registry::schema::{ enums, structs::{DnsManagement, DnsServer, Domain}, }; use rustls_pki_types::PrivatePkcs8KeyDer; use std::{ net::SocketAddr, sync::Arc, time::{Duration, Instant}, }; use trc::DnsEvent; use types::id::Id; pub struct DnsUpdater { pub updater: dns_update::DnsUpdater, pub polling_interval: Duration, pub propagation_timeout: Duration, pub propagation_delay: Option, pub ttl: Duration, core: Arc, } #[cfg(feature = "test_mode")] pub static DNS_RECORDS: std::sync::LazyLock< Arc>>, > = std::sync::LazyLock::new(|| Arc::new(std::sync::Mutex::new(Vec::new()))); impl DnsUpdater { pub async fn build(server: DnsServer, core: Arc) -> Result { match server { DnsServer::Tsig(server) => Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_rfc2136_tsig( match server.protocol { enums::IpProtocol::Udp => DnsAddress::Tcp(SocketAddr::new( server.host.into_inner(), server.port as u16, )), enums::IpProtocol::Tcp => DnsAddress::Udp(SocketAddr::new( server.host.into_inner(), server.port as u16, )), }, server.key_name, server.key.secret().await?.into_owned().into_bytes(), match server.tsig_algorithm { enums::TsigAlgorithm::HmacMd5 => TsigAlgorithm::HmacMd5, enums::TsigAlgorithm::Gss => TsigAlgorithm::Gss, enums::TsigAlgorithm::HmacSha1 => TsigAlgorithm::HmacSha1, enums::TsigAlgorithm::HmacSha224 => TsigAlgorithm::HmacSha224, enums::TsigAlgorithm::HmacSha256 => TsigAlgorithm::HmacSha256, enums::TsigAlgorithm::HmacSha256128 => TsigAlgorithm::HmacSha256_128, enums::TsigAlgorithm::HmacSha384 => TsigAlgorithm::HmacSha384, enums::TsigAlgorithm::HmacSha384192 => TsigAlgorithm::HmacSha384_192, enums::TsigAlgorithm::HmacSha512 => TsigAlgorithm::HmacSha512, enums::TsigAlgorithm::HmacSha512256 => TsigAlgorithm::HmacSha512_256, }, ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }), DnsServer::Sig0(server) => { let key_bytes = server.key.secret().await?; let pem_parsed = pem::parse(key_bytes.as_bytes()) .map_err(|err| format!("Failed to parse PEM key: {}", err))?; let pkcs8_der = PrivatePkcs8KeyDer::from(pem_parsed.contents()); let signing_key: Box = match server.sig0_algorithm { enums::Sig0Algorithm::EcdsaP256Sha256 => Box::new( EcdsaSigningKey::from_pkcs8(&pkcs8_der, dnssec::Algorithm::ECDSAP256SHA256) .map_err(|err| { format!("Failed to build ECDSA P-256 signing key: {}", err) })?, ), enums::Sig0Algorithm::EcdsaP384Sha384 => Box::new( EcdsaSigningKey::from_pkcs8(&pkcs8_der, dnssec::Algorithm::ECDSAP384SHA384) .map_err(|err| { format!("Failed to build ECDSA P-384 signing key: {}", err) })?, ), enums::Sig0Algorithm::Ed25519 => { Box::new(Ed25519SigningKey::from_pkcs8(&pkcs8_der).map_err(|err| { format!("Failed to build Ed25519 signing key: {}", err) })?) } }; #[allow(deprecated)] Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_rfc2136_sig0( match server.protocol { enums::IpProtocol::Udp => DnsAddress::Tcp(SocketAddr::new( server.host.into_inner(), server.port as u16, )), enums::IpProtocol::Tcp => DnsAddress::Udp(SocketAddr::new( server.host.into_inner(), server.port as u16, )), }, server.signer_name, signing_key, server.public_key, match server.sig0_algorithm { enums::Sig0Algorithm::EcdsaP256Sha256 => Algorithm::ECDSAP256SHA256, enums::Sig0Algorithm::EcdsaP384Sha384 => Algorithm::ECDSAP384SHA384, enums::Sig0Algorithm::Ed25519 => Algorithm::ED25519, }, ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }) } DnsServer::Cloudflare(server) => { let updater = { #[cfg(feature = "test_mode")] match server.email.as_deref() { Some("test@pebble.org") => dns_update::DnsUpdater::new_pebble( "http://localhost:8055", server.timeout.into_inner().into(), ), Some("test@memory.org") => { dns_update::DnsUpdater::new_in_memory(DNS_RECORDS.clone()) } _ => dns_update::DnsUpdater::new_cloudflare( server.secret.secret().await?, server.email, server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, } #[cfg(not(feature = "test_mode"))] dns_update::DnsUpdater::new_cloudflare( server.secret.secret().await?, server.email, server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))? }; Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater, }) } DnsServer::DigitalOcean(server) => Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_digitalocean( server.secret.secret().await?, server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }), DnsServer::DeSEC(server) => Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_desec( server.secret.secret().await?, server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }), DnsServer::Ovh(server) => Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_ovh( server.application_key, server.application_secret.secret().await?, server.consumer_key.secret().await?, match server.ovh_endpoint { enums::OvhEndpoint::OvhEu => OvhEndpoint::OvhEu, enums::OvhEndpoint::OvhCa => OvhEndpoint::OvhCa, enums::OvhEndpoint::KimsufiEu => OvhEndpoint::KimsufiEu, enums::OvhEndpoint::KimsufiCa => OvhEndpoint::KimsufiCa, enums::OvhEndpoint::SoyoustartEu => OvhEndpoint::SoyoustartEu, enums::OvhEndpoint::SoyoustartCa => OvhEndpoint::SoyoustartCa, }, server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }), DnsServer::Bunny(server) => Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_bunny( server.secret.secret().await?, server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }), DnsServer::Porkbun(server) => Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_porkbun( server.api_key.as_str(), server.secret.secret().await?, server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }), DnsServer::Dnsimple(server) => Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_dnsimple( server.secret.secret().await?, server.account_identifier.as_str(), server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }), DnsServer::Spaceship(server) => Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_spaceship( server.api_key.as_str(), server.secret.secret().await?, server.timeout.into_inner().into(), ) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }), DnsServer::Route53(server) => { let secret_access_key = server.secret_access_key.secret().await?.into_owned(); let session_token = server.session_token.secret().await?.map(|c| c.into_owned()); let config = dns_update::providers::route53::Route53Config { access_key_id: server.access_key_id, secret_access_key, session_token, region: Some(server.region), hosted_zone_id: server.hosted_zone_id, private_zone_only: Some(server.private_zone_only), }; Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_route53(config) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }) } DnsServer::GoogleCloudDns(server) => { let service_account_json = server.service_account_json.secret().await?.into_owned(); let config = dns_update::providers::google_cloud_dns::GoogleCloudDnsConfig { service_account_json, project_id: server.project_id, managed_zone: server.managed_zone, private_zone: server.private_zone, impersonate_service_account: server.impersonate_service_account, request_timeout: Some(server.timeout.into_inner()), }; Ok(DnsUpdater { polling_interval: server.polling_interval.into_inner(), propagation_timeout: server.propagation_timeout.into_inner(), propagation_delay: server.propagation_delay.map(|d| d.into_inner()), ttl: server.ttl.into_inner(), core, updater: dns_update::DnsUpdater::new_google_cloud_dns(config) .map_err(|err| format!("Failed to build DNS updater: {}", err))?, }) } } } pub async fn create( &self, origin: &str, name: &str, record: DnsRecord, verify: bool, delete_before_create: bool, ) -> Result { // First try deleting the record if delete_before_create && let Err(err) = self.updater.delete(name, origin, record.as_type()).await { // Errors are expected if the record does not exist trc::event!( Dns(DnsEvent::RecordDeletionFailed), Hostname = name.to_string(), Reason = err.to_string(), Details = origin.to_string(), ); } // Create the record if let Err(err) = self .updater .create(name, record.clone(), self.ttl.as_secs() as u32, origin) .await { return Err(format!("Failed to create DNS record: {}", err)); } trc::event!( Dns(DnsEvent::RecordCreated), Hostname = name.to_string(), Details = origin.to_string(), ); if verify && let DnsRecord::TXT(txt_record) = &record { #[cfg(feature = "test_mode")] if matches!( self.updater, dns_update::DnsUpdater::Pebble(_) | dns_update::DnsUpdater::InMemory(_) ) { return Ok(true); } // Wait for changes to propagate if let Some(initial_wait) = self.propagation_delay { tokio::time::sleep(initial_wait).await; } let wait_until = Instant::now() + self.propagation_timeout; let mut did_propagate = false; while Instant::now() < wait_until { match self.core.smtp.resolvers.dns.txt_raw_lookup(&name).await { Ok(result) => { let result = std::str::from_utf8(&result).unwrap_or_default(); if result.contains(txt_record) { did_propagate = true; break; } else { trc::event!( Dns(DnsEvent::RecordNotPropagated), Hostname = name.to_string(), Details = origin.to_string(), Result = result.to_string(), ); } } Err(err) => { trc::event!( Dns(DnsEvent::RecordLookupFailed), Hostname = name.to_string(), Details = origin.to_string(), Reason = err.to_string(), ); } } tokio::time::sleep(self.polling_interval).await; } if did_propagate { trc::event!( Dns(DnsEvent::RecordPropagated), Hostname = name.to_string(), Details = origin.to_string(), ); } else { trc::event!( Dns(DnsEvent::RecordPropagationTimeout), Hostname = name.to_string(), Details = origin.to_string(), ); } Ok(did_propagate) } else { Ok(true) } } pub async fn delete( &self, origin: &str, name: &str, record_type: DnsRecordType, ) -> Result<(), String> { // First try deleting the record match self.updater.delete(name, origin, record_type).await { Ok(_) => Ok(()), Err(err) => { trc::event!( Dns(DnsEvent::RecordDeletionFailed), Hostname = name.to_string(), Reason = err.to_string(), Details = origin.to_string(), ); Err(err.to_string()) } } } } impl Server { pub async fn build_dns_updater_for_domain( &self, domain_id: Id, ) -> trc::Result> { if let Some(domain) = self.registry().object::(domain_id).await? { match domain.dns_management { DnsManagement::Automatic(props) => { self.build_dns_updater(props.dns_server_id).await } DnsManagement::Manual => Ok(Err(format!( "Domain with ID {} is set to manual DNS management", domain_id ))), } } else { Ok(Err(format!("Domain with ID {} not found", domain_id))) } } pub async fn build_dns_updater( &self, dns_server_id: Id, ) -> trc::Result> { if let Some(settings) = self.registry().object::(dns_server_id).await? { Ok(DnsUpdater::build(settings, self.core.clone()).await) } else { Ok(Err(format!( "DNS server with ID {} not found", dns_server_id ))) } } }