mod resolver;
mod web_context;
use crate::builder::dns_websys::resolver::Resolver;
pub use crate::builder::dns_websys::resolver::{Config, DnsResolution, ResolveError};
use futures::{future, prelude::*};
use libp2p::core::{
multiaddr::{Multiaddr, Protocol},
transport::{DialOpts, ListenerId, TransportError, TransportEvent},
};
use parking_lot::Mutex;
use send_wrapper::SendWrapper;
use std::{
error, fmt, io,
ops::DerefMut,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
const DNSADDR_PREFIX: &str = "_dnsaddr.";
const MAX_DIAL_ATTEMPTS: usize = 16;
const MAX_DNS_LOOKUPS: usize = 32;
const MAX_TXT_RECORDS: usize = 16;
#[derive(Debug)]
pub struct Transport<T> {
inner: Arc<Mutex<T>>,
resolver: Resolver,
}
impl<T> Transport<T> {
pub fn with_config(inner: T, config: Config) -> Self {
Transport {
inner: Arc::new(Mutex::new(inner)),
resolver: Resolver::new(config),
}
}
}
impl<T> libp2p::Transport for Transport<T>
where
T: libp2p::Transport + Send + Unpin + 'static,
T::Error: Send,
T::Dial: Send,
{
type Output = T::Output;
type Error = Error<T::Error>;
type ListenerUpgrade = future::MapErr<T::ListenerUpgrade, fn(T::Error) -> Self::Error>;
type Dial = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
fn listen_on(
&mut self,
id: ListenerId,
addr: Multiaddr,
) -> Result<(), TransportError<Self::Error>> {
self.inner
.lock()
.listen_on(id, addr)
.map_err(|e| e.map(Error::Transport))
}
fn remove_listener(&mut self, id: ListenerId) -> bool {
self.inner.lock().remove_listener(id)
}
fn dial(
&mut self,
addr: Multiaddr,
dial_opts: DialOpts,
) -> Result<Self::Dial, TransportError<Self::Error>> {
Ok(self.do_dial(addr, dial_opts))
}
fn poll(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<TransportEvent<Self::ListenerUpgrade, Self::Error>> {
let mut inner = self.inner.lock();
libp2p::Transport::poll(Pin::new(inner.deref_mut()), cx).map(|event| {
event
.map_upgrade(|upgr| upgr.map_err::<_, fn(_) -> _>(Error::Transport))
.map_err(Error::Transport)
})
}
}
impl<T> Transport<T>
where
T: libp2p::Transport + Send + Unpin + 'static,
T::Error: Send,
T::Dial: Send,
{
fn do_dial(
&mut self,
addr: Multiaddr,
dial_opts: DialOpts,
) -> <Self as libp2p::Transport>::Dial {
let resolver = self.resolver.clone();
let inner = self.inner.clone();
let dns_resolution = self.resolver.dns_resolution();
SendWrapper::new(async move {
let mut dial_errors: Vec<Error<T::Error>> = Vec::new();
let mut dns_lookups = 0;
let mut dial_attempts = 0;
let mut unresolved = Vec::new();
unresolved.push(addr.clone());
while let Some(addr) = unresolved.pop() {
let resolve_dns = should_resolve_dns(&addr, dns_resolution);
if let Some((i, name)) = addr
.iter()
.enumerate()
.find(|(_, p)| is_resolvable(p, resolve_dns))
{
if dns_lookups == MAX_DNS_LOOKUPS {
tracing::debug!(address=%addr, "Too many DNS lookups, dropping unresolved address");
dial_errors.push(Error::TooManyLookups);
continue;
}
dns_lookups += 1;
match resolve(&name, &resolver).await {
Err(e) => {
dial_errors.push(e);
}
Ok(Resolved::One(ip)) => {
tracing::trace!(protocol=%name, resolved=%ip);
let addr = addr.replace(i, |_| Some(ip)).expect("`i` is a valid index");
unresolved.push(addr);
}
Ok(Resolved::Many(ips)) => {
for ip in ips {
tracing::trace!(protocol=%name, resolved=%ip);
let addr =
addr.replace(i, |_| Some(ip)).expect("`i` is a valid index");
unresolved.push(addr);
}
}
Ok(Resolved::Addrs(addrs)) => {
let suffix = addr.iter().skip(i + 1).collect::<Multiaddr>();
let prefix = addr.iter().take(i).collect::<Multiaddr>();
let mut n = 0;
for a in addrs {
if a.ends_with(&suffix) {
if n < MAX_TXT_RECORDS {
n += 1;
tracing::trace!(protocol=%name, resolved=%a);
let addr =
prefix.iter().chain(a.iter()).collect::<Multiaddr>();
unresolved.push(addr);
} else {
tracing::debug!(
resolved=%a,
"Too many TXT records, dropping resolved"
);
}
}
}
}
}
} else {
tracing::debug!(address=%addr, "Dialing address");
let transport = inner.clone();
let dial = transport.lock().dial(addr, dial_opts);
let result = match dial {
Ok(out) => {
dial_attempts += 1;
out.await.map_err(Error::Transport)
}
Err(TransportError::MultiaddrNotSupported(a)) => {
Err(Error::MultiaddrNotSupported(a))
}
Err(TransportError::Other(err)) => Err(Error::Transport(err)),
};
match result {
Ok(out) => return Ok(out),
Err(err) => {
tracing::debug!("Dial error: {:?}.", err);
dial_errors.push(err);
if unresolved.is_empty() {
break;
}
if dial_attempts == MAX_DIAL_ATTEMPTS {
tracing::debug!(
"Aborting dialing after {} attempts.",
MAX_DIAL_ATTEMPTS
);
break;
}
}
}
}
}
if !dial_errors.is_empty() {
Err(Error::Dial(dial_errors))
} else {
Err(Error::ResolveError(ResolveError::Fetch(
"no matching records found".to_owned(),
)))
}
})
.boxed()
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Error<TErr> {
Transport(TErr),
#[allow(clippy::enum_variant_names)]
ResolveError(ResolveError),
MultiaddrNotSupported(Multiaddr),
TooManyLookups,
Dial(Vec<Error<TErr>>),
}
impl<TErr> fmt::Display for Error<TErr>
where
TErr: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Transport(err) => write!(f, "{err}"),
Error::ResolveError(err) => write!(f, "{err}"),
Error::MultiaddrNotSupported(a) => write!(f, "Unsupported resolved address: {a}"),
Error::TooManyLookups => write!(f, "Too many DNS lookups"),
Error::Dial(errs) => {
write!(f, "Multiple dial errors occurred:")?;
for err in errs {
write!(f, "\n - {err}")?;
}
Ok(())
}
}
}
}
impl<TErr> error::Error for Error<TErr>
where
TErr: error::Error + 'static,
{
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Error::Transport(err) => Some(err),
Error::ResolveError(err) => Some(err),
Error::MultiaddrNotSupported(_) => None,
Error::TooManyLookups => None,
Error::Dial(errs) => errs.last().and_then(|e| e.source()),
}
}
}
enum Resolved<'a> {
One(Protocol<'a>),
Many(Vec<Protocol<'a>>),
Addrs(Vec<Multiaddr>),
}
fn should_resolve_dns(addr: &Multiaddr, policy: DnsResolution) -> bool {
match policy {
DnsResolution::Always => true,
DnsResolution::Never => false,
DnsResolution::Auto => addr.iter().any(|p| matches!(p, Protocol::WebRTCDirect)),
}
}
fn is_resolvable(proto: &Protocol<'_>, resolve_dns: bool) -> bool {
match proto {
Protocol::Dnsaddr(_) => true,
Protocol::Dns(_) | Protocol::Dns4(_) | Protocol::Dns6(_) => resolve_dns,
_ => false,
}
}
async fn resolve<'a, E>(
proto: &Protocol<'a>,
resolver: &Resolver,
) -> Result<Resolved<'a>, Error<E>> {
match proto {
Protocol::Dns(name) => {
let v4 = resolver.ipv4_lookup(name.as_ref()).await;
let v6 = resolver.ipv6_lookup(name.as_ref()).await;
if let (Err(e), Err(_)) = (&v4, &v6) {
return Err(Error::ResolveError(e.clone()));
}
let mut ips: Vec<Protocol<'a>> = Vec::new();
ips.extend(v4.into_iter().flatten().map(Protocol::from));
ips.extend(v6.into_iter().flatten().map(Protocol::from));
collect(ips)
}
Protocol::Dns4(name) => {
let ips = resolver
.ipv4_lookup(name.as_ref())
.await
.map_err(Error::ResolveError)?;
collect(ips.into_iter().map(Protocol::from).collect())
}
Protocol::Dns6(name) => {
let ips = resolver
.ipv6_lookup(name.as_ref())
.await
.map_err(Error::ResolveError)?;
collect(ips.into_iter().map(Protocol::from).collect())
}
Protocol::Dnsaddr(name) => {
let lookup = [DNSADDR_PREFIX, name].concat();
let txts = resolver
.txt_lookup(&lookup)
.await
.map_err(Error::ResolveError)?;
let mut addrs = Vec::new();
for txt in txts {
match parse_dnsaddr_txt(&txt) {
Ok(a) => addrs.push(a),
Err(e) => tracing::debug!("Invalid TXT record: {:?}", e),
}
}
Ok(Resolved::Addrs(addrs))
}
proto => Ok(Resolved::One(proto.clone())),
}
}
fn collect<'a, E>(mut protocols: Vec<Protocol<'a>>) -> Result<Resolved<'a>, Error<E>> {
match protocols.len() {
0 => Err(Error::ResolveError(ResolveError::Fetch(
"no matching records found".to_owned(),
))),
1 => Ok(Resolved::One(protocols.remove(0))),
_ => Ok(Resolved::Many(protocols)),
}
}
fn parse_dnsaddr_txt(txt: &str) -> io::Result<Multiaddr> {
match txt.strip_prefix("dnsaddr=") {
None => Err(invalid_data("Missing `dnsaddr=` prefix.")),
Some(a) => Ok(Multiaddr::try_from(a).map_err(invalid_data)?),
}
}
fn invalid_data(e: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, e)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dnsaddr_is_always_resolvable() {
let dnsaddr = Protocol::Dnsaddr("bootstrap.libp2p.io".into());
assert!(is_resolvable(&dnsaddr, false));
assert!(is_resolvable(&dnsaddr, true));
let dns4 = Protocol::Dns4("example.com".into());
assert!(!is_resolvable(&dns4, false));
assert!(is_resolvable(&dns4, true));
}
#[test]
fn auto_resolves_dns_only_for_webrtc_direct() {
let wss: Multiaddr = "/dns4/example.com/tcp/443/wss".parse().unwrap();
let webrtc: Multiaddr =
"/dns4/example.com/udp/4001/webrtc-direct/certhash/uEiDDq4_xNyDorZBH3TlGazyJdOWSwvo4PUo0dVwsfStPnQ"
.parse()
.unwrap();
assert!(!should_resolve_dns(&wss, DnsResolution::Auto));
assert!(should_resolve_dns(&webrtc, DnsResolution::Auto));
assert!(should_resolve_dns(&wss, DnsResolution::Always));
assert!(should_resolve_dns(&webrtc, DnsResolution::Always));
assert!(!should_resolve_dns(&wss, DnsResolution::Never));
assert!(!should_resolve_dns(&webrtc, DnsResolution::Never));
}
#[test]
fn parse_dnsaddr_txt_requires_prefix() {
let addr = parse_dnsaddr_txt("dnsaddr=/dns4/example.com/tcp/443/wss").unwrap();
assert_eq!(
addr,
"/dns4/example.com/tcp/443/wss"
.parse::<Multiaddr>()
.unwrap()
);
assert!(parse_dnsaddr_txt("/dns4/example.com").is_err());
}
}