Skip to main content

abuse_contact/
bootstrap.rs

1//! The IANA bootstrap registries, which say who answers for an address or a name.
2//!
3//! IANA publishes three files. Two map an address range to the RDAP server of the
4//! regional registry that holds it, and one maps a top-level domain to the server of
5//! its registry. RFC 9224 gives the format and the matching rules.
6//!
7//! Nothing here fetches. A [`Registry`] is read from bytes you already have, so the
8//! rules that pick a server run in a test without a network.
9
10use std::net::IpAddr;
11
12use serde::Deserialize;
13
14use crate::query::DomainName;
15
16/// Where IANA publishes the registry for IPv4 addresses.
17pub const IPV4_URL: &str = "https://data.iana.org/rdap/ipv4.json";
18
19/// Where IANA publishes the registry for IPv6 addresses.
20pub const IPV6_URL: &str = "https://data.iana.org/rdap/ipv6.json";
21
22/// Where IANA publishes the registry for domain names.
23pub const DNS_URL: &str = "https://data.iana.org/rdap/dns.json";
24
25/// One bootstrap registry file.
26///
27/// A file holds services. A service is a pair: the keys it answers for, and the
28/// servers that answer. A key is an address range in one file and a domain suffix in
29/// another.
30#[derive(Clone, Debug, Default, Deserialize)]
31pub struct Registry {
32    /// Each entry is `[[key, ...], [url, ...]]`, the shape RFC 9224 gives.
33    #[serde(default)]
34    services: Vec<Vec<Vec<String>>>,
35}
36
37impl Registry {
38    /// Reads a registry from the bytes of a bootstrap file.
39    ///
40    /// # Errors
41    ///
42    /// Returns the parse error when the bytes are not a bootstrap file.
43    pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
44        serde_json::from_slice(bytes)
45    }
46
47    /// Returns the base URL of the server that answers for this address.
48    ///
49    /// The most specific range wins, as RFC 9224 requires. A registry holding both
50    /// `10.0.0.0/8` and `10.1.0.0/16` answers the second for an address in it.
51    pub fn server_for_ip(&self, ip: IpAddr) -> Option<&str> {
52        let mut best: Option<(u8, &str)> = None;
53
54        for (keys, urls) in self.services() {
55            for key in keys {
56                let Some(length) = prefix_length_containing(key, ip) else {
57                    continue;
58                };
59                if best.is_some_and(|(found, _)| found >= length) {
60                    continue;
61                }
62                if let Some(url) = preferred(urls) {
63                    best = Some((length, url));
64                }
65            }
66        }
67
68        best.map(|(_, url)| url)
69    }
70
71    /// Returns the base URL of the server that answers for this name.
72    ///
73    /// The longest run of labels wins. A registry holding both `uk` and `co.uk`
74    /// answers the second for `example.co.uk`.
75    pub fn server_for_domain(&self, domain: &DomainName) -> Option<&str> {
76        let name = domain.as_str();
77        let mut best: Option<(usize, &str)> = None;
78
79        for (keys, urls) in self.services() {
80            for key in keys {
81                let key = key.trim_matches('.').to_lowercase();
82                if !suffix_matches(name, &key) {
83                    continue;
84                }
85                let labels = key.split('.').count();
86                if best.is_some_and(|(found, _)| found >= labels) {
87                    continue;
88                }
89                if let Some(url) = preferred(urls) {
90                    best = Some((labels, url));
91                }
92            }
93        }
94
95        best.map(|(_, url)| url)
96    }
97
98    /// Returns each service as its keys and its servers.
99    ///
100    /// A service in a shape RFC 9224 does not describe is skipped, so one bad entry
101    /// does not lose the rest of the file.
102    fn services(&self) -> impl Iterator<Item = (&Vec<String>, &Vec<String>)> {
103        self.services
104            .iter()
105            .filter_map(|entry| Some((entry.first()?, entry.get(1)?)))
106    }
107}
108
109/// Returns the prefix length when the range holds the address, and `None` otherwise.
110///
111/// A range of another address family never holds it.
112fn prefix_length_containing(range: &str, ip: IpAddr) -> Option<u8> {
113    let network: ipnet::IpNet = range.parse().ok()?;
114
115    network.contains(&ip).then_some(network.prefix_len())
116}
117
118/// Returns whether the name sits at or under the suffix.
119///
120/// `example.com` matches `com`, and `com` matches `com`. `mycom` does not, because a
121/// suffix match runs on whole labels.
122fn suffix_matches(name: &str, suffix: &str) -> bool {
123    if suffix.is_empty() {
124        return false;
125    }
126    let Some(rest) = name.strip_suffix(suffix) else {
127        return false;
128    };
129    rest.is_empty() || rest.ends_with('.')
130}
131
132/// Returns the server to use out of the servers a service lists.
133///
134/// RFC 9224 asks for HTTPS where a service offers it. A few registries list only
135/// HTTP, and the crate uses those rather than refusing to answer for them. A server
136/// with any other scheme is never picked: the client cannot use it, and picking it
137/// would lose a usable server listed after it.
138fn preferred(urls: &[String]) -> Option<&str> {
139    let with_scheme = |scheme: &str| urls.iter().find(|url| url.starts_with(scheme));
140
141    with_scheme("https://")
142        .or_else(|| with_scheme("http://"))
143        .map(String::as_str)
144}
145
146/// The three registries together.
147///
148/// Hold one of these for as long as the process runs. IANA changes the files rarely,
149/// and the answer for an address does not move between them.
150#[derive(Clone, Debug, Default)]
151pub struct Bootstrap {
152    /// The registry for IPv4 addresses.
153    pub ipv4: Registry,
154    /// The registry for IPv6 addresses.
155    pub ipv6: Registry,
156    /// The registry for domain names.
157    pub dns: Registry,
158}
159
160impl Bootstrap {
161    /// Returns the base URL of the server that answers for this address.
162    pub fn server_for_ip(&self, ip: IpAddr) -> Option<&str> {
163        match ip {
164            IpAddr::V4(_) => self.ipv4.server_for_ip(ip),
165            IpAddr::V6(_) => self.ipv6.server_for_ip(ip),
166        }
167    }
168
169    /// Returns the base URL of the server that answers for this name.
170    pub fn server_for_domain(&self, domain: &DomainName) -> Option<&str> {
171        self.dns.server_for_domain(domain)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    fn registry(json: &str) -> Registry {
180        Registry::from_slice(json.as_bytes()).unwrap()
181    }
182
183    const IPV4: &str = r#"{"version":"1.0","services":[
184      [["41.0.0.0/8","102.0.0.0/8"],["https://rdap.afrinic.net/rdap/"]],
185      [["1.0.0.0/8","27.0.0.0/8"],["https://rdap.apnic.net/"]],
186      [["8.0.0.0/8"],["https://rdap.arin.net/registry/","http://rdap.arin.net/registry/"]]
187    ]}"#;
188
189    const DNS: &str = r#"{"version":"1.0","services":[
190      [["com","net"],["https://rdap.verisign.com/com/v1/"]],
191      [["kg"],["http://rdap.cctld.kg/"]],
192      [["uk"],["https://rdap.nominet.uk/uk/"]],
193      [["co.uk"],["https://rdap.example.co.uk/"]]
194    ]}"#;
195
196    #[test]
197    fn finds_the_server_for_an_ipv4_address() {
198        assert_eq!(
199            registry(IPV4).server_for_ip("8.8.8.8".parse().unwrap()),
200            Some("https://rdap.arin.net/registry/")
201        );
202    }
203
204    #[test]
205    fn finds_the_server_for_a_second_registry() {
206        assert_eq!(
207            registry(IPV4).server_for_ip("27.1.2.3".parse().unwrap()),
208            Some("https://rdap.apnic.net/")
209        );
210    }
211
212    #[test]
213    fn gives_nothing_for_an_address_no_service_holds() {
214        assert_eq!(
215            registry(IPV4).server_for_ip("192.0.2.1".parse().unwrap()),
216            None
217        );
218    }
219
220    #[test]
221    fn prefers_the_https_server() {
222        // The ARIN entry lists HTTPS first and HTTP second.
223        let ipv4 = registry(IPV4);
224
225        let found = ipv4.server_for_ip("8.8.8.8".parse().unwrap());
226
227        assert_eq!(found, Some("https://rdap.arin.net/registry/"));
228    }
229
230    #[test]
231    fn uses_an_http_server_when_a_registry_lists_no_other() {
232        let domain = "example.kg".parse().unwrap();
233
234        assert_eq!(
235            registry(DNS).server_for_domain(&domain),
236            Some("http://rdap.cctld.kg/")
237        );
238    }
239
240    #[test]
241    fn skips_a_server_with_a_scheme_the_client_cannot_use() {
242        let listed = |urls: &[&str]| urls.iter().map(|url| (*url).to_owned()).collect::<Vec<_>>();
243
244        assert_eq!(
245            preferred(&listed(&["ftp://bad.example/", "http://usable.example/"])),
246            Some("http://usable.example/")
247        );
248        assert_eq!(
249            preferred(&listed(&[
250                "http://plain.example/",
251                "https://secure.example/"
252            ])),
253            Some("https://secure.example/")
254        );
255        assert_eq!(preferred(&listed(&["ftp://bad.example/"])), None);
256        assert_eq!(preferred(&[]), None);
257    }
258
259    #[test]
260    fn takes_the_most_specific_range() {
261        let wide_and_narrow = registry(
262            r#"{"services":[
263                 [["10.0.0.0/8"],["https://wide.example/"]],
264                 [["10.1.0.0/16"],["https://narrow.example/"]]
265               ]}"#,
266        );
267
268        assert_eq!(
269            wide_and_narrow.server_for_ip("10.1.2.3".parse().unwrap()),
270            Some("https://narrow.example/")
271        );
272        assert_eq!(
273            wide_and_narrow.server_for_ip("10.2.2.3".parse().unwrap()),
274            Some("https://wide.example/")
275        );
276    }
277
278    #[test]
279    fn takes_the_longest_run_of_labels() {
280        let domain = "shop.example.co.uk".parse().unwrap();
281
282        assert_eq!(
283            registry(DNS).server_for_domain(&domain),
284            Some("https://rdap.example.co.uk/")
285        );
286    }
287
288    #[test]
289    fn matches_a_suffix_on_whole_labels() {
290        // "mycom" ends with the letters of "com" but is not under it.
291        let domain = "mycom".parse::<DomainName>();
292
293        assert!(domain.is_err(), "a name with no dot is not a domain");
294        assert!(!suffix_matches("mycom", "com"));
295        assert!(suffix_matches("example.com", "com"));
296        assert!(suffix_matches("com", "com"));
297    }
298
299    #[test]
300    fn finds_the_server_for_an_ipv6_address() {
301        let ipv6 = registry(
302            r#"{"services":[
303                 [["2001:4200::/23","2c00::/12"],["https://rdap.afrinic.net/rdap/"]],
304                 [["2001:4800::/23"],["https://rdap.arin.net/registry/"]]
305               ]}"#,
306        );
307
308        assert_eq!(
309            ipv6.server_for_ip("2c00::1".parse().unwrap()),
310            Some("https://rdap.afrinic.net/rdap/")
311        );
312    }
313
314    #[test]
315    fn an_address_of_another_family_matches_nothing() {
316        assert_eq!(
317            registry(IPV4).server_for_ip("2c00::1".parse().unwrap()),
318            None
319        );
320    }
321
322    #[test]
323    fn a_zero_length_prefix_holds_every_address() {
324        let catch_all = registry(r#"{"services":[[["0.0.0.0/0"],["https://any.example/"]]]}"#);
325
326        assert_eq!(
327            catch_all.server_for_ip("203.0.113.9".parse().unwrap()),
328            Some("https://any.example/")
329        );
330    }
331
332    #[test]
333    fn skips_a_service_in_a_shape_the_format_does_not_describe() {
334        let mixed = registry(
335            r#"{"services":[
336                 [["8.0.0.0/8"]],
337                 [["not-a-range"],["https://bad.example/"]],
338                 [["8.0.0.0/8"],["https://good.example/"]]
339               ]}"#,
340        );
341
342        assert_eq!(
343            mixed.server_for_ip("8.8.8.8".parse().unwrap()),
344            Some("https://good.example/")
345        );
346    }
347
348    #[test]
349    fn reads_an_empty_registry() {
350        assert_eq!(
351            registry("{}").server_for_ip("8.8.8.8".parse().unwrap()),
352            None
353        );
354    }
355
356    #[test]
357    fn bootstrap_picks_the_registry_by_address_family() {
358        let bootstrap = Bootstrap {
359            ipv4: registry(IPV4),
360            ipv6: registry(r#"{"services":[[["2c00::/12"],["https://v6.example/"]]]}"#),
361            dns: registry(DNS),
362        };
363
364        assert_eq!(
365            bootstrap.server_for_ip("8.8.8.8".parse().unwrap()),
366            Some("https://rdap.arin.net/registry/")
367        );
368        assert_eq!(
369            bootstrap.server_for_ip("2c00::1".parse().unwrap()),
370            Some("https://v6.example/")
371        );
372    }
373}