Support for CNAME records

This commit is contained in:
Brian Picciano 2023-07-11 19:16:09 +02:00
parent 2693e0eac2
commit af1dc183ec
8 changed files with 136 additions and 44 deletions

View File

@ -61,7 +61,10 @@ service:
# DNS records which users must add to their domain's DNS so that # DNS records which users must add to their domain's DNS so that
# Domani can serve the domains. All records given must route to this Domani # Domani can serve the domains. All records given must route to this Domani
# instance. At least one record must be given. # instance.
#
# A CNAME record with the primary_domain of this server is automatically
# included.
dns_records: dns_records:
#- type: A #- type: A
# addr: 127.0.0.1 # addr: 127.0.0.1
@ -69,6 +72,10 @@ service:
#- type: AAAA #- type: AAAA
# addr: ::1 # addr: ::1
# NOTE that the name given here must resolve to the Domani server.
#- type: CNAME
# name: domain.com
# The domain name which will be used to serve the web interface of Domani. If # The domain name which will be used to serve the web interface of Domani. If
# service.http.https_addr is enabled then an HTTPS certificate for this domain # service.http.https_addr is enabled then an HTTPS certificate for this domain
# will be retrieved automatically. # will be retrieved automatically.
@ -116,9 +123,14 @@ Within the shell which opens you can do `cargo run` to start a local instance.
## Roadmap ## Roadmap
* Support for CNAME records
* Support for more backends than just git repositories, including: * Support for more backends than just git repositories, including:
* IPFS/IPNS * IPFS/IPNS
* Alternative URLs (reverse proxy) * Alternative URLs (reverse proxy)
* Google Drive * Google Drive
* Dropbox * Dropbox
* Better support for dDNS servers. If a server is behind dDNS then users can
only add it to their domain via CNAME. If they add the CNAME (or ALIAS or
whatever) to the zone apex then Domani can't see that actual CNAME record (the
DNS server will flatten it to A records). This breaks Domani.

0
TODO
View File

View File

@ -24,32 +24,10 @@ pub enum CheckDomainError {
pub enum DNSRecord { pub enum DNSRecord {
A(net::Ipv4Addr), A(net::Ipv4Addr),
AAAA(net::Ipv6Addr), AAAA(net::Ipv6Addr),
CNAME(domain::Name),
} }
impl DNSRecord { impl DNSRecord {
async fn check_aaaa(
client: &mut AsyncClient,
domain: &trust_dns_client::rr::Name,
addr: &net::Ipv6Addr,
) -> Result<bool, unexpected::Error> {
let response = client
.query(domain.clone(), DNSClass::IN, RecordType::AAAA)
.await
.or_unexpected_while("querying A record")?;
let records = response.answers();
if records.len() != 1 {
return Ok(false);
}
// if the single record isn't a AAAA, or it's not the target AAAA, then return false
match records[0].data() {
Some(RData::AAAA(remote_addr)) if remote_addr == addr => Ok(true),
_ => return Ok(false),
}
}
async fn check_a( async fn check_a(
client: &mut AsyncClient, client: &mut AsyncClient,
domain: &trust_dns_client::rr::Name, domain: &trust_dns_client::rr::Name,
@ -62,16 +40,62 @@ impl DNSRecord {
let records = response.answers(); let records = response.answers();
if records.len() != 1 { for record in records {
if let Some(RData::A(record_addr)) = record.data() {
if record_addr == addr {
return Ok(true);
}
}
}
return Ok(false); return Ok(false);
} }
// if the single record isn't a A, or it's not the target A, then return false async fn check_aaaa(
match records[0].data() { client: &mut AsyncClient,
Some(RData::A(remote_addr)) if remote_addr == addr => Ok(true), domain: &trust_dns_client::rr::Name,
_ => return Ok(false), addr: &net::Ipv6Addr,
) -> Result<bool, unexpected::Error> {
let response = client
.query(domain.clone(), DNSClass::IN, RecordType::AAAA)
.await
.or_unexpected_while("querying AAAA record")?;
let records = response.answers();
for record in records {
if let Some(RData::AAAA(record_addr)) = record.data() {
if record_addr == addr {
return Ok(true);
} }
} }
}
return Ok(false);
}
async fn check_cname(
client: &mut AsyncClient,
domain: &trust_dns_client::rr::Name,
cname: &trust_dns_client::rr::Name,
) -> Result<bool, unexpected::Error> {
let response = client
.query(domain.clone(), DNSClass::IN, RecordType::CNAME)
.await
.or_unexpected_while("querying CNAME record")?;
let records = response.answers();
for record in records {
if let Some(RData::CNAME(record_cname)) = record.data() {
if record_cname == cname {
return Ok(true);
}
}
}
return Ok(false);
}
async fn check( async fn check(
&self, &self,
@ -81,6 +105,7 @@ impl DNSRecord {
match self { match self {
Self::A(addr) => Self::check_a(client, domain, &addr).await, Self::A(addr) => Self::check_a(client, domain, &addr).await,
Self::AAAA(addr) => Self::check_aaaa(client, domain, &addr).await, Self::AAAA(addr) => Self::check_aaaa(client, domain, &addr).await,
Self::CNAME(name) => Self::check_cname(client, domain, name.as_rr()).await,
} }
} }
} }

View File

@ -1,5 +1,5 @@
use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use std::{cmp, fmt};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use trust_dns_client::rr as trust_dns_rr; use trust_dns_client::rr as trust_dns_rr;
@ -40,6 +40,12 @@ impl FromStr for Name {
} }
} }
impl cmp::PartialEq for Name {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl Serialize for Name { impl Serialize for Name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where

View File

@ -34,14 +34,6 @@ struct Cli {
async fn main() { async fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
let config: domani::config::Config = {
let path = &cli.config_path;
let f = std::fs::File::open(path)
.unwrap_or_else(|e| panic!("failed to open config file at {}: {e}", path.display()));
serde_yaml::from_reader(f)
.unwrap_or_else(|e| panic!("failed to parse config file at {}: {e}", path.display()))
};
env_logger::Builder::new() env_logger::Builder::new()
.filter_level(cli.log_level) .filter_level(cli.log_level)
.format_timestamp( .format_timestamp(
@ -50,14 +42,47 @@ async fn main() {
) )
.init(); .init();
let config = {
let mut config: domani::config::Config = {
let path = &cli.config_path;
let f = std::fs::File::open(path).unwrap_or_else(|e| {
panic!("failed to open config file at {}: {e}", path.display())
});
serde_yaml::from_reader(f).unwrap_or_else(|e| {
panic!("failed to parse config file at {}: {e}", path.display())
})
};
// primary_cname is a CNAME record which points to the primary domain of the service. Since
// the primary domain _must_ point to the service (otherwise HTTPS wouldn't work) it's
// reasonable to assume that a CNAME on any domain would suffice to point that domain to
// the service.
let primary_cname = domani::service::ConfigDNSRecord::CNAME {
name: config.service.primary_domain.clone(),
};
let dns_records_have_primary_cname = config
.service
.dns_records
.iter()
.any(|r| r == &primary_cname);
if !dns_records_have_primary_cname {
log::info!(
"Adding 'CNAME {}' to service.dns_records",
&config.service.primary_domain
);
config.service.dns_records.push(primary_cname);
}
config
};
let origin_store = domani::origin::git::FSStore::new(&config.origin) let origin_store = domani::origin::git::FSStore::new(&config.origin)
.expect("git origin store initialization failed"); .expect("git origin store initialization failed");
let domain_checker = { let domain_checker = {
let dns_records = config.service.dns_records.clone(); let dns_records = config.service.dns_records.clone();
if dns_records.len() == 0 {
panic!("service.dns_records must have at least one record defined")
}
domani::domain::checker::DNSChecker::new( domani::domain::checker::DNSChecker::new(
&config.domain.dns, &config.domain.dns,

View File

@ -9,11 +9,12 @@ fn default_primary_domain() -> domain::Name {
domain::Name::from_str("localhost").unwrap() domain::Name::from_str("localhost").unwrap()
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ConfigDNSRecord { pub enum ConfigDNSRecord {
A { addr: net::Ipv4Addr }, A { addr: net::Ipv4Addr },
AAAA { addr: net::Ipv6Addr }, AAAA { addr: net::Ipv6Addr },
CNAME { name: domain::Name },
} }
impl From<ConfigDNSRecord> for domain::checker::DNSRecord { impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
@ -21,6 +22,7 @@ impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
match r { match r {
ConfigDNSRecord::A { addr } => Self::A(addr), ConfigDNSRecord::A { addr } => Self::A(addr),
ConfigDNSRecord::AAAA { addr } => Self::AAAA(addr), ConfigDNSRecord::AAAA { addr } => Self::AAAA(addr),
ConfigDNSRecord::CNAME { name } => Self::CNAME(name),
} }
} }
} }

View File

@ -230,6 +230,9 @@ impl<'svc> Service {
flat_config: service::util::FlatConfig, flat_config: service::util::FlatConfig,
dns_records: &'a [service::ConfigDNSRecord], dns_records: &'a [service::ConfigDNSRecord],
challenge_token: String, challenge_token: String,
domain_is_zone_apex: bool,
dns_records_have_cname: bool,
} }
let config: domain::Domain = match domain_config.try_into() { let config: domain::Domain = match domain_config.try_into() {
@ -249,6 +252,12 @@ impl<'svc> Service {
} }
}; };
let domain_is_zone_apex = args.domain.as_rr().num_labels() == 2;
let dns_records_have_cname = self.config.dns_records.iter().any(|r| match r {
service::ConfigDNSRecord::CNAME { .. } => true,
_ => false,
});
self.render_page( self.render_page(
"/domain_init.html", "/domain_init.html",
Response { Response {
@ -256,6 +265,9 @@ impl<'svc> Service {
flat_config: config.into(), flat_config: config.into(),
dns_records: &self.config.dns_records, dns_records: &self.config.dns_records,
challenge_token: config_hash, challenge_token: config_hash,
domain_is_zone_apex,
dns_records_have_cname,
}, },
) )
} }

View File

@ -49,11 +49,21 @@ query for your domain name. It can be <strong>one or more of</strong>:</p>
<tr> <tr>
<td>{{ this.type }}</td> <td>{{ this.type }}</td>
<td>{{ lookup ../data "domain" }}</td> <td>{{ lookup ../data "domain" }}</td>
{{ #if this.name }}
<td>{{ this.name }}</td>
{{ else }}
<td>{{ this.addr }}</td> <td>{{ this.addr }}</td>
{{ /if }}
</tr> </tr>
{{ /each }} {{ /each }}
</table> </table>
{{ #if data.domain_is_zone_apex }}{{ #if data.dns_records_have_cname }}
<p>(Please note that not all DNS providers support putting a CNAME at the zone
apex, while others support it via an alternative record type like ALIAS or
ANAME.)</p>
{{ /if }}{{ /if }}
<p>Once both entries are installed, you can hit the following button to check <p>Once both entries are installed, you can hit the following button to check
your configuration and set up your domain.</p> your configuration and set up your domain.</p>