feat: DNS client + DANE impl with Hickory
This commit is contained in:
parent
2a097fb800
commit
5b43635b62
7 changed files with 734 additions and 9 deletions
|
@ -18,6 +18,8 @@ pub const SHA512_B64_LEN: usize = 88; // 4 * ((512 / 8) as f64 / 3 as f64).ceil(
|
|||
pub enum HashAlgo {
|
||||
Sha256,
|
||||
Sha512,
|
||||
/// Do not hash, compare the whole cert
|
||||
Raw,
|
||||
}
|
||||
|
||||
/// Structure holding a TLS cert hash
|
||||
|
|
117
src/dns.rs
Normal file
117
src/dns.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use std::net::IpAddr;
|
||||
|
||||
use bytes::Bytes;
|
||||
use hickory_client::{
|
||||
client::{AsyncClient, ClientHandle},
|
||||
proto::iocompat::AsyncIoTokioAsStd,
|
||||
rr::{
|
||||
rdata::tlsa::{CertUsage, Matching, Selector},
|
||||
DNSClass, IntoName, RData, RecordType,
|
||||
},
|
||||
tcp::TcpClientStream,
|
||||
};
|
||||
use tokio::net::ToSocketAddrs;
|
||||
|
||||
use crate::{certs::fingerprint::HashAlgo, LibError};
|
||||
|
||||
pub struct DnsClient(AsyncClient);
|
||||
|
||||
impl DnsClient {
|
||||
pub async fn init(server: impl ToSocketAddrs) -> Result<Self, LibError> {
|
||||
for addr in tokio::net::lookup_host(server).await? {
|
||||
let (stream, sender) =
|
||||
TcpClientStream::<AsyncIoTokioAsStd<tokio::net::TcpStream>>::new(addr);
|
||||
|
||||
if let Ok((client, bg)) = AsyncClient::new(stream, sender, None).await {
|
||||
tokio::spawn(bg);
|
||||
return Ok(DnsClient(client));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Err(LibError::HostLookupError)
|
||||
}
|
||||
|
||||
pub async fn query_ipv4(
|
||||
&mut self,
|
||||
name: &str,
|
||||
) -> Result<impl Iterator<Item = IpAddr>, LibError> {
|
||||
self.query_ip(name, RecordType::A).await
|
||||
}
|
||||
|
||||
pub async fn query_ipv6(
|
||||
&mut self,
|
||||
name: &str,
|
||||
) -> Result<impl Iterator<Item = IpAddr>, LibError> {
|
||||
self.query_ip(name, RecordType::AAAA).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn query_ip(
|
||||
&mut self,
|
||||
name: &str,
|
||||
rtype: RecordType,
|
||||
) -> Result<impl Iterator<Item = IpAddr>, LibError> {
|
||||
let answers = self
|
||||
.0
|
||||
.query(name.into_name()?, DNSClass::IN, rtype)
|
||||
.await?
|
||||
.into_message()
|
||||
.take_answers();
|
||||
if !answers.is_empty() {
|
||||
Ok(answers.into_iter().filter_map(|rec| match rec.data() {
|
||||
Some(RData::A(addr)) => Some(IpAddr::V4(addr.0)),
|
||||
Some(RData::AAAA(addr)) => Some(IpAddr::V6(addr.0)),
|
||||
_ => None,
|
||||
}))
|
||||
} else {
|
||||
Err(LibError::HostLookupError)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_tlsa(
|
||||
&mut self,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
) -> Result<impl Iterator<Item = (HashAlgo, Bytes)>, LibError> {
|
||||
let answers = self
|
||||
.0
|
||||
.query(
|
||||
format!("_{}._tcp.{}", port, domain).into_name()?,
|
||||
DNSClass::IN,
|
||||
RecordType::TLSA,
|
||||
)
|
||||
.await?
|
||||
.into_message()
|
||||
.take_answers();
|
||||
if !answers.is_empty() {
|
||||
Ok(answers.into_iter().filter_map(|rec| {
|
||||
if let Some(RData::TLSA(tlsa)) = rec.data() {
|
||||
if tlsa.cert_usage() == CertUsage::DomainIssued
|
||||
&& tlsa.selector() == Selector::Spki
|
||||
{
|
||||
let hash_algo = match tlsa.matching() {
|
||||
Matching::Sha256 => HashAlgo::Sha256,
|
||||
Matching::Sha512 => HashAlgo::Sha512,
|
||||
Matching::Raw => HashAlgo::Raw,
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// TODO: optimize?
|
||||
// if tlsa.cert_data() returned inner Vec<u8>,
|
||||
// i could do this with zero-copy
|
||||
Some((hash_algo, Bytes::copy_from_slice(tlsa.cert_data())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
Err(LibError::HostLookupError)
|
||||
}
|
||||
}
|
||||
}
|
29
src/error.rs
29
src/error.rs
|
@ -1,5 +1,10 @@
|
|||
//! Library error structures and enums
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
use hickory_client::{
|
||||
error::ClientError as HickoryClientError, proto::error::ProtoError as HickoryProtoError,
|
||||
};
|
||||
|
||||
/// Main error structure, also a wrapper for everything else
|
||||
#[derive(Debug)]
|
||||
pub enum LibError {
|
||||
|
@ -8,6 +13,8 @@ pub enum LibError {
|
|||
IoError(std::io::Error),
|
||||
/// URL parse or check error
|
||||
InvalidUrlError(InvalidUrl),
|
||||
///
|
||||
HostLookupError,
|
||||
/// Response status code is out of [10; 69] range
|
||||
StatusOutOfRange(u8),
|
||||
/// Response metadata or content cannot be parsed
|
||||
|
@ -15,6 +22,12 @@ pub enum LibError {
|
|||
DataNotUtf8(std::string::FromUtf8Error),
|
||||
/// Provided string is not a valid MIME type
|
||||
InvalidMime(mime::FromStrError),
|
||||
///
|
||||
#[cfg(feature = "hickory")]
|
||||
DnsClientError(HickoryClientError),
|
||||
///
|
||||
#[cfg(feature = "hickory")]
|
||||
DnsProtoError(HickoryProtoError),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for LibError {
|
||||
|
@ -59,6 +72,22 @@ impl From<mime::FromStrError> for LibError {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
impl From<HickoryClientError> for LibError {
|
||||
#[inline]
|
||||
fn from(err: HickoryClientError) -> Self {
|
||||
Self::DnsClientError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
impl From<HickoryProtoError> for LibError {
|
||||
#[inline]
|
||||
fn from(err: HickoryProtoError) -> Self {
|
||||
Self::DnsProtoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// URL parse or check error
|
||||
#[derive(Debug)]
|
||||
pub enum InvalidUrl {
|
||||
|
|
|
@ -6,6 +6,9 @@ pub mod client;
|
|||
pub mod error;
|
||||
pub mod status;
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
pub mod dns;
|
||||
|
||||
pub use client::Client;
|
||||
pub use error::*;
|
||||
pub use status::*;
|
||||
|
|
Loading…
Add table
Reference in a new issue