Use the build scripts to run tests. They manage temp directories and cleanup automatically. These work on Linux, macOS, and Windows.
| Flag | What runs |
|---|---|
./build.sh -test |
Unit tests + integration tests + fuzz seed corpus |
./build.sh -testsmoke |
Smoke tests only (builds binary, requires network) |
./build.sh -teste2e |
End-to-end tests only (requires network) |
./build.sh -testall |
Smoke + unit + integration + fuzz seeds + e2e (full suite) |
./build.sh -coverage |
Unit tests with coverage report |
On Windows use .\build.ps1 with the same flags. On Windows or Linux,
build.cmd also accepts these flags.
Smoke tests build the DNSieve binary from source and run it as a real OS process. They verify binary startup, config file generation, graceful shutdown, and live query handling over plain DNS, DoH, and DoT.
Unlike e2e tests (which start the server in-process), smoke tests exercise the compiled binary end-to-end and are the appropriate check before shipping a release.
Requirements: a working internet connection (queries are forwarded to real upstreams) and a toolchain capable of building the binary.
./build.sh -testsmokeThe smoke suite is split across focused files:
| File | Contents |
|---|---|
smoke_startup_test.go |
Binary exists, --version flag, config generation, startup/shutdown, missing config exits non-zero |
smoke_dns_test.go |
Plain DNS UDP A/AAAA, NXDOMAIN, cache hit latency, multiple query types |
smoke_protocols_test.go |
DoH POST, DoH content type, DoT basic query, all-protocols combined |
smoke_service_test.go |
Service install with default config; service install with custom label and config dir |
smoke_helpers_test.go |
Shared helpers: port finder, wait, UDP query, config writer |
smoke_main_test.go |
TestMain: builds binary once into a temp dir (cleaned up on exit) |
./build.sh -testThis also runs integration tests and fuzz seed corpus. To run unit tests alone with the go tool:
go test ./..../build.sh -coverageThe coverage report is written to coverage.html. To run manually:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.outIntegration tests start a real in-process DNSieve server on loopback ports and send queries over the wire. No binary build is required. They require network access.
./build.sh -testOr directly:
go test -tags integration -v -count=1 -timeout 120s ./tests/integration/The integration suite is split across focused files:
| File | Contents |
|---|---|
integration_basic_test.go |
BasicQuery, IPv4 answer, AAAA, IPv6 answer, NXDOMAIN |
integration_block_test.go |
Blocked domain IPv4/IPv6, blocked responses never leak real IPs |
integration_cache_test.go |
Cache hit returns same answer within TTL |
integration_helpers_test.go |
Shared helpers: free port, start server, wait, query |
End-to-end tests spin up a real DNSieve server on loopback ports, send DNS queries over the wire, and verify protocol behaviour end-to-end. They require a working internet connection (the proxy resolves queries via the configured DoH upstreams).
./build.sh -teste2eOr directly:
go test -tags e2e -v -count=1 ./tests/e2e/The e2e suite is split across focused files:
| File | Contents |
|---|---|
e2e_protocol_test.go |
Plain DNS (UDP/TCP), DoH, DoT, all-protocols tests |
e2e_cache_block_test.go |
Cache behaviour, MinTTL, background refresh, block detection |
e2e_edns_test.go |
ECS, DNS Cookies, NSID, TCP keepalive |
e2e_rfc_test.go |
DDR (RFC 9461), RFC 9715, DNSSEC OK bit (RFC 3225), ANY (RFC 8482), DNAME (RFC 6672) |
e2e_complex_test.go |
Whitelist bypass, multi-EDNS, concurrency, performance |
e2e_rfc_security_additional_test.go |
CNAME chains, unknown qtypes, EDNS truncation, cookie robustness, security injection tests |
Provider tests verify that specific DNS providers correctly block known malicious domains.
go test -tags providers -v -count=1 ./tests/providers/RFC compliance tests verify adherence to DNS protocol standards. Tests are split per-RFC for clarity. They query live public resolvers and require network access.
go test -tags rfc -v -count=1 ./tests/rfc/Test files:
rfc1034_2181_foundation_test.go-- RFC 1034/2181 CNAME chain and RR-set TTL uniformityrfc1035_basic_dns_test.go-- Standard DNS (UDP, TCP, question echo, RA bit, NXDOMAIN)rfc3225_do_bit_dnssec_test.go-- DNSSEC OK (DO) bit handlingrfc3597_unknown_rr_test.go-- Handling of unknown DNS RR typesrfc4343_case_insensitivity_test.go-- DNS case insensitivityrfc4592_wildcards_test.go-- Wildcard query style handlingrfc5001_nsid_test.go-- Name Server Identifier (NSID)rfc5966_tcp_requirements_test.go-- TCP requirements (UDP truncation fallback)rfc6672_dname_synthesis_test.go-- DNAME redirection and synthesisrfc6891_edns0_test.go-- EDNS0 OPT record, version 0, buffer sizerfc7828_tcp_keepalive_test.go-- TCP keepaliverfc7858_dot_test.go-- DNS-over-TLS (DoT)rfc7871_ecs_test.go-- EDNS Client Subnet (ECS)rfc7873_dns_cookies_test.go-- DNS Cookiesrfc8482_any_query_test.go-- Minimal responses to ANY queriesrfc8484_doh_test.go-- DNS-over-HTTPS (DoH) POST, GET, ID=0, content typerfc8914_ede_test.go-- Extended DNS Errors (EDE)rfc9460_svcb_https_test.go-- SVCB/HTTPS record transportrfc9461_ddr_test.go-- Discovery of Designated Resolvers (DDR)rfc9715_udp_buffer_size_test.go-- UDP fragmentation avoidance
Fuzz tests exercise config parsing, DNS message handling, and wire format
parsing with random inputs. Run each test for a desired duration with -fuzztime.
# Config parsing
go test -fuzz FuzzConfigParse -fuzztime=60s ./internal/config/
# DNS wire format and block inspection
go test -fuzz FuzzInspectWireResponse -fuzztime=60s ./internal/dnsmsg/
# Cache TTL and renewal logic
go test -fuzz FuzzCacheRenewPercent -fuzztime=60s ./internal/cache/
go test -fuzz FuzzCacheKeys -fuzztime=60s ./internal/cache/
go test -fuzz FuzzCacheConcurrentRefresh -fuzztime=60s ./internal/cache/
# DNS query handler
go test -fuzz FuzzHandleQuery -fuzztime=60s ./internal/server/
go test -fuzz FuzzHandleQueryDomainNames -fuzztime=60s ./internal/server/
go test -fuzz FuzzHandleQueryWithCacheRefresh -fuzztime=60s ./internal/server/
go test -fuzz FuzzHandleQueryIPv6 -fuzztime=60s ./internal/server/
# DoH security and payload fuzzing
go test -fuzz FuzzReadDOHWireQueryPOSTLimit -fuzztime=60s ./internal/server/
go test -fuzz FuzzBuildQueryFromJSONParamsNoPanic -fuzztime=60s ./internal/server/
go test -fuzz FuzzDoHPayloadParsing -fuzztime=60s ./internal/server/
# EDNS0 middleware
go test -fuzz FuzzPrepareUpstreamQuery -fuzztime=60s ./internal/edns/
go test -fuzz FuzzProcessUpstreamResponse -fuzztime=60s ./internal/edns/
go test -fuzz FuzzSynthesizeDNAME -fuzztime=60s ./internal/edns/
# Upstream resolver and whitelist
go test -fuzz FuzzResolveWithMockResponses -fuzztime=60s ./internal/upstream/
go test -fuzz FuzzWhitelistIsWhitelisted -fuzztime=60s ./internal/upstream/The CI pipeline runs each fuzz target for 30 seconds automatically.
The unit tests cover both IPv4 and IPv6 query handling. Listener binding to IPv6 addresses is tested when the host supports IPv6 (tests are skipped automatically if IPv6 is unavailable):
go test -run TestServePlain_IPv6_UDPQuery -v ./internal/server/IPv6-specific unit tests include:
TestHandleQuery_IPv6_AAAA_Normal-- legitimate AAAA address not treated as blockedTestHandleQuery_IPv6_AAAA_Blocked_Unspecified--::treated as block signalTestHandleQuery_MixedIPv4IPv6-- A and AAAA cache entries are independentTestResolve_IPv6_AAAA_Normal-- resolver correctly classifies real IPv6 addressesTestResolve_IPv6_AAAA_Blocked-- resolver detects::as block signalTestResolve_IPv4_Blocked_ZeroAddr-- resolver detects0.0.0.0as block signalTestNewPlainClient_BareIPv6-- bare IPv6 address normalised to[addr]:53TestNewDoTClient_BareIPv6-- bare IPv6 address normalised to[addr]:853
GitHub-hosted runners do not have outbound IPv6 connectivity. The default bootstrap behaviour -- racing both an A and a AAAA lookup -- can therefore return an IPv6 address that is unreachable, causing every upstream DoH/DoT connection attempt to fail with "network is unreachable" and the proxy to return SERVFAIL.
The bootstrap_ip_family config option locks the bootstrap resolver to one
address family:
| Value | Behaviour |
|---|---|
"auto" |
Race A and AAAA; fastest answer wins (default) |
"ipv4" |
Send only A queries; IPv4 address always returned |
"ipv6" |
Send only AAAA queries; IPv6 address always returned |
Example TOML:
[upstream_settings]
bootstrap_ip_family = "ipv4"E2E tests that depend on live upstream responses use
startServerReachable instead of startServer. This helper tries three
strategies in order -- "auto", "ipv4", "ipv6" -- sending a health-check
query (example.com A) after each start. Trying "auto" first verifies that
the default code path works on the current runner. The first strategy that
returns a non-SERVFAIL response is used for the remainder of that test. If all
three strategies fail the test fails immediately (t.Fatal): a genuine network
outage or proxy bug is always visible, never silently skipped.
Integration tests use the same "auto" then "ipv4" then "ipv6" probe loop
inside startTestServer. A free port is acquired for each attempt; if the
upstream probe passes the server stays on that port for the duration of the
test.
Smoke tests detect IPv6 connectivity once in TestMain by attempting a
TCP connection to [2620:fe::fe]:53 (Quad9 IPv6). When unreachable, all
generated TOML configs include bootstrap_ip_family = "ipv4" automatically.
This approach is deterministic: there is no test skipping. A genuine upstream connectivity failure always produces a test failure.