abuse_contact/lib.rs
1//! Find where to report abuse for an IP address or a domain name.
2//!
3//! The contact lives in a different place for each kind of target, and the answers
4//! are not interchangeable. The registrar of a domain can suspend the name. The
5//! network that holds an IP address can take the host off the air. The operator of
6//! the domain runs the service. A report goes to the one that can act on it, so this
7//! crate returns every contact it finds with the scope it covers, and leaves the
8//! choice to you.
9//!
10//! # Sources
11//!
12//! | Source | Target | What it gives |
13//! | --- | --- | --- |
14//! | RDAP | IP, domain | The contact the registry publishes |
15//! | Abusix | IP | The contact for the network, over DNS |
16//! | abuse.net | domain | The contact the operator registered, over DNS |
17//! | RFC 2142 | domain | `abuse@` at the domain, a guess |
18//!
19//! # Reading a result
20//!
21//! ```
22//! use abuse_contact::{Contact, EmailAddress, Scope, Source, rank};
23//!
24//! let found = vec![
25//! Contact {
26//! email: EmailAddress::new("abuse@example.com")?,
27//! scope: Scope::Domain,
28//! source: Source::Rfc2142,
29//! },
30//! Contact {
31//! email: EmailAddress::new("registrar-abuse@example.net")?,
32//! scope: Scope::Registrar,
33//! source: Source::Rdap { server: "rdap.example.net".to_owned() },
34//! },
35//! ];
36//!
37//! // The registry answer comes first. The guess comes last.
38//! let ranked = rank(found);
39//! assert_eq!(ranked[0].scope, Scope::Registrar);
40//!
41//! // Pick by what you want to happen, not by what is first.
42//! let takedown = ranked.iter().find(|c| c.scope == Scope::Registrar);
43//! assert!(takedown.is_some());
44//! # Ok::<(), abuse_contact::ValidationError>(())
45//! ```
46//!
47//! # Design
48//!
49//! Values that go out are checked when you build them. [`DomainName`] refuses a name
50//! that no registry can hold, so a lookup that is certain to find nothing never
51//! reaches the network.
52//!
53//! Values that come back are not checked the same way. A change on a registry side
54//! must not turn a working lookup into a parse error, so [`rdap::Response`] reads the
55//! few fields an abuse lookup needs and ignores the rest.
56//!
57//! One value that comes back is refused: a registry that hides contact data puts a
58//! placeholder such as `DATA REDACTED` in the email field. [`EmailAddress`] rejects
59//! it, so the placeholder never reaches a mail queue.
60//!
61//! A domain takes one request, not two. The registry record names the registrar and
62//! carries its abuse address under the registrar entity, so one lookup is enough.
63//! [`rdap::Response::related_href`] gives the registrar record for the details the
64//! registry leaves out, such as the abuse telephone number.
65//!
66//! Registries do not agree on where the abuse entity goes, or on whether to publish
67//! one. ARIN puts it under the registrant and again at the top level. RIPE, APNIC and
68//! LACNIC put it at the top level. APNIC marks the abuse mailbox with `pref` and lists
69//! a help desk beside it. registro.br marks one entity both technical and abuse and
70//! gives it no address. AFRINIC publishes no abuse entity at all. The reader walks the
71//! whole tree, sorts by preference, and returns each address one time.
72//!
73//! RDAP therefore does not answer everywhere. For AFRINIC space and for registro.br
74//! space, DNS is the only source that gives an address. Ask more than one source.
75//!
76//! # Fetching a record
77//!
78//! `Client` picks the server from the IANA bootstrap registries and fetches the
79//! record. It needs the `http` feature, which is on by default, and its own
80//! documentation shows a lookup.
81//!
82//! Turn the default features off with `default-features = false` to take the readers
83//! alone, with no HTTP stack and no resolver. Then fetch with the client you already
84//! have and call
85//! [`rdap::Response::abuse_contacts`], [`dns::contacts_from_txt`] and [`rank`] on what
86//! comes back.
87//!
88//! # Asking the DNS sources
89//!
90//! `Resolver` asks the Abusix and abuse.net zones, and checks that a domain takes mail
91//! before it gives `abuse@` at the domain. It needs the `dns` feature, which is on by
92//! default. For AFRINIC space, where RDAP publishes no abuse entity, Abusix is the only
93//! source that answers.
94//!
95//! # Asking every source
96//!
97//! `Finder` asks every source for a target at the same time, and gives the contacts
98//! ordered by [`rank`]. A source that fails does not fail the lookup: its error is
99//! returned beside the contacts from the sources that answered. It needs the `http`
100//! and `dns` features.
101//!
102//! `Finder` holds the RDAP answers in a `Cache`, so it does not ask a registry about
103//! the same network or the same domain again. The answer for an address is held for
104//! the whole range the registry returned. The resolver holds the DNS answers, for the
105//! TTL of each record.
106
107#![forbid(unsafe_code)]
108
109pub mod bootstrap;
110pub mod dns;
111pub mod rdap;
112
113#[cfg(all(feature = "http", feature = "dns"))]
114mod cache;
115#[cfg(feature = "http")]
116mod client;
117mod contact;
118#[cfg(feature = "http")]
119mod destination;
120mod error;
121#[cfg(all(feature = "http", feature = "dns"))]
122mod finder;
123mod nat64;
124mod query;
125#[cfg(feature = "dns")]
126mod resolver;
127
128#[cfg(all(feature = "http", feature = "dns"))]
129pub use cache::Cache;
130#[cfg(feature = "http")]
131pub use client::{Client, MAX_BOOTSTRAP_BYTES, MAX_RECORD_BYTES, Record};
132pub use contact::{Contact, EmailAddress, Scope, Source, rank};
133#[cfg(feature = "http")]
134pub use destination::Destinations;
135pub use error::{Error, ValidationError};
136#[cfg(all(feature = "http", feature = "dns"))]
137pub use finder::{Failure, Finder, Found, Origin};
138pub use query::{DomainName, Query, is_public};
139#[cfg(feature = "dns")]
140pub use resolver::Resolver;