abuse_contact/
resolver.rs1use std::net::{IpAddr, SocketAddr};
6
7use hickory_resolver::TokioResolver;
8use hickory_resolver::config::{NameServerConfig, ResolverConfig};
9use hickory_resolver::net::NetError;
10use hickory_resolver::net::runtime::TokioRuntimeProvider;
11use hickory_resolver::proto::rr::RData;
12
13use crate::contact::{Contact, Scope, Source};
14use crate::dns;
15use crate::error::Error;
16use crate::query::DomainName;
17
18#[derive(Clone, Debug)]
36pub struct Resolver {
37 inner: TokioResolver,
38}
39
40impl Resolver {
41 pub fn new() -> Result<Self, Error> {
47 let inner = TokioResolver::builder_tokio()
48 .and_then(|builder| builder.build())
49 .map_err(|source| dns_error("the system resolver configuration", source))?;
50
51 Ok(Self { inner })
52 }
53
54 pub fn with_nameservers(nameservers: &[SocketAddr]) -> Result<Self, Error> {
60 let servers = nameservers
61 .iter()
62 .map(|address| {
63 let mut server = NameServerConfig::udp_and_tcp(address.ip());
64 for connection in &mut server.connections {
65 connection.port = address.port();
66 }
67 server
68 })
69 .collect();
70
71 let config = ResolverConfig::from_parts(None, Vec::new(), servers);
72 let inner = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
73 .build()
74 .map_err(|source| dns_error("the nameservers given", source))?;
75
76 Ok(Self { inner })
77 }
78
79 pub async fn abusix(&self, ip: IpAddr) -> Result<Vec<Contact>, Error> {
88 let ip = crate::query::unmap(ip);
91 if !crate::is_public(ip) {
92 return Err(Error::NotPublic {
93 target: ip.to_string(),
94 });
95 }
96
97 let records = self.txt(&dns::abusix_name(ip)).await?;
98 Ok(dns::contacts_from_txt(
99 &records,
100 Scope::Network,
101 Source::Abusix,
102 ))
103 }
104
105 pub async fn abuse_net(&self, domain: &DomainName) -> Result<Vec<Contact>, Error> {
111 let name = dns::abuse_net_name(domain);
114 if name.len() > DomainName::MAX_BYTES {
115 return Ok(Vec::new());
116 }
117
118 let records = self.txt(&name).await?;
119 Ok(dns::contacts_from_txt(
120 &records,
121 Scope::Domain,
122 Source::AbuseNet,
123 ))
124 }
125
126 pub async fn rfc2142(&self, domain: &DomainName) -> Result<Option<Contact>, Error> {
136 let name = absolute(domain.as_str());
137
138 let exchanges: Vec<String> = match self.inner.mx_lookup(name.as_str()).await {
139 Ok(lookup) => lookup
140 .answers()
141 .iter()
142 .filter_map(|record| match &record.data {
143 RData::MX(mx) => Some(mx.exchange.to_string()),
144 _ => None,
145 })
146 .collect(),
147 Err(error) if error.is_nx_domain() => return Ok(None),
148 Err(error) if error.is_no_records_found() => Vec::new(),
149 Err(source) => return Err(dns_error(&name, source)),
150 };
151
152 let takes_mail = if exchanges.is_empty() {
153 self.has_address(&name).await?
155 } else {
156 dns::names_a_mail_host(exchanges.iter().map(String::as_str))
157 };
158 if !takes_mail {
159 return Ok(None);
160 }
161
162 dns::rfc2142_contact(domain).map(Some).map_err(Error::from)
163 }
164
165 async fn txt(&self, name: &str) -> Result<Vec<String>, Error> {
170 let name = absolute(name);
171
172 match self.inner.txt_lookup(name.as_str()).await {
173 Ok(lookup) => Ok(lookup
174 .answers()
175 .iter()
176 .filter_map(|record| match &record.data {
177 RData::TXT(txt) => txt_value(&txt.txt_data),
178 _ => None,
179 })
180 .collect()),
181 Err(error) if error.is_no_records_found() => Ok(Vec::new()),
182 Err(source) => Err(dns_error(&name, source)),
183 }
184 }
185
186 async fn has_address(&self, name: &str) -> Result<bool, Error> {
188 match self.inner.lookup_ip(name).await {
189 Ok(lookup) => Ok(lookup.iter().next().is_some()),
190 Err(error) if error.is_no_records_found() => Ok(false),
191 Err(source) => Err(dns_error(name, source)),
192 }
193 }
194}
195
196fn absolute(name: &str) -> String {
201 format!("{}.", name.trim_end_matches('.'))
202}
203
204fn txt_value(strings: &[Box<[u8]>]) -> Option<String> {
211 String::from_utf8(strings.concat()).ok()
212}
213
214fn dns_error(name: &str, source: NetError) -> Error {
216 Error::Dns {
217 name: name.to_owned(),
218 source: Box::new(source),
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn a_name_gets_the_trailing_dot_that_keeps_it_from_a_search_domain() {
228 assert_eq!(
229 absolute("229.132.16.104.abuse-contacts.abusix.zone"),
230 "229.132.16.104.abuse-contacts.abusix.zone."
231 );
232 assert_eq!(absolute("example.com."), "example.com.");
233 }
234
235 #[test]
236 fn the_strings_of_a_record_join_into_its_value() {
237 let strings: Vec<Box<[u8]>> = vec![
238 b"arin-contact@google.com,".to_vec().into_boxed_slice(),
239 b"network-abuse@google.com".to_vec().into_boxed_slice(),
240 ];
241
242 assert_eq!(
243 txt_value(&strings),
244 Some("arin-contact@google.com,network-abuse@google.com".to_owned())
245 );
246 }
247
248 #[test]
249 fn a_character_split_across_two_strings_is_kept_whole() {
250 let strings: Vec<Box<[u8]>> = vec![
252 b"abuse@ex\xc3".to_vec().into_boxed_slice(),
253 b"\xa6mple.com".to_vec().into_boxed_slice(),
254 ];
255
256 assert_eq!(txt_value(&strings), Some("abuse@exæmple.com".to_owned()));
257 }
258
259 #[test]
260 fn a_record_that_is_not_utf8_has_no_value() {
261 let strings: Vec<Box<[u8]>> = vec![b"abuse@\xffexample.com".to_vec().into_boxed_slice()];
262
263 assert_eq!(txt_value(&strings), None);
264 }
265}