use crate::{
Error,
peer_address::{AddressKind, parse, quic_first},
peer_gossip::is_public,
};
use hickory_resolver::TokioResolver;
use libp2p::{Multiaddr, multiaddr::Protocol};
use std::time::Duration;
#[derive(Clone, Debug, Default)]
pub enum Names {
#[default]
System,
Fixed(Vec<(String, Vec<Multiaddr>)>),
}
impl Names {
pub const RECORDS: usize = 4;
const LOOKUP_TIME: Duration = Duration::from_secs(5);
pub(crate) fn expand(&self, address: &Multiaddr) -> Result<Vec<Multiaddr>, Error> {
let Some(Protocol::Dnsaddr(name)) = address.iter().next() else {
return Ok(vec![address.clone()]);
};
let suffix: Multiaddr = address.iter().skip(1).collect();
let mut records = self.lookup(&name)?;
records.retain(|record| record.ends_with(&suffix) && dialable(record));
let mut records = quic_first(records);
records.truncate(Self::RECORDS);
match records.is_empty() {
true => Err(Error::Transport(format!("{address}: no dnsaddr records"))),
false => Ok(records),
}
}
fn lookup(&self, name: &str) -> Result<Vec<Multiaddr>, Error> {
match self {
Self::System => system(name),
Self::Fixed(table) => Ok(table
.iter()
.filter(|(known, _)| known == name)
.flat_map(|(_, records)| records.iter().cloned())
.collect()),
}
}
}
fn dialable(record: &Multiaddr) -> bool {
let Ok(shape) = parse(record) else {
return false;
};
shape.kind == AddressKind::Direct
&& matches!(shape.bare.iter().next(), Some(Protocol::Ip4(_)))
&& is_public(&shape.bare)
}
fn system(name: &str) -> Result<Vec<Multiaddr>, Error> {
let failed =
|error: &dyn std::fmt::Display| Error::Transport(format!("dnsaddr {name}: {error}"));
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| failed(&error))?;
runtime.block_on(async {
let resolver = TokioResolver::builder_tokio()
.map_err(|error| failed(&error))?
.build();
let lookup = resolver.txt_lookup(format!("_dnsaddr.{name}."));
let txt = tokio::time::timeout(Names::LOOKUP_TIME, lookup)
.await
.map_err(|error| failed(&error))?
.map_err(|error| failed(&error))?;
let mut records = Vec::new();
for record in txt.iter() {
for data in record.txt_data() {
match parse_txt(data) {
Some(address) => records.push(address),
None => tracing::debug!(%name, "skipped a malformed dnsaddr record"),
}
}
}
Ok(records)
})
}
fn parse_txt(data: &[u8]) -> Option<Multiaddr> {
std::str::from_utf8(data)
.ok()?
.strip_prefix("dnsaddr=")?
.parse()
.ok()
}
#[cfg(test)]
mod tests {
use super::{Names, parse_txt};
use libp2p::{Multiaddr, PeerId};
fn address(text: &str) -> Multiaddr {
text.parse().unwrap()
}
#[test]
fn seed_records_are_dialed_quic_first_and_capped() {
let (one, two) = (PeerId::random(), PeerId::random());
let records = vec![
address(&format!("/ip4/11.0.0.1/tcp/28332/p2p/{one}")),
address(&format!("/ip4/11.0.0.1/udp/28332/quic-v1/p2p/{one}")),
address(&format!("/ip4/12.0.0.1/tcp/28332/p2p/{two}")),
address(&format!("/ip4/12.0.0.1/udp/28332/quic-v1/p2p/{two}")),
address(&format!("/ip4/13.0.0.1/tcp/28332/p2p/{two}")),
];
let names = Names::Fixed(vec![("seed.test".into(), records.clone())]);
let expanded = names.expand(&address("/dnsaddr/seed.test")).unwrap();
assert_eq!(
expanded,
vec![
records[1].clone(),
records[3].clone(),
records[0].clone(),
records[2].clone()
]
);
let named = address(&format!("/dnsaddr/seed.test/p2p/{two}"));
assert_eq!(
names.expand(&named).unwrap(),
vec![records[3].clone(), records[2].clone(), records[4].clone()],
"a seed naming its peer dials only that peer's records"
);
assert!(names.expand(&address("/dnsaddr/other.test")).is_err());
let literal = address("/ip4/11.0.0.1/tcp/1");
assert_eq!(names.expand(&literal).unwrap(), vec![literal]);
}
#[test]
fn only_public_ipv4_direct_records_are_dialed() {
let (peer, relay) = (PeerId::random(), PeerId::random());
let kept = address(&format!("/ip4/11.0.0.1/udp/28332/quic-v1/p2p/{peer}"));
let dropped = [
format!("/ip4/127.0.0.1/tcp/28332/p2p/{peer}"),
format!("/ip4/10.0.0.1/tcp/28332/p2p/{peer}"),
format!("/ip4/192.168.1.1/udp/28332/quic-v1/p2p/{peer}"),
format!("/ip4/100.64.0.1/tcp/28332/p2p/{peer}"),
format!("/ip6/2001:4860::1/tcp/28332/p2p/{peer}"),
format!("/dns4/seed.example/tcp/28332/p2p/{peer}"),
format!("/dnsaddr/other.test/p2p/{peer}"),
format!("/ip4/12.0.0.1/tcp/28332/p2p/{relay}/p2p-circuit/p2p/{peer}"),
format!("/ip4/11.0.0.1/tcp/0/p2p/{peer}"),
];
let mut records: Vec<Multiaddr> = dropped.iter().map(|text| address(text)).collect();
records.push(kept.clone());
let names = Names::Fixed(vec![("seed.test".into(), records)]);
assert_eq!(
names.expand(&address("/dnsaddr/seed.test")).unwrap(),
vec![kept]
);
let hostile = Names::Fixed(vec![("seed.test".into(), vec![address(&dropped[0])])]);
assert!(
hostile.expand(&address("/dnsaddr/seed.test")).is_err(),
"a seed with no dialable record is a failed lookup"
);
}
#[test]
fn only_dnsaddr_txt_records_parse() {
assert_eq!(
parse_txt(b"dnsaddr=/ip4/1.2.3.4/udp/1/quic-v1"),
Some(address("/ip4/1.2.3.4/udp/1/quic-v1"))
);
assert_eq!(parse_txt(b"v=spf1 -all"), None);
assert_eq!(parse_txt(b"dnsaddr=nonsense"), None);
}
}