web_faith_dns/lib.rs
1//! A caching DNS resolver for HTTP clients.
2//!
3//! # Transports and server order
4//!
5//! The resolver is configured to consult a list of nameservers in order, or defaults to the system
6//! configuration. Nameservers are specified by URL:
7//!
8//! - `udp://IP:PORT` uses classic plain text DNS over UDP, port 53 by default.
9//! - `tcp://IP:PORT` the same over TCP, port 53 by default.
10//! - `tls://IP:PORT` uses DNS over TLS ([RFC 7858](https://www.rfc-editor.org/rfc/rfc7858)), port
11//! 853 by default.
12//! - `https://HOST:PORT/PATH` uses DNS over HTTPS
13//! ([RFC 8484](https://www.rfc-editor.org/rfc/rfc8484)), port 443 and `/dns-query` by default.
14//! - `quic://IP:PORT` uses DNS over QUIC ([RFC 9250](https://www.rfc-editor.org/rfc/rfc9250)),
15//! port 853 by default.
16//! - `h3://HOST:PORT/PATH` uses DNS over HTTP/3, port 443 and `/dns-query` by default.
17//!
18//! The encrypted transports always authenticate the nameserver. A hostname authenticates against
19//! itself, a bare IP against the address, and a URL fragment (`tls://1.1.1.1#cloudflare-dns.com`)
20//! gives the certificate to expect instead.
21//!
22//! When available, opportunistic encryption upgrade
23//! ([RFC 9539](https://www.rfc-editor.org/rfc/rfc9539)) is used to secure nameservers.
24//!
25//! Some names are exempt from DNS resolution, and are always served by the system:
26//!
27//! - `localhost` and anything under it.
28//! - `.local` and anything under it.
29//! - The system's own DNS domain and search suffixes.
30//! - Anything listed in [`exempt_domains`](ResolverConfig::exempt_domains).
31//!
32//! # Examples
33//!
34//! Consulting a named list of nameservers, in order:
35//!
36//! ```no_run
37//! use web_faith_dns::{FaithResolver, ResolverConfig};
38//!
39//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
40//! let resolver = FaithResolver::new(ResolverConfig {
41//! servers: vec![
42//! "tls://1.1.1.1#cloudflare-dns.com".parse()?,
43//! "udp://9.9.9.9".parse()?,
44//! ],
45//! ..Default::default()
46//! });
47//!
48//! resolver.prefetch("example.com").await;
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! Or from the system's own configuration:
54//!
55//! ```no_run
56//! use web_faith_dns::{FaithResolver, ResolverConfig};
57//!
58//! # async fn example() {
59//! let resolver = FaithResolver::new(ResolverConfig::default());
60//!
61//! resolver.prefetch("example.com").await;
62//! for report in resolver.resolvers() {
63//! println!("{} over {} ({})", report.address, report.transport, report.source);
64//! }
65//! # }
66//! ```
67//!
68//! # Warming the cache
69//!
70//! [`prefetch`](FaithResolver::prefetch) resolves a name ahead of the request that needs it.
71//!
72//! ```no_run
73//! use web_faith_dns::{FaithResolver, ResolverConfig};
74//!
75//! # async fn example() {
76//! let resolver = FaithResolver::new(ResolverConfig::default());
77//! resolver.prefetch("example.com").await;
78//! # }
79//! ```
80//!
81//! # Beyond addresses
82//!
83//! Lookups can also read a queried name's `HTTPS` record. This is used to resolve an HTTP/3 server
84//! address without first needing to connect to the HTTP/1 server.
85//!
86//! ```no_run
87//! use std::sync::Arc;
88//!
89//! use web_faith_dns::{FaithResolver, HttpsAdvertisement, HttpsSink, ResolverConfig};
90//!
91//! struct Upgrades;
92//!
93//! impl HttpsSink for Upgrades {
94//! fn wants(&self, _host: &str) -> bool {
95//! true
96//! }
97//!
98//! fn record(&self, host: &str, advertisement: HttpsAdvertisement) {
99//! println!("{host} advertises HTTP/3 on port {:?}", advertisement.port);
100//! }
101//! }
102//!
103//! # async fn example() {
104//! let resolver = FaithResolver::new(ResolverConfig::default());
105//! resolver.set_https_sink(Arc::new(Upgrades));
106//!
107//! // Any lookup from here also asks for the `HTTPS` record.
108//! resolver.prefetch("example.com").await;
109//! # }
110//! ```
111//!
112//! To avoid delays and momentary outages, the resolver will answer a query with a stale entry from
113//! cache while looking up the updated answer in the background for future queries.
114//!
115//! ```no_run
116//! use std::time::Duration;
117//!
118//! use web_faith_dns::{FaithResolver, ResolverConfig};
119//!
120//! # async fn example() {
121//! let resolver = FaithResolver::new(ResolverConfig {
122//! serve_stale: Some(Duration::from_secs(3600)),
123//! ..Default::default()
124//! });
125//!
126//! resolver.prefetch("example.com").await;
127//! // Whether the next lookup would be answered from an expired entry.
128//! println!("serving stale: {}", resolver.served_stale("example.com"));
129//! # }
130//! ```
131//!
132//! The resolver can be instructed to clear its caches and other learned information at runtime,
133//! for example to handle network-change events.
134//!
135//! ```no_run
136//! use web_faith_dns::{FaithResolver, ResolverConfig};
137//!
138//! # async fn example() {
139//! let resolver = FaithResolver::new(ResolverConfig::default());
140//! resolver.prefetch("example.com").await;
141//!
142//! // The interface changed, so what was learned about the old network goes.
143//! resolver.reset();
144//! # }
145//! ```
146//!
147//! Domain lists are built from [`Name`], re-exported here so a caller needs no hickory dependency:
148//!
149//! ```
150//! use web_faith_dns::{Name, ResolverConfig};
151//!
152//! let config = ResolverConfig {
153//! exempt_domains: vec![Name::from_utf8("corp.internal").expect("a valid domain")],
154//! ..Default::default()
155//! };
156//! assert_eq!(config.exempt_domains.len(), 1);
157//! ```
158//!
159//! # Features
160//!
161//! The `reqwest` feature enables support to use this resolver with `reqwest::ClientBuilder`.
162
163#![deny(missing_docs)]
164// Lets docs.rs label each item with the feature or platform it needs.
165#![cfg_attr(docsrs, feature(doc_cfg))]
166
167// spec:WARM spec:DNS
168
169mod discovery;
170mod https;
171mod resolver;
172mod settings;
173mod transport;
174
175pub use hickory_resolver::proto::rr::Name;
176
177pub use https::{HttpsAdvertisement, HttpsSink};
178pub use resolver::FaithResolver;
179pub use settings::{DEFAULT_MAX_STALE, ResolverConfig, ResolverReport, ResolverSource};
180pub use transport::{ServerSpec, ServerSpecError, Transport};
181
182/// Parse a list of domain names, for the search or exempt lists, or return a message
183/// for the first entry that is not a valid domain name.
184pub fn parse_domains(list: Option<Vec<String>>) -> Result<Option<Vec<Name>>, String> {
185 list.map(|items| {
186 items
187 .iter()
188 .map(|item| Name::from_utf8(item).map_err(|err| format!("{item:?}: {err}")))
189 .collect()
190 })
191 .transpose()
192}