Numa DNS Resolver

repository·main·Indexed 23 days ago

https://github.com/razvandimescu/numa

A portable, single-binary DNS resolver written in Rust (v0.22.0). Numa features ad-blocking, local service naming with .numa domains, LAN service discovery via mDNS, and advanced privacy options including Oblivious DNS-over-HTTPS (ODoH), native DNS-over-TLS (DoT), and recursive resolution with DNSSEC validation. It can be deployed as a system service or via Docker, and includes a web-based dashboard and REST API for service configuration.

Tokens
23.5K
Snippets
45
Records
138
Agent score
79%

What's inside numa

  1. Configure DNS Resolution Modes

    main

    Numa supports three primary resolution modes, which can be configured in numa.toml:

    • forward (default): Acts as a transparent proxy to your existing system DNS. It adds caching and ad blocking while respecting captive portals, VPNs, and corporate DNS.
    • recursive: Resolves directly from root nameservers, removing upstream dependencies. You can enable full DNSSEC chain-of-trust validation by setting [dnssec] enabled = true.
    • auto: Probes root servers on startup. It uses recursive if reachable, otherwise falls back to encrypted DoH.
  2. Implement lazy TTL expiration in DNS caching

    main

    Instead of using a background thread to decrement TTLs, use a lazy expiration strategy.

    1. Store the original_ttl and the cached_at timestamp for each record.
    2. On lookup, calculate elapsed = current_time - cached_at.
    3. If elapsed >= original_ttl, the entry is stale; remove it from the cache and return None.
    4. If the entry is valid, return the packet but adjust the TTLs in the response to original_ttl - elapsed so clients receive the correct remaining time.
    pub fn lookup(&mut self, domain: &str, qtype: QueryType) -> Option<DnsPacket> {
        let key = (domain.to_lowercase(), qtype);
        let entry = self.entries.get(&key)?;
        let elapsed = entry.cached_at.elapsed().as_secs() as u32;
    
        if elapsed >= entry.original_ttl {
            self.entries.remove(&key);
            return None;
        }
    
        // Adjust TTLs in the response to reflect remaining time
        let mut packet = entry.packet.clone();
        for answer in &mut packet.answers {
            answer.set_ttl(entry.original_ttl.saturating_sub(elapsed));
        }
        Some(packet)
    }
  3. How NSEC and NSEC3 provide denial of existence proofs

    main

    To cryptographically prove that a domain name does not exist (NXDOMAIN), Numa uses NSEC or NSEC3 records.

    NSEC (Next Secure)

    NSEC records create a chain of existing names. An NSEC record defines a gap between two existing names (e.g., alphagamma). If a query for beta falls within that gap, it proves beta does not exist.

    • Ordering: Uses canonical DNS name ordering (RFC 4034), comparing labels right-to-left, case-insensitively.

    NSEC3

    NSEC3 prevents zone enumeration by hashing names (iterated SHA-1 with a salt) instead of using plain text. To prove non-existence, Numa performs a 3-part closest encloser proof (RFC 5155 §8.4):

    1. Find an ancestor whose hash matches an NSEC3 owner.
    2. Prove the next-closer name falls within a hash range gap.
    3. Prove the wildcard at the closest encloser also falls within a gap.

    Security Note: Numa caps NSEC3 iterations at 500 to prevent Denial of Service (DoS) attacks, as higher iteration counts increase CPU load for verification.

  4. Understand the Numa Resolution Pipeline

    main

    Numa operates as a forwarding resolver using a deterministic pipeline. Each incoming UDP packet triggers a task that walks through several stages. Each stage either provides a response (terminating the pipeline) or passes the query to the next stage.

    Pipeline Stages:

    1. Overrides: Matches specific queries to provide temporary responses (e.g., for debugging). These can be configured to auto-revert after a set number of minutes.
    2. .numa TLD: Handles the custom .numa top-level domain, providing reverse proxying and TLS.
    3. Blocklist: Checks if the domain is blocked. If blocked, it returns 0.0.0.0 (effectively making the 'ad gone').
    4. Zones: Matches queries against static records defined in configured zones.
    5. Cache: Checks for a cache hit. If found, it responds with a TTL-adjusted record.
    6. DoH (DNS-over-HTTPS): If no previous stage responds, the query is forwarded to an encrypted upstream DoH provider.
  5. How iterative DNS resolution works in Numa

    main

    Numa implements recursive resolution using an iterative approach. Instead of trusting an upstream provider, Numa starts at the root nameservers and follows the delegation chain (Root → TLD → Authoritative) by querying each level sequentially.

    The resolution loop in src/recursive.rs handles three primary outcomes for every query:

    1. Answer: The server provides the requested record. Numa caches this and returns it.
    2. Referral: The server delegates authority to another zone. Numa extracts the NS records and 'glue' records (A/AAAA records for the nameservers found in the additional section) to query the next server in the chain.
    3. NXDOMAIN/REFUSED: The name does not exist or the server refuses the query. Numa caches this negative result.

    Note on CNAMEs: If a query results in a CNAME (Canonical Name) redirect, Numa restarts the resolution process for the new name, capped at 8 levels of chasing to prevent infinite loops.

    resolve("cloudflare.com", A)
      → ask 198.41.0.4 (a.root-servers.net)
        ← "try .com: ns1.gtld-servers.net (192.5.6.30)"  [referral + glue]
      → ask 192.5.6.30 (ns1.gtld-servers.net)
        ← "try cloudflare: ns1.cloudflare.com (173.245.58.51)"  [referral + glue]
      → ask 173.245.58.51 (ns1.cloudflare.com)
        ← "104.16.132.229"  [answer]
  6. Understand DNS-over-TLS (DoT) in Numa

    main

    Numa implements RFC 7858 (DNS-over-TLS) on port 853. Unlike DNS-over-HTTPS (DoH), which wraps queries in HTTP/2, DoT uses DNS-over-TCP with a TLS layer.

    Key characteristics:

    • Wire Format: Uses a 2-byte length prefix followed by the DNS message.
    • Persistent Connections: Clients (like iOS, Android, and systemd) are encouraged to reuse the same TCP+TLS connection for multiple queries to avoid the 3-RTT handshake penalty.
    • Security (ALPN): Numa uses Application-Layer Protocol Negotiation (ALPN) to prevent cross-protocol attacks. The server advertises the "dot" protocol; clients offering other protocols (like "h2") are rejected during the TLS handshake.
  7. Configure DNS-over-HTTPS (DoH) Upstreams

    main

    Numa supports DNS-over-HTTPS (RFC 8484) via the Upstream enum. The selection between plain UDP and DoH is determined automatically by the URL scheme of the configured address:

    • DoH: Use a URL starting with https:// (e.g., https://dns.quad9.net/dns-query).
    • UDP: Use a standard socket address (e.g., 1.1.1.1:53).

    Important Note for DoH: Providers like Quad9 require HTTP/2. If you are implementing or extending DoH logic, ensure the underlying HTTP client (like reqwest) has the http2 feature enabled to avoid 400 Bad Request errors and to benefit from connection multiplexing.

    pub enum Upstream {
        Udp(SocketAddr),
        Doh { url: String, client: reqwest::Client },
    }
  8. How DNS label compression works

    main

    DNS uses label compression to reduce packet size by avoiding repeated domain names. Domain names are stored as a sequence of length-prefixed labels (e.g., example.com is [7]example[3]com[0]).

    To compress, DNS uses compression pointers: if the top two bits of a length byte are 11 (hex 0xC0), the remaining 14 bits represent an offset pointing back to a previous occurrence of the name within the packet.

    When implementing a parser, you must handle these jumps carefully: when following a pointer, you must advance the read position past the 2-byte pointer, but the actual label data is read from the offset location without advancing the main buffer position past the jump point.

  9. Understand the DNSSEC chain of trust

    main

    DNSSEC provides cryptographic proof for DNS records using a chain of trust. Numa validates this chain by verifying signatures (RRSIG) against a zone's DNSKEY, then verifying that DNSKEY against the parent zone's DS (Delegation Signer) record, continuing up to the root trust anchor.

    The Root Trust Anchor: Numa relies on a hardcoded public key for the IANA root KSK (Key Signing Key). This is the single point of out-of-band trust in the system. If IANA rolls this key, a Numa binary update is required.

    Key technical details:

    • Root KSK Key Tag: 20326
    • Algorithm: 8 (RSA/SHA-256)
    • Public Key Size: 256 bytes
    const ROOT_KSK_PUBLIC_KEY: &[u8] = &[ 
        0x03, 0x01, 0x00, 0x01, 0xac, 0xff, 0xb4, 0x09, 
        // ... 256 bytes total
    ];
    const ROOT_KSK_PUBLIC_KEY: &[u8] = &[ 
        0x03, 0x01, 0x00, 0x01, 0xac, 0xff, 0xb4, 0x09, 
        // ... 256 bytes total
    ];
  10. Accessing services via HTTP vs HTTPS on Tailscale

    main

    When using Numa to serve services over a Tailscale tailnet, you have two transport options:

    1. Plain HTTP (Port 80): This is the simplest method. Since Tailscale (WireGuard) encrypts all traffic end-to-end, plain HTTP is secure. No certificates or special profiles are required on client devices (like phones).
    2. HTTPS (Port 443): Numa can mint certificates from its own CA. However, this requires the client device to have the Numa CA installed and explicitly trusted in the device's certificate settings (e.g., the iOS 'Certificate Trust Settings' process).
  11. Privacy and security considerations for ODoH in Numa

    main

    When using Numa with ODoH, be aware of the following technical limitations and security models:

    • Target Visibility: ODoH moves trust but does not eliminate it; the target resolver can still see the DNS question, though it is unattributed to your IP.
    • Recursive Mode Egress: If the target operates in recursive mode, the subsequent walk to root/TLD/authoritative servers is plaintext UDP/TCP. Numa's recursive mode protects the client $\rightarrow$ target hop and validates against the IANA root key via DNSSEC.
    • Traffic Analysis: Small, self-hosted relays are susceptible to timing correlation attacks. Privacy scales with the volume of users on a relay (anonymity set).
    • Key Distribution: Numa fetches the target's HPKE configuration over HTTPS from the target's well-known endpoint. This relies on the security of the WebPKI.
    • DNSSEC: ODoH provides path encryption, while DNSSEC provides answer authenticity. Numa's recursive mode implements both.
  12. Understand Numa's DNS resolution modes

    main

    Numa operates in two primary resolution modes:

    1. Forward mode: Relays DNS queries to an upstream provider (such as Quad9, Cloudflare, or any DoH provider).
    2. Recursive mode: Performs full iterative resolution by walking the delegation chain from the root servers, including TLD and authoritative nameservers, with full DNSSEC validation.

    In both modes, Numa performs local processing including caching, ad blocking, domain overrides, and local service domain management before attempting to resolve the query.