//! Provides a library to encode and decode [DNS stamp].
//!
//! [DNS stamp]: https://dnscrypt.info/stamps-specifications
#![warn(
missing_debug_implementations,
missing_docs,
rust_2018_idioms,
unreachable_pub,
non_snake_case,
non_upper_case_globals
)]
#![allow(clippy::cognitive_complexity)]
#![deny(broken_intra_doc_links)]
#![doc(test(
no_crate_inject,
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
))]
mod decode;
mod encode;
mod error;
pub use crate::error::{DecodeError, DecodeResult, EncodeError, EncodeResult};
use bitflags::bitflags;
use std::{
convert::TryFrom,
io,
net::{IpAddr, SocketAddr},
};
bitflags! {
/// Represent the [`props`].
///
/// [`props`]: https://dnscrypt.info/stamps-specifications#dnscrypt-stamps
pub struct Props: u64 {
/// If this flag is present then the server supports [DNSSEC].
///
/// [DNSSEC]: https://tools.ietf.org/html/rfc3833
const DNSSEC = 0x01;
/// If this flag is present then the server does not keep logs
const NO_LOGS = 0x02;
/// If this flag is present then the server does not intentionally block domains
const NO_FILTER = 0x04;
}
}
/// This enum represent an address.
/// An address in DNS Stamp can have port or a IP-Address and port.
#[derive(Debug, Copy, PartialEq, Eq, Clone)]
pub enum Addr {
/// a SocketAddr
SocketAddr(SocketAddr),
/// port number represented as a u16
Port(u16),
}
/// This enum represent all DNS Stamp type.
#[repr(u8)]
#[derive(Debug, Copy, Clone)]
pub enum DnsStampType {
/// See [Plain DNS stamps].
///
/// [Plain DNS stamps]: https://dnscrypt.info/stamps-specifications#plain-dns-stamps
Plain = 0x00,
/// See [DNSCrypt stamps].
///
/// [DNSCrypt stamps]: https://dnscrypt.info/stamps-specifications#dnscrypt-stamps
DnsCrypt = 0x01,
/// See [DNS-over-HTTPS stamps].
///
/// [DNS-over-HTTPS stamps]: https://dnscrypt.info/stamps-specifications#dns-over-https-stamps
DnsOverHttps = 0x02,
/// See [DNS-over-TLS stamps].
///
/// [DNS-over-TLS stamps]: https://dnscrypt.info/stamps-specifications#dns-over-tls-stamps
DnsOverTls = 0x03,
/// See [DNS-over-QUIC stamps].
///
/// [DNS-over-QUIC stamps]: https://dnscrypt.info/stamps-specifications#dns-over-quic-stamps
DnsOverQuic = 0x04,
/// See [Oblivious DoH target stamps].
///
/// [Oblivious DoH target stamps]: https://dnscrypt.info/stamps-specifications#oblivious-doh-target-stamps
ObliviousDoHTarget = 0x05,
/// See [Anonymized DNSCrypt relay stamps].
///
/// [Anonymized DNSCrypt relay stamps]: https://dnscrypt.info/stamps-specifications#anonymized-dnscrypt-relay-stamps
AnonymizedDnsCryptRelay = 0x81,
/// See [Oblivious DoH relay stamps].
///
/// [Oblivious DoH relay stamps]: https://dnscrypt.info/stamps-specifications#oblivious-doh-relay-stamps
ObliviousDoHRelay = 0x85,
}
impl TryFrom<u8> for DnsStampType {
type Error = io::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(DnsStampType::Plain),
0x01 => Ok(DnsStampType::DnsCrypt),
0x02 => Ok(DnsStampType::DnsOverHttps),
0x03 => Ok(DnsStampType::DnsOverTls),
0x04 => Ok(DnsStampType::DnsOverQuic),
0x05 => Ok(DnsStampType::ObliviousDoHTarget),
0x81 => Ok(DnsStampType::AnonymizedDnsCryptRelay),
0x85 => Ok(DnsStampType::ObliviousDoHRelay),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
"dns stamp type not found",
)),
}
}
}
/// This enum represent a [DNS Stamp].
///
/// [DNS Stamp]: https://dnscrypt.info/stamps-specifications/
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DnsStamp {
/// See [Plain DNS stamps].
///
/// [Plain DNS stamps]: https://dnscrypt.info/stamps-specifications#plain-dns-stamps
DnsPlain(DnsPlain),
/// See [DNSCrypt stamps].
///
/// [DNSCrypt stamps]: https://dnscrypt.info/stamps-specifications#dnscrypt-stamps
DnsCrypt(DnsCrypt),
/// See [DNS-over-HTTPS stamps].
///
/// [DNS-over-HTTPS stamps]: https://dnscrypt.info/stamps-specifications#dns-over-https-stamps
DnsOverHttps(DnsOverHttps),
/// See [DNS-over-TLS stamps].
///
/// [DNS-over-TLS stamps]: https://dnscrypt.info/stamps-specifications#dns-over-tls-stamps
DnsOverTls(DnsOverTls),
/// See [DNS-over-QUIC stamps].
///
/// [DNS-over-QUIC stamps]: https://dnscrypt.info/stamps-specifications#dns-over-quic-stamps
DnsOverQuic(DnsOverTls),
/// See [Oblivious DoH target stamps].
///
/// [Oblivious DoH target stamps]: https://dnscrypt.info/stamps-specifications#oblivious-doh-target-stamps
ObliviousDoHTarget(ObliviousDoHTarget),
/// See [Plain DNS stamps].
///
/// [Plain DNS stamps]: https://dnscrypt.info/stamps-specifications#anonymized-dnscrypt-relay-stamps
AnonymizedDnsCryptRelay(AnonymizedDnsCryptRelay),
/// See [Oblivious DoH relay stamps].
///
/// [Oblivious DoH relay stamps]: https://dnscrypt.info/stamps-specifications#oblivious-doh-relay-stamps
ObliviousDoHRelay(DnsOverHttps),
}
/// Dnscrypt configuration parsed from dnsstamp
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DnsCrypt {
/// server properties
props: Props,
/// addr is the IP address, as a string, with a port number if the server
/// is not accessible over the standard port for the protocol (443).
addr: SocketAddr,
/// pk is the DNSCrypt provider’s Ed25519 public key, as 32 raw bytes.
pk: [u8; 32],
/// providerName is the DNSCrypt provider name.
provider_name: String,
}
/// DoH configuration parsed from a dnsstamp
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DnsOverHttps {
/// server properties
pub props: Props,
/// addr is the IP address of the server. It can be an empty string,
/// or just a port number, represented with a preceding colon (:443).
/// In that case, the host name will be resolved to an IP address using another resolver.
pub addr: Option<Addr>,
/// hashi is the SHA256 digest of one of the TBS certificate found in the validation chain,
/// typically the certificate used to sign the resolver’s certificate. Multiple hashes can
/// be provided for seamless rotations.
pub hashi: Vec<[u8; 32]>,
/// hostname is the server host name which will also be used as a SNI name.
/// If the host name contains characters outside the URL-permitted range,
/// these characters should be sent as-is, without any extra encoding
/// (neither URL-encoded nor punycode).
pub hostname: String,
/// path is the absolute URI path, such as /dns-query.
pub path: String,
/// bootstrap_ipi are IP addresses of recommended resolvers accessible over standard DNS
/// in order to resolve hostname. This is optional, and clients can ignore this information.
pub bootstrap_ipi: Vec<IpAddr>,
}
/// DNS over TLS configuration
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DnsOverTls {
/// server properties
pub props: Props,
/// addr is the IP address of the server. It can be an empty string,
/// or just a port number, represented with a preceding colon (:443).
/// In that case, the host name will be resolved to an IP address using another resolver.
pub addr: Option<Addr>,
/// hashi is the SHA256 digest of one of the TBS certificate found in the validation chain,
/// typically the certificate used to sign the resolver’s certificate. Multiple hashes can
/// be provided for seamless rotations.
pub hashi: Vec<[u8; 32]>,
/// hostname is the server host name which will also be used as a SNI name.
/// If the host name contains characters outside the URL-permitted range,
/// these characters should be sent as-is, without any extra encoding
/// (neither URL-encoded nor punycode).
pub hostname: String,
/// bootstrap_ipi are IP addresses of recommended resolvers accessible over standard DNS
/// in order to resolve hostname. This is optional, and clients can ignore this information.
pub bootstrap_ipi: Vec<IpAddr>,
}
/// Oblivious DoH target stamps
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObliviousDoHTarget {
/// server properties
pub props: Props,
/// hostname is the server host name which, for relays, will also be used as a SNI name.
/// If the host name contains characters outside the URL-permitted range, these characters
/// should be sent as-is, without any extra encoding (neither URL-encoded nor punycode).
pub hostname: String,
/// path is the absolute URI path, such as /dns-query.
pub path: String,
}
/// Plain dns configuration parsed from a dnsstamp
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DnsPlain {
/// server properties
pub props: Props,
/// addr is the IP address of the server.
/// IPv6 strings must be included in square brackets: `[fe80::6d6d:f72c:3ad:60b8]`.
/// Scopes are permitted.
pub addr: IpAddr,
}
/// Anonymized dnscrypt relay configuration parsed from a dnsstamp
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnonymizedDnsCryptRelay {
/// 0x81 is the protocol identifier for a DNSCrypt relay.
/// addr is the IP address and port, as a string.
/// IPv6 strings must be included in square brackets: `[fe80::6d6d:f72c:3ad:60b8]:443`.
pub addr: SocketAddr,
}
impl DnsOverHttps {
/// get hostname for DOH config
pub fn hostname(&self) -> String {
hostname(self.addr, &self.hostname)
}
/// get hostname based on the bootstrap information
#[cfg(feature = "resolve")]
pub fn bootstrap_hostname(&self) -> io::Result<String> {
bootstrap_hostname(self.addr, &self.hostname, &self.bootstrap_ipi)
}
}
impl DnsOverTls {
/// get hostname from config
pub fn hostname(&self) -> String {
hostname(self.addr, &self.hostname)
}
/// get hostname based on the bootstrap information
#[cfg(feature = "resolve")]
pub fn bootstrap_hostname(&self) -> io::Result<String> {
bootstrap_hostname(self.addr, &self.hostname, &self.bootstrap_ipi)
}
}
#[inline]
fn hostname(addr: Option<Addr>, host: &str) -> String {
match addr {
None => format!("{}:443", host),
Some(Addr::Port(port)) => format!("{}:{}", host, port),
Some(Addr::SocketAddr(addr)) => addr.to_string(),
}
}
#[cfg(feature = "resolve")]
#[inline]
fn bootstrap_hostname(addr: Option<Addr>, host: &str, bootstrap: &[IpAddr]) -> io::Result<String> {
use trust_dns_resolver::config::*;
use trust_dns_resolver::Resolver;
if !bootstrap.is_empty() {
let mut config = ResolverConfig::new();
for ip in bootstrap {
let socket_addr = SocketAddr::new(*ip, 53);
config.add_name_server(NameServerConfig {
socket_addr,
protocol: Protocol::Udp,
tls_dns_name: None,
trust_nx_responses: false,
});
}
let resolver = Resolver::new(config, ResolverOpts::default()).unwrap();
let resp = resolver.lookup_ip(host)?;
Ok(resp
.iter()
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "address not found"))?
.to_string())
} else {
Ok(hostname(addr, host))
}
}