Languages
🇺🇸 English | 🇷🇺 Русский
DNS Lattice is a programmable, embeddable DNS resolver/server engine for Rust. It provides split DNS, caching, Fake IP, dynamic route selection, structured observability, and UDP/TCP/DoT/DoH/DoQ transports behind one typed library API.
Think of it as the DNS equivalent of an embeddable HTTP server core: the host application owns the process and configuration, while DNS Lattice owns DNS protocol handling, resolution, serving, routing, cache behavior, and transport execution.
Status: stable
1.xreleases are published on crates.io (dns-lattice,dns-lattice-core,dns-lattice-model). Stages 0.0 through 1.0 are complete: the public API is frozen and the workspace follows ordinary SemVer within the1.xline — a breaking change requires an explicit major version bump. See CHANGELOG.md for the current version.
Applications that need custom DNS behavior often end up combining several concerns manually: DNS wire parsing, split-DNS policy, cache semantics, transport fallback, encrypted DNS, Fake IP state, server listeners, and application-specific routing. DNS Lattice keeps those concerns separate but composable.
The resolver pipeline is explicit:
DNS query
→ terminal Fake IP handling when selected
→ static split-DNS candidate
→ optional RouteHook
→ validate effective upstream group
→ route-scoped cache
→ ordered upstream failover
→ answer
Inbound listeners reuse the same resolver pipeline:
Client → Server → Resolver → Cache/Policy/Hook/Fake IP → UpstreamBackend → Resolver → Server → Client
DNS Lattice is published as three crates:
| Crate | Responsibility |
|---|---|
dns-lattice |
Public facade plus resolver/server runtime implementation |
dns-lattice-model |
DNS message model, names, matcher, split-DNS policy |
dns-lattice-core |
Shared typed Error / Result boundary |
Most applications should depend only on dns-lattice.
Baseline UDP/TCP support has no TLS/HTTP/QUIC feature requirement:
[dependencies]
dns-lattice = "1.0"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }Enable encrypted transports only when needed:
[dependencies]
dns-lattice = { version = "1.0", features = ["dot", "doh", "doq"] }Cargo features are independent and default-off:
dot— DNS-over-TLS;doh— DNS-over-HTTPS over HTTP/1.1, HTTP/2, and HTTP/3;doq— DNS-over-QUIC.
Use canonical domain modules; the facade intentionally exposes no flat root aliases.
use std::{net::SocketAddr, sync::Arc, time::Duration};
use dns_lattice::{
core::Result,
engine::Resolver,
model::{SplitDnsPolicy, UpstreamGroupId},
server::ServerBuilder,
upstream::{UdpBackend, UdpBackendConfig},
};
# async fn run() -> Result<()> {
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?;
# Ok(())
# }Resolver owns routing/cache/failover. Server owns inbound listening and
framing. UpstreamBackend implementations own outbound transport execution.
Canonical public paths are:
| Module | Purpose |
|---|---|
dns_lattice::core |
Shared typed errors/results |
dns_lattice::model |
DNS messages, records, names, matchers, policies |
dns_lattice::engine |
Resolver / ResolverBuilder |
dns_lattice::upstream |
Outbound backend trait and transports |
dns_lattice::server |
Inbound listener configuration/lifecycle |
dns_lattice::fakeip |
Fake IP pool, policy, TTL, snapshots |
dns_lattice::hooks |
Dynamic route-selection hook |
dns_lattice::observability |
Structured resolver events/sink |
dns-lattice-model provides deterministic exact/suffix/wildcard matching and
SplitDnsPolicy. The resolver first obtains a static upstream-group candidate
from that policy.
The matching/model layer performs no network I/O and has no OS dependency. Stage-0.6 hardening added deterministic property-style coverage for matcher precedence, message parsing, and DNS name compression bounds.
The resolver has an in-memory TTL-respecting answer cache, including negative caching. Ordinary cache identity includes the effective upstream group. That matters when a route hook sends equal DNS questions to different routes: an answer obtained from one group cannot be reused for another group.
Fake IP terminal answers bypass the ordinary answer cache; their lifetime is owned by the Fake IP mapping.
ResolverBuilder::route_hook installs one caller-owned RouteHook for
ordinary queries. The hook receives the first DNS question and tentative
static group:
Use(group)selects an existing, nonempty upstream group;Abstainkeeps the static candidate.
A hook error, unknown group, or empty group fails resolution without silently falling back to another static route. Hooks are selection-only: DNS Lattice does not give them resolver/backend handles, cache authority, client transport metadata, or OS/network side-effect capabilities.
Hook implementations own timeout, retry, cancellation cleanup, and any external calls. Re-entering the same resolver from its hook is prohibited.
use async_trait::async_trait;
use dns_lattice::{
hooks::{RouteDecision, RouteHook, RouteHookError, RouteRequest},
model::UpstreamGroupId,
};
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")))
}
}fakeip::FakeIpPool provides deterministic, concurrent synthetic IPv4/IPv6
state:
- inclusive IPv4 and/or IPv6 ranges;
- deterministic domain → address allocation/reuse;
- address → active-domain reverse lookup;
- per-family LRU eviction on exhaustion;
- required whole-second TTL and expiry;
- caller-owned process-local in-memory snapshot/restore.
ResolverBuilder::fake_ip explicitly enables local synthesis through a
FakeIpPolicy:
- matching IN A/AAAA → local synthetic answer;
- selected but disabled address family → local NODATA;
- canonical in-range IN PTR → active name or NXDOMAIN.
Fake IP answers are terminal before static routing, hooks, ordinary cache, and upstream calls. Their DNS TTL never exceeds the mapping's remaining lifetime.
DNS Lattice intentionally does not define durable Fake IP persistence or a snapshot serialization format.
ResolverBuilder::observability_sink accepts an optional
observability::ObservabilitySink. The resolver emits immutable, bounded
events for the important state transitions in the pipeline, including:
- query receipt;
- Fake IP terminal handling;
- static/effective route and hook outcomes;
- cache hit/miss;
- upstream attempts/outcomes;
- timeout and terminal error paths.
The sink is non-authoritative. It cannot alter routing, answers, cache state, or retries; it receives no resolver/backend handles; resolver locks are released before callbacks run; callback panics are isolated from resolver correctness. DNS Lattice does not require a logging/tracing framework or own a background telemetry queue.
The resolver tries backends registered in an upstream group in registration order. Timeout/transport/TLS failures can fail over to the next backend. If all backends fail, the last error is returned and no successful answer is cached.
| Transport | Feature | Implementation notes |
|---|---|---|
| UDP | default | Falls back to TCP on TC=1 |
| TCP | default | RFC 1035 length-prefixed framing |
| DoT | dot |
rustls / tokio-rustls |
| DoH HTTP/1.1 + HTTP/2 | doh |
hyper / hyper-rustls |
| DoH HTTP/3 | doh |
h3 / quinn, ALPN h3 |
| DoQ | doq |
quinn, ALPN doq |
DoQ and HTTP/3 use QUIC/TLS 1.3. TCP DoH supports HTTP/1.1 and HTTP/2 over TLS 1.2/1.3 according to the supplied configuration.
Server / ServerBuilder provide an embeddable inbound DNS server over a
shared Arc<Resolver>:
- UDP/TCP in the default build;
- DoT through
ServerBuilder::dot_addrwithdot; - DoH HTTP/1.1/HTTP/2 through
ServerBuilder::doh_addrwithdoh; - DoH HTTP/3 through
ServerBuilder::doh3_addrwithdoh; - DoQ through
ServerBuilder::doq_addrwithdoq.
The host application supplies TLS/QUIC server configuration and certificate material. DNS Lattice does not provision certificates and does not own privileged-port setup.
MSRV: Rust 1.93.
CI validates the supported facade surface on:
- Linux;
- Windows;
- macOS.
CI runs workspace formatting, linting, checking, tests, and docs, plus strict
per-feature check/test/rustdoc coverage for:
--no-default-features
dot
doh
doq
--all-features
CI also verifies workspace package contents and runs a hermetic regression of the release automation. Those checks do not publish crates.
| Capability | Status |
|---|---|
| DNS message encode/decode and name decompression | ✅ |
| Exact/suffix/wildcard domain matcher | ✅ |
| Static split-DNS policy | ✅ |
| Resolver + TTL/negative cache | ✅ |
| Route-scoped cache identity | ✅ |
| UDP/TCP upstreams | ✅ |
| DoT/DoH/DoQ upstreams | ✅ |
| Ordered upstream failover | ✅ |
| UDP/TCP inbound server | ✅ |
| DoT/DoH/DoH3/DoQ inbound server | ✅ |
| Fake IP pool + resolver synthesis | ✅ |
Dynamic RouteHook |
✅ |
Structured ObservabilitySink |
✅ |
| Linux/Windows/macOS feature-matrix validation | ✅ |
| Package/release automation hardening | ✅ |
| Stable public API / SemVer guarantee | ✅ |
DNS Lattice is one component of the wider Lattice networking stack:
net-lattice OS network configuration and inspection
tunnel-lattice TUN/TAP data-plane primitives
dns-lattice DNS resolver/server control plane
flow-lattice Policy compiler
sdk-lattice Application-facing composition
DNS Lattice does not mutate OS DNS settings, manage TUN/TAP devices, compile a rule language, or ship a standalone daemon product. Those responsibilities belong to the host application or sibling Lattice components.
Completed:
- 0.0 — repository/architecture baseline;
- 0.1 — core DNS model;
- 0.2 — resolver and static split DNS;
- 0.3 — upstream transports, failover, inbound server;
- 0.4 — Fake IP;
- 0.5 — dynamic route hooks;
- 0.6 — hardening, cross-platform validation, observability, package and release checks;
- 1.0 — audited/froze the public API, established the stable SemVer
contract, and published the first stable release (
dns-lattice,dns-lattice-core,dns-lattice-model1.0.0on crates.io).
The public API is now frozen: within the 1.x line, additive changes land as
minor releases and fixes as patch releases; a breaking change requires an
explicit major version bump.
See ROADMAP.md and ARCHITECTURE.md for the full delivery and contract details.
Runnable examples live in
crates/dns-lattice/examples:
split_dns_policy— matcher and static policy behavior;message_round_trip— DNS wire encode/decode;resolver— in-process resolver/cache behavior.
Run one with:
cargo run -p dns-lattice --example <name>See CONTRIBUTING.md for contribution requirements, SECURITY.md for private vulnerability reporting, and SUPPORT.md for project support status.
Mozilla Public License 2.0. See LICENSE.