Support for CNAME records
This commit is contained in:
parent
2693e0eac2
commit
af1dc183ec
16
README.md
16
README.md
@ -61,7 +61,10 @@ service:
|
||||
|
||||
# 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
|
||||
# instance. At least one record must be given.
|
||||
# instance.
|
||||
#
|
||||
# A CNAME record with the primary_domain of this server is automatically
|
||||
# included.
|
||||
dns_records:
|
||||
#- type: A
|
||||
# addr: 127.0.0.1
|
||||
@ -69,6 +72,10 @@ service:
|
||||
#- type: AAAA
|
||||
# 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
|
||||
# service.http.https_addr is enabled then an HTTPS certificate for this domain
|
||||
# will be retrieved automatically.
|
||||
@ -116,9 +123,14 @@ Within the shell which opens you can do `cargo run` to start a local instance.
|
||||
|
||||
## Roadmap
|
||||
|
||||
* Support for CNAME records
|
||||
* Support for more backends than just git repositories, including:
|
||||
* IPFS/IPNS
|
||||
* Alternative URLs (reverse proxy)
|
||||
* Google Drive
|
||||
* 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.
|
||||
|
||||
|
@ -24,32 +24,10 @@ pub enum CheckDomainError {
|
||||
pub enum DNSRecord {
|
||||
A(net::Ipv4Addr),
|
||||
AAAA(net::Ipv6Addr),
|
||||
CNAME(domain::Name),
|
||||
}
|
||||
|
||||
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(
|
||||
client: &mut AsyncClient,
|
||||
domain: &trust_dns_client::rr::Name,
|
||||
@ -62,15 +40,61 @@ impl DNSRecord {
|
||||
|
||||
let records = response.answers();
|
||||
|
||||
if records.len() != 1 {
|
||||
return Ok(false);
|
||||
for record in records {
|
||||
if let Some(RData::A(record_addr)) = record.data() {
|
||||
if record_addr == addr {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the single record isn't a A, or it's not the target A, then return false
|
||||
match records[0].data() {
|
||||
Some(RData::A(remote_addr)) if remote_addr == addr => Ok(true),
|
||||
_ => return Ok(false),
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
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 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(
|
||||
@ -81,6 +105,7 @@ impl DNSRecord {
|
||||
match self {
|
||||
Self::A(addr) => Self::check_a(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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::{cmp, fmt};
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
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 {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
47
src/main.rs
47
src/main.rs
@ -34,14 +34,6 @@ struct Cli {
|
||||
async fn main() {
|
||||
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()
|
||||
.filter_level(cli.log_level)
|
||||
.format_timestamp(
|
||||
@ -50,14 +42,47 @@ async fn main() {
|
||||
)
|
||||
.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)
|
||||
.expect("git origin store initialization failed");
|
||||
|
||||
let domain_checker = {
|
||||
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(
|
||||
&config.domain.dns,
|
||||
|
@ -9,11 +9,12 @@ fn default_primary_domain() -> domain::Name {
|
||||
domain::Name::from_str("localhost").unwrap()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ConfigDNSRecord {
|
||||
A { addr: net::Ipv4Addr },
|
||||
AAAA { addr: net::Ipv6Addr },
|
||||
CNAME { name: domain::Name },
|
||||
}
|
||||
|
||||
impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
|
||||
@ -21,6 +22,7 @@ impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
|
||||
match r {
|
||||
ConfigDNSRecord::A { addr } => Self::A(addr),
|
||||
ConfigDNSRecord::AAAA { addr } => Self::AAAA(addr),
|
||||
ConfigDNSRecord::CNAME { name } => Self::CNAME(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,6 +230,9 @@ impl<'svc> Service {
|
||||
flat_config: service::util::FlatConfig,
|
||||
dns_records: &'a [service::ConfigDNSRecord],
|
||||
challenge_token: String,
|
||||
|
||||
domain_is_zone_apex: bool,
|
||||
dns_records_have_cname: bool,
|
||||
}
|
||||
|
||||
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(
|
||||
"/domain_init.html",
|
||||
Response {
|
||||
@ -256,6 +265,9 @@ impl<'svc> Service {
|
||||
flat_config: config.into(),
|
||||
dns_records: &self.config.dns_records,
|
||||
challenge_token: config_hash,
|
||||
|
||||
domain_is_zone_apex,
|
||||
dns_records_have_cname,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -49,11 +49,21 @@ query for your domain name. It can be <strong>one or more of</strong>:</p>
|
||||
<tr>
|
||||
<td>{{ this.type }}</td>
|
||||
<td>{{ lookup ../data "domain" }}</td>
|
||||
{{ #if this.name }}
|
||||
<td>{{ this.name }}</td>
|
||||
{{ else }}
|
||||
<td>{{ this.addr }}</td>
|
||||
{{ /if }}
|
||||
</tr>
|
||||
{{ /each }}
|
||||
</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
|
||||
your configuration and set up your domain.</p>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user