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 Test Suite: Mock Daemon, Integration Tests, and Fuzzing

File: kinetic-dns/src/tests.rs Crate: kinetic-dns | Stage: 10 Reading Time: 30 minutes


1. What Is This?

This file contains the complete integration test suite for kinetic-dns. Unlike unit tests that call individual functions in isolation, these tests run the entire request handling pipeline end-to-end — from constructing a raw DNS query packet to asserting the correct DNS response code.

It uses a mock HTTP server (built with Axum) that simulates the kinetic-daemon’s REST API, and a MockResponseHandler that captures the DNS responses produced by KineticDnsHandler without needing an actual OS socket.

There are also property-based fuzz tests that throw random bytes and strings at the resolution pipeline to ensure it never panics.


2. Why Kinetic Needs This

Important

The DNS resolution pipeline is deeply stateful and security-critical. It must:

  • Never panic on malformed input from the network (the internet is adversarial).
  • Correctly map daemon API errors to the appropriate DNS response codes.
  • Correctly handle casing normalization (DNS is case-insensitive).
  • Correctly handle subdomain lookups, wildcard fallbacks, and wrong record type queries.

Integration tests are the only way to verify that the full pipeline behaves correctly end-to-end. Unit tests for individual helper functions would miss bugs that only appear when components interact.

The fuzz tests are essential because the daemon can receive data from untrusted P2P peers. A single panic in the DNS handler would kill the server and break all name resolution for the user.


3. How The Test Infrastructure Works

The Mock Daemon (start_mock_daemon())

-> See: kinetic-dns/src/tests.rs — Lines 71-108

The tests cannot depend on a real running kinetic-daemon. Instead, start_mock_daemon() builds a minimal Axum router with one route: GET /api/resolve/:domain.

The mock returns different responses based on the requested domain:

  • test1.kin: Returns a fully valid, signed Reveal containing a DnsZone with an A record pointing to 1.2.3.4. This is the “happy path” test fixture.
  • invalid-payload.kin: Returns raw garbage bytes ([0, 1, 2, 3]) as the HTTP body. Tests that the JSON parser fails gracefully.
  • invalid-zone.kin: Returns a valid signed Reveal, but the inner payload is not a valid DnsZone JSON. Tests that DnsZone::parse_payload() fails gracefully.
  • 500.kin: Returns HTTP 500. Tests that server errors map to ServFail.
  • Anything else: Returns HTTP 404. Tests that missing domains map to NXDOMAIN.

The mock server binds to 127.0.0.1:0 (OS-assigned random port) to avoid port conflicts. It returns the bound address so the test can point KineticDnsHandler at it.

The tokio::time::sleep(50ms) after spawning gives the Axum server time to become ready before the first test query fires.

The mock_reveal() Helper

-> See: kinetic-dns/src/tests.rs — Lines 43-69

This function constructs a fully valid, cryptographically signed Reveal (which contains the DNS payload).

It generates a real ML-DSA-65 keypair on every call, signs the signable_bytes() of the reveal, and includes the public key. This means the signature verification step in resolve_kinetic() will actually pass for test1.kin and invalid-zone.kin.

This is important because it tests the real verification code, not a mocked bypass.

The MockResponseHandler

-> See: kinetic-dns/src/tests.rs — Lines 16-41

Hickory’s RequestHandler returns a ResponseInfo struct and calls response_handle.send_response() to deliver the DNS response to the client. In tests, there is no real client socket to send to.

MockResponseHandler implements ResponseHandler by capturing the Message (DNS response packet) into a Vec protected by a tokio::sync::Mutex. Tests can then inspect what responses were generated.

The build_request() Helper

-> See: kinetic-dns/src/tests.rs — Lines 110-123

Creates a raw Hickory Request (what the DNS handler receives) from a domain name string and record type. The process:

  1. Constructs a DNS Message with one query.
  2. Serializes it to wire bytes using BinEncodable.
  3. Deserializes it back into a MessageRequest to simulate what Hickory would produce from a real UDP packet.
  4. Wraps it in a Request with a fake source address of 127.0.0.1:12345.

4. Test Coverage — What Each Test Verifies

Test 1: test_resolve_standard_domain

-> See: kinetic-dns/src/tests.rs — Lines 126-146

Sends a query for google.com. (not .kin). The handler must route this to resolve_upstream() using the internet DNS resolver.

The assertion allows either NoError (resolved successfully) or ServFail (no internet access in CI). This prevents test failures in sandboxed CI environments where outbound DNS is blocked.

Test 2: test_resolve_kin_success

-> See: kinetic-dns/src/tests.rs — Lines 149-173

The core happy path. Sends test1.kin. query for A record. The mock daemon returns a valid signed record with 1.2.3.4.

The entire pipeline runs:

  1. Cache miss → daemon API call.
  2. NameRecord deserialization.
  3. Signature verification (using the freshly generated ML-DSA key).
  4. DnsZone parsing.
  5. Subdomain mapping to @.
  6. SSRF check on 1.2.3.4 (passes — it’s a public IP).
  7. hickory A record construction and response.

Asserts NoError.

Test 3: test_resolve_kin_api_404_nxdomain

-> See: kinetic-dns/src/tests.rs — Lines 176-196

Sends missing.kin. which the mock daemon returns 404 for. Asserts the response code is NXDomain.

This verifies the HTTP 404 → DNS NXDOMAIN mapping in the daemon API call path inside resolve_kinetic().

Test 4: test_resolve_kin_api_500_servfail

-> See: kinetic-dns/src/tests.rs — Lines 199-219

Sends 500.kin. which the mock daemon returns HTTP 500 for. Asserts the response code is ServFail.

This verifies that unrecoverable daemon errors (database crashes, internal errors) correctly map to ServFail rather than incorrectly returning NXDOMAIN.

Test 5: test_resolve_kin_invalid_payload

-> See: kinetic-dns/src/tests.rs — Lines 222-242

Sends invalid-payload.kin.. The mock daemon returns raw bytes [0,1,2,3] that cannot be deserialized as JSON or as a NameRecord.

Asserts NXDomain. This tests the serde_json::from_slice::<NameRecord>() failure path — the code logs a warning and falls through to NXDOMAIN rather than panicking.

Test 6: test_resolve_kin_invalid_zone

-> See: kinetic-dns/src/tests.rs — Lines 245-265

Sends invalid-zone.kin.. The mock daemon returns a valid signed Reveal, but the inner payload is [1,2,3,4] which is not a valid DnsZone JSON.

This test verifies that signature verification passes (the Reveal is signed correctly) but DnsZone::parse_payload() fails gracefully and logs a warning rather than panicking.

Test 7: test_resolve_kin_subdomain_fallback

-> See: kinetic-dns/src/tests.rs — Lines 268-289

Sends www.test1.kin.. The test1.kin zone only has a @ (apex) record, no www record and no * wildcard.

Asserts NXDomain. This verifies that subdomain lookup falls through correctly when neither the exact subdomain nor a wildcard is present in the zone.

Test 8: test_resolve_kin_uppercase

-> See: kinetic-dns/src/tests.rs — Lines 292-313

Sends TEST1.KIN. in uppercase. DNS is case-insensitive by RFC, so this must resolve to the same record as test1.kin..

This tests the clean_name.to_lowercase() normalization in handler.rs.

Test 9: test_resolve_kin_wrong_record_type

-> See: kinetic-dns/src/tests.rs — Lines 316-336

Sends test1.kin. but queries for AAAA (IPv6). The zone only has an A (IPv4) record.

Asserts NXDomain. This verifies that the record type matching in the zone loop correctly skips A records when the client asked for AAAA.

Test 10: test_cache_invalidation

-> See: kinetic-dns/src/tests.rs — Lines 339-366

Tests that invalidate_cache() works correctly:

  1. Calls invalidate_cache("test1.kin") on an empty cache (no-op, should not panic).
  2. Queries test1.kin. — cache miss → API call → NoError.
  3. Calls invalidate_cache("test1.kin") again. The next query would hit the API again.

5. Fuzz Tests

doesnt_crash_on_random_payload_parsing

-> See: kinetic-dns/src/tests.rs — Lines 375-381

Uses proptest to generate completely random Vec<u8> and passes them directly to serde_json::from_slice::<Reveal>().

The DNS server receives raw bytes from the daemon which in turn receives them from the P2P network. A panic here would kill the server. The fuzz test runs hundreds of iterations to prove that no byte sequence can trigger a panic.

doesnt_crash_on_random_reveal_strings

-> See: kinetic-dns/src/tests.rs — Lines 383-390

Generates random valid UTF-8 strings (using proptest’s regex ".*") and passes them to DnsZone::parse_payload(). Similar goal: no UTF-8 string should be able to panic the zone parser.

doesnt_crash_on_random_domain_normalization

-> See: kinetic-dns/src/tests.rs — Lines 392-398

Generates random UTF-8 strings and passes them to normalize_name() and extract_apex_name(). Domain names in DNS can be malformed or contain non-ASCII bytes. These functions must never panic regardless of input.


6. Key Pieces

struct MockResponseHandler

Implements Hickory’s ResponseHandler trait to capture DNS responses in memory. The responses field holds Arc<Mutex<Vec<Message>>> — an atomically shared, async-locked vector. -> See: kinetic-dns/src/tests.rs — Lines 16-41

fn mock_reveal(name: &str, payload: Vec<u8>) -> Reveal

Generates a real cryptographic signature using the ML-DSA-65 algorithm. The key is freshly generated per call — different key per test run — ensuring signature verification tests are not hardcoded. -> See: kinetic-dns/src/tests.rs — Lines 43-69

async fn start_mock_daemon() -> String

Runs a real Axum HTTP server on a random port in a background Tokio task. Returns the base URL for KineticDnsHandler to use instead of the real daemon. -> See: kinetic-dns/src/tests.rs — Lines 71-108

async fn build_request(name: &str, rtype: RecordType) -> Request

Converts a domain name and query type into a real Hickory Request by round-tripping through DNS binary wire format. -> See: kinetic-dns/src/tests.rs — Lines 110-123


7. Cross-Crate Connections

  • kinetic_core::types::Reveal: The full signed record type that the daemon API returns. mock_reveal() constructs one with a real ML-DSA signature.
  • kinetic_core::types::DnsZone: The zone record embedded inside the reveal. start_mock_daemon() creates one with a simple A record.
  • kinetic_core::types::normalize_name() and extract_apex_name(): Fuzz tested directly.
  • Hickory DNS (hickory_proto, hickory_server): Used for request/response construction, BinEncodable, and the RequestHandler trait.
  • proptest: Property-based test framework used in the fuzzing submodule.

8. Quick Reference

TestWhat it verifies
test_resolve_standard_domainNon-.kin queries pass through to upstream
test_resolve_kin_successFull pipeline: cache miss → API → verify → SSRF → NoError
test_resolve_kin_api_404_nxdomainHTTP 404 maps to DNS NXDomain
test_resolve_kin_api_500_servfailHTTP 500 maps to DNS ServFail
test_resolve_kin_invalid_payloadGarbage bytes don’t panic, return NXDomain
test_resolve_kin_invalid_zoneInvalid zone JSON doesn’t panic, return NXDomain
test_resolve_kin_subdomain_fallbackMissing subdomain + no wildcard → NXDomain
test_resolve_kin_uppercaseUppercase domain names normalize correctly
test_resolve_kin_wrong_record_typeQuerying AAAA when only A exists → NXDomain
test_cache_invalidationinvalidate_cache() works without panic
Fuzz: doesnt_crash_on_random_payload_parsingRandom bytes never panic serde_json
Fuzz: doesnt_crash_on_random_reveal_stringsRandom UTF-8 never panics DnsZone::parse_payload
Fuzz: doesnt_crash_on_random_domain_normalizationRandom domains never panic normalization

9. Rust Concepts

  • #[tokio::test]: Marks an async function as a test case and wraps it in a single-threaded Tokio runtime. Required because the DNS handler and mock server use async I/O.
  • proptest! macro: Generates hundreds of random inputs bounded by a strategy (like any::<Vec<u8>>()). If any input panics, proptest reports the minimal failing case.
  • Arc<Mutex<Vec<Message>>> in test infrastructure: Shares the response collector between the handler (which calls send_response) and the test assertion (which reads responses). The Mutex is tokio::sync::Mutex (async) because send_response is async.
  • TcpListener::bind("127.0.0.1:0"): Binding to port 0 lets the OS choose a free port, avoiding hardcoded port conflicts between concurrent test runs.
  • BinEncodable / BinDecodable: Hickory’s DNS wire format serialization traits. Round-tripping through wire format is the only way to construct a MessageRequest, because the type has no public constructor.