Skip to main content

Crate dns_lattice

Crate dns_lattice 

Source
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 dot Cargo feature, DNS-over-TLS (RFC 7858), handing decoded queries to crate::engine::Resolver, fulfilling the embeddable-server-engine goal named in ARCHITECTURE.md.
upstream
Public async upstream DNS backend trait plus baseline UDP and TCP transport implementations.