Expand description
Programmable Rust DNS control plane for the Lattice networking stack: split DNS, Fake IP, address pools, and dynamic routing hooks.
§Quick start
The usual inbound path is Server → Resolver → static split-DNS
policy → UpstreamBackend. This no_run example uses the baseline UDP
transport; it needs a Tokio runtime, an available local listen address,
and a reachable upstream to run.
use std::{net::SocketAddr, sync::Arc, time::Duration};
use dns_lattice::{
core::Result,
engine::Resolver,
model::{SplitDnsPolicy, UpstreamGroupId},
server::ServerBuilder,
upstream::{UdpBackend, UdpBackendConfig},
};
let group = UpstreamGroupId::new("default");
let policy = SplitDnsPolicy::builder().default_group(group.clone()).build();
let resolver = Arc::new(
Resolver::builder(policy)
.backend(
group,
UdpBackend::new(UdpBackendConfig {
server: "1.1.1.1:53".parse::<SocketAddr>().unwrap(),
timeout: Duration::from_secs(5),
bind_addr: None,
}),
)
.build(),
);
let server = ServerBuilder::new(resolver)
.udp_addr("127.0.0.1:5353".parse().unwrap())
.bind()
.await?;
server.serve().await?;§Fake IP
fakeip::FakeIpPool and fakeip::FakeIpPolicy are opt-in through
engine::ResolverBuilder::fake_ip. Matching IN A/AAAA and canonical,
in-range IN PTR questions receive local synthetic answers that bypass the
ordinary cache and upstreams. The emitted DNS TTL never exceeds the
mapping’s remaining lifetime; pools remain caller-owned and have no
durable persistence built in.
§Dynamic routing hooks
hooks::RouteHook is an opt-in, selection-only extension point for an
ordinary query. The resolver first obtains the static split-DNS candidate,
then invokes one configured hook, validates the resulting group, looks in
that group’s cache scope, and finally tries that group’s upstreams in
order. A local Fake IP answer is terminal before this sequence.
This in-process example installs a hook and an in-process backend; it
opens no socket. Add async-trait to an application’s dependencies when
implementing hooks::RouteHook.
use async_trait::async_trait;
use dns_lattice::{
core::Result,
engine::Resolver,
hooks::{RouteDecision, RouteHook, RouteHookError, RouteRequest},
model::{Message, SplitDnsPolicy, UpstreamGroupId},
upstream::UpstreamBackend,
};
struct PreferFiltered;
#[async_trait]
impl RouteHook for PreferFiltered {
async fn select(
&self,
request: RouteRequest<'_>,
) -> std::result::Result<RouteDecision, RouteHookError> {
let _question = request.question();
let _static_candidate = request.static_group();
Ok(RouteDecision::Use(UpstreamGroupId::new("filtered")))
}
}
struct InProcessBackend;
#[async_trait]
impl UpstreamBackend for InProcessBackend {
async fn resolve(&self, query: &Message) -> Result<Message> {
Ok(query.clone())
}
}
let resolver = Resolver::builder(SplitDnsPolicy::builder().build())
.backend(UpstreamGroupId::new("filtered"), InProcessBackend)
.route_hook(PreferFiltered)
.build();A hook error, an unknown selected group, or an empty selected group is a
resolver error: it never falls back to static routing, touches the cache,
or calls an upstream. Cache entries are scoped by the validated effective
group, so equal DNS questions selected to different groups cannot share an
answer. The hook implementation owns timeout, retry, and cancellation
cleanup; dropping engine::Resolver::resolve drops its in-flight hook
future. A hook must not re-enter the same resolver directly or indirectly.
Hooks receive neither resolver/backend handles nor client metadata, and
DNS Lattice gives them no OS or networking side-effect capability; a host
application composes such work outside this crate.
§Transport features
UDP and TCP are available without Cargo features. The default-off dot,
doh, and doq features respectively add DNS-over-TLS,
DNS-over-HTTPS (including HTTP/3 over QUIC), and DNS-over-QUIC. doh
therefore includes HTTP/3/QUIC dependencies; doq remains an independent
feature for DNS-over-QUIC without the HTTP stack.
§Canonical module imports
use dns_lattice::model::{DomainPattern, Name, SplitDnsPolicy, UpstreamGroupId};
let policy = SplitDnsPolicy::builder()
.rule(
DomainPattern::suffix(Name::from_ascii("corp.internal").unwrap()),
UpstreamGroupId::new("corp"),
)
.build();
let name = Name::from_ascii("host.corp.internal").unwrap();
assert_eq!(policy.resolve_group(&name), Some(&UpstreamGroupId::new("corp")));§Facade design
The canonical imports are domain-scoped: model for message, matcher,
and policy types; engine for query orchestration; upstream for
outbound transports; server for inbound listeners; and fakeip for
synthetic-address pools, policies, and snapshots. Error and Result are
shared across those domains. engine::ResolverBuilder::fake_ip explicitly
connects a pool and policy to local Fake IP DNS synthesis.
There are no flat root aliases. Use the domain-scoped module paths above, which make ownership and responsibility explicit.
Each legacy root import is intentionally rejected. These compile-fail examples are checked in every supported feature documentation build; the encrypted transport checks therefore also run with all transport features.
use dns_lattice::Error;use dns_lattice::Result;use dns_lattice::Name;use dns_lattice::Class;use dns_lattice::Message;use dns_lattice::Header;use dns_lattice::Question;use dns_lattice::RData;use dns_lattice::ResourceRecord;use dns_lattice::RecordType;use dns_lattice::Rcode;use dns_lattice::Opcode;use dns_lattice::DomainMatcher;use dns_lattice::DomainPattern;use dns_lattice::SplitDnsPolicy;use dns_lattice::SplitDnsPolicyBuilder;use dns_lattice::UpstreamGroupId;use dns_lattice::Resolver;use dns_lattice::ResolverBuilder;use dns_lattice::FakeIpPool;use dns_lattice::FakeIpPoolBuilder;use dns_lattice::FakeIpPoolSnapshot;use dns_lattice::FakeIpPolicy;use dns_lattice::FakeIpPolicyBuilder;use dns_lattice::FakeIpMappingSnapshot;use dns_lattice::Server;use dns_lattice::ServerBuilder;use dns_lattice::UpstreamBackend;use dns_lattice::UdpBackend;use dns_lattice::UdpBackendConfig;use dns_lattice::TcpBackend;use dns_lattice::TcpBackendConfig;use dns_lattice::DotBackend;use dns_lattice::DotBackendConfig;use dns_lattice::DohBackend;use dns_lattice::DohBackendConfig;use dns_lattice::DohMethod;use dns_lattice::Doh3Backend;use dns_lattice::Doh3BackendConfig;use dns_lattice::DohListenerConfig;use dns_lattice::DoqBackend;use dns_lattice::DoqBackendConfig;Modules§
- core
- Shared error and result types.
- engine
- Query orchestration for decoded DNS messages.
- fakeip
- Stateful, deterministic Fake IP address allocation.
- hooks
- Caller-supplied dynamic upstream-group selection types.
- model
- DNS message, domain-matching, and split-DNS policy types.
- observability
- Optional, non-authoritative resolver event sink. Optional, non-authoritative resolver observability.
- server
- Inbound DNS server listener: binds UDP/TCP (baseline) and, behind the
dotCargo feature, DNS-over-TLS (RFC 7858), handing decoded queries tocrate::engine::Resolver, fulfilling the embeddable-server-engine goal named inARCHITECTURE.md. - upstream
- Public async upstream DNS backend trait plus baseline UDP and TCP transport implementations.