Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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: Defines KineticDnsHandler — 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: Implements RequestHandler for KineticDnsHandler. This is the single function Hickory calls for every DNS packet. It classifies the query and routes it either to resolve_kinetic() or resolve_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 .kin queries 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 .com or .org query, the user’s entire internet connectivity breaks. The dispatcher in handler.rs is 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::Client is internally an Arc-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 (or None for 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 1 for None entries). 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 in handler.rs before passing the name to resolve_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.kinsaif.kin).
  • Hickory DNS (hickory-server, hickory-resolver, hickory-proto): The underlying DNS library that handles UDP packet parsing, the RequestHandler trait, and resolver machinery.
  • Moka (moka::future::Cache): The async-safe, concurrent, TTL-based in-memory cache.

6. Quick Reference

ComponentRole
KineticDnsHandlerCentral 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
KineticExpiry5-min TTL for positive hits, 30-sec for NXDOMAIN
Background reload taskRe-reads OS DNS config every 5 minutes

7. Open Questions

  • Atlas TLD registration: The atlas_tlds set 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 calls atlas_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 on resolver and atlas_tlds so 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::Expiry trait: A custom hook into Moka’s eviction policy. By implementing expire_after_create(), the cache can assign different TTLs to different values at insertion time.
  • reqwest::Client cloneability: reqwest::Client wraps an internal Arc-ed connection pool. Cloning it is O(1) and all clones share the same pool, preventing connection exhaustion.