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, signedRevealcontaining aDnsZonewith an A record pointing to1.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 signedReveal, but the inner payload is not a validDnsZoneJSON. Tests thatDnsZone::parse_payload()fails gracefully.500.kin: Returns HTTP 500. Tests that server errors map toServFail.- 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:
- Constructs a DNS
Messagewith one query. - Serializes it to wire bytes using
BinEncodable. - Deserializes it back into a
MessageRequestto simulate what Hickory would produce from a real UDP packet. - Wraps it in a
Requestwith a fake source address of127.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:
- Cache miss → daemon API call.
NameRecorddeserialization.- Signature verification (using the freshly generated ML-DSA key).
DnsZoneparsing.- Subdomain mapping to
@. - SSRF check on
1.2.3.4(passes — it’s a public IP). hickoryA 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:
- Calls
invalidate_cache("test1.kin")on an empty cache (no-op, should not panic). - Queries
test1.kin.— cache miss → API call →NoError. - 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 simpleArecord.kinetic_core::types::normalize_name()andextract_apex_name(): Fuzz tested directly.- Hickory DNS (
hickory_proto,hickory_server): Used for request/response construction,BinEncodable, and theRequestHandlertrait. proptest: Property-based test framework used in thefuzzingsubmodule.
8. Quick Reference
| Test | What it verifies |
|---|---|
test_resolve_standard_domain | Non-.kin queries pass through to upstream |
test_resolve_kin_success | Full pipeline: cache miss → API → verify → SSRF → NoError |
test_resolve_kin_api_404_nxdomain | HTTP 404 maps to DNS NXDomain |
test_resolve_kin_api_500_servfail | HTTP 500 maps to DNS ServFail |
test_resolve_kin_invalid_payload | Garbage bytes don’t panic, return NXDomain |
test_resolve_kin_invalid_zone | Invalid zone JSON doesn’t panic, return NXDomain |
test_resolve_kin_subdomain_fallback | Missing subdomain + no wildcard → NXDomain |
test_resolve_kin_uppercase | Uppercase domain names normalize correctly |
test_resolve_kin_wrong_record_type | Querying AAAA when only A exists → NXDomain |
test_cache_invalidation | invalidate_cache() works without panic |
Fuzz: doesnt_crash_on_random_payload_parsing | Random bytes never panic serde_json |
Fuzz: doesnt_crash_on_random_reveal_strings | Random UTF-8 never panics DnsZone::parse_payload |
Fuzz: doesnt_crash_on_random_domain_normalization | Random 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 (likeany::<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 callssend_response) and the test assertion (which readsresponses). TheMutexistokio::sync::Mutex(async) becausesend_responseis async.TcpListener::bind("127.0.0.1:0"): Binding to port0lets 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 aMessageRequest, because the type has no public constructor.