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
|
# 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.
|
||||||
|
|
||||||
|
@ -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,15 +40,61 @@ impl DNSRecord {
|
|||||||
|
|
||||||
let records = response.answers();
|
let records = response.answers();
|
||||||
|
|
||||||
if records.len() != 1 {
|
for record in records {
|
||||||
return Ok(false);
|
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
|
return Ok(false);
|
||||||
match records[0].data() {
|
}
|
||||||
Some(RData::A(remote_addr)) if remote_addr == addr => Ok(true),
|
|
||||||
_ => 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(
|
async fn check(
|
||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
47
src/main.rs
47
src/main.rs
@ -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,
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user