For nerds

Inside TelltaleDNS

Every crate, how a query moves through them, and why each piece of the stack was chosen. Generated from the repository on every site build, so it can't drift from the code.

14 crates · 146,480 lines of Rust in 245 files · 151 decisions recorded (ADRs)

The map

Crates and how they connect

One Cargo workspace, one binary. An arrow points from a crate to a crate it uses (read from the Cargo.toml files). Select a crate to see what it does, its main types, its budget, and the decisions behind it.

Network edgeData plane: answering a queryTelemetry and storageControl planeThe binarynet 3,509 linesproto 1,858 linespolicy 2,851 linesfilter 8,159 linescache 1,855 linesupstream 7,795 linesrecursor 1,372 linestelemetry 7,231 linesstore 6,800 linesapi 28,950 linescluster 7,950 linesconfig 6,191 linesgit 942 linestelltale (binary) 61,017 lines
The binary wires everything together; its arrows are left out to keep the picture readable. With JavaScript off, each crate links to its description below.

Every crate

What each crate does

telltale-net Network edge

Listeners: UDP worker threads, TCP, DNS over TLS and HTTPS, PROXY protocol v2.

One UDP socket per worker thread and listen address (SO_REUSEPORT), so cores never contend for a socket. TCP, DoT and DoH (HTTP/2) run as tokio tasks and call the same synchronous handler, so every transport behaves the same. Certificates reload without a restart; DoT SNI and the DoH path can carry a client ID. This is the only crate allowed to use unsafe (socket options and syscalls), each use with a SAFETY note.

Main types
UdpListener TcpServer DohServer QueryHandler CertStore ClientId
Uses
telltale-proto
Used by
telltale, telltale-filter
Libraries
bytes, h3, h3-quinn, http-body-util, hyper, hyper-util, libc, libmimalloc-sys, quinn, rustls, rustls-pki-types, socket2, tokio, tokio-rustls, tracing, webpki
Code
crates/telltale-net/src: 3,509 lines of Rust in 12 files

ADR-015 ADR-023 ADR-039 DNS-001 DNS-002

telltale-proto Data plane: answering a query

Zero-copy DNS wire parser and writer for the hot path.

Reads the header, question and EDNS straight from the packet buffer without allocating, fingerprints the name (xxh3) for the cache and filter, and writes answers in place: blocked answers, error answers with extended DNS errors, and TTL patches. Anything it can't parse becomes FORMERR or a drop, never a panic.

Main types
Query Header Edns ResponseBuilder Writer NameBuf
Budget
Reading a question and fingerprinting it: about 53 ns.
Uses
no other TelltaleDNS crate
Used by
telltale, telltale-cache, telltale-filter, telltale-net, telltale-policy, telltale-recursor, telltale-upstream
Libraries
thiserror, xxhash-rust
Code
crates/telltale-proto/src: 1,858 lines of Rust in 9 files

ADR-002 DNS-019

telltale-policy Data plane: answering a query

Who is asking, which groups they're in, and the early checks.

Identifies the client by address, MAC (from EDNS or the neighbour table), or client ID; resolves its groups (devices and network groups); applies allowed networks, the per-client token-bucket rate limit, pauses, special-use names, and answers local names. The tables are read-mostly behind ArcSwap, so lookups never take a lock.

Main types
ClientTable Identity RateLimiter LocalData Special Pause
Uses
telltale-config, telltale-proto
Used by
telltale
Libraries
arc-swap, parking_lot, xxhash-rust
Code
crates/telltale-policy/src: 2,851 lines of Rust in 8 files

ADR-014 ADR-021 ADR-050 FLT-005 FLT-006 DNS-010 DNS-014

telltale-filter Data plane: answering a query

Blocklists: fetch, parse, compile into a snapshot, and match.

Downloads lists with ETag and If-Modified-Since, parses hosts files, AdBlock syntax and Pi-hole regexes, and compiles everything off the DNS path into one compact snapshot: FSTs of names plus a regex DFA, with a hash index for query time. A new snapshot replaces the old one atomically. Every decision keeps its list and line, which is what 'Why?' and explain show.

Main types
Fetcher Snapshot Decision Explained CompileReport ListMeta
Budget
About 9 bytes per blocked name; a lookup takes under a microsecond.
Uses
telltale-config, telltale-net, telltale-proto
Used by
telltale
Libraries
blake3, bytes, fst, http, http-body-util, hyper, hyper-util, idna, rand, regex-automata, regex-syntax, rustls, serde, serde_json, thiserror, tokio, tokio-rustls, tracing, webpki-roots, xxhash-rust, zstd
Code
crates/telltale-filter/src: 8,159 lines of Rust in 19 files

ADR-003 ADR-016 ADR-017 ADR-018 ADR-020 ADR-022 ADR-024 ADR-025 FLT-003 FLT-013

telltale-cache Data plane: answering a query

Sharded S3-FIFO cache of finished wire responses.

Keeps each upstream reply as bytes plus the offsets of its TTLs, so a hit is a copy and a few small writes (the client's ID, case, and the aged TTLs). Negative answers and SERVFAIL are cached too. Concurrent misses for one name share one upstream request (singleflight); popular names are prefetched before they expire, expired answers can be served while upstreams are down, and the cache can be saved across restarts.

Main types
Cache CacheKey S3Fifo Lookup Singleflight
Budget
Copy and patch a cached answer: about 5 ns; a full cache hit end to end: about 153 ns.
Uses
telltale-proto
Used by
telltale
Libraries
parking_lot, tokio
Code
crates/telltale-cache/src: 1,855 lines of Rust in 5 files

ADR-002 DNS-006

telltale-upstream Data plane: answering a query

Upstream servers: transports, groups, strategies, hedging, breakers, DNSSEC.

Plain DNS, DoT and DoH upstreams with connection pools and bootstrap addresses; groups with failover, round-robin, weighted, fastest, or parallel strategies; routes that send a domain (optionally only for some groups) to a chosen server. A slow server is hedged after about 3x its usual time and a failing one is benched by a circuit breaker. DNSSEC validation uses hickory's validator over the same upstream groups.

Main types
Router Group Strategy Breaker Health Endpoint
Uses
telltale-config, telltale-proto, telltale-recursor
Used by
telltale
Libraries
bytes, crypto_box, ed25519-dalek, futures-util, h2, h3, h3-quinn, hickory-net, http, parking_lot, quinn, rand, ring, rustls, serde, thiserror, tokio, tokio-rustls, toml, tracing, webpki-roots
Code
crates/telltale-upstream/src: 7,795 lines of Rust in 18 files

ADR-013 ADR-060 UPS-007 DNS-011

telltale-recursor Data plane: answering a query

Reserved for full recursive resolution; empty today.

A placeholder for iterative resolution from the root (planned after 1.0). DNSSEC validation, which the plan once put here, lives in telltale-upstream instead (ADR-060).

Uses
telltale-proto
Used by
telltale-upstream
Libraries
parking_lot, rand, thiserror, tokio, tracing
Code
crates/telltale-recursor/src: 1,372 lines of Rust in 4 files

ADR-060

telltale-telemetry Telemetry and storage

QueryEvents, lock-free rings, the aggregator, rollups, and Prometheus.

Every answer leaves a fixed-size QueryEvent in its worker's single-producer ring; the hot path never waits on telemetry, and a full ring drops and counts. One aggregator thread turns events into counters, top-K lists, HDR latency histograms, per-device baselines for anomaly detection, the live tail, and the query log.

Main types
QueryEvent Aggregator Hub Engine Finding PromWriter
Uses
no other TelltaleDNS crate
Used by
telltale, telltale-api, telltale-store
Libraries
blake3, hdrhistogram, rtrb, serde
Code
crates/telltale-telemetry/src: 7,231 lines of Rust in 15 files

ADR-006 ADR-019 ADR-026 ADR-031 ADR-043 OBS-001 OBS-002 OBS-004 OBS-005 OBS-013

telltale-store Telemetry and storage

The columnar query-log store and SQLite state.

The query log is one file per hour: 19 columns in zstd-compressed blocks with a name dictionary and a per-hour name filter, so a search skips hours and columns it doesn't need. SQLite (WAL) holds users, tokens, the hash-chained audit log, rollups, and devices and names made in the UI.

Main types
Segment Reader Builder NameFilter Rollups AuditEntry
Budget
About 14 bytes per logged query.
Uses
telltale-telemetry
Used by
telltale, telltale-api
Libraries
blake3, memchr, regex-automata, rusqlite, serde, thiserror, xxhash-rust, zstd
Code
crates/telltale-store/src: 6,800 lines of Rust in 14 files

ADR-006 ADR-027 ADR-033 ADR-040 OBS-003

telltale-api Control plane

The REST API, sign-in, OpenAPI, live tail, MCP, and the embedded UI.

axum on its own listener (8053 by default). Browser sessions with CSRF tokens, scoped API and agent tokens, opt-in HTTP Basic for scrapers, and OIDC; three roles. The OpenAPI document is generated from the code (utoipa). Server-Sent Events stream the live tail, and POST /mcp serves read-only MCP tools that call the same routes with the caller's rights. The Svelte UI is built into the binary.

Main types
Auth AgentGrant Backend ApiDoc Mcp
Uses
telltale-store, telltale-telemetry
Used by
telltale
Libraries
argon2, axum, base64, blake3, futures-util, openidconnect, rand, rust-embed, serde, serde_json, tokio, totp-rs, tower, tracing, utoipa
Code
crates/telltale-api/src: 28,950 lines of Rust in 36 files

ADR-028 ADR-029 ADR-030 ADR-032 ADR-034 ADR-064 ADR-065 API-003 API-005 AGT-001 AGT-006

telltale-cluster Control plane

Membership, PKI, replication, failover, and federation over mTLS.

Every node has a certificate from the cluster CA; nodes talk over one HTTP/2 mTLS channel carrying protobuf frames (hello, heartbeats with RTT and config version, manifests, key shares, votes). The primary publishes signed manifests of content-addressed blobs; replicas fetch only what changed and keep answering from their last copy. Failover is manual or elected with fencing epochs; the CA can be rotated without downtime.

Main types
Cluster Identity ClusterManifest BlobStore Elector Heartbeat
Uses
no other TelltaleDNS crate
Used by
telltale
Libraries
blake3, bytes, http-body-util, hyper, hyper-util, prost, rand, rcgen, ring, rustix, rustls, rustls-pki-types, serde, serde_json, thiserror, time, tokio, tokio-rustls, tracing, x509-parser
Code
crates/telltale-cluster/src: 7,950 lines of Rust in 13 files

ADR-005 ADR-044 ADR-047 ADR-051 ADR-056 ADR-057 ADR-059 ADR-066 CLU-001 CLU-003 CLU-004 CLU-005 CLU-010

telltale-config Control plane

The TOML schema, validation, defaults, and shared vs node-local sections.

One serde schema for every setting, with errors that point at the exact key. It also prints the JSON Schema that the configuration reference on this site is built from, and decides which sections are shared across a cluster and which stay on each node.

Main types
Config ConfigError ClusterConfig FilterConfig GroupConfig
Uses
no other TelltaleDNS crate
Used by
telltale, telltale-filter, telltale-policy, telltale-upstream
Libraries
jiff, schemars, serde, serde_json, serde_path_to_error, thiserror, toml
Code
crates/telltale-config/src: 6,191 lines of Rust in 9 files

ADR-047 ADR-052 CLU-006 CLU-010

telltale-git Control plane

Reads one file at a ref from a Git server (smart-HTTP v2), no Git binary.

A small client for the Git smart-HTTP protocol v2: it asks for one ref, fetches only the objects for one file, inflates them, and can check commit signatures against allowed signers. This is how the cluster's configuration can come from a Git repository.

Main types
Request Fetched Commit AllowedSigners
Uses
no other TelltaleDNS crate
Used by
telltale
Libraries
base64, miniz_oxide, ring
Code
crates/telltale-git/src: 942 lines of Rust in 5 files

ADR-049

telltale The binary

The binary: wires every crate together, plus the CLI.

Owns the query pipeline that calls the crates above in order, the server roles, replication and the Git source, query-log shipping, the importers (Pi-hole, Technitium), backups, self-update and the update check, the MCP stdio bridge, and every command (run, explain, lists, auth, cluster, import, backup, mcp, self-update).

Main types
Pipeline
Uses
telltale-api, telltale-cache, telltale-cluster, telltale-config, telltale-filter, telltale-git, telltale-net, telltale-policy, telltale-proto, telltale-store, telltale-telemetry, telltale-upstream
Used by
nothing (it's the top)
Libraries
arc-swap, axum, base64, blake3, bytes, clap, dhat, http, hyper, hyper-util, mimalloc, minisign-verify, miniz_oxide, rand, rcgen, ring, rusqlite, rustls, serde, serde_json, socket2, tokio, tokio-rustls, toml, tracing, tracing-subscriber, webpki-roots, zstd
Code
crates/telltale/src: 61,017 lines of Rust in 78 files

ADR-001 ADR-038 ADR-046 ADR-061 ADR-062 ADR-063 OBS-001

The hot path

A query, stage by stage

The order in crates/telltale/src/pipeline.rs. Everything up to the cache is synchronous and allocation-free on the worker thread that received the packet; only a cache miss goes async. Open a stage for details.

  1. 1 Receive telltale-net

    A worker thread reads a batch of UDP packets from its own socket, or a TCP/DoT/DoH task reads one request.

    ADR-023 DNS-001

  2. 2 Parse telltale-proto

    Header, question and EDNS read in place. Malformed input becomes FORMERR or a drop.

    ADR-002 DNS-019

  3. 3 Early checks telltale-policy

    Allowed networks (never an open resolver), the per-client rate limit, our own loop tag, ANY (a tiny RFC 8482 answer), zone transfers (refused), and special-use names like localhost.

    ADR-014 DNS-014 DNS-019

  4. 4 Your names telltale-policy

    Names on your network are answered right here, before the cache or any upstream.

    ADR-041 ADR-042 DNS-010

  5. 5 Who's asking telltale-policy

    The client by address, MAC, or client ID, and its groups.

    ADR-021 ADR-050 FLT-005 FLT-006

  6. 6 Block or allow telltale-filter

    The groups' lists decide; a block is answered immediately and records the list and line.

    ADR-020 ADR-022 FLT-003 FLT-013

  7. 7 Route telltale-upstream

    Which upstream group: a route for this domain (optionally only for some groups), else the default. Private reverse lookups stay local.

    UPS-007

  8. 8 Cache telltale-cache

    A hit is copied and patched, CNAME targets are checked against the lists, and popular names are prefetched. A miss or an expired entry goes on.

    DNS-006

  9. 9 Ask upstream telltale-upstream

    Async: the group's strategy picks a server, a slow one is hedged, a failing one is benched. If every server fails, a stale answer is better than none.

    ADR-013

  10. 10 Validate telltale-upstream

    Optional DNSSEC validation; a bogus answer becomes SERVFAIL with an extended error, never a stale answer.

    ADR-060 DNS-011

  11. 11 Answer and record telltale-telemetry

    The reply is cached and sent, and a QueryEvent with per-stage timings goes into the worker's ring for the aggregator.

    ADR-026 OBS-001 OBS-002

Many nodes, one system

The cluster

Joining

telltale cluster init makes a CA; a join token pins it; the joining node gets a certificate. Only the primary's port has to be reachable.

ADR-044 CLU-001

Replication

The primary signs a manifest of BLAKE3-addressed blobs (config, compiled lists). Replicas fetch the blobs they lack, verify, and swap atomically.

ADR-047 CLU-003

DNS independence

Every node answers from its last applied snapshot, from disk at cold start, whatever the cluster's state.

CLU-004

Failover

Manual promote, or elected with three or more voters (a witness counts). Epochs fence an old primary out.

ADR-051 ADR-056 CLU-005

One management plane

Any node's UI reads the whole cluster (federated queries) and forwards changes to the primary, vouching for the user over mTLS.

ADR-053 ADR-054 CLU-002 CLU-007

Kubernetes pods

Resolver pods join with a shared bootstrap secret, ship their query log to the controller, and are swept when they disappear.

ADR-055 ADR-058 CLU-009

Upgrades and keys

Versions N and N-1 interoperate; certificates renew over the channel; the CA rotates in three member-gated phases.

ADR-057 ADR-059 ADR-066 CLU-010

Built for automation

Agents and MCP

Agent tokens

Scoped (analytics:read, config:write:clients, ops:pause, ...), deny by default per route, a reason header on every change, per-token rate limits, and a kill switch.

ADR-064 AGT-004 AGT-005 AGT-009

MCP server

POST /mcp on every node, plus telltale mcp --stdio for desktop clients. Tools call the REST routes in-process with the caller's credentials, so scopes apply inside tools too. Resources (cluster status, the configuration, a daily summary) and prompts for common questions. Assistants can sign in with the OIDC provider (OAuth 2.1, RFC 9728 metadata) instead of a token. Write tools make plans from the REST dry run; apply_plan replays them with If-Match, after an operator's approval when required.

ADR-065 ADR-010 ADR-070 ADR-071 AGT-006 AGT-007 AGT-008 AGT-010

Tech stack

What it's built with, and why

"Used in" comes from the crates' Cargo.toml files. Every dependency passes cargo-deny (licenses and advisories): no copyleft code.

Runtime and networking

  • tokio

    Async runtime for TCP, TLS, HTTP and upstream work; the UDP fast path runs on its own threads.

    used in binary api cache cluster filter net recursor upstream

  • socket2

    SO_REUSEPORT and other socket options for one socket per worker.

    used in binary net

  • rustls

    TLS for DoT, DoH, upstreams and the cluster channel, without OpenSSL.

    used in binary cluster filter net upstream

  • hyper / h2

    HTTP/2 for DoH and the cluster channel.

    used in binary cluster filter net

  • mimalloc

    A fast allocator for the control plane; the hot path barely allocates.

    used in binary

  • arc-swap

    Readers of the snapshot and policy tables never lock; a swap is one pointer store.

    used in binary policy

DNS and filtering

  • fst

    Finite-state transducers: millions of blocked names in a few bytes each.

    used in filter

  • regex-automata

    Compiles every regex rule into one DFA, checked once per name.

    used in filter store

  • xxhash-rust (xxh3)

    Name fingerprints for the cache key and the snapshot's hash index.

    used in filter policy proto store

  • hickory-net

    DNSSEC chain-of-trust validation: the long tail, not the hot path.

    used in upstream

  • idna

    Unicode domain names in lists, turned into their wire form.

    used in filter

Telemetry and storage

  • rtrb

    Wait-free single-producer rings from each worker to the aggregator.

    used in telemetry

  • hdrhistogram

    Latency percentiles with fixed memory.

    used in telemetry

  • zstd

    Compresses query-log blocks, blobs on the wire, and backups.

    used in binary filter store

  • rusqlite (bundled SQLite)

    Users, tokens, audit log, rollups; one file, WAL mode.

    used in binary store

  • blake3

    Content addresses for snapshot blobs and backup checksums.

    used in binary api cluster filter store telemetry

API, security, cluster

  • axum

    The REST API and the MCP endpoint.

    used in binary api

  • utoipa

    OpenAPI generated from the handlers, so the docs and UI types can't drift.

    used in api

  • argon2 + totp-rs

    Password hashing and authenticator-app codes.

    used in api

  • openidconnect

    Sign-in with Keycloak, Authentik, or any OIDC provider.

    used in api

  • rcgen + x509-parser

    The cluster CA and node certificates.

    used in binary cluster

  • prost

    Protobuf frames on the cluster channel, versioned for N/N-1.

    used in cluster

  • minisign-verify

    Checks release signatures in self-update and the update check.

    used in binary

  • rust-embed

    Builds the UI into the binary.

    used in api

  • clap

    The command line.

    used in binary

Web UI

  • Svelte 5 + Vite

    A small, compiled UI; the bundle budget is 400 KiB gzipped.

  • uPlot

    Fast canvas charts with a table view for each.

  • openapi-typescript

    TypeScript types generated from the API's OpenAPI document.

  • Playwright

    End-to-end tests against a real server, and the screenshots on this site.

Build, test, ship

  • GitHub Actions

    Clippy, tests, fuzzing, kind clusters, a Pi-hole and Technitium import, OIDC against Keycloak and Authentik, upgrade tests.

  • cargo-deny

    License and advisory checks on every dependency (no copyleft code).

  • cargo-fuzz

    Fuzzes the query and response parsers, list parsing, and query-log segments.

  • musl + FROM scratch

    A static binary in an image of about 9 MiB for amd64, arm64 and armv7.

  • Helm (OCI)

    The chart, published to ghcr.io on every build.

  • minisign

    Signed release binaries and a signed release index.

On disk and on the wire

Data formats

Filter snapshot

Per-group FSTs of names, a regex DFA, rule metadata (list and line), and a hash index; content-addressed blobs listed in a manifest. code

ADR-003 ADR-018 ADR-020

Cluster manifest

Version, epoch, config hash, blob hashes and CA bundle, signed by the primary; JSON over the mTLS channel. code

ADR-047 ADR-066

Query-log segment (.seg)

One file per hour: a header, a name dictionary and name filter, then zstd blocks of 19 columns, each block checksummed. code

ADR-027

QueryEvent

A fixed-size record per answer with per-stage timings in microseconds, written into byte rings. code

ADR-026

Backup (.ttbk)

A zstd-compressed tar with a manifest of BLAKE3 checksums, written owner-only; restore verifies everything first. code

ADR-063

Release index (releases.json)

Every published build per channel, signed with minisign; nodes trust it only after checking the signature. code

ADR-046

Measured, not hoped for

The 1.0 budgets

The release gates from the specification. Current measurements are on the Performance page.

MetricTarget
Cache-hit throughput, 4-core x86-64≥ 150k qps at < 1% loss
Cache-hit throughput, Raspberry Pi 4≥ 25k qps
Added latency for cache hit / blocked answer (p99, at 50% of max load)≤ 250 µs
Idle RSS, no lists≤ 20 MiB
RSS with 1M blocklist domains + 100k cache entries + telemetry on≤ 64 MiB
Compressed container image≤ 15 MiB per arch
List recompile (2M domains, Pi 4)≤ 8 s, with p99 query-latency regression ≤ 10% during compile
Cold start to serving (with compiled snapshot on disk)≤ 500 ms
Query-log search, 30 days / 50M rows, filtered by client + domain substring, Pi 4≤ 2 s
Config change propagation, primary → all reachable nodes (incl. cross-site Pi)≤ 5 s p95
DNS availability when the primary/controller is down100%: every replica keeps answering with its last snapshot
Primary failover (witness configured)Config writes available again ≤ 30 s
Telemetry loss under sustained max load0% for counters/histograms; query-log sampling only if explicitly configured, and drops always counted