1use crate::{
6 Error,
7 peer_address::{AddressKind, parse, quic_first},
8 peer_gossip::is_public,
9};
10use hickory_resolver::TokioResolver;
11use libp2p::{Multiaddr, multiaddr::Protocol};
12use std::time::Duration;
13
14#[derive(Clone, Debug, Default)]
17pub enum Names {
18 #[default]
19 System,
20 Fixed(Vec<(String, Vec<Multiaddr>)>),
21}
22
23impl Names {
24 pub const RECORDS: usize = 4;
26 const LOOKUP_TIME: Duration = Duration::from_secs(5);
27
28 pub(crate) fn expand(&self, address: &Multiaddr) -> Result<Vec<Multiaddr>, Error> {
32 let Some(Protocol::Dnsaddr(name)) = address.iter().next() else {
33 return Ok(vec![address.clone()]);
34 };
35 let suffix: Multiaddr = address.iter().skip(1).collect();
36 let mut records = self.lookup(&name)?;
37 records.retain(|record| record.ends_with(&suffix) && dialable(record));
38 let mut records = quic_first(records);
39 records.truncate(Self::RECORDS);
40 match records.is_empty() {
41 true => Err(Error::Transport(format!("{address}: no dnsaddr records"))),
42 false => Ok(records),
43 }
44 }
45
46 fn lookup(&self, name: &str) -> Result<Vec<Multiaddr>, Error> {
47 match self {
48 Self::System => system(name),
49 Self::Fixed(table) => Ok(table
50 .iter()
51 .filter(|(known, _)| known == name)
52 .flat_map(|(_, records)| records.iter().cloned())
53 .collect()),
54 }
55 }
56}
57
58fn dialable(record: &Multiaddr) -> bool {
62 let Ok(shape) = parse(record) else {
63 return false;
64 };
65 shape.kind == AddressKind::Direct
66 && matches!(shape.bare.iter().next(), Some(Protocol::Ip4(_)))
67 && is_public(&shape.bare)
68}
69
70fn system(name: &str) -> Result<Vec<Multiaddr>, Error> {
73 let failed =
74 |error: &dyn std::fmt::Display| Error::Transport(format!("dnsaddr {name}: {error}"));
75 let runtime = tokio::runtime::Builder::new_current_thread()
76 .enable_all()
77 .build()
78 .map_err(|error| failed(&error))?;
79 runtime.block_on(async {
80 let resolver = TokioResolver::builder_tokio()
81 .map_err(|error| failed(&error))?
82 .build();
83 let lookup = resolver.txt_lookup(format!("_dnsaddr.{name}."));
84 let txt = tokio::time::timeout(Names::LOOKUP_TIME, lookup)
85 .await
86 .map_err(|error| failed(&error))?
87 .map_err(|error| failed(&error))?;
88 let mut records = Vec::new();
89 for record in txt.iter() {
90 for data in record.txt_data() {
91 match parse_txt(data) {
92 Some(address) => records.push(address),
93 None => tracing::debug!(%name, "skipped a malformed dnsaddr record"),
94 }
95 }
96 }
97 Ok(records)
98 })
99}
100
101fn parse_txt(data: &[u8]) -> Option<Multiaddr> {
103 std::str::from_utf8(data)
104 .ok()?
105 .strip_prefix("dnsaddr=")?
106 .parse()
107 .ok()
108}
109
110#[cfg(test)]
111mod tests {
112 use super::{Names, parse_txt};
113 use libp2p::{Multiaddr, PeerId};
114
115 fn address(text: &str) -> Multiaddr {
116 text.parse().unwrap()
117 }
118
119 #[test]
120 fn seed_records_are_dialed_quic_first_and_capped() {
121 let (one, two) = (PeerId::random(), PeerId::random());
122 let records = vec![
123 address(&format!("/ip4/11.0.0.1/tcp/28332/p2p/{one}")),
124 address(&format!("/ip4/11.0.0.1/udp/28332/quic-v1/p2p/{one}")),
125 address(&format!("/ip4/12.0.0.1/tcp/28332/p2p/{two}")),
126 address(&format!("/ip4/12.0.0.1/udp/28332/quic-v1/p2p/{two}")),
127 address(&format!("/ip4/13.0.0.1/tcp/28332/p2p/{two}")),
128 ];
129 let names = Names::Fixed(vec![("seed.test".into(), records.clone())]);
130 let expanded = names.expand(&address("/dnsaddr/seed.test")).unwrap();
131 assert_eq!(
132 expanded,
133 vec![
134 records[1].clone(),
135 records[3].clone(),
136 records[0].clone(),
137 records[2].clone()
138 ]
139 );
140 let named = address(&format!("/dnsaddr/seed.test/p2p/{two}"));
141 assert_eq!(
142 names.expand(&named).unwrap(),
143 vec![records[3].clone(), records[2].clone(), records[4].clone()],
144 "a seed naming its peer dials only that peer's records"
145 );
146 assert!(names.expand(&address("/dnsaddr/other.test")).is_err());
147 let literal = address("/ip4/11.0.0.1/tcp/1");
148 assert_eq!(names.expand(&literal).unwrap(), vec![literal]);
149 }
150
151 #[test]
152 fn only_public_ipv4_direct_records_are_dialed() {
153 let (peer, relay) = (PeerId::random(), PeerId::random());
154 let kept = address(&format!("/ip4/11.0.0.1/udp/28332/quic-v1/p2p/{peer}"));
155 let dropped = [
156 format!("/ip4/127.0.0.1/tcp/28332/p2p/{peer}"),
157 format!("/ip4/10.0.0.1/tcp/28332/p2p/{peer}"),
158 format!("/ip4/192.168.1.1/udp/28332/quic-v1/p2p/{peer}"),
159 format!("/ip4/100.64.0.1/tcp/28332/p2p/{peer}"),
160 format!("/ip6/2001:4860::1/tcp/28332/p2p/{peer}"),
161 format!("/dns4/seed.example/tcp/28332/p2p/{peer}"),
162 format!("/dnsaddr/other.test/p2p/{peer}"),
163 format!("/ip4/12.0.0.1/tcp/28332/p2p/{relay}/p2p-circuit/p2p/{peer}"),
164 format!("/ip4/11.0.0.1/tcp/0/p2p/{peer}"),
165 ];
166 let mut records: Vec<Multiaddr> = dropped.iter().map(|text| address(text)).collect();
167 records.push(kept.clone());
168 let names = Names::Fixed(vec![("seed.test".into(), records)]);
169 assert_eq!(
170 names.expand(&address("/dnsaddr/seed.test")).unwrap(),
171 vec![kept]
172 );
173 let hostile = Names::Fixed(vec![("seed.test".into(), vec![address(&dropped[0])])]);
174 assert!(
175 hostile.expand(&address("/dnsaddr/seed.test")).is_err(),
176 "a seed with no dialable record is a failed lookup"
177 );
178 }
179
180 #[test]
181 fn only_dnsaddr_txt_records_parse() {
182 assert_eq!(
183 parse_txt(b"dnsaddr=/ip4/1.2.3.4/udp/1/quic-v1"),
184 Some(address("/ip4/1.2.3.4/udp/1/quic-v1"))
185 );
186 assert_eq!(parse_txt(b"v=spf1 -all"), None);
187 assert_eq!(parse_txt(b"dnsaddr=nonsense"), None);
188 }
189}