DNS Request Routing: Handler, Upstream Resolver, and Cache
Files: handler.rs, upstream.rs, cache.rs, lib.rs
Crate: kinetic-dns | Stage: 10
Reading Time: 25 minutes
1. What Is This?
These four files form the structural backbone of the DNS server. Together they define the KineticDnsHandler struct, the packet dispatcher, the upstream forwarding, and the in-memory cache.
lib.rs: DefinesKineticDnsHandler— the central struct holding all shared state. It also spawns a background task to hot-reload the OS DNS config every 5 minutes.handler.rs: ImplementsRequestHandlerforKineticDnsHandler. This is the single function Hickory calls for every DNS packet. It classifies the query and routes it either toresolve_kinetic()orresolve_upstream().upstream.rs: Creates the upstream resolver (system DNS or Cloudflare DoH fallback) and executes upstream queries, translating results back into Hickory wire format.cache.rs: Configures the Moka cache with an asymmetric TTL policy — 5 minutes for positive hits, 30 seconds for NXDOMAIN.
2. Why Kinetic Needs This
Important
The critical design constraint is: the DNS server must handle
.kinqueries specially, but must pass everything else to the normal internet DNS infrastructure completely transparently.This is non-negotiable. If the Kinetic DNS server drops or mishandles any
.comor.orgquery, the user’s entire internet connectivity breaks. The dispatcher inhandler.rsis the gatekeeper that enforces this boundary.
The cache is equally critical for performance. DNS is called for every single network request a browser makes — loading a webpage can trigger dozens of DNS queries. Without caching, every one of those would hit the daemon’s HTTP API and then the P2P network. The Moka cache absorbs repeated queries and makes .kin resolution feel as fast as regular DNS.
3. How It Works
The KineticDnsHandler struct (lib.rs)
-> See: kinetic-dns/src/lib.rs — Lines 37-50
The struct holds:
api_url: String: The daemon’s HTTP API URL. Stored as a simple string — cheap to pass.http_client: reqwest::Client: A connection-pooled HTTP client.reqwest::Clientis internally anArc-wrapped pool, so.clone()is cheap and shares connections.resolver: Arc<RwLock<TokioAsyncResolver>>: The upstream DNS resolver, wrapped in a read-write lock so the background hot-reload task can atomically swap it out without dropping active queries.cache: Cache<String, Option<Vec<u8>>>: The Moka async cache. Keyed by apex domain string, values are raw payload bytes (orNonefor NXDOMAIN).atlas_tlds: Arc<RwLock<HashSet<String>>>: The set of foreign TLDs that kinetic-atlas has registered. Used in the router to know when to use the Atlas resolver instead of the internet resolver.atlas_resolver: Arc<RwLock<TokioAsyncResolver>>: A second upstream resolver that points specifically at the local kinetic-atlas bridge process.
The struct derives Clone — this is required by Hickory because it clones the handler for each request handling task.
Background hot-reload task (lib.rs)
-> See: kinetic-dns/src/lib.rs — Lines 73-85
KineticDnsHandler::new() spawns a tokio::spawn background task that runs every 5 minutes using tokio::time::interval.
Each iteration calls upstream::create_resolver() to read fresh OS DNS configuration and then acquires a write lock on resolver_clone to swap in the new resolver. This handles the case where the user’s network changes (e.g., switching from home Wi-Fi to a corporate VPN that uses a different DNS server).
The write lock ensures that no concurrent read query sees the resolver mid-swap. Once the lock is released, all subsequent reads see the new resolver atomically.
The packet dispatcher (handler.rs)
-> See: kinetic-dns/src/handler.rs — Lines 11-91
Every DNS UDP packet flows through handle_request(). The dispatch logic:
Step 1: Strip the trailing dot from the query name (DNS wire format always includes a trailing dot; comparing against string suffixes requires removing it).
Step 2: Check if the name ends with kinetic_core::constants::TLD_SUFFIX (i.e., .kin.). If yes, dispatch to resolve_kinetic().
Step 3: If not a .kin query, check if the apex TLD is in the Atlas TLD set (atlas_tlds). Atlas TLDs are domains from the traditional internet (e.g., .eth, .bit) that kinetic-atlas has bridged into the Kinetic namespace. These go to the atlas_resolver (pointing at the local bridge).
Step 4: Everything else goes to the main resolve_upstream() with the standard internet resolver.
The read lock on atlas_tlds is acquired briefly for the iteration. Even under heavy load, read locks don’t block each other, so this check is safe for concurrent requests.
Upstream resolution (upstream.rs)
create_resolver():
-> See: kinetic-dns/src/upstream.rs — Lines 17-26
Calls hickory_resolver::system_conf::read_system_conf() to parse /etc/resolv.conf (Linux/macOS) or the Windows Registry DNS configuration. If this fails, it falls back to ResolverConfig::cloudflare_https() — Cloudflare’s 1.1.1.1 DNS-over-HTTPS endpoint.
create_atlas_resolver(port: u16):
-> See: kinetic-dns/src/upstream.rs — Lines 29-46
Builds a resolver that points specifically to 127.0.0.1:<port> on both UDP and TCP. This is the port where the kinetic-atlas bridge daemon is listening.
resolve_upstream():
-> See: kinetic-dns/src/upstream.rs — Lines 51-100
Calls resolver.lookup(name, query_type) on the Hickory async resolver. On success, collects the returned Record iterator into a Vec<Record> and builds a Hickory response. On error, maps NoRecordsFound to NXDOMAIN and everything else to ServFail.
The asymmetric TTL cache (cache.rs)
-> See: kinetic-dns/src/cache.rs — Lines 1-60
The cache uses a custom KineticExpiry struct implementing Moka’s Expiry trait. The key method is expire_after_create():
- If the value is
Some(bytes)(positive hit), return a 5-minute duration. - If the value is
None(NXDOMAIN), return a 30-second duration.
This asymmetry is important: NXDOMAIN results must expire quickly because a new .kin domain might be registered at any moment. Positive results can be cached longer since registered domains change rarely.
The cache is configured with:
- Max capacity: 10MB total memory.
- Weigher function: Each entry weighs its byte length (or
1forNoneentries). This prevents a flood of tiny entries from exhausting the 10MB budget while also preventing one giant entry from evicting everything else.
The expire_after_read() method returns duration_until_expiry unchanged — TTL is not extended on reads. A record’s lifetime starts the moment it was fetched from the daemon, not the moment it was last accessed.
4. Key Pieces
impl RequestHandler for KineticDnsHandler
The Hickory trait implementation. This is the single entry point for all DNS packets. The function signature is:
#![allow(unused)]
fn main() {
async fn handle_request<R: ResponseHandler>(&self, request: &Request, response_handle: R) -> ResponseInfo
}
-> See: kinetic-dns/src/handler.rs — Lines 11-91
struct KineticExpiry
Implements moka::Expiry to control cache TTL per value type, not per key. The same cache can hold entries with completely different lifetimes based on whether they represent a found record or a NXDOMAIN result.
-> See: kinetic-dns/src/cache.rs — Lines 9-49
pub fn create_cache() -> Cache<String, Option<Vec<u8>>>
Builds the Moka cache with the custom expiry policy, 10MB memory ceiling, and a byte-accurate weigher function.
-> See: kinetic-dns/src/cache.rs — Lines 52-60
pub fn create_resolver() -> TokioAsyncResolver
The OS-aware resolver factory. Reads system DNS config first, falls back to Cloudflare if unavailable.
-> See: kinetic-dns/src/upstream.rs — Lines 17-26
5. Cross-Crate Connections
kinetic_core::constants::TLD_SUFFIX: The suffix string (e.g.,.kin) used to classify incoming queries.kinetic_core::types::normalize_name(): Called inhandler.rsbefore passing the name toresolve_kinetic(). Strips trailing dots and normalizes formatting.kinetic_core::types::extract_apex_name(): Extracts the root domain from a potentially subdomain-prefixed name (e.g.,www.saif.kin→saif.kin).- Hickory DNS (
hickory-server,hickory-resolver,hickory-proto): The underlying DNS library that handles UDP packet parsing, theRequestHandlertrait, and resolver machinery. - Moka (
moka::future::Cache): The async-safe, concurrent, TTL-based in-memory cache.
6. Quick Reference
| Component | Role |
|---|---|
KineticDnsHandler | Central state holder, Clone-able for concurrent requests |
handle_request() | Routes every packet to the correct resolver |
resolve_kinetic() | Handles .kin queries via DHT (in kinetic_records.rs) |
resolve_upstream() | Handles standard queries via OS/Cloudflare DNS |
create_resolver() | Reads OS DNS config or falls back to Cloudflare DoH |
create_atlas_resolver() | Points to local kinetic-atlas bridge |
KineticExpiry | 5-min TTL for positive hits, 30-sec for NXDOMAIN |
| Background reload task | Re-reads OS DNS config every 5 minutes |
7. Open Questions
- Atlas TLD registration: The
atlas_tldsset is initialized empty. The mechanism by which the kinetic-atlas bridge registers new TLDs at runtime is not shown in these files. Likely uses an HTTP endpoint that callsatlas_tlds.write(). - Cache invalidation across processes:
invalidate_cache()clears the in-process Moka cache, but the DNS server and daemon are separate processes. Cross-process invalidation would need IPC. - Concurrent resolver swap: The background hot-reload acquires a write lock on
resolver. If many queries are in flight and holding read locks, the write lock will block. Under heavy load, a 5-minute reload interval might cause brief resolution delays.
8. Rust Concepts
RwLock<T>(Read-Write Lock): Allows unlimited concurrent readers but only one exclusive writer. Used onresolverandatlas_tldsso hot-reload swaps don’t block incoming queries except during the instant of the swap itself.tokio::time::interval(Duration): Creates a recurring timer that fires at fixed intervals. Unlike a sleep loop, it accounts for the time spent executing each iteration to maintain consistent intervals.moka::Expirytrait: A custom hook into Moka’s eviction policy. By implementingexpire_after_create(), the cache can assign different TTLs to different values at insertion time.reqwest::Clientcloneability:reqwest::Clientwraps an internalArc-ed connection pool. Cloning it isO(1)and all clones share the same pool, preventing connection exhaustion.