Skip to main content

dns_lattice/
lib.rs

1//! Programmable Rust DNS control plane for the Lattice networking stack:
2//! split DNS, Fake IP, address pools, and dynamic routing hooks.
3//!
4//! # Quick start
5//!
6//! The usual inbound path is `Server` → `Resolver` → static split-DNS
7//! policy → `UpstreamBackend`. This `no_run` example uses the baseline UDP
8//! transport; it needs a Tokio runtime, an available local listen address,
9//! and a reachable upstream to run.
10//!
11//! ```no_run
12//! use std::{net::SocketAddr, sync::Arc, time::Duration};
13//!
14//! use dns_lattice::{
15//!     core::Result,
16//!     engine::Resolver,
17//!     model::{SplitDnsPolicy, UpstreamGroupId},
18//!     server::ServerBuilder,
19//!     upstream::{UdpBackend, UdpBackendConfig},
20//! };
21//!
22//! # async fn run() -> Result<()> {
23//! let group = UpstreamGroupId::new("default");
24//! let policy = SplitDnsPolicy::builder().default_group(group.clone()).build();
25//! let resolver = Arc::new(
26//!     Resolver::builder(policy)
27//!         .backend(
28//!             group,
29//!             UdpBackend::new(UdpBackendConfig {
30//!                 server: "1.1.1.1:53".parse::<SocketAddr>().unwrap(),
31//!                 timeout: Duration::from_secs(5),
32//!                 bind_addr: None,
33//!             }),
34//!         )
35//!         .build(),
36//! );
37//! let server = ServerBuilder::new(resolver)
38//!     .udp_addr("127.0.0.1:5353".parse().unwrap())
39//!     .bind()
40//!     .await?;
41//! server.serve().await?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # Fake IP
47//!
48//! [`fakeip::FakeIpPool`] and [`fakeip::FakeIpPolicy`] are opt-in through
49//! [`engine::ResolverBuilder::fake_ip`]. Matching IN A/AAAA and canonical,
50//! in-range IN PTR questions receive local synthetic answers that bypass the
51//! ordinary cache and upstreams. The emitted DNS TTL never exceeds the
52//! mapping's remaining lifetime; pools remain caller-owned and have no
53//! durable persistence built in.
54//!
55//! # Dynamic routing hooks
56//!
57//! [`hooks::RouteHook`] is an opt-in, selection-only extension point for an
58//! ordinary query. The resolver first obtains the static split-DNS candidate,
59//! then invokes one configured hook, validates the resulting group, looks in
60//! that group's cache scope, and finally tries that group's upstreams in
61//! order. A local Fake IP answer is terminal before this sequence.
62//!
63//! This in-process example installs a hook and an in-process backend; it
64//! opens no socket. Add `async-trait` to an application's dependencies when
65//! implementing [`hooks::RouteHook`].
66//!
67//! ```no_run
68//! use async_trait::async_trait;
69//! use dns_lattice::{
70//!     core::Result,
71//!     engine::Resolver,
72//!     hooks::{RouteDecision, RouteHook, RouteHookError, RouteRequest},
73//!     model::{Message, SplitDnsPolicy, UpstreamGroupId},
74//!     upstream::UpstreamBackend,
75//! };
76//!
77//! struct PreferFiltered;
78//!
79//! #[async_trait]
80//! impl RouteHook for PreferFiltered {
81//!     async fn select(
82//!         &self,
83//!         request: RouteRequest<'_>,
84//!     ) -> std::result::Result<RouteDecision, RouteHookError> {
85//!         let _question = request.question();
86//!         let _static_candidate = request.static_group();
87//!         Ok(RouteDecision::Use(UpstreamGroupId::new("filtered")))
88//!     }
89//! }
90//!
91//! struct InProcessBackend;
92//!
93//! #[async_trait]
94//! impl UpstreamBackend for InProcessBackend {
95//!     async fn resolve(&self, query: &Message) -> Result<Message> {
96//!         Ok(query.clone())
97//!     }
98//! }
99//!
100//! let resolver = Resolver::builder(SplitDnsPolicy::builder().build())
101//!     .backend(UpstreamGroupId::new("filtered"), InProcessBackend)
102//!     .route_hook(PreferFiltered)
103//!     .build();
104//! # let _ = resolver;
105//! ```
106//!
107//! A hook error, an unknown selected group, or an empty selected group is a
108//! resolver error: it never falls back to static routing, touches the cache,
109//! or calls an upstream. Cache entries are scoped by the validated effective
110//! group, so equal DNS questions selected to different groups cannot share an
111//! answer. The hook implementation owns timeout, retry, and cancellation
112//! cleanup; dropping [`engine::Resolver::resolve`] drops its in-flight hook
113//! future. A hook must not re-enter the same resolver directly or indirectly.
114//! Hooks receive neither resolver/backend handles nor client metadata, and
115//! DNS Lattice gives them no OS or networking side-effect capability; a host
116//! application composes such work outside this crate.
117//!
118//! # Transport features
119//!
120//! UDP and TCP are available without Cargo features. The default-off `dot`,
121//! `doh`, and `doq` features respectively add DNS-over-TLS,
122//! DNS-over-HTTPS (including HTTP/3 over QUIC), and DNS-over-QUIC. `doh`
123//! therefore includes HTTP/3/QUIC dependencies; `doq` remains an independent
124//! feature for DNS-over-QUIC without the HTTP stack.
125//!
126//! # Canonical module imports
127//!
128//! ```
129//! use dns_lattice::model::{DomainPattern, Name, SplitDnsPolicy, UpstreamGroupId};
130//!
131//! let policy = SplitDnsPolicy::builder()
132//!     .rule(
133//!         DomainPattern::suffix(Name::from_ascii("corp.internal").unwrap()),
134//!         UpstreamGroupId::new("corp"),
135//!     )
136//!     .build();
137//!
138//! let name = Name::from_ascii("host.corp.internal").unwrap();
139//! assert_eq!(policy.resolve_group(&name), Some(&UpstreamGroupId::new("corp")));
140//! ```
141//!
142//! # Facade design
143//!
144//! The canonical imports are domain-scoped: [`model`] for message, matcher,
145//! and policy types; [`engine`] for query orchestration; [`upstream`] for
146//! outbound transports; [`server`] for inbound listeners; and [`fakeip`] for
147//! synthetic-address pools, policies, and snapshots. `Error` and `Result` are
148//! shared across those domains. [`engine::ResolverBuilder::fake_ip`] explicitly
149//! connects a pool and policy to local Fake IP DNS synthesis.
150//!
151//! There are no flat root aliases. Use the domain-scoped module paths above,
152//! which make ownership and responsibility explicit.
153//!
154//! Each legacy root import is intentionally rejected. These compile-fail
155//! examples are checked in every supported feature documentation build; the
156//! encrypted transport checks therefore also run with all transport features.
157//!
158//! ```compile_fail
159//! use dns_lattice::Error;
160//! ```
161//! ```compile_fail
162//! use dns_lattice::Result;
163//! ```
164//! ```compile_fail
165//! use dns_lattice::Name;
166//! ```
167//! ```compile_fail
168//! use dns_lattice::Class;
169//! ```
170//! ```compile_fail
171//! use dns_lattice::Message;
172//! ```
173//! ```compile_fail
174//! use dns_lattice::Header;
175//! ```
176//! ```compile_fail
177//! use dns_lattice::Question;
178//! ```
179//! ```compile_fail
180//! use dns_lattice::RData;
181//! ```
182//! ```compile_fail
183//! use dns_lattice::ResourceRecord;
184//! ```
185//! ```compile_fail
186//! use dns_lattice::RecordType;
187//! ```
188//! ```compile_fail
189//! use dns_lattice::Rcode;
190//! ```
191//! ```compile_fail
192//! use dns_lattice::Opcode;
193//! ```
194//! ```compile_fail
195//! use dns_lattice::DomainMatcher;
196//! ```
197//! ```compile_fail
198//! use dns_lattice::DomainPattern;
199//! ```
200//! ```compile_fail
201//! use dns_lattice::SplitDnsPolicy;
202//! ```
203//! ```compile_fail
204//! use dns_lattice::SplitDnsPolicyBuilder;
205//! ```
206//! ```compile_fail
207//! use dns_lattice::UpstreamGroupId;
208//! ```
209//! ```compile_fail
210//! use dns_lattice::Resolver;
211//! ```
212//! ```compile_fail
213//! use dns_lattice::ResolverBuilder;
214//! ```
215//! ```compile_fail
216//! use dns_lattice::FakeIpPool;
217//! ```
218//! ```compile_fail
219//! use dns_lattice::FakeIpPoolBuilder;
220//! ```
221//! ```compile_fail
222//! use dns_lattice::FakeIpPoolSnapshot;
223//! ```
224//! ```compile_fail
225//! use dns_lattice::FakeIpPolicy;
226//! ```
227//! ```compile_fail
228//! use dns_lattice::FakeIpPolicyBuilder;
229//! ```
230//! ```compile_fail
231//! use dns_lattice::FakeIpMappingSnapshot;
232//! ```
233//! ```compile_fail
234//! use dns_lattice::Server;
235//! ```
236//! ```compile_fail
237//! use dns_lattice::ServerBuilder;
238//! ```
239//! ```compile_fail
240//! use dns_lattice::UpstreamBackend;
241//! ```
242//! ```compile_fail
243//! use dns_lattice::UdpBackend;
244//! ```
245//! ```compile_fail
246//! use dns_lattice::UdpBackendConfig;
247//! ```
248//! ```compile_fail
249//! use dns_lattice::TcpBackend;
250//! ```
251//! ```compile_fail
252//! use dns_lattice::TcpBackendConfig;
253//! ```
254//! ```compile_fail
255//! use dns_lattice::DotBackend;
256//! ```
257//! ```compile_fail
258//! use dns_lattice::DotBackendConfig;
259//! ```
260//! ```compile_fail
261//! use dns_lattice::DohBackend;
262//! ```
263//! ```compile_fail
264//! use dns_lattice::DohBackendConfig;
265//! ```
266//! ```compile_fail
267//! use dns_lattice::DohMethod;
268//! ```
269//! ```compile_fail
270//! use dns_lattice::Doh3Backend;
271//! ```
272//! ```compile_fail
273//! use dns_lattice::Doh3BackendConfig;
274//! ```
275//! ```compile_fail
276//! use dns_lattice::DohListenerConfig;
277//! ```
278//! ```compile_fail
279//! use dns_lattice::DoqBackend;
280//! ```
281//! ```compile_fail
282//! use dns_lattice::DoqBackendConfig;
283//! ```
284
285#![warn(missing_docs)]
286
287pub mod engine;
288pub mod fakeip;
289/// Shared error and result types.
290///
291/// This is the canonical facade path for [`dns_lattice_core::Error`] and
292/// [`dns_lattice_core::Result`], supplied by `dns-lattice-core`.
293pub mod core {
294    pub use dns_lattice_core::{Error, Result};
295}
296/// Caller-supplied dynamic upstream-group selection types.
297///
298/// This is the canonical facade path for [`hooks::RouteHook`] and its
299/// request, decision, and error types. Route hooks are intentionally not
300/// re-exported from the crate root.
301pub mod hooks;
302/// Optional, non-authoritative resolver event sink.
303pub mod observability;
304/// DNS message, domain-matching, and split-DNS policy types.
305///
306/// This is the canonical facade path for types supplied by
307/// `dns-lattice-model`; for example, import [`dns_lattice_model::Name`] as
308/// `dns_lattice::model::Name`.
309pub mod model {
310    pub use dns_lattice_model::{
311        Class, DomainMatcher, DomainPattern, Header, Message, Name, Opcode, Question, RData, Rcode,
312        RecordType, ResourceRecord, SplitDnsPolicy, SplitDnsPolicyBuilder, UpstreamGroupId,
313    };
314}
315pub mod server;
316pub mod upstream;
317
318#[cfg(test)]
319mod facade_path_tests {
320    use super::{core, engine, fakeip, hooks, model, server, upstream};
321
322    #[test]
323    fn canonical_module_paths_expose_the_public_surface() {
324        let _: model::Name = model::Name::root();
325        let _: Option<model::DomainMatcher<()>> = Some(model::DomainMatcher::new());
326        let _: fn(model::Name) -> model::DomainPattern = model::DomainPattern::suffix;
327        let _: fn(model::SplitDnsPolicy) -> engine::ResolverBuilder = engine::Resolver::builder;
328
329        let _: Option<hooks::RouteDecision> = Some(hooks::RouteDecision::Abstain);
330        let _: Option<&dyn hooks::RouteHook> = None;
331        let _: Option<fakeip::FakeIpPool> = None;
332        let _: Option<server::Server> = None;
333        let _: Option<upstream::UdpBackend> = None;
334        let _: Option<core::Error> = None;
335        let _: Option<core::Result<()>> = None;
336    }
337}