Compare commits

23 Commits

Author SHA1 Message Date
4fd7a8305e nixpkgs: 2024-07-09 (23.11) -> 2024-08-14 (unstable) 2024-08-19 13:15:18 +00:00
3dd14db72b Cargo.lock: update
this will fix a compilation error in the 'time' crate for future versions of rustc
2024-08-19 13:13:48 +00:00
b4c553e79c Recursor: handle NS responses with a different type and no SOA
e.g. `dig m.wikipedia.org. NS` replies with `m.wikipedia.org. CNAME
dyna.wikimedia.org` and NO SOA record. the CNAME is worthless, but it's
evidence that the nameserver we queried is in fact authoritative for
`m.wikipedia.org.`.
2024-07-17 15:14:13 +00:00
2f6f1eab4a Recursor: ignore CNAME responses to NS queries; satisfy them with SOA instead
the recursor can now resolve api.mangadex.org
2024-07-17 09:39:05 +00:00
bcea3de9b0 NameServerPool: prefer to return a NoRecordsFound response over a truncated non-error UDP response 2024-07-17 09:32:39 +00:00
8d4d123d4f nixpkgs: 2024-06-24 -> 2024-07-09 2024-07-17 02:20:48 +00:00
70cc19bf67 nixpkgs -> latest 2024-06-26 12:00:35 +00:00
67649863fa recursor_test: backfill a test for CNAMEs which point to nonexistent records 2024-05-10 05:15:52 +00:00
338b35bc25 recursor_test: backfill a test which ensures the resolver is resilient to infinite cycles 2024-05-10 05:00:22 +00:00
6e251e348d recursor_test: backfill a test which follows CNAMEs through more than one layer 2024-05-10 04:51:58 +00:00
c669e3d397 recursor_test: backfill a test which follows CNAMEs across NS 2024-05-10 04:46:26 +00:00
999cdf4950 recursor_test: backfill a test which follows CNAMEs across zones 2024-05-10 04:33:31 +00:00
fd265a9ae4 recursor: fix to resolve most CNAMEs 2024-05-10 04:04:59 +00:00
c43bef87f9 recursor_test: backfill a CNAME test
there are more cname tests to add, but they would fail without code change :)
2024-05-09 06:59:54 +00:00
2aa98d0799 recursor_test: port to Catalog, and enable full recurse test
the SOA logic requires iana-servers and example.com to properly be in two separate zones, hence a Catalog
2024-05-08 08:33:28 +00:00
50e0653373 recursor_test: implement test_v4_domain 2024-05-08 01:01:56 +00:00
d95b4202b2 recursor_test: implement test_tld_txt text 2024-05-07 22:19:04 +00:00
9c6b064dba recursor_test: add a minimal recursor test 2024-05-07 22:09:14 +00:00
80f2a17bff recursor: make the test helpers more capable
they did not previously allow any way to mock DNS query sequences
in a manner compatible with the RecursorPool, which prefers to create
new NameServers itself, rather than via anything injectable by the test.
2024-05-05 20:07:46 +00:00
ec4e22817a recursor: define the bare minimum integration test 2024-04-29 19:39:47 +00:00
591a4a9fb2 reintroduce the recursor integration test
it was deleted during some cleanup work in 10d2ffcb04.
2024-04-29 14:36:08 +00:00
f6b7fc1287 svcb: fix build error
when running tests:
```
error[E0277]: the trait bound `Vec<u8>: From<&[u8; 5]>` is not satisfied
```
2024-04-29 14:33:56 +00:00
6e4af5c549 flake: init 2024-04-29 13:25:19 +00:00
481 changed files with 20109 additions and 40345 deletions

9
.github/CODEOWNERS vendored
View File

@@ -1,9 +0,0 @@
# who must review changes to this file
/.github/CODEOWNERS @bluejekyll @djc @marcus0x62
# default owners for the whole repository
* @bluejekyll @djc @marcus0x62
# Ferrous team can review changes to the E2E / integration tests
/tests @japaric @justahero @listochkin @pvdrz
/conformance @japaric @justahero @listochkin @pvdrz

View File

@@ -131,6 +131,9 @@ updates:
- dependency-name: h2
versions:
- 0.3.1
- dependency-name: env_logger
versions:
- 0.8.3
- dependency-name: idna
versions:
- 0.2.1

View File

@@ -1,49 +0,0 @@
name: conformance
on:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
jobs:
everything:
# host is irrelevant because everything will run in Docker containers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: clippy, rustfmt
- uses: extractions/setup-just@v2
- name: run test-framework tests
run: just conformance-framework
- name: run conformance tests against unbound
run: just conformance-unbound
- name: run conformance tests against BIND
run: just conformance-bind
- name: run conformance tests against hickory-dns
run: just conformance-hickory
- name: check that all the tests that now pass with hickory-dns are not marked as `#[ignore]`-d
run: just conformance-ignored
- name: run end-to-end tests that use the `dns-test` framework
run: just e2e-tests
- name: check public tests that use the `dns-test` framework
run: just ede-dot-com-check
- name: lint code
run: just conformance-clippy
- name: check that code is formatted
run: just conformance-fmt

View File

@@ -6,17 +6,10 @@ on:
- main
- release/**
- "*_dev"
# does not work properly due to an issue in GitHub's branch protection
# rules: https://github.com/orgs/community/discussions/13690
# paths-ignore:
# - "conformance/**"
pull_request:
branches:
- main
- release/**
# paths-ignore:
# - "conformance/**"
merge_group:
schedule:
- cron: "0 3 * * 4"
@@ -28,12 +21,10 @@ jobs:
platform-matrix:
name: platform
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
env:
RUST_LOG: info,hickory_proto::udp::udp_stream=trace # Extra logging for issue #2649
#os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
@@ -48,35 +39,13 @@ jobs:
version: ${{ env.CARGO_WS_VERSION }}
- name: just all-features
if: matrix.os != 'windows-latest'
run: just all-features
- name: just windows-features
if: matrix.os == 'windows-latest'
run: just windows-features
- name: just test-docs
if: ${{ !cancelled() && matrix.os != 'windows-latest' }} # uses all features, avoid openssl
run: just test-docs
wasm:
name: wasm
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-wasip1
- name: check
run: cargo check -p hickory-proto --target wasm32-wasip1 --no-default-features
## Measure test coverage, only on linux.
## Run all default oriented feature sets across all platforms.
code-coverage:
name: coverage
needs: platform-matrix
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
@@ -95,19 +64,21 @@ jobs:
run: just coverage
- name: upload coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
#files: coverage/hickory-dns-coverage.json
#files: target/llvm-cov-target/hickory-dns-coverage.json
## Work through all of the variations of the different features, only on linux to save concurrent resources
exhaustive-features-matrix:
name: exhaustive
# Let's wait for the all-features cache
needs: platform-matrix
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
# feature: [default-features, no-default-features, dns-over-rustls, dns-over-https-rustls, dns-over-native-tls, dns-over-openssl, dnssec-openssl, dnssec-ring, mdns, async-std]
feature:
[
default,
@@ -117,7 +88,6 @@ jobs:
dns-over-quic,
dns-over-h3,
dns-over-native-tls,
dns-over-openssl,
dnssec-openssl,
dnssec-ring,
doc,
@@ -138,40 +108,16 @@ jobs:
- name: just
run: just ${{ matrix.feature }}
check-all-features-matrix:
name: cargo-all-features
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
chunk:
- 1
- 2
- 3
- 4
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: cargo install cargo-all-features
uses: baptiste0928/cargo-install@v3
with:
crate: cargo-all-features
version: 1.10.0
- name: cargo check-all-features
run: cargo check-all-features --n-chunks 4 --chunk ${{ matrix.chunk }}
## Check past and future versions
## this enforces the minimum version of rust this project works with
past-future-matrix:
name: past-future
# Let's wait for the all-features cache
needs: platform-matrix
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
version: ["1.71.1", beta, "nightly-2024-05-23"]
version: ["1.67.0", beta, nightly]
steps:
- uses: actions/checkout@v4
@@ -191,14 +137,13 @@ jobs:
run: just no-default-features
- name: just build-bench
if: contains( matrix.version, 'nightly' )
if: matrix.version == 'nightly'
run: just build-bench
## Execute the clippy checks
cleanliness:
name: cleanliness
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
@@ -231,18 +176,17 @@ jobs:
run: just clippy
# Rustfmt
- name: just fmt
if: ${{ !cancelled() }}
run: just fmt
# Audit
- name: cargo audit
if: ${{ !cancelled() }}
run: just audit
# Build and run bind to test our compatibility with standard name servers
compatibility:
name: compatibility
# wait for the cache from all-features
needs: platform-matrix
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4

4
.gitignore vendored
View File

@@ -24,7 +24,5 @@ rls/**
# ignore fuzzing corpus
fuzz/artifacts/**
fuzz/corpus/**
fuzz/Cargo.lock
*~
# Code coverage output
/coverage

View File

@@ -5,313 +5,6 @@ This project adheres to [Semantic Versioning](https://semver.org/).
All notes should be prepended with the location of the change, e.g. `(proto)` or `(resolver)`.
## 0.25.0-alpha.4
* ci: stop blocking on the platform matrix jobs by @djc in https://github.com/hickory-dns/hickory-dns/pull/2563
* feat: Implement Round Robin server selection for DNS lookups by @hingbong in https://github.com/hickory-dns/hickory-dns/pull/2557
* resolver: make ForwarderAuthority generic by @Stormshield-robinc in https://github.com/hickory-dns/hickory-dns/pull/2568
* Replace DnsResponse::new() constructor by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2573
* fix key tag collision issue in zone signer by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2556
* CI: Check hickory-proto with WASI preview 1 by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2575
* resolver NameServerPool tweaks by @djc in https://github.com/hickory-dns/hickory-dns/pull/2567
* More modularization of DNSSEC crypto code by @djc in https://github.com/hickory-dns/hickory-dns/pull/2566
* Start untangling rustls ClientConfig setup by @djc in https://github.com/hickory-dns/hickory-dns/pull/2569
* dns: improve error message when Cargo feature is missing by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2580
* Fix dns-over-openssl by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2584
* util: Fix building with rustls_native_certs only by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2585
* Switch DnsLru from lru-cache to moka by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2576
* Clean up references to AsyncClient by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2588
* Dnssec insecure delegations by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2546
* Extend dig timeout in bad referral tests by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2594
* server: Fix compilation of recursor authority without DNSSEC by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2586
* Add cargo-all-features configuration by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2587
* Remove redundant cache insert in resolve_cnames() by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2596
* Update default logging filter to match all crates by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2597
* Update READMEs by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2591
* Run cargo check-all-features in CI by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2592
* Coverage improvements by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2599
* Update url in fuzzer lockfile by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2607
* Box the query in ProtoErrorKind::Nsec by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2610
* Update semver-compatible dependencies & bump MSRV by @djc in https://github.com/hickory-dns/hickory-dns/pull/2617
* Redirect output of command to clean up test output by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2611
* Don't send DAU/DHU options in responses by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2614
* Conformance: tests for handling of TC=1 responses by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2609
* build(deps): bump codecov/codecov-action from 4 to 5 by @dependabot in https://github.com/hickory-dns/hickory-dns/pull/2582
* Simplify PublicKey trait by @djc in https://github.com/hickory-dns/hickory-dns/pull/2616
* bump idna to 1.0 and url to 2.5 by @zh-jq in https://github.com/hickory-dns/hickory-dns/pull/2564
* Document suggested rust-analyzer configuration by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2606
* Drop privileges on Unix-family platforms by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2598
* proto: apply timeout to TLS/QUIC/H3 handshake phase by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2583
* Update authority documentation by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2623
* Remove mention of Subject Public Key Info in docs by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2624
* Update copied documentation by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2625
* Don't implicitly enable DNSSEC when DoT is enabled by @djc in https://github.com/hickory-dns/hickory-dns/pull/2615
* Timeout tweaks by @djc in https://github.com/hickory-dns/hickory-dns/pull/2620
* accept idle timeouts for TLS and HTTPS futures by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2622
## 0.25.0-alpha.3
* util: add a newline between records in resolve report by @bluejekyll in https://github.com/hickory-dns/hickory-dns/pull/2347
* proto: make time dependency optional by @djc in https://github.com/hickory-dns/hickory-dns/pull/2349
* Manifest cleanup by @djc in https://github.com/hickory-dns/hickory-dns/pull/2351
* Add marcus0x62 as a code owner by @djc in https://github.com/hickory-dns/hickory-dns/pull/2350
* fix the bad label compression from original query by @bluejekyll in https://github.com/hickory-dns/hickory-dns/pull/2352
* Remove mentions of Makefile.toml in CONTRIBUTING.md by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2356
* Fix extra length prefix in unknown SVCB parameter by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2354
* Fix copied comment by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2355
* Explicitly limit H2 support to rustls by @djc in https://github.com/hickory-dns/hickory-dns/pull/2366
* Fix panic in NSEC3 hash function by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2368
* Make AsyncResolver take hosts file into account by @hch12907 in https://github.com/hickory-dns/hickory-dns/pull/2149
* Update dependencies by @djc in https://github.com/hickory-dns/hickory-dns/pull/2374
* Fix warning in Dockerfile by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2370
* Fix stdout handling during hickory startup by @justahero in https://github.com/hickory-dns/hickory-dns/pull/2361
* Increase validation log level by @justahero in https://github.com/hickory-dns/hickory-dns/pull/2360
* Revert "Fix stdout handling during hickory startup" by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2376
* ci: trigger workflows for merge queue branches by @djc in https://github.com/hickory-dns/hickory-dns/pull/2378
* Fix CAA parameter value validation by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2373
* Trivariant LookupControlFlow type to allow authorities to decline to respond to a query by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2160
* Conformance test cleanup by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2371
* use a "test" TLD in conformance tests by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2359
* Strict parsing of configuration files by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2375
* Use container names, not IDs, in "explore" example by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2372
* trust_anchor::Parser: accept records without TTL field by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2384
* dns-test: add helper to pause and inspect a unit test's containers by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2362
* dns-test: write logs to file by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2377
* Change domain name used in 'explore' example by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2386
* Fix Issue #2306 / infinite recursion in ns_pool_for_zone by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2332
* dns-test: bump unbound to 1.21.0 by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2387
* [RFC] (temporarily) add tests that rely on public DNS infrastructure by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2385
* DNSSEC validation fixes by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2392
* proto: simplify verify_dnskey_rrset() by @djc in https://github.com/hickory-dns/hickory-dns/pull/2397
* proto: simplify verify_dnskey_rrset() some more by @djc in https://github.com/hickory-dns/hickory-dns/pull/2398
* proto: replace Borrow<Name> impl for LowerName with Deref by @djc in https://github.com/hickory-dns/hickory-dns/pull/2394
* Add support for CNAME records to dns-test by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2338
* Add "do not query" configuration to recursor by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2369
* conformance: test resolver with query about unsigned zone by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2380
* dns-test: parse multiple EDE codes by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2381
* CI: fix conformance tests by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2405
* Forwarder: fix NXDOMAIN status code and allow it to forward SOA records by @hch12907 in https://github.com/hickory-dns/hickory-dns/pull/2379
* dnssec: validate DS records by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2396
* build(deps): bump rustls-native-certs from 0.7.2 to 0.7.3 by @dependabot in https://github.com/hickory-dns/hickory-dns/pull/2407
* ensure DNSKEY is validated with a KSK by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2399
* Add method and test cases to randomize ASCII alpha case in Name labels by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2403
* conformance/dns: add bad referral scenarios by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2410
* conformance: test against deprecated algorithms by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2413
* Add NSEC3 support to `hickory-server` by @pvdrz in https://github.com/hickory-dns/hickory-dns/pull/2391
* Fix semantic merge conflict by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2414
* Use u32 internally when randomizing case of labels by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2416
* Store invalid CAA property value as Value::Unknown by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2418
* Encode and decode CAA issuer name as ASCII only by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2419
* Ignore escaped dots when determining FQDN status by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2420
* conformance: DS of child's ZSK in parent zone by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2409
* fix key tag calculation in dns-test and semantic merge conflict in conformance test by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2427
* conformance: use `push_label` API and update variable names by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2408
* Fix corruption of signature expiration in flaky test by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2426
* Update semver-compatible dependencies by @djc in https://github.com/hickory-dns/hickory-dns/pull/2442
* Clean up server features by @djc in https://github.com/hickory-dns/hickory-dns/pull/2441
* Clean other target directories in `just clean` by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2446
* Stop using pseudo-TTYs with Docker by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2439
* dnssec: report Insecure outcome as NOERROR+AD=0 by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2438
* CAA: Preserve reserved flags by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2434
* NSEC3 validation by @listochkin in https://github.com/hickory-dns/hickory-dns/pull/2313
* Fix dns-over-openssl compilation and CI coverage by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2449
* Unify integration tests by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2448
* treat zone as Insecure if all DNSKEY algorithms are unsupported by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2443
* Minor recursor tweaks by @djc in https://github.com/hickory-dns/hickory-dns/pull/2450
* Set up tracing subscriber in various tests by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2453
* CNAME resolution support for the recursor. by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2339
* Extract tracing-subscriber setup to new crate by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2454
* implement rfc4398 CERT record type by @zsdsys in https://github.com/hickory-dns/hickory-dns/pull/2417
* SignSettings: rm use_dnssec field by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2451
* build(deps): bump enum-as-inner from 0.6.0 to 0.6.1 by @dependabot in https://github.com/hickory-dns/hickory-dns/pull/2460
* Update semver-compatible dependencies by @djc in https://github.com/hickory-dns/hickory-dns/pull/2463
* Kill quad9 tests by @djc in https://github.com/hickory-dns/hickory-dns/pull/2467
* Move RuntimeProvider into proto by @djc in https://github.com/hickory-dns/hickory-dns/pull/2464
* Catalog cleanup in preparation for the chained authority. by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2461
* add hickory-server to the info and debug log line configs by @bluejekyll in https://github.com/hickory-dns/hickory-dns/pull/2469
* server: avoid wrapping Arc in Box by @djc in https://github.com/hickory-dns/hickory-dns/pull/2471
* conformance: unsigned leaf zone; other zones use NSEC by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2436
* Regenerate test certificates by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2475
* proto: use RuntimeProvider to connect TCP by @djc in https://github.com/hickory-dns/hickory-dns/pull/2472
* proto: change RecordSet::new() to take owned Name by @djc in https://github.com/hickory-dns/hickory-dns/pull/2473
* fix compilation failed by @hingbong in https://github.com/hickory-dns/hickory-dns/pull/2476
* Allow changing URI paths for DNS-over-HTTPS by @hch12907 in https://github.com/hickory-dns/hickory-dns/pull/2470
* conformance: add NSEC & NSEC3 tests by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2437
* Chained authority implementation by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2161
* Remove unneeded vecs in ForwardNSData and wrap in an Arc by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2482
* Config tweaks by @djc in https://github.com/hickory-dns/hickory-dns/pull/2480
* Fix two issues with the config integration test by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2484
* Listen on IPv6 by default by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2478
* build(deps): bump once_cell from 1.19.0 to 1.20.1 by @dependabot in https://github.com/hickory-dns/hickory-dns/pull/2483
* Remove workaround in clippy justfile target by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2485
* Move StoreConfig to bin crate by @djc in https://github.com/hickory-dns/hickory-dns/pull/2486
* Update windows.rs to use crate::proto::xfer::Protocol by @zsdsys in https://github.com/hickory-dns/hickory-dns/pull/2488
* Add resolver/recursor configuration to avoid udp ports by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2487
* Disable client_tests::test_nsec3_query_name_is_soa_name by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2492
* Take advantage of match ergonomics by @djc in https://github.com/hickory-dns/hickory-dns/pull/2490
* Blocklist authority by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2162
* Update deps (futures-util and once_cell) by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2496
* Use custom serde visitor to fix store error messages by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2495
* Update semver-compatible dependencies by @djc in https://github.com/hickory-dns/hickory-dns/pull/2497
* Docs: resolver no longer returns background future by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2504
* proto: leverage simpler PEM reading API by @djc in https://github.com/hickory-dns/hickory-dns/pull/2505
* Update the NSEC/NSEC3 Truth Table to correctly log responses with NSEC and NSEC3 records by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2506
* Switch to using doc_auto_cfg by @djc in https://github.com/hickory-dns/hickory-dns/pull/2507
* State that `hickory-server` supports NSEC3 by @pvdrz in https://github.com/hickory-dns/hickory-dns/pull/2512
* Resolver cleanups by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2513
* Propagate NX domain and no record found errors by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2502
* Replace TryParseIp trait with IntoName::to_ip() by @djc in https://github.com/hickory-dns/hickory-dns/pull/2509
* Clean up rustdoc warnings by @djc in https://github.com/hickory-dns/hickory-dns/pull/2508
* DNSSEC tweaks by @djc in https://github.com/hickory-dns/hickory-dns/pull/2517
* Kill the sync Resolver by @djc in https://github.com/hickory-dns/hickory-dns/pull/2515
* Use async client for sig0 compatibility tests by @djc in https://github.com/hickory-dns/hickory-dns/pull/2518
* Add justfile target to export lcov file by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2520
* client: remove synchronous API by @djc in https://github.com/hickory-dns/hickory-dns/pull/2521
* tests: restore shorter timeout window in test by @djc in https://github.com/hickory-dns/hickory-dns/pull/2528
* Conformance dnslib support by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2523
* Conformance support for multiple zones on a nameserver by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2525
* Simplify socket address literals by @djc in https://github.com/hickory-dns/hickory-dns/pull/2527
* Add caching policy configuration to recursor by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2524
* Recursor builder tweaks by @djc in https://github.com/hickory-dns/hickory-dns/pull/2529
* Add resource limits for DNSKEY, DS, and RRSIG validation by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2533
* conformance: zone that lacks DS in parent zone by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2388
* Skip copying configuration file for dnslib by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2537
* Add resolver logging to bad_txid test by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2536
* Recursor recursion improvements by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2522
* CI: Remove continue-on-error from steps by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2538
* Conformance dig timeout by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2540
* Make error modules private by @djc in https://github.com/hickory-dns/hickory-dns/pull/2530
* Start cleaning up DNSSEC API by @djc in https://github.com/hickory-dns/hickory-dns/pull/2534
* ci: don't build benchmarks, only check them by @djc in https://github.com/hickory-dns/hickory-dns/pull/2542
* Fix NSEC3 validation bug for covering records by @pvdrz in https://github.com/hickory-dns/hickory-dns/pull/2543
* justfile: fix the conformance-ignored task by @japaric in https://github.com/hickory-dns/hickory-dns/pull/2535
* Clarify `KeyPair` type by @djc in https://github.com/hickory-dns/hickory-dns/pull/2541
* Recursor CNAME resource limit improvements by @marcus0x62 in https://github.com/hickory-dns/hickory-dns/pull/2531
* Update hashbrown by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2547
* Check in fuzzer target lock file by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2552
* Recursor: Create DnsResponse with consistent buffer by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2553
* proto: encode EDNS flags in a separate type by @djc in https://github.com/hickory-dns/hickory-dns/pull/2549
* fix windows build, ResolveError changed to ResolveResult by @zsdsys in https://github.com/hickory-dns/hickory-dns/pull/2548
* Recursor: set RD=0 in queries to nameservers by @divergentdave in https://github.com/hickory-dns/hickory-dns/pull/2551
* recursor: use async/await for RecursorDnsHandle implementation by @djc in https://github.com/hickory-dns/hickory-dns/pull/2554
## 0.25.0
### Fixed
- (build) Suppress implicit features from optional dependencies #2337 by djc
- (recursor) Fix SOA referrals #2331 by marcus0x62
- (all) Update OpenSSL to fix security issue #2316 by justahero
- (recursor) fix DNSSEC validation of NS somedomain.com. #2300 by japaric
- (recursor) DnssecDnsHandle: do not recurse infinitely when query DS . fails #2271 by japaric
- (recursor) answer with SERVFAIL when DNSSEC validation fails #2286 by japaric
- (tests) Assert status for every NSEC3 test #2254 by pvdrz
- (tests) dns-test: make unit tests use the checked out version of this repo #2268 by japaric
- (tests) just: warn when the index is dirty and DNS_TEST_SUBJECT=hickory #2267 by japaric
- (recursor) strip dnssec records on cache hit #2245 by japaric
- (build) make just to compile bind #2248 by sabify
- (recursor) send DS queries to the parent zone #2203 by japaric
- (docs) add RFC2931 SIG(0) as supported #2216 by bluejekyll
- (recursor) respect DO bit in incoming queries #2196 by japaric
- (docs) doc: fix misc typos in md files #2198 by divagant-martian
- (test) update ip of example.com #2187 by situ2001
- (all) Update mio to 0.8.11 to fix RUSTSEC-2024-0019 #2166 by marcus0x62
- (proto) Fix formatting issue in crates/proto/src/op/message.rs #2165 by marcus0x62
- (proto) fix internal representation of OPT #2151 by esensar
- (proto) ECH service parameter key corrected from "echconfig" to "ech" #2183 by cpu
- (proto) SVCB/HTTPS record parsing fixes (quoted values, arbitrary numeric keys, lists containing delim) #2183 by cpu
### Changed
- (resolver) only retry I/O errors over TCP #2336 by lrouquette
- (proto) Simplify TBS construction API #2335 by djc
- (recursor) take is_subzone() arguments as &Name #2334 by djc
- (proto) Use SerialNumber type for signature timestamps #2318 by justahero
- (recursor) Improve recursor logic by eliminating redundant NS requests and adding recursor support for NS referrals. #2325 by marcus0x62
- (resolver) Return error when no nameservers in resolv.conf #2327 by dav1do
- (resolver) Make QuicSocketBinder as public as RuntimeProvider #2328 by mokeyish
- (resolver) Make sure Lookup futures are Sync #2326 by djc
- (server) leave query/opt in truncated msg #2307 by leshow
- (tests) justfile: use --locked to stick with Cargo.lock dependencies #2323 by djc
- (proto) Allow to modify a RRSIG record before signing #2315 by justahero
- (all) Bump MSRV to 1.70 #2322 by djc
- (recursor) Adjust TTL of RRSIG + RR during validation #2311 by justahero
- (resolver) avoid moving self in read_hosts_confreading from multiple files#2314 by mokeyish
- (tests) dns-test: cache target directory across docker build invocations #2305 by japaric
- (server) empty the answer section when DNSSEC validation fails #2304 by japaric
- (tests) Adjust timestamps to pass unbound validation result #2303 by justahero
- (recursor) validating recursor: return answer from cache #2297 by japaric
- (proto) DnssecDnsHandle: also update the RRSIG's proof #2293 by japaric
- (recursor) put tokio::test behind cfg attribute #2291 by japaric
- (resolver) Refactor start method in Resolver #2281 by justahero
- (server) improved server binary, added config validation and control over protocols #2247 by sabify
- (tests) dns-test: use non-deprecated algorithm (RSASHA256) #2258 by japaric
- (recursor) Recursor::resolve: reject queries with relative domain names #2246
- (tests) CI: also run hickory unit tests when only /conformance changes #2269 by japaric
- (all) Upgrade to rustls 0.23, quinn 0.11, etc #2217 by djc
- (proto) DnssecDnsHandle: check RRSIG validity as per RFC4035 #2213 by japaric
- (proto) NextRandomUdpSocket: fall back to port 0 if no port was found #2260 by Luap99
- (tests) dns-test: do not run docker network create in parallel #2265 by japaric
- (resolver) DnsLru: cache RRSIG records together with the record they cover #2239 by japaric
- (proto) dns-test: make NameServer's FQDN more stable #2235 by japaric
- (proto) refactor the Resource data structure #2231 by japaric
- (tests) Add just recipes to clean leftover containers and networks #2232 by pvdrz
- (tests) ci: pin nightly version #2224 by japaric
- (server) cargo: Enable LTO on release build #2141 by jpds
- (resolver) Retry tcp on udp io errors #2215 by bluejekyll
- (recursor) tweaks for security awareness #2208 by djc
- (all) address new clippy lint assigning-clones #2205 by divagant-martian
- (proto) error: wrap io::Error in Arc for clone #2181 by cpu
- (resolver) err for dns-over-rustls w/o roots #2179 by cpu
- (resolver) Forward hickory-dns's root cert features to hickory-resolver #2153 by hch12907
- (proto) Better DNSSEC proofs #2084 by bluejekyll
- (proto) update version for http/h2/h3 #2138 by zh-jq
- (server) Use cargo environment variables for path to executable #2130 by sjbronner
- (proto) Only DNSKEY zone keys are allowed to match DS RR #2131 by justahero
- (docs) Fix a typo in crate description #2132 by wiktor-k
- (all) Gate tests on required features #2114 by alexanderkjall
- (resolver) Fixup lookup docs #2123 by bluejekyll
- (proto) when comparing IP addresses for UDP, only check IP and Port #2124 by bluejekyll
- (recursor) Recursor: make nameserver and record cache sizes configurable #2117 by marcus0x62
- (proto) Validate response query section #2118 by marcus0x62
- (proto) Increase source port entropy in UDP client #2116 by marcus0x62
- (all) get(0) to first() and zerocopy package updates to fix clippy and cargo audit errors #2121 by marcus0x62
- (resolver) Add getters for resolver config and options #2093 by hoxxep
- (client) updated h2_client_connection and web-pki-roots config #2088 by marcbrevoort-cyberhive
- (proto) EchConfig renamed to EchConfigList to match content #2183 by cpu
- (proto) EchConfigList updated to wrap TLS presentation language encoding of content #2183 by cpu
### Added
- (tests) Add information on cargo ws plugin #2319 by justahero
- (recursor) Add support for PTR query #2308 by mokeyish
- (tests) add regression test for #2306, #2309 by japaric
- (tests) Add method to capture expected number of packets #2278 by justahero
- (tests) test that answer section is empty on failed DNSSEC validation #2302 by japaric
- (tests) Test invalid signature timestamps in DNSSEC validation #2298 by justahero
- (tests) test caching of chain of trust link #2289 by japaric
- (tests) test that DO=1 does not change the outcome of DNSSEC validation #2287 by japaric
- (tests) Add test to check cache hit with DO bit #2280 by justahero
- (tests) test caching of DNSSEC validation and of DNSSEC records #2244 by japaric
- (recursor) add DNSSEC validation to the recursive resolver #2253
- (proto) add a trust anchor file parser #2257 by japaric
- (tests) just: document conformance-* tasks #2266 by japaric
- (tests) Add conformance tests for NSEC3 #2238 by pvdrz
- (tests) import DNSSEC conformance test suite repository #2222 by japaric
- (client) Adds deref call in assertion for hickory-client README example #2173 by akappel
- (proto) Make hickory_proto::h3::H3ClientStream Clonable #2182 by 0xffffharry
- (proto) Make hickory_proto::quic::QuicClientStream Clonable #2176 by 0xffffharry
- (proto) feat: add setter methods for Message struct to improve configurability #2147 by situ2001
- (proto) add getter/setter methods to ClientSubnet #2146 by leshow
- (server) Add option to specify a restricted set of networks capable of accessing the Hickory DNS server #2126 by bluejekyll
- (recursor) Bailiwick checking for the recursor #2119 by marcus0x62
- (proto) Support getting and setting the EDNS Z flags #2111 by mattias-p
### Removed
- (all) Remove broken mtls code to fix CI #2218 by djc
- (proto) Remove generic Error from DnsHandle #2094 by bluejekyll
## 0.24.1
### Fixed
@@ -364,7 +57,7 @@ All notes should be prepended with the location of the change, e.g. `(proto)` or
### Fixed
- (resolver) Malformed label: -- when parsing resolv.conf #1985 by Jake-Shadle
- (proto) Fix truncation for UDP #1975 by nmittler
- (proto) Fix truncation for UDP #1975 by nmittler
- (proto) avoid panicking in parse_time() #1964 by djc
- (server) Merge up deny response in requests to server #1954 by djc
- (proto) remove duplicate is_soa function #1948 by mattsse

View File

@@ -15,7 +15,7 @@ Please read the [Architecture](ARCHITECTURE.md) to understand the general design
Before submitting a PR it would be good to discuss the change in an issue so as to avoid wasted work, also feel free to reach out on the Discord channel listed on the front page of the GitHub project. Please, consider keep PRs focused on one issue at a time. While issues are not required for a PR to be accepted they are encouraged, especially for anything that would change behavior, change an API, or be a medium to large change.
When submitting PRs please keep refactoring commits separate from functional change commits. Breaking up the PR into multiple commits such that a reviewer can follow the change improves the review experience. This is not necessary, but can make it easier for a reviewer to follow the changes and will result in PRs getting merged more quickly.
When making submitting PRs please keep refactoring commits separate from functional change commits. Breaking up the PR into multiple commits such that a reviewer can follow the change improves the review experience. This is not necessary, but can make it easier for a reviewer to follow the changes and will result in PRs getting merged more quickly.
### Test policy
@@ -49,12 +49,12 @@ Releases are somewhat automated. The github action, `publish`, watches for any t
1. Go to [Releases](https://github.com/hickory-dns/hickory-dns/releases) and `Draft a new release`
1. Give it a `Tag Version` of `vX.x.x`, e.g. `v0.20.1`, *make sure this is tagging the correct branch, e.g. `main` or `release/0.19`*
1. Give it a `Release Title` of something key to the release
1. Copy and paste the part of the CHANGELOG.md for this release into `Describe this release`
1. Copy and pase the part of the CHANGELOG.md for this release into `Describe this release`
1. `Publish Release`, this will kick off the publish workflow
After approximately 45 minutes it should be published. This may fail.
**TBD**: add instructions to skip already published crates
**TBD**: add instructions about using Makefile.toml to skip already published crates
## Updating Security Related Tests
@@ -62,6 +62,10 @@ After approximately 45 minutes it should be published. This may fail.
TBD: add notes on updating certificates in test directories
### Windows OpenSSL tests are failing
When the OpenSSL related tests fail on Windows, this is often due to a new minor version of the OpenSSL implementation there being increased. There is no good way to get this updated automatically right now. The library for Windows is maintained by Shining Light Productions, available here: [slproweb.com/products/Win32OpenSSL](https://slproweb.com/products/Win32OpenSSL.html). On that page the currently published version can be seen, e.g. `Win64 OpenSSL v1.1.1j Light`. The version downloaded is specified in [Makefile.toml](Makefile.toml), look for `OPENSSL_VERSION = "1_1_1j"` and replace with the correct string.
## FAQ
- Why are there so few maintainers?
@@ -72,23 +76,6 @@ There have not been that many people familiar with DNS internals, networking, se
Yes! There is no formal process, and generally it's a goal to open up to anyone who's been committing regularly to the project. We'd ask that you are committed to the goals of an open DNS implementation that anyone can freely use as they see fit. Please reach out on Discord if you'd like to become a maintainer and discuss with us.
## Configuring rust-analyzer
This repository contains multiple workspaces, so rust-analyzer will require additional configuration to provide hints in all files. If you are using the VS Code extension, create a `.vscode` directory inside the repository if it doesn't exist already, and edit `.vscode/settings.json` as follows:
```jsonc
{
"rust-analyzer.linkedProjects": [
"/path/to/hickory-dns/Cargo.toml",
"/path/to/hickory-dns/conformance/Cargo.toml",
"/path/to/hickory-dns/fuzz/Cargo.toml",
"/path/to/hickory-dns/tests/e2e-tests/Cargo.toml"
],
"rust-analyzer.cargo.features": "all"
// etc.
}
```
## Thank you!
Seriously, thank you for contributing to this project. Hickory DNS would not be where it is today without the support of contributors like you.

1338
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,15 +11,14 @@ members = [
"util",
"tests/compatibility-tests",
"tests/integration-tests",
"tests/test-support",
]
exclude = ["fuzz"]
[workspace.package]
version = "0.25.0-alpha.4"
version = "0.24.1"
authors = ["The contributors to Hickory DNS"]
edition = "2021"
rust-version = "1.71.1"
rust-version = "1.67.0"
homepage = "https://hickory-dns.org/"
repository = "https://github.com/hickory-dns/hickory-dns"
keywords = ["DNS", "BIND", "dig", "named", "dnssec"]
@@ -29,18 +28,17 @@ license = "MIT OR Apache-2.0"
[workspace.dependencies]
# hickory
hickory-client = { version = "0.25.0-alpha.4", path = "crates/client", default-features = false }
hickory-recursor = { version = "0.25.0-alpha.4", path = "crates/recursor", default-features = false }
hickory-resolver = { version = "0.25.0-alpha.4", path = "crates/resolver", default-features = false }
hickory-server = { version = "0.25.0-alpha.4", path = "crates/server", default-features = false }
hickory-proto = { version = "0.25.0-alpha.4", path = "crates/proto", default-features = false }
test-support.path = "tests/test-support"
hickory-client = { version = "0.24.0", path = "crates/client", default-features = false }
hickory-recursor = { version = "0.24.0", path = "crates/recursor", default-features = false }
hickory-resolver = { version = "0.24.0", path = "crates/resolver", default-features = false }
hickory-server = { version = "0.24.0", path = "crates/server", default-features = false }
hickory-proto = { version = "0.24.0", path = "crates/proto", default-features = false }
# logging
tracing = "0.1.30"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "std"] }
thiserror = "2"
tracing-subscriber = "0.3"
thiserror = "1.0.20"
# async/await
@@ -55,35 +53,32 @@ async-std = "1.6"
tokio = "1.21"
tokio-native-tls = "0.3.0"
tokio-openssl = "0.6.0"
tokio-rustls = { version = "0.26", default-features = false }
tokio-rustls = "0.24.0"
tokio-util = "0.7.9"
parking_lot = "0.12"
pin-project-lite = "0.2"
# ssl
native-tls = "0.2"
openssl = "0.10.55"
rustls = { version = "0.23.14", default-features = false, features = [
"logging",
"std",
"tls12",
] }
rustls-native-certs = "0.8"
webpki-roots = "0.26"
rustls = "0.21.8"
rustls-native-certs = "0.6.3"
rustls-pemfile = "1.0.0"
webpki-roots = "0.25.0"
ring = "0.17"
# net proto
quinn = { version = "0.11.2", default-features = false }
quinn = { version = "0.10", default-features = false }
h2 = "0.4.0"
h3 = "0.0.6"
h3-quinn = "0.0.7"
h3 = "0.0.4"
h3-quinn = "0.0.5"
http = "1.1"
# others
backtrace = "0.3.50"
basic-toml = "0.1"
bitflags = "2.4.1"
bytes = "1"
cfg-if = "1"
@@ -91,27 +86,24 @@ clap = { version = "4.0", default-features = false }
console = "0.15.0"
data-encoding = "2.2.0"
enum-as-inner = "0.6"
idna = "1.0"
idna = "0.5"
ipconfig = "0.3.0"
ipnet = "2.3.0"
libc = "0.2"
js-sys = "0.3.44"
once_cell = "1.18.0"
lru-cache = "0.1.2"
moka = "0.12"
once_cell = "1.20.0"
pin-utils = "0.1.0"
prefix-trie = "0.5"
prefix-trie = "0.3"
radix_trie = "0.2.0"
rand = "0.8"
regex = "1.3.4"
resolv-conf = "0.7.0"
rusqlite = "0.32"
rusqlite = "0.31"
serde = "1.0"
smallvec = "1.6"
socket2 = "0.5"
time = "0.3"
tinyvec = "1.1.1"
toml = "0.8.14"
url = "2.4.0"
wasm-bindgen-crate = { version = "0.2.58", package = "wasm-bindgen" }
@@ -119,12 +111,3 @@ wasm-bindgen-crate = { version = "0.2.58", package = "wasm-bindgen" }
# tokio = { path = "../tokio/tokio" }
# mio = { git = "https://github.com/tokio-rs/mio.git" }
# h2 = { git = "https://github.com/hyperium/h2.git" }
[profile.release]
lto = true
codegen-units = 1
opt-level = 3
strip = "symbols"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(nightly)'] }

View File

@@ -1,4 +1,4 @@
[![minimum rustc: 1.70](https://img.shields.io/badge/minimum%20rustc-1.70-green?logo=rust)](https://www.whatrustisit.com)
[![minimum rustc: 1.67](https://img.shields.io/badge/minimum%20rustc-1.67-green?logo=rust)](https://www.whatrustisit.com)
[![Build Status](https://github.com/hickory-dns/hickory-dns/workflows/test/badge.svg?branch=main)](https://github.com/hickory-dns/hickory-dns/actions?query=workflow%3Atest)
[![codecov](https://codecov.io/gh/hickory-dns/hickory-dns/branch/main/graph/badge.svg)](https://codecov.io/gh/hickory-dns/hickory-dns)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE-MIT)
@@ -13,19 +13,18 @@
</div>
A Rust based DNS client, server, and resolver, built to be safe and secure from the
A Rust based DNS client, server, and Resolver, built to be safe and secure from the
ground up.
This repo consists of multiple crates:
| Library | Description |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Hickory DNS** | [![](https://img.shields.io/crates/v/hickory-dns.svg)](https://crates.io/crates/hickory-dns) Provides the `hickory-dns` binary for running a DNS server. |
| **Proto** | [![](https://img.shields.io/crates/v/hickory-proto.svg)](https://crates.io/crates/hickory-proto) [![hickory-proto](https://docs.rs/hickory-proto/badge.svg)](https://docs.rs/hickory-proto) Low-level DNS library, including message encoding/decoding and DNS transports. |
| **Client** | [![](https://img.shields.io/crates/v/hickory-client.svg)](https://crates.io/crates/hickory-client) [![hickory-client](https://docs.rs/hickory-client/badge.svg)](https://docs.rs/hickory-client) Used for sending `query`, `update`, and `notify` messages directly to a DNS server. |
| **Server** | [![](https://img.shields.io/crates/v/hickory-server.svg)](https://crates.io/crates/hickory-server) [![hickory-server](https://docs.rs/hickory-server/badge.svg)](https://docs.rs/hickory-server) Used to build DNS servers. The `hickory-dns` binary makes use of this library. |
| **Resolver** | [![](https://img.shields.io/crates/v/hickory-resolver.svg)](https://crates.io/crates/hickory-resolver) [![hickory-resolver](https://docs.rs/hickory-resolver/badge.svg)](https://docs.rs/hickory-resolver) Utilizes the client library to perform DNS resolution. Can be used in place of the standard OS resolution facilities. |
| **Recursor** | [![](https://img.shields.io/crates/v/hickory-recursor.svg)](https://crates.io/crates/hickory-recursor) [![hickory-recursor](https://docs.rs/hickory-recursor/badge.svg)](https://docs.rs/hickory-recursor) Performs recursive DNS resolution, looking up records from their authoritative name servers. |
| Library | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Hickory DNS** | [![](https://img.shields.io/crates/v/hickory-dns.svg)](https://crates.io/crates/hickory-dns) Binaries for running a DNS authoritative server. |
| **Proto** | [![](https://img.shields.io/crates/v/hickory-proto.svg)](https://crates.io/crates/hickory-proto) [![hickory-proto](https://docs.rs/hickory-proto/badge.svg)](https://docs.rs/hickory-proto) Raw DNS library, exposes an unstable API and only for use by the other Hickory DNS libraries, not intended for end-user use. |
| **Client** | [![](https://img.shields.io/crates/v/hickory-client.svg)](https://crates.io/crates/hickory-client) [![hickory-client](https://docs.rs/hickory-client/badge.svg)](https://docs.rs/hickory-client) Used for sending `query`, `update`, and `notify` messages directly to a DNS server. |
| **Server** | [![](https://img.shields.io/crates/v/hickory-server.svg)](https://crates.io/crates/hickory-server) [![hickory-server](https://docs.rs/hickory-server/badge.svg)](https://docs.rs/hickory-server) Use to host DNS records, this also has a `hickory-dns` binary for running in a daemon form. |
| **Resolver** | [![](https://img.shields.io/crates/v/hickory-resolver.svg)](https://crates.io/crates/hickory-resolver) [![hickory-resolver](https://docs.rs/hickory-resolver/badge.svg)](https://docs.rs/hickory-resolver) Utilizes the client library to perform DNS resolution. Can be used in place of the standard OS resolution facilities. |
**NOTICE** This project was rebranded from Trust-DNS to Hickory DNS and has been moved to the https://github.com/hickory-dns/hickory-dns organization and repo.
@@ -45,23 +44,19 @@ This repo consists of multiple crates:
The Hickory DNS Resolver is a native Rust implementation for stub resolution in Rust applications. The Resolver supports many common query patterns, all of which can be configured when creating the Resolver. It is capable of using system configuration on Unix and Windows. On Windows there is a known issue that relates to a large set of interfaces being registered for use, so might require ignoring the system configuration.
The Resolver will properly follow CNAME chains as well as SRV record lookups.
The Resolver will properly follow CNAME chains as well as SRV record lookups. There is a long term plan to make the Resolver capable of fully recursive queries, but that's not currently possible.
## Client
The Hickory DNS Client is intended to be used for operating against a DNS server
directly. It can be used for verifying records or updating records for servers
that support SIG0 and dynamic update. The Client is also capable of validating
DNSSEC. NSEC and NSEC3 validation are supported. Today, the Tokio async runtime
is required.
The Hickory DNS Client is intended to be used for operating against a DNS server directly. It can be used for verifying records or updating records for servers that support SIG0 and dynamic update. The Client is also capable of validating DNSSEC. As of now NSEC3 validation is not yet supported, though NSEC is. There are two interfaces that can be used, the async/await compatible AsyncClient and a blocking Client for ease of use. Today, Tokio is required for the executor Runtime.
### Unique client side implementations
These are standards supported by the DNS protocol. The client implements them
as high level interfaces, which is a bit more rare.
| Feature | Description |
| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| Feature | Description |
| ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| [SyncDnssecClient](https://docs.rs/hickory-client/latest/hickory_client/client/struct.SyncDnssecClient.html) | DNSSEC validation |
| [create](https://docs.rs/hickory-client/latest/hickory_client/client/trait.Client.html#method.create) | atomic create of a record, with authenticated request |
| [append](https://docs.rs/hickory-client/latest/hickory_client/client/trait.Client.html#method.append) | verify existence of a record and append to it |
@@ -93,8 +88,8 @@ if enabled, a journal file will be stored next to the zone file with the
`jrnl` suffix. _Note_: if the key is changed or updated, it is currently the
operators responsibility to remove the only public key from the zone, this
allows for the `DNSKEY` to exist for some unspecified period of time during
key rotation. Rotating the key while online is not currently supported, so
a restart of the server process is required.
key rotation. Rotating the key currently is not available online and requires
a restart of the server process.
### DNS-over-TLS and DNS-over-HTTPS on the Server
@@ -102,21 +97,19 @@ Support of TLS on the Server is managed through a pkcs12 der file. The documenta
## DNS-over-TLS and DNS-over-HTTPS
DoT and DoH are supported. This is accomplished through the use of one of `native-tls`, `openssl`, or `rustls` (only `rustls` is currently supported for DoH). The Resolver requires valid DoT or DoH resolvers being registered in order to be used.
DoT and DoH are supported. This is accomplished through the use of one of `native-tls`, `openssl`, or `rustls` (only `rustls` is currently supported for DoH). The Resolver requires only requires valid DoT or DoH resolvers being registered in order to be used.
To use DoT or DoH with the `Client`, construct it with `TlsClientStream` or
`HttpsClientStream`. Client authentication/mTLS is currently not supported,
there are some issues still being worked on. TLS is useful for Server
authentication and connection privacy.
To use with the `Client`, the `TlsClientConnection` or `HttpsClientConnection` should be used. Similarly, to use with the tokio `AsyncClient` the `TlsClientStream` or `HttpsClientStream` should be used. ClientAuth, mTLS, is currently not supported, there are some issues still being worked on. TLS is useful for Server authentication and connection privacy.
To enable DoT, one of the features `dns-over-native-tls`, `dns-over-openssl`, or
`dns-over-rustls` must be enabled. `dns-over-https-rustls` is used for DoH.
To enable DoT one of the features `dns-over-native-tls`, `dns-over-openssl`, or `dns-over-rustls` must be enabled, `dns-over-https-rustls` is used for DoH.
## DNSSEC status
The current root key is bundled into the system, and used by default. This gives
validation of DNSKEY and DS records back to the root. NSEC and NSEC3 are
implemented.
Currently the root key is hardcoded into the system. This gives validation of
DNSKEY and DS records back to the root. NSEC is implemented, but not NSEC3.
Because caching is not yet enabled, it has been noticed that some DNS servers
appear to rate limit the connections, validating RRSIG records back to the root
can require a significant number of additional queries for those records.
Zones will be automatically resigned on any record updates via dynamic DNS. To enable DNSSEC, one of the features `dnssec-openssl` or `dnssec-ring` must be enabled.
@@ -143,12 +136,10 @@ Zones will be automatically resigned on any record updates via dynamic DNS. To e
### Secure DNS operations
- [RFC 2931](https://datatracker.ietf.org/doc/html/rfc2931): SIG(0)
- [RFC 3007](https://tools.ietf.org/html/rfc3007): Secure Dynamic Update
- [RFC 4034](https://tools.ietf.org/html/rfc4034): DNSSEC Resource Records
- [RFC 4035](https://tools.ietf.org/html/rfc4035): Protocol Modifications for DNSSEC
- [RFC 4509](https://tools.ietf.org/html/rfc4509): SHA-256 in DNSSEC Delegation Signer
- [RFC 5155](https://tools.ietf.org/html/rfc5155): DNSSEC Hashed Authenticated Denial of Existence
- [RFC 5702](https://tools.ietf.org/html/rfc5702): SHA-2 Algorithms with RSA in DNSKEY and RRSIG for DNSSEC
- [RFC 6844](https://tools.ietf.org/html/rfc6844): DNS Certification Authority Authorization (CAA) Resource Record
- [RFC 6698](https://tools.ietf.org/html/rfc6698): The DNS-Based Authentication of Named Entities (DANE) Transport Layer Security (TLS) Protocol: TLSA
@@ -174,6 +165,7 @@ Zones will be automatically resigned on any record updates via dynamic DNS. To e
### Secure DNS operations
- [RFC 5155](https://tools.ietf.org/html/rfc5155): DNSSEC Hashed Authenticated Denial of Existence
- [DNSCrypt](https://dnscrypt.org): Trusted DNS queries
- [S/MIME](https://tools.ietf.org/html/draft-ietf-dane-smime-09): Domain Names For S/MIME
@@ -189,7 +181,7 @@ presume that the hickory-dns repos have already been synced to the local system:
### Minimum Rust Version
- The current minimum rustc version for this project is `1.70`
- The current minimum rustc version for this project is `1.67`
- OpenSSL development libraries (optional in client and resolver, min version 1.0.2)
### Mac OS X: using homebrew
@@ -211,7 +203,7 @@ presume that the hickory-dns repos have already been synced to the local system:
## Testing
Hickory DNS uses `just` for build workflow management. While running `cargo test` at the project root will work, this is not exhaustive. Install `just` with `cargo install just`. A few of the `just` recipes require [`cargo-workspaces`](https://github.com/pksunkara/cargo-workspaces) to be installed, a plugin to optimize the workflow around cargo workspaces. Install the plugin with `cargo install cargo-workspaces`.
Hickory DNS uses `just` for build workflow management. While running `cargo test` at the project root will work, this is not exhaustive. Install `just` with `cargo install just`.
- Default tests
@@ -233,12 +225,7 @@ just all-features
- Individual feature tests
Hickory DNS has many features, each individual feature can be tested
independently. See individual crates for all their features, here is a not
necessarily up to date list: `dns-over-rustls`, `dns-over-https-rustls`,
`dns-over-native-tls`, `dns-over-openssl`, `dns-dnssec-openssl`,
`dns-dnssec-openssl`, `dns-dnssec-ring`, `mdns`. Each feature can be tested
with itself as the task target for `just`:
Hickory DNS has many features, each individual feature can be tested in dependently, see individual crates for all their features, here is a not necessarily up to date list: `dns-over-rustls`, `dns-over-https-rustls`, `dns-over-native-tls`, `dns-over-openssl`, `dns-dnssec-openssl`, `dns-dnssec-openssl`, `dns-dnssec-ring`, `mdns`. Each feature can be tested with itself as the task target for `just`:
```shell
just dns-over-https-rustls
@@ -276,9 +263,7 @@ so this should allow it to work with most internal loads.
- Launch `hickory-dns` server with test config
Note that if the `-p` parameter is not passed, the server will run on default
DNS ports. There are separate port options for DoT and DoH servers, see
`hickory-dns --help`
You may want not passing the `-p` parameter will run on default DNS ports. For the tls features, there are also port options for those, see `hickory-dns --help`
```shell
./target/release/hickory-dns -c ./tests/test-data/test_configs/example.toml -z ./tests/test-data/test_configs/ -p 24141
@@ -318,7 +303,7 @@ Success for query name: www.example.com. type: A class: IN
The Client has a few features which can be disabled for different reasons when embedding in other software.
- `dnssec-openssl`
Uses OpenSSL for DNSSEC validation.
It is a default feature, so default-features will need to be set to false (this will disable all other default features in hickory-dns). Until there are other crypto libraries supported, this will also disable DNSSEC validation. The functions will still exist, but will always return errors on validation. The below example line will disable all default features and enable OpenSSL, remove `"openssl"` to remove the dependency on OpenSSL.
- `dnssec-ring`
Ring support can be used for RSA and ED25519 DNSSEC validation.
@@ -330,7 +315,7 @@ The Client has a few features which can be disabled for different reasons when e
Uses `openssl` for DNS-over-TLS implementation supported in server and client, resolver does not have default CA chains.
- `dns-over-rustls`
Uses `rustls` for DNS-over-TLS implementation. This is the best option where a pure Rust toolchain is desired. Supported in client, resolver, and server.
Uses `rustls` for DNS-over-TLS implementation, only supported in client and resolver, not server. This is the best option where a pure Rust toolchain is desired. Supported in client, resolver, and server.
- `dns-over-https-rustls`
Uses `rustls` for DNS-over-HTTPS (and DNS-over-TLS will be enabled) implementation, only supported in client, resolver, and server. This is the best option where a pure Rust toolchain is desired.
@@ -343,7 +328,7 @@ Using custom features in dependencies:
```
[dependencies]
...
hickory-client = { version = "*", default-features = false, features = ["dnssec-openssl"] }
hickory-dns = { version = "*", default-features = false, features = ["dnssec-openssl"] }
```
Using custom features during build:

View File

@@ -26,31 +26,75 @@ keywords.workspace = true
categories.workspace = true
license.workspace = true
[badges]
#github-actions = { repository = "bluejekyll/hickory", branch = "main", workflow = "test" }
codecov = { repository = "hickory-dns/hickory-dns", branch = "main", service = "github" }
maintenance = { status = "actively-developed" }
[features]
default = ["sqlite", "resolver", "native-certs", "ascii-art"]
# if enabled, the hickory-dns binary will print ascii-art on start, disable to reduce the binary size
ascii-art = []
blocklist = ["hickory-server/blocklist"]
dnssec-openssl = ["dnssec", "hickory-server/dnssec-openssl", "dep:openssl"]
dnssec-ring = ["dnssec", "hickory-server/dnssec-ring"]
dnssec-openssl = [
"dnssec",
"hickory-client/dnssec-openssl",
"hickory-proto/dnssec-openssl",
"hickory-server/dnssec-openssl",
]
dnssec-ring = [
"dnssec",
"hickory-client/dnssec-ring",
"hickory-proto/dnssec-ring",
"hickory-server/dnssec-ring",
]
dnssec = []
recursor = ["hickory-server/recursor"]
# Recursive Resolution is Experimental!
resolver = ["hickory-server/resolver"]
sqlite = ["hickory-server/sqlite", "dep:rusqlite"]
sqlite = ["hickory-server/sqlite"]
dns-over-https-rustls = ["dns-over-rustls", "hickory-server/dns-over-https-rustls"]
# TODO: Need to figure out how to be consistent with ring/openssl usage...
# dns-over-https-openssl = ["dns-over-openssl", "hickory-client/dns-over-https-openssl", "dns-over-https"]
dns-over-https-rustls = [
"dns-over-https",
"dns-over-rustls",
"hickory-proto/dns-over-https-rustls",
"hickory-client/dns-over-https-rustls",
"hickory-server/dns-over-https-rustls",
]
dns-over-https = ["hickory-server/dns-over-https"]
dns-over-quic = ["dns-over-rustls", "hickory-server/dns-over-quic"]
dns-over-h3 = ["dns-over-rustls", "hickory-server/dns-over-h3"]
dns-over-openssl = ["dns-over-tls", "hickory-server/dns-over-openssl", "dep:openssl"]
dns-over-rustls = ["dns-over-tls", "dep:rustls", "hickory-server/dns-over-rustls"]
# TODO: migrate all tls and tls-openssl features to dns-over-tls, et al
dns-over-openssl = [
"dns-over-tls",
"dnssec-openssl",
"hickory-proto/dns-over-openssl",
"hickory-client/dns-over-openssl",
"hickory-server/dns-over-openssl",
]
dns-over-rustls = [
"dns-over-tls",
"dnssec-ring",
"rustls",
"hickory-proto/dns-over-rustls",
"hickory-client/dns-over-rustls",
"hickory-server/dns-over-rustls",
]
dns-over-tls = []
webpki-roots = ["hickory-server/webpki-roots"]
native-certs = ["hickory-server/native-certs"]
# This is a deprecated feature...
tls-openssl = ["dns-over-openssl"]
tls = ["dns-over-openssl"]
# WARNING: there is a bug in the mutual tls auth code at the moment see issue #100
# mtls = ["hickory-client/mtls"]
webpki-roots = ["hickory-client/webpki-roots", "hickory-server/webpki-roots"]
native-certs = ["hickory-client/native-certs", "hickory-server/native-certs"]
[[bin]]
name = "hickory-dns"
@@ -58,46 +102,38 @@ path = "src/hickory-dns.rs"
[dependencies]
# clap features:
# - `suggestions` for advanced help with error in cli
# - `derive` for clap derive api
# - `help` to generate --help
cfg-if.workspace = true
clap = { workspace = true, default-features = false, features = ["cargo", "derive", "help", "std", "suggestions"] }
futures-util = { workspace = true, default-features = false, features = ["std"] }
ipnet = { workspace = true, features = ["serde"] }
openssl = { workspace = true, features = ["v102", "v110"], optional = true }
# rusqlite is actually only needed for test situations, but we need an optional dependency
# here so we can disable it for MSRV tests (rusqlite only supports latest stable)
rusqlite = { workspace = true, features = ["bundled", "time"], optional = true }
socket2.workspace = true
# - suggestion for advanced help with error in cli
# - derive for clap derive api
# - help to generate --help
clap = { workspace = true, default-features = false, features = [
"std",
"cargo",
"help",
"derive",
"suggestions",
] }
futures-util = { workspace = true, default-features = false, features = [
"std",
] }
rustls = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
time.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
tracing-subscriber = { workspace = true, features = [
"std",
"fmt",
"env-filter",
] }
tokio = { workspace = true, features = ["time", "rt"] }
toml.workspace = true
hickory-client.workspace = true
hickory-proto.workspace = true
hickory-server = { workspace = true, features = ["toml"] }
[target.'cfg(unix)'.dependencies]
libc.workspace = true
[dev-dependencies]
futures-executor = { workspace = true, default-features = false, features = ["std"] }
native-tls.workspace = true
regex.workspace = true
hickory-proto = { workspace = true, features = ["dns-over-native-tls", "testing"] }
hickory-proto = { workspace = true, features = [
"testing",
"dns-over-native-tls",
] }
hickory-resolver.workspace = true
test-support.workspace = true
toml.workspace = true
webpki-roots.workspace = true
[lints]
workspace = true
[package.metadata.cargo-all-features]
skip_optional_dependencies = true
denylist = ["dnssec", "dns-over-tls"]
max_combination_size = 2

View File

@@ -2,45 +2,39 @@
Hickory DNS provides a binary for hosting or forwarding DNS zones.
This a named implementation for DNS zone hosting, stub resolvers, and recursive
resolvers. It is capable of performing signing all records in the zone for
server DNSSEC RRSIG records associated with all records in a zone. There is also
a `hickory-dns` binary that can be generated from the library with `cargo
install hickory-dns`. Dynamic updates are supported via `SIG0` (an mTLS
authentication method is under development).
This a named implementation for DNS zone hosting. It is capable of performing signing all records in the zone for server DNSSEC RRSIG records associated with all records in a zone. There is also a `hickory-dns` binary that can be generated from the library with `cargo install hickory-dns`. Dynamic updates are supported via `SIG0` (an mTLS authentication method is under development).
**NOTICE** This project was rebranded from Trust-DNS to Hickory DNS and has been moved to the https://github.com/hickory-dns/hickory-dns organization and repo, this crate/binary has been moved to [hickory-dns](https://crates.io/crates/hickory-dns), from `0.24` and onward, for prior versions see [trust-dns](https://crates.io/crates/trust-dns).
**NOTICE** This project was rebranded fromt Trust-DNS to Hickory DNS and has been moved to the https://github.com/hickory-dns/hickory-dns organization and repo, this crate/binary has been moved to [hickory-dns](https://crates.io/crates/hickory-dns), from `0.24` and onward, for prior versions see [trust-dns](https://crates.io/crates/trust-dns).
## Features
- Dynamic Update with sqlite journaling backend (SIG0)
- DNSSEC online signing (with NSEC and NSEC3)
- DNSSEC online signing (NSEC not NSEC3)
- DNS over TLS (DoT)
- DNS over HTTPS/2 (DoH)
- DNS over HTTPS/3 (DoH3)
- DNS over Quic (DoQ)
- Forwarding stub resolver
- ANAME resolution, for zone mapping aliases to A and AAAA records
- ANAME resolution, for zone mapping aliass to A and AAAA records
- Additionals section generation for aliasing record types
## DNS-over-TLS and DNS-over-HTTPS
Support of TLS on the Server is managed through a pkcs12 der file. The documentation is captured in the example test config file, [example.toml](https://github.com/hickory-dns/hickory-dns/blob/main/tests/test-data/test_configs/example.toml). A registered certificate to the server can be pinned to the Client with the `add_ca()` method. Alternatively, as the client uses the rust-native-tls library, it should work with certificate signed by any standard CA.
DoT and DoH are supported. This is accomplished through the use of one of `native-tls`, `openssl`, or `rustls` (only `rustls` is currently supported for DoH). The Resolver requires valid DoT or DoH resolvers being registered in order to be used.
DoT and DoH are supported. This is accomplished through the use of one of `native-tls`, `openssl`, or `rustls` (only `rustls` is currently supported for DoH). The Resolver requires only requires valid DoT or DoH resolvers being registered in order to be used.
Client authentication/mTLS is currently not supported, there are some issues
still being worked on. TLS is useful for Server authentication and connection
privacy.
To use with the `Client`, the `TlsClientConnection` or `HttpsClientConnection` should be used. Similarly, to use with the tokio `AsyncClient` the `TlsClientStream` or `HttpsClientStream` should be used. ClientAuth, mTLS, is currently not supported, there are some issues still being worked on. TLS is useful for Server authentication and connection privacy.
To enable DoT, one of the features `dns-over-native-tls`, `dns-over-openssl`, or
`dns-over-rustls` must be enabled. `dns-over-https-rustls` is used for DoH.
To enable DoT one of the features `dns-over-native-tls`, `dns-over-openssl`, or `dns-over-rustls` must be enabled, `dns-over-https-rustls` is used for DoH.
## DNSSEC status
The current root key is bundled into the system, and used by default. This gives
validation of DNSKEY and DS records back to the root. NSEC and NSEC3 are
implemented.
Currently the root key is hardcoded into the system. This gives validation of
DNSKEY and DS records back to the root. NSEC is implemented, but not NSEC3.
Because caching is not yet enabled, it has been noticed that some DNS servers
appear to rate limit the connections, validating RRSIG records back to the root
can require a significant number of additional queries for those records.
Zones will be automatically resigned on any record updates via dynamic DNS. To enable DNSSEC, one of the features `dnssec-openssl` or `dnssec-ring` must be enabled.
@@ -49,11 +43,12 @@ Zones will be automatically resigned on any record updates via dynamic DNS. To e
- Distributed dynamic DNS updates, with consensus
- mTLS based authorization for Dynamic Updates
- Online NSEC creation for queries
- Maybe NSEC5 support
- Full hint based resolving
- Maybe NSEC3 and/or NSEC5 support
## Minimum Rust Version
The current minimum rustc version for this project is `1.70`
The current minimum rustc version for this project is `1.67`
## Versioning

View File

@@ -5,27 +5,30 @@ extern crate test;
use std::env;
use std::fs::{DirBuilder, File};
use std::future::Future;
use std::mem;
use std::net::{Ipv4Addr, SocketAddr};
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::str::FromStr;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use hickory_proto::runtime::TokioRuntimeProvider;
use futures::Future;
use test::Bencher;
use tokio::net::TcpStream;
use tokio::net::UdpSocket;
use tokio::runtime::Runtime;
use hickory_client::client::{Client, ClientHandle};
use hickory_proto::op::ResponseCode;
use hickory_proto::rr::rdata::A;
use hickory_proto::rr::{DNSClass, Name, RData, RecordType};
use hickory_proto::tcp::TcpClientStream;
use hickory_proto::udp::UdpClientStream;
use hickory_proto::xfer::{DnsMultiplexer, DnsRequestSender};
use hickory_proto::ProtoError;
use hickory_client::client::*;
use hickory_client::op::*;
use hickory_client::rr::*;
use hickory_client::tcp::*;
use hickory_client::udp::*;
use hickory_proto::error::*;
use hickory_proto::iocompat::AsyncIoTokioAsStd;
use hickory_proto::op::NoopMessageFinalizer;
use hickory_proto::xfer::*;
fn find_test_port() -> u16 {
let server = std::net::UdpSocket::bind(("0.0.0.0", 0)).unwrap();
@@ -46,17 +49,20 @@ impl Drop for NamedProcess {
fn wrap_process(named: Child, server_port: u16) -> NamedProcess {
let mut started = false;
let provider = TokioRuntimeProvider::new();
for _ in 0..20 {
let io_loop = Runtime::new().unwrap();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, server_port));
let stream = UdpClientStream::builder(addr, provider.clone()).build();
let client = Client::connect(stream);
let addr: SocketAddr = ("127.0.0.1", server_port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let stream = UdpClientStream::<UdpSocket>::new(addr);
let client = AsyncClient::connect(stream);
let (mut client, bg) = io_loop.block_on(client).expect("failed to create client");
io_loop.spawn(bg);
let name = Name::from_str("www.example.com.").unwrap();
let name = domain::Name::from_str("www.example.com.").unwrap();
let response = io_loop.block_on(client.query(name.clone(), DNSClass::IN, RecordType::A));
if response.is_ok() {
@@ -111,11 +117,11 @@ where
S: DnsRequestSender,
{
let io_loop = Runtime::new().unwrap();
let client = Client::connect(stream);
let client = AsyncClient::connect(stream);
let (mut client, bg) = io_loop.block_on(client).expect("failed to create client");
io_loop.spawn(bg);
let name = Name::from_str("www.example.com.").unwrap();
let name = domain::Name::from_str("www.example.com.").unwrap();
// validate the request
let query = client.query(name.clone(), DNSClass::IN, RecordType::A);
@@ -124,8 +130,8 @@ where
assert_eq!(response.response_code(), ResponseCode::NoError);
let record = &response.answers()[0];
if let RData::A(address) = record.data() {
assert_eq!(address, &A(Ipv4Addr::LOCALHOST));
if let Some(RData::A(ref address)) = record.data() {
assert_eq!(address, &Ipv4Addr::new(127, 0, 0, 1));
} else {
unreachable!();
}
@@ -140,8 +146,12 @@ where
fn hickory_udp_bench(b: &mut Bencher) {
let (named, server_port) = hickory_process();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, server_port));
let stream = UdpClientStream::builder(addr, TokioRuntimeProvider::new()).build();
let addr: SocketAddr = ("127.0.0.1", server_port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let stream = UdpClientStream::<UdpSocket>::new(addr);
bench(b, stream);
// cleaning up the named process
@@ -153,8 +163,12 @@ fn hickory_udp_bench(b: &mut Bencher) {
fn hickory_udp_bench_prof(b: &mut Bencher) {
let server_port = 6363;
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, server_port));
let stream = UdpClientStream::builder(addr, TokioRuntimeProvider::new()).build();
let addr: SocketAddr = ("127.0.0.1", server_port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let stream = UdpClientStream::<UdpSocket>::new(addr);
bench(b, stream);
}
@@ -162,9 +176,13 @@ fn hickory_udp_bench_prof(b: &mut Bencher) {
fn hickory_tcp_bench(b: &mut Bencher) {
let (named, server_port) = hickory_process();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, server_port));
let (stream, sender) = TcpClientStream::new(addr, None, None, TokioRuntimeProvider::new());
let mp = DnsMultiplexer::new(stream, sender, None);
let addr: SocketAddr = ("127.0.0.1", server_port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TcpStream>>::new(addr);
let mp = DnsMultiplexer::new(stream, sender, None::<Arc<NoopMessageFinalizer>>);
bench(b, mp);
// cleaning up the named process
@@ -216,8 +234,12 @@ fn bind_process() -> (NamedProcess, u16) {
fn bind_udp_bench(b: &mut Bencher) {
let (named, server_port) = bind_process();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, server_port));
let stream = UdpClientStream::builder(addr, TokioRuntimeProvider::new()).build();
let addr: SocketAddr = ("127.0.0.1", server_port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let stream = UdpClientStream::<UdpSocket>::new(addr);
bench(b, stream);
// cleaning up the named process
@@ -229,9 +251,13 @@ fn bind_udp_bench(b: &mut Bencher) {
fn bind_tcp_bench(b: &mut Bencher) {
let (named, server_port) = bind_process();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, server_port));
let (stream, sender) = TcpClientStream::new(addr, None, None, TokioRuntimeProvider::new());
let mp = DnsMultiplexer::new(stream, sender, None);
let addr: SocketAddr = ("127.0.0.1", server_port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TcpStream>>::new(addr);
let mp = DnsMultiplexer::new(stream, sender, None::<Arc<NoopMessageFinalizer>>);
bench(b, mp);
// cleaning up the named process

File diff suppressed because it is too large Load Diff

View File

@@ -1,507 +0,0 @@
// Copyright 2015-2018 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// https://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
//! Configuration module for the server binary, `named`.
pub mod dnssec;
use std::{
fmt,
fs::File,
io::Read,
net::{AddrParseError, Ipv4Addr, Ipv6Addr},
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};
use cfg_if::cfg_if;
use ipnet::IpNet;
use serde::de::{self, MapAccess, SeqAccess, Visitor};
use serde::{self, Deserialize, Deserializer};
use hickory_proto::rr::Name;
use hickory_proto::ProtoError;
use hickory_server::authority::ZoneType;
#[cfg(feature = "dnssec")]
use hickory_server::dnssec::NxProofKind;
#[cfg(feature = "blocklist")]
use hickory_server::store::blocklist::BlocklistConfig;
use hickory_server::store::file::FileConfig;
#[cfg(feature = "resolver")]
use hickory_server::store::forwarder::ForwardConfig;
#[cfg(feature = "recursor")]
use hickory_server::store::recursor::RecursiveConfig;
#[cfg(feature = "sqlite")]
use hickory_server::store::sqlite::SqliteConfig;
use hickory_server::ConfigError;
static DEFAULT_PATH: &str = "/var/named"; // TODO what about windows (do I care? ;)
static DEFAULT_PORT: u16 = 53;
static DEFAULT_TLS_PORT: u16 = 853;
static DEFAULT_HTTPS_PORT: u16 = 443;
static DEFAULT_QUIC_PORT: u16 = 853; // https://www.ietf.org/archive/id/draft-ietf-dprive-dnsoquic-11.html#name-reservation-of-dedicated-po
static DEFAULT_H3_PORT: u16 = 443;
static DEFAULT_TCP_REQUEST_TIMEOUT: u64 = 5;
/// Server configuration
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Config {
/// The list of IPv4 addresses to listen on
#[serde(default)]
listen_addrs_ipv4: Vec<String>,
/// This list of IPv6 addresses to listen on
#[serde(default)]
listen_addrs_ipv6: Vec<String>,
/// Port on which to listen (associated to all IPs)
listen_port: Option<u16>,
/// Secure port to listen on
tls_listen_port: Option<u16>,
/// HTTPS port to listen on
https_listen_port: Option<u16>,
/// QUIC port to listen on
quic_listen_port: Option<u16>,
/// HTTP/3 port to listen on
h3_listen_port: Option<u16>,
/// Disable TCP protocol
disable_tcp: Option<bool>,
/// Disable UDP protocol
disable_udp: Option<bool>,
/// Disable TLS protocol
disable_tls: Option<bool>,
/// Disable HTTPS protocol
disable_https: Option<bool>,
/// Disable QUIC protocol
disable_quic: Option<bool>,
/// Timeout associated to a request before it is closed.
tcp_request_timeout: Option<u64>,
/// Level at which to log, default is INFO
log_level: Option<String>,
/// Base configuration directory, i.e. root path for zones
directory: Option<String>,
/// User to run the server as.
///
/// Only supported on Unix-like platforms. If the real or effective UID of the hickory process
/// is root, we will attempt to change to this user (or to nobody if no user is specified here.)
pub user: Option<String>,
/// Group to run the server as.
///
/// Only supported on Unix-like platforms. If the real or effective UID of the hickory process
/// is root, we will attempt to change to this group (or to nobody if no group is specified here.)
pub group: Option<String>,
/// List of configurations for zones
#[serde(default)]
zones: Vec<ZoneConfig>,
/// Certificate to associate to TLS connections (currently the same is used for HTTPS and TLS)
#[cfg(feature = "dns-over-tls")]
tls_cert: Option<dnssec::TlsCertConfig>,
/// The HTTP endpoint where the DNS-over-HTTPS server provides service. Applicable
/// to both HTTP/2 and HTTP/3 servers. Typically `/dns-query`.
#[cfg(any(feature = "dns-over-https-rustls", feature = "dns-over-h3"))]
http_endpoint: Option<String>,
/// Networks denied to access the server
#[serde(default)]
deny_networks: Vec<IpNet>,
/// Networks allowed to access the server
#[serde(default)]
allow_networks: Vec<IpNet>,
}
impl Config {
/// read a Config file from the file specified at path.
pub fn read_config(path: &Path) -> Result<Self, ConfigError> {
let mut file = File::open(path)?;
let mut toml = String::new();
file.read_to_string(&mut toml)?;
Self::from_toml(&toml)
}
/// Read a [`Config`] from the given TOML string.
pub fn from_toml(toml: &str) -> Result<Self, ConfigError> {
Ok(toml::from_str(toml)?)
}
/// set of listening ipv4 addresses (for TCP and UDP)
pub fn listen_addrs_ipv4(&self) -> Result<Vec<Ipv4Addr>, AddrParseError> {
self.listen_addrs_ipv4.iter().map(|s| s.parse()).collect()
}
/// set of listening ipv6 addresses (for TCP and UDP)
pub fn listen_addrs_ipv6(&self) -> Result<Vec<Ipv6Addr>, AddrParseError> {
self.listen_addrs_ipv6.iter().map(|s| s.parse()).collect()
}
/// port on which to listen for connections on specified addresses
pub fn listen_port(&self) -> u16 {
self.listen_port.unwrap_or(DEFAULT_PORT)
}
/// port on which to listen for TLS connections
pub fn tls_listen_port(&self) -> u16 {
self.tls_listen_port.unwrap_or(DEFAULT_TLS_PORT)
}
/// port on which to listen for HTTPS connections
pub fn https_listen_port(&self) -> u16 {
self.https_listen_port.unwrap_or(DEFAULT_HTTPS_PORT)
}
/// port on which to listen for QUIC connections
pub fn quic_listen_port(&self) -> u16 {
self.quic_listen_port.unwrap_or(DEFAULT_QUIC_PORT)
}
/// port on which to listen for HTTP/3 connections
pub fn h3_listen_port(&self) -> u16 {
self.h3_listen_port.unwrap_or(DEFAULT_H3_PORT)
}
/// get if TCP protocol should be disabled
pub fn disable_tcp(&self) -> bool {
self.disable_tcp.unwrap_or_default()
}
/// get if UDP protocol should be disabled
pub fn disable_udp(&self) -> bool {
self.disable_udp.unwrap_or_default()
}
/// get if TLS protocol should be disabled
pub fn disable_tls(&self) -> bool {
self.disable_tls.unwrap_or_default()
}
/// get if HTTPS protocol should be disabled
pub fn disable_https(&self) -> bool {
self.disable_https.unwrap_or_default()
}
/// get if QUIC protocol should be disabled
pub fn disable_quic(&self) -> bool {
self.disable_quic.unwrap_or_default()
}
/// default timeout for all TCP connections before forcibly shutdown
pub fn tcp_request_timeout(&self) -> Duration {
Duration::from_secs(
self.tcp_request_timeout
.unwrap_or(DEFAULT_TCP_REQUEST_TIMEOUT),
)
}
/// specify the log level which should be used, ["Trace", "Debug", "Info", "Warn", "Error"]
pub fn log_level(&self) -> tracing::Level {
if let Some(level_str) = &self.log_level {
tracing::Level::from_str(level_str).unwrap_or(tracing::Level::INFO)
} else {
tracing::Level::INFO
}
}
/// the path for all zone configurations, defaults to `/var/named`
pub fn directory(&self) -> &Path {
self.directory
.as_ref()
.map_or(Path::new(DEFAULT_PATH), Path::new)
}
/// the set of zones which should be loaded
pub fn zones(&self) -> &[ZoneConfig] {
&self.zones
}
/// the tls certificate to use for accepting tls connections
pub fn tls_cert(&self) -> Option<&dnssec::TlsCertConfig> {
cfg_if! {
if #[cfg(feature = "dns-over-tls")] {
self.tls_cert.as_ref()
} else {
None
}
}
}
/// the HTTP endpoint from where requests are received
#[cfg(any(feature = "dns-over-https-rustls", feature = "dns-over-h3"))]
pub fn http_endpoint(&self) -> &str {
self.http_endpoint
.as_deref()
.unwrap_or(hickory_proto::http::DEFAULT_DNS_QUERY_PATH)
}
/// get the networks denied access to this server
pub fn deny_networks(&self) -> &[IpNet] {
&self.deny_networks
}
/// get the networks allowed to connect to this server
pub fn allow_networks(&self) -> &[IpNet] {
&self.allow_networks
}
}
/// Configuration for a zone
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct ZoneConfig {
/// name of the zone
pub zone: String, // TODO: make Domain::Name decodable
/// type of the zone
pub zone_type: ZoneType,
/// location of the file (short for StoreConfig::FileConfig{zone_file_path})
pub file: Option<String>,
/// Deprecated allow_update, this is a Store option
pub allow_update: Option<bool>,
/// Allow AXFR (TODO: need auth)
pub allow_axfr: Option<bool>,
/// Enable DnsSec TODO: should this move to StoreConfig?
pub enable_dnssec: Option<bool>,
/// Keys for use by the zone
#[serde(default)]
pub keys: Vec<dnssec::KeyConfig>,
/// Store configurations. Note: we specify a default handler to get a Vec containing a
/// StoreConfig::Default, which is used for authoritative file-based zones and legacy sqlite
/// configurations. #[serde(default)] cannot be used, because it will invoke Default for Vec,
/// i.e., an empty Vec and we cannot implement Default for StoreConfig and return a Vec. The
/// custom visitor is used to handle map (single store) or sequence (chained store) configurations.
#[serde(default = "store_config_default")]
#[serde(deserialize_with = "store_config_visitor")]
pub stores: Vec<StoreConfig>,
/// The kind of non-existence proof provided by the nameserver
#[cfg(feature = "dnssec")]
pub nx_proof_kind: Option<NxProofKind>,
}
impl ZoneConfig {
/// Return a new zone configuration
///
/// # Arguments
///
/// * `zone` - name of a zone, e.g. example.com
/// * `zone_type` - Type of zone, e.g. Primary, Secondary, etc.
/// * `file` - relative to Config base path, to the zone file
/// * `allow_update` - enable dynamic updates
/// * `allow_axfr` - enable AXFR transfers
/// * `enable_dnssec` - enable signing of the zone for DNSSEC
/// * `keys` - list of private and public keys used to sign a zone
/// * `nx_proof_kind` - the kind of non-existence proof provided by the nameserver
#[cfg_attr(feature = "dnssec", allow(clippy::too_many_arguments))]
pub fn new(
zone: String,
zone_type: ZoneType,
file: String,
allow_update: Option<bool>,
allow_axfr: Option<bool>,
enable_dnssec: Option<bool>,
keys: Vec<dnssec::KeyConfig>,
#[cfg(feature = "dnssec")] nx_proof_kind: Option<NxProofKind>,
) -> Self {
Self {
zone,
zone_type,
file: Some(file),
allow_update,
allow_axfr,
enable_dnssec,
keys,
stores: store_config_default(),
#[cfg(feature = "dnssec")]
nx_proof_kind,
}
}
// TODO this is a little ugly for the parse, b/c there is no terminal char
/// returns the name of the Zone, i.e. the `example.com` of `www.example.com.`
pub fn zone(&self) -> Result<Name, ProtoError> {
Name::parse(&self.zone, Some(&Name::new()))
}
/// the type of the zone
pub fn zone_type(&self) -> ZoneType {
self.zone_type
}
/// path to the zone file, i.e. the base set of original records in the zone
///
/// this is only used on first load, if dynamic update is enabled for the zone, then the journal
/// file is the actual source of truth for the zone.
pub fn file(&self) -> PathBuf {
// TODO: Option on PathBuf
PathBuf::from(self.file.as_ref().expect("file was none"))
}
/// enable dynamic updates for the zone (see SIG0 and the registered keys)
pub fn is_update_allowed(&self) -> bool {
self.allow_update.unwrap_or(false)
}
/// enable AXFR transfers
pub fn is_axfr_allowed(&self) -> bool {
self.allow_axfr.unwrap_or(false)
}
/// declare that this zone should be signed, see keys for configuration of the keys for signing
pub fn is_dnssec_enabled(&self) -> bool {
cfg_if! {
if #[cfg(feature = "dnssec")] {
self.enable_dnssec.unwrap_or(false)
} else {
false
}
}
}
/// the configuration for the keys used for auth and/or dnssec zone signing.
#[cfg(feature = "dnssec")]
pub fn keys(&self) -> &[dnssec::KeyConfig] {
&self.keys
}
}
/// Enumeration over all store types
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum StoreConfig {
/// Blocklist configuration
#[cfg(feature = "blocklist")]
Blocklist(BlocklistConfig),
/// File based configuration
File(FileConfig),
/// Sqlite based configuration file
#[cfg(feature = "sqlite")]
Sqlite(SqliteConfig),
/// Forwarding Resolver
#[cfg(feature = "resolver")]
Forward(ForwardConfig),
/// Recursive Resolver
#[cfg(feature = "recursor")]
Recursor(RecursiveConfig),
/// This is used by the configuration processing code to represent a deprecated or main-block config without an associated store.
Default,
}
/// Create a default value for serde for StoreConfig.
fn store_config_default() -> Vec<StoreConfig> {
vec![StoreConfig::Default]
}
/// Custom serde visitor that can deserialize a map (single configuration store, expressed as a TOML
/// table) or sequence (chained configuration stores, expressed as a TOML array of tables.)
/// This is used instead of an untagged enum because serde cannot provide variant-specific error
/// messages when using an untagged enum.
fn store_config_visitor<'de, D>(deserializer: D) -> Result<Vec<StoreConfig>, D::Error>
where
D: Deserializer<'de>,
{
struct MapOrSequence;
impl<'de> Visitor<'de> for MapOrSequence {
type Value = Vec<StoreConfig>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("map or sequence")
}
fn visit_seq<S>(self, seq: S) -> Result<Vec<StoreConfig>, S::Error>
where
S: SeqAccess<'de>,
{
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
}
fn visit_map<M>(self, map: M) -> Result<Vec<StoreConfig>, M::Error>
where
M: MapAccess<'de>,
{
match Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) {
Ok(seq) => Ok(vec![seq]),
Err(e) => Err(e),
}
}
}
deserializer.deserialize_any(MapOrSequence)
}
#[cfg(all(test, any(feature = "resolver", feature = "recursor")))]
mod tests {
use super::*;
#[cfg(feature = "recursor")]
#[test]
fn example_recursor_config() {
toml::from_str::<Config>(include_str!(
"../../tests/test-data/test_configs/example_recursor.toml"
))
.unwrap();
}
#[cfg(feature = "resolver")]
#[test]
fn single_store_config_error_message() {
match toml::from_str::<Config>(
r#"[[zones]]
zone = "."
zone_type = "Forward"
[zones.stores]
ype = "forward""#,
) {
Ok(val) => panic!("expected error value; got ok: {val:?}"),
Err(e) => assert!(e.to_string().contains("missing field `type`")),
}
}
#[cfg(feature = "resolver")]
#[test]
fn chained_store_config_error_message() {
match toml::from_str::<Config>(
r#"[[zones]]
zone = "."
zone_type = "Forward"
[[zones.stores]]
type = "forward"
[[zones.stores.name_servers]]
socket_addr = "8.8.8.8:53"
protocol = "udp"
trust_negative_responses = false
[[zones.stores]]
type = "forward"
[[zones.stores.name_servers]]
socket_addr = "1.1.1.1:53"
rotocol = "udp"
trust_negative_responses = false"#,
) {
Ok(val) => panic!("expected error value; got ok: {val:?}"),
Err(e) => assert!(dbg!(e).to_string().contains("unknown field `rotocol`")),
}
}
#[cfg(feature = "resolver")]
#[test]
fn empty_store_default_value() {
match toml::from_str::<Config>(
r#"[[zones]]
zone = "localhost"
zone_type = "Primary"
file = "default/localhost.zone""#,
) {
Ok(val) => {
assert_eq!(val.zones[0].stores.len(), 1);
assert!(matches!(val.zones[0].stores[0], StoreConfig::Default));
}
Err(e) => panic!("expected successful parse: {e:?}"),
}
}
}

View File

@@ -1,497 +0,0 @@
/*
* Copyright (C) 2015 Benjamin Fry <benjaminfry@me.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::env;
use std::fs::{read_dir, File};
use std::io::Read;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::time::Duration;
use toml::map::Keys;
use toml::value::Array;
use toml::{Table, Value};
use hickory_dns::Config;
use hickory_server::authority::ZoneType;
#[test]
fn test_read_config() {
let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "..".to_owned());
let path: PathBuf =
PathBuf::from(server_path).join("tests/test-data/test_configs/example.toml");
if !path.exists() {
panic!("can't locate example.toml and other configs: {:?}", path)
}
println!("reading config");
let config = Config::read_config(&path).unwrap();
assert_eq!(config.listen_port(), 53);
assert_eq!(config.listen_addrs_ipv4(), Ok(Vec::<Ipv4Addr>::new()));
assert_eq!(config.listen_addrs_ipv6(), Ok(Vec::<Ipv6Addr>::new()));
assert_eq!(config.tcp_request_timeout(), Duration::from_secs(5));
assert_eq!(config.log_level(), tracing::Level::INFO);
assert_eq!(config.directory(), Path::new("/var/named"));
assert_eq!(config.zones()[0].zone, "localhost");
assert_eq!(config.zones()[0].zone_type, ZoneType::Primary);
assert_eq!(
config.zones()[0].file.as_deref(),
Some("default/localhost.zone")
);
assert_eq!(config.zones()[1].zone, "0.0.127.in-addr.arpa");
assert_eq!(config.zones()[1].zone_type, ZoneType::Primary);
assert_eq!(
config.zones()[1].file.as_deref(),
Some("default/127.0.0.1.zone")
);
assert_eq!(
config.zones()[2].zone,
"0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa"
);
assert_eq!(config.zones()[2].zone_type, ZoneType::Primary);
assert_eq!(
config.zones()[2].file.as_deref(),
Some("default/ipv6_1.zone")
);
assert_eq!(config.zones()[3].zone, "255.in-addr.arpa");
assert_eq!(config.zones()[3].zone_type, ZoneType::Primary);
assert_eq!(config.zones()[3].file.as_deref(), Some("default/255.zone"));
assert_eq!(config.zones()[4].zone, "0.in-addr.arpa");
assert_eq!(config.zones()[4].zone_type, ZoneType::Primary);
assert_eq!(config.zones()[4].file.as_deref(), Some("default/0.zone"));
assert_eq!(config.zones()[5].zone, "example.com");
assert_eq!(config.zones()[5].zone_type, ZoneType::Primary);
assert_eq!(config.zones()[5].file.as_deref(), Some("example.com.zone"));
}
#[test]
fn test_parse_toml() {
let config = Config::from_toml("listen_port = 2053").unwrap();
assert_eq!(config.listen_port(), 2053);
let config = Config::from_toml("listen_addrs_ipv4 = [\"0.0.0.0\"]").unwrap();
assert_eq!(config.listen_addrs_ipv4(), Ok(vec![Ipv4Addr::UNSPECIFIED]));
let config = Config::from_toml("listen_addrs_ipv4 = [\"0.0.0.0\", \"127.0.0.1\"]").unwrap();
assert_eq!(
config.listen_addrs_ipv4(),
Ok(vec![Ipv4Addr::UNSPECIFIED, Ipv4Addr::LOCALHOST])
);
let config = Config::from_toml("listen_addrs_ipv6 = [\"::0\"]").unwrap();
assert_eq!(config.listen_addrs_ipv6(), Ok(vec![Ipv6Addr::UNSPECIFIED]));
let config = Config::from_toml("listen_addrs_ipv6 = [\"::0\", \"::1\"]").unwrap();
assert_eq!(
config.listen_addrs_ipv6(),
Ok(vec![
Ipv6Addr::UNSPECIFIED,
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1),
])
);
let config = Config::from_toml("tcp_request_timeout = 25").unwrap();
assert_eq!(config.tcp_request_timeout(), Duration::from_secs(25));
let config = Config::from_toml("log_level = \"Debug\"").unwrap();
assert_eq!(config.log_level(), tracing::Level::DEBUG);
let config = Config::from_toml("directory = \"/dev/null\"").unwrap();
assert_eq!(config.directory(), Path::new("/dev/null"));
}
#[cfg(feature = "dnssec")]
#[test]
fn test_parse_zone_keys() {
use hickory_proto::dnssec::Algorithm;
use hickory_proto::rr::Name;
let config = Config::from_toml(
"
[[zones]]
zone = \"example.com\"
zone_type = \"Primary\"
file = \"example.com.zone\"
\
[[zones.keys]]
key_path = \"/path/to/my_ed25519.pem\"
algorithm = \"ED25519\"
\
signer_name = \"ns.example.com.\"
is_zone_signing_key = false
is_zone_update_auth = true
[[zones.keys]]
key_path = \"/path/to/my_rsa.pem\"
algorithm = \
\"RSASHA256\"
signer_name = \"ns.example.com.\"
",
)
.unwrap();
assert_eq!(
config.zones()[0].keys()[0].key_path(),
Path::new("/path/to/my_ed25519.pem")
);
assert_eq!(
config.zones()[0].keys()[0].algorithm().unwrap(),
Algorithm::ED25519
);
assert_eq!(
config.zones()[0].keys()[0].signer_name().unwrap().unwrap(),
Name::parse("ns.example.com.", None).unwrap()
);
assert!(!config.zones()[0].keys()[0].is_zone_signing_key(),);
assert!(config.zones()[0].keys()[0].is_zone_update_auth(),);
assert_eq!(
config.zones()[0].keys()[1].key_path(),
Path::new("/path/to/my_rsa.pem")
);
assert_eq!(
config.zones()[0].keys()[1].algorithm().unwrap(),
Algorithm::RSASHA256
);
assert_eq!(
config.zones()[0].keys()[1].signer_name().unwrap().unwrap(),
Name::parse("ns.example.com.", None).unwrap()
);
assert!(!config.zones()[0].keys()[1].is_zone_signing_key(),);
assert!(!config.zones()[0].keys()[1].is_zone_update_auth(),);
}
#[test]
#[cfg(feature = "dns-over-tls")]
fn test_parse_tls() {
// defaults
let config = Config::from_toml("").unwrap();
assert_eq!(config.tls_listen_port(), 853);
assert_eq!(config.tls_cert(), None);
let config = Config::from_toml(
"tls_cert = { path = \"path/to/some.pkcs12\", endpoint_name = \"ns.example.com\" }
tls_listen_port = 8853
",
)
.unwrap();
assert_eq!(config.tls_listen_port(), 8853);
assert_eq!(
config.tls_cert().unwrap().path(),
Path::new("path/to/some.pkcs12")
);
}
fn test_config(path: &str) {
let workspace = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "..".to_owned());
let path = PathBuf::from(workspace)
.join("tests/test-data/test_configs")
.join(path)
.with_extension("toml");
assert!(path.exists(), "does not exist: {}", path.display());
println!("reading: {}", path.display());
Config::read_config(&path).expect("failed to read");
}
macro_rules! define_test_config {
($name:ident) => {
#[test]
fn $name() {
test_config(stringify!($name));
}
};
}
define_test_config!(all_supported_dnssec);
#[cfg(feature = "blocklist")]
define_test_config!(chained_blocklist);
#[cfg(feature = "blocklist")]
define_test_config!(consulting_blocklist);
#[cfg(feature = "dns-over-https-rustls")]
define_test_config!(dns_over_https);
#[cfg(feature = "dns-over-tls")]
define_test_config!(dns_over_tls_rustls_and_openssl);
#[cfg(feature = "dns-over-tls")]
define_test_config!(dns_over_tls);
#[cfg(feature = "sqlite")]
define_test_config!(dnssec_with_update);
define_test_config!(dnssec_with_update_deprecated);
define_test_config!(example);
define_test_config!(ipv4_and_ipv6);
define_test_config!(ipv4_only);
define_test_config!(ipv6_only);
define_test_config!(openssl_dnssec);
define_test_config!(ring_dnssec);
#[cfg(feature = "resolver")]
define_test_config!(example_forwarder);
/// Iterator that yields modified TOML tables with an extra field added, and recurses down the
/// table's values.
struct TableMutator<'a> {
original: &'a Table,
yielded_base_case: bool,
key_iter: Keys<'a>,
nested_table_mutator: Option<(&'a str, Box<TableMutator<'a>>)>,
nested_array_mutator: Option<(&'a str, Box<ArrayMutator<'a>>)>,
}
impl<'a> TableMutator<'a> {
fn new(table: &'a Table) -> Self {
Self {
original: table,
yielded_base_case: false,
key_iter: table.keys(),
nested_table_mutator: None,
nested_array_mutator: None,
}
}
}
impl Iterator for TableMutator<'_> {
type Item = Table;
fn next(&mut self) -> Option<Self::Item> {
if !self.yielded_base_case {
self.yielded_base_case = true;
let mut table = self.original.clone();
table.insert("test_only_invalid_config_key".into(), Value::Integer(1));
return Some(table);
}
loop {
if let Some((key, iter)) = self.nested_table_mutator.as_mut() {
if let Some(table) = iter.next() {
let mut output = self.original.clone();
output[*key] = Value::Table(table);
return Some(output);
} else {
self.nested_table_mutator = None;
}
}
if let Some((key, iter)) = self.nested_array_mutator.as_mut() {
if let Some(array) = iter.next() {
let mut output = self.original.clone();
output[*key] = Value::Array(array);
return Some(output);
} else {
self.nested_array_mutator = None;
}
}
if let Some(key) = self.key_iter.next() {
match self.original.get(key).unwrap() {
Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_) => {}
Value::Array(array) => {
self.nested_array_mutator = Some((key, Box::new(ArrayMutator::new(array))));
}
Value::Table(table) => {
self.nested_table_mutator = Some((key, Box::new(TableMutator::new(table))));
}
}
} else {
return None;
}
}
}
}
/// Iterator that yields modified TOML arrays, working with [`TableMutator`], and recurses down the
/// array's contents.
struct ArrayMutator<'a> {
original: &'a Array,
index_iter: Range<usize>,
nested_table_mutator: Option<(usize, Box<TableMutator<'a>>)>,
nested_array_mutator: Option<(usize, Box<ArrayMutator<'a>>)>,
}
impl<'a> ArrayMutator<'a> {
fn new(array: &'a Array) -> Self {
Self {
original: array,
index_iter: 0..array.len(),
nested_table_mutator: None,
nested_array_mutator: None,
}
}
}
impl Iterator for ArrayMutator<'_> {
type Item = Array;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some((key, iter)) = self.nested_table_mutator.as_mut() {
if let Some(table) = iter.next() {
let mut output = self.original.clone();
output[*key] = Value::Table(table);
return Some(output);
} else {
self.nested_table_mutator = None;
}
}
if let Some((key, iter)) = self.nested_array_mutator.as_mut() {
if let Some(array) = iter.next() {
let mut output = self.original.clone();
output[*key] = Value::Array(array);
return Some(output);
} else {
self.nested_array_mutator = None;
}
}
if let Some(index) = self.index_iter.next() {
match self.original.get(index).unwrap() {
Value::String(_)
| Value::Integer(_)
| Value::Float(_)
| Value::Boolean(_)
| Value::Datetime(_) => {}
Value::Array(array) => {
self.nested_array_mutator =
Some((index, Box::new(ArrayMutator::new(array))));
}
Value::Table(table) => {
self.nested_table_mutator =
Some((index, Box::new(TableMutator::new(table))));
}
}
} else {
return None;
}
}
}
}
/// Check that unknown fields in configuration files are rejected. This uses each example
/// configuration file as a seed, and tries adding invalid fields to each table.
#[test]
fn test_reject_unknown_fields() {
let test_configs_dir =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../tests/test-data/test_configs");
for result in read_dir(test_configs_dir).unwrap() {
let entry = result.unwrap();
let file_name = entry.file_name().into_string().unwrap();
if !file_name.ends_with(".toml") {
continue;
}
println!("seed file: {file_name}");
let mut file = File::open(entry.path()).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let value = toml::from_str::<toml::Value>(&contents).unwrap();
let config_table = value.as_table().unwrap();
// Skip over configs that can't be read with the current set of features.
#[allow(unused_mut)]
let mut skip = false;
#[cfg(not(any(
feature = "dns-over-tls",
feature = "dns-over-https-rustls",
feature = "dns-over-quic"
)))]
if config_table.contains_key("tls_cert") {
println!("skipping due to tls_cert setting");
skip = true;
}
let zones = config_table.get("zones").unwrap().as_array().unwrap();
for zone in zones {
if let Some(stores) = zone.get("stores") {
let vec_stores: Vec<Value>;
let stores = if !stores.is_array() {
vec_stores = vec![stores.clone()];
&vec_stores
} else {
stores.as_array().unwrap()
};
for store in stores {
let store = store.as_table().unwrap();
let _store_type = store.get("type").unwrap().as_str().unwrap();
#[cfg(not(feature = "blocklist"))]
if _store_type == "blocklist" {
println!("skipping due to blocklist store");
skip = true;
break;
}
#[cfg(not(feature = "sqlite"))]
if _store_type == "sqlite" {
println!("skipping due to sqlite store");
skip = true;
break;
}
#[cfg(not(feature = "resolver"))]
if _store_type == "forward" {
println!("skipping due to forward store");
skip = true;
break;
}
#[cfg(not(feature = "recursor"))]
if _store_type == "recursor" {
println!("skipping due to recursor store");
skip = true;
break;
}
}
};
}
if skip {
continue;
}
// Confirm the example config file can be read as-is.
toml::from_str::<Config>(&contents).unwrap();
// Recursively add a key to every table in the configuration file, and confirm that each
// modified config file is rejected.
for modified_config in TableMutator::new(config_table) {
let serialized = toml::to_string(&modified_config).unwrap();
match toml::from_str::<Config>(&serialized) {
Ok(_) => panic!(
"config with spurious key was accepted:\n{}",
toml::to_string_pretty(&modified_config).unwrap()
),
Err(error) => assert!(
error
.message()
.starts_with("data did not match any variant")
|| error.message().starts_with("unknown field"),
"unexpected error: {error:?} for {modified_config:?}"
),
}
}
}
}

View File

@@ -1,17 +0,0 @@
#[macro_use]
mod authority_battery;
mod config_tests;
mod forwarder;
mod in_memory;
mod named_https_tests;
mod named_openssl_tests;
mod named_quic_tests;
mod named_rustls_tests;
mod named_test_rsa_dnssec;
mod named_tests;
mod server_harness;
mod sqlite_tests;
mod store_file_tests;
mod store_sqlite_tests;
mod timeout_stream_tests;
mod txt_tests;

View File

@@ -6,7 +6,9 @@
// copied, modified, or distributed except according to those terms.
#![cfg(not(windows))]
#![cfg(feature = "dns-over-https-rustls")]
#![cfg(feature = "dns-over-https")]
mod server_harness;
use std::env;
use std::fs::File;
@@ -14,20 +16,19 @@ use std::io::*;
use std::net::*;
use std::sync::Arc;
use rustls::pki_types::CertificateDer;
use rustls::{ClientConfig, RootCertStore};
use hickory_client::client::*;
use hickory_proto::h2::HttpsClientStreamBuilder;
use hickory_proto::iocompat::AsyncIoTokioAsStd;
use hickory_server::server::Protocol;
use rustls::{Certificate, ClientConfig, OwnedTrustAnchor, RootCertStore};
use tokio::net::TcpStream as TokioTcpStream;
use tokio::runtime::Runtime;
use crate::server_harness::{named_test_harness, query_a};
use hickory_client::client::Client;
use hickory_proto::h2::HttpsClientStreamBuilder;
use hickory_proto::runtime::TokioRuntimeProvider;
use hickory_proto::xfer::Protocol;
use test_support::subscribe;
use server_harness::{named_test_harness, query_a};
#[test]
fn test_example_https_toml_startup() {
subscribe();
// env_logger::try_init().ok();
const ALPN_H2: &[u8] = b"h2";
@@ -45,33 +46,47 @@ fn test_example_https_toml_startup() {
.expect("failed to read cert");
let mut io_loop = Runtime::new().unwrap();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, https_port.expect("no https_port")));
let addr: SocketAddr = ("127.0.0.1", https_port.expect("no https_port"))
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
// using the mozilla default root store
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
root_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
root_store.add(CertificateDer::from(cert_der)).unwrap();
let cert = to_trust_anchor(&cert_der);
root_store.add(&cert).unwrap();
let mut client_config =
ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.unwrap()
.with_root_certificates(root_store)
.with_no_client_auth();
let mut client_config = ClientConfig::builder()
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_safe_default_protocol_versions()
.unwrap()
.with_root_certificates(root_store)
.with_no_client_auth();
client_config.alpn_protocols.push(ALPN_H2.to_vec());
let client_config = Arc::new(client_config);
let provider = TokioRuntimeProvider::new();
let https_builder = HttpsClientStreamBuilder::with_client_config(client_config, provider);
let mp = https_builder.build(addr, "ns.example.com".to_string(), "/dns-query".to_string());
let client = Client::connect(mp);
let https_builder = HttpsClientStreamBuilder::with_client_config(client_config);
let mp = https_builder
.build::<AsyncIoTokioAsStd<TokioTcpStream>>(addr, "ns.example.com".to_string());
let client = AsyncClient::connect(mp);
// ipv4 should succeed
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
@@ -79,3 +94,7 @@ fn test_example_https_toml_startup() {
query_a(&mut io_loop, &mut client);
})
}
fn to_trust_anchor(cert_der: &[u8]) -> Certificate {
Certificate(cert_der.to_vec())
}

View File

@@ -11,20 +11,22 @@
// TODO: enable this test for rustls as well using below config
// #![cfg(feature = "dns-over-tls")]
mod server_harness;
use std::env;
use std::fs::File;
use std::io::*;
use std::net::*;
use native_tls::Certificate;
use tokio::net::TcpStream as TokioTcpStream;
use tokio::runtime::Runtime;
use hickory_client::client::*;
use hickory_proto::native_tls::TlsClientStreamBuilder;
use hickory_proto::runtime::TokioRuntimeProvider;
use hickory_proto::xfer::Protocol;
use crate::server_harness::{named_test_harness, query_a};
use hickory_proto::iocompat::AsyncIoTokioAsStd;
use server_harness::{named_test_harness, query_a};
#[test]
fn test_example_tls_toml_startup() {
@@ -37,9 +39,8 @@ fn test_example_tls_rustls_and_openssl_toml_startup() {
}
fn test_startup(toml: &'static str) {
named_test_harness(toml, move |socket_ports| {
named_test_harness(toml, move |_, _, tls_port, _, _| {
let mut cert_der = vec![];
let tls_port = socket_ports.get_v4(Protocol::Tls);
let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "..".to_owned());
println!("using server src path: {}", server_path);
@@ -51,27 +52,36 @@ fn test_startup(toml: &'static str) {
.read_to_end(&mut cert_der)
.expect("failed to read cert");
let provider = TokioRuntimeProvider::new();
let mut io_loop = Runtime::new().unwrap();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tls_port.expect("no tls_port")));
let mut tls_conn_builder = TlsClientStreamBuilder::new(provider.clone());
let addr: SocketAddr = ("127.0.0.1", tls_port.expect("no tls_port"))
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let mut tls_conn_builder =
TlsClientStreamBuilder::<AsyncIoTokioAsStd<TokioTcpStream>>::new();
let cert = to_trust_anchor(&cert_der);
tls_conn_builder.add_ca(cert);
let (stream, sender) = tls_conn_builder.build(addr, "ns.example.com".to_string());
let client = Client::new(stream, sender, None);
let client = AsyncClient::new(stream, sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tls_port.expect("no tls_port")));
let mut tls_conn_builder = TlsClientStreamBuilder::new(provider);
let addr: SocketAddr = ("127.0.0.1", tls_port.expect("no tls_port"))
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let mut tls_conn_builder =
TlsClientStreamBuilder::<AsyncIoTokioAsStd<TokioTcpStream>>::new();
let cert = to_trust_anchor(&cert_der);
tls_conn_builder.add_ca(cert);
let (stream, sender) = tls_conn_builder.build(addr, "ns.example.com".to_string());
let client = Client::new(stream, sender, None);
let client = AsyncClient::new(stream, sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv6 should succeed
query_a(&mut io_loop, &mut client);

View File

@@ -8,20 +8,21 @@
#![cfg(not(windows))]
#![cfg(feature = "dns-over-quic")]
use std::{env, fs::File, io::*, net::*, sync::Arc};
mod server_harness;
use rustls::{pki_types::CertificateDer, ClientConfig, RootCertStore};
use std::{env, fs::File, io::*, net::*};
use hickory_client::client::*;
use hickory_proto::quic::QuicClientStream;
use hickory_server::server::Protocol;
use rustls::{Certificate, ClientConfig, OwnedTrustAnchor, RootCertStore};
use tokio::runtime::Runtime;
use crate::server_harness::{named_test_harness, query_a};
use hickory_client::client::Client;
use hickory_proto::quic::QuicClientStream;
use hickory_proto::xfer::Protocol;
use test_support::subscribe;
use server_harness::{named_test_harness, query_a};
#[test]
fn test_example_quic_toml_startup() {
subscribe();
// env_logger::try_init().ok();
named_test_harness("dns_over_quic.toml", move |socket_ports| {
let mut cert_der = vec![];
@@ -37,30 +38,41 @@ fn test_example_quic_toml_startup() {
.expect("failed to read cert");
let mut io_loop = Runtime::new().unwrap();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, quic_port.expect("no quic_port")));
let addr: SocketAddr = ("127.0.0.1", quic_port.expect("no quic_port"))
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
// using the mozilla default root store
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
root_store.add(CertificateDer::from(cert_der)).unwrap();
root_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
let client_config =
ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.unwrap()
.with_root_certificates(root_store)
.with_no_client_auth();
let cert = to_trust_anchor(&cert_der);
root_store.add(&cert).unwrap();
let client_config = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let mut quic_builder = QuicClientStream::builder();
quic_builder.crypto_config(client_config);
let mp = quic_builder.build(addr, "ns.example.com".to_string());
let client = Client::connect(mp);
let client = AsyncClient::connect(mp);
// ipv4 should succeed
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
@@ -68,3 +80,7 @@ fn test_example_quic_toml_startup() {
query_a(&mut io_loop, &mut client);
})
}
fn to_trust_anchor(cert_der: &[u8]) -> Certificate {
Certificate(cert_der.to_vec())
}

View File

@@ -8,21 +8,26 @@
#![cfg(not(windows))]
#![cfg(feature = "dns-over-rustls")]
mod server_harness;
use std::env;
use std::fs::File;
use std::io::*;
use std::net::*;
use std::sync::Arc;
use rustls::pki_types::CertificateDer;
use rustls::{ClientConfig, RootCertStore};
use hickory_server::server::Protocol;
use rustls::Certificate;
use rustls::ClientConfig;
use rustls::RootCertStore;
use tokio::net::TcpStream as TokioTcpStream;
use tokio::runtime::Runtime;
use crate::server_harness::{named_test_harness, query_a};
use hickory_client::client::Client;
use hickory_proto::runtime::TokioRuntimeProvider;
use hickory_client::client::*;
use hickory_proto::iocompat::AsyncIoTokioAsStd;
use hickory_proto::rustls::tls_client_connect;
use hickory_proto::xfer::Protocol;
use server_harness::{named_test_harness, query_a};
#[test]
fn test_example_tls_toml_startup() {
@@ -42,47 +47,57 @@ fn test_example_tls_toml_startup() {
.expect("failed to read cert");
let mut io_loop = Runtime::new().unwrap();
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tls_port.expect("no tls_port")));
let mut root_store = RootCertStore::empty();
root_store
.add(CertificateDer::from(cert_der))
.expect("bad certificate");
let addr: SocketAddr = ("127.0.0.1", tls_port.expect("no tls_port"))
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let config = ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()
.unwrap()
.with_root_certificates(root_store)
.with_no_client_auth();
let cert = to_trust_anchor(&cert_der);
let mut root_store = RootCertStore::empty();
root_store.add(&cert).expect("bad certificate");
let config = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let config = Arc::new(config);
let provider = TokioRuntimeProvider::new();
let (stream, sender) = tls_client_connect(
let (stream, sender) = tls_client_connect::<AsyncIoTokioAsStd<TokioTcpStream>>(
addr,
"ns.example.com".to_string(),
config.clone(),
provider.clone(),
);
let client = Client::new(stream, sender, None);
let client = AsyncClient::new(stream, sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv4 should succeed
query_a(&mut io_loop, &mut client);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tls_port.expect("no tls_port")));
let (stream, sender) =
tls_client_connect(addr, "ns.example.com".to_string(), config, provider);
let client = Client::new(stream, sender, None);
let addr: SocketAddr = ("127.0.0.1", tls_port.expect("no tls_port"))
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let (stream, sender) = tls_client_connect::<AsyncIoTokioAsStd<TokioTcpStream>>(
addr,
"ns.example.com".to_string(),
config,
);
let client = AsyncClient::new(stream, sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv6 should succeed
query_a(&mut io_loop, &mut client);
},
)
}
fn to_trust_anchor(cert_der: &[u8]) -> Certificate {
Certificate(cert_der.to_vec())
}

View File

@@ -1,24 +1,26 @@
#![cfg(feature = "dnssec")]
#![cfg(not(windows))]
mod server_harness;
use std::env;
use std::fs::File;
use std::io::Read;
use std::net::*;
use std::path::Path;
use std::sync::Arc;
use hickory_server::server::Protocol;
use tokio::net::TcpStream as TokioTcpStream;
use tokio::runtime::Runtime;
use crate::server_harness::{
named_test_harness, query_a, query_all_dnssec_with_rfc6975, query_all_dnssec_wo_rfc6975,
};
use hickory_client::client::Client;
use hickory_proto::dnssec::{decode_key, Algorithm, KeyFormat, TrustAnchor};
use hickory_proto::runtime::{RuntimeProvider, TokioRuntimeProvider, TokioTime};
use hickory_proto::tcp::TcpClientStream;
use hickory_proto::xfer::{DnsExchangeBackground, DnsMultiplexer, Protocol};
use hickory_proto::DnssecDnsHandle;
use hickory_client::client::{Signer, *};
use hickory_client::proto::tcp::TcpClientStream;
use hickory_client::proto::DnssecDnsHandle;
use hickory_proto::rr::dnssec::*;
use hickory_proto::xfer::{DnsExchangeBackground, DnsMultiplexer};
use hickory_proto::{iocompat::AsyncIoTokioAsStd, TokioTime};
use server_harness::*;
#[cfg(all(not(feature = "dnssec-ring"), feature = "dnssec-openssl"))]
fn confg_toml() -> &'static str {
@@ -35,38 +37,41 @@ fn confg_toml() -> &'static str {
"all_supported_dnssec.toml"
}
fn trust_anchor(
public_key_path: &Path,
format: KeyFormat,
algorithm: Algorithm,
) -> Arc<TrustAnchor> {
fn trust_anchor(public_key_path: &Path, format: KeyFormat, algorithm: Algorithm) -> TrustAnchor {
let mut file = File::open(public_key_path).expect("key not found");
let mut buf = Vec::<u8>::new();
file.read_to_end(&mut buf).expect("could not read key");
let key_pair =
decode_key(&buf, Some("123456"), algorithm, format).expect("could not decode key");
let key_pair = format
.decode_key(&buf, Some("123456"), algorithm)
.expect("could not decode key");
let public_key = key_pair.to_public_key().unwrap();
let mut trust_anchor = TrustAnchor::new();
trust_anchor.insert_trust_anchor(&public_key);
Arc::new(trust_anchor)
trust_anchor
}
#[allow(clippy::type_complexity)]
async fn standard_tcp_conn<P: RuntimeProvider>(
async fn standard_tcp_conn(
port: u16,
provider: P,
) -> (
Client,
DnsExchangeBackground<DnsMultiplexer<TcpClientStream<P::Tcp>>, TokioTime>,
AsyncClient,
DnsExchangeBackground<
DnsMultiplexer<TcpClientStream<AsyncIoTokioAsStd<TokioTcpStream>>, Signer>,
TokioTime,
>,
) {
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider);
Client::new(stream, sender, None)
let addr: SocketAddr = ("127.0.0.1", port)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
AsyncClient::new(stream, sender, None)
.await
.expect("new Client failed")
.expect("new AsyncClient failed")
}
fn generic_test(config_toml: &str, key_path: &str, key_format: KeyFormat, algorithm: Algorithm) {
@@ -76,27 +81,26 @@ fn generic_test(config_toml: &str, key_path: &str, key_format: KeyFormat, algori
let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "..".to_owned());
let server_path = Path::new(&server_path);
let provider = TokioRuntimeProvider::new();
named_test_harness(config_toml, |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
// verify all records are present
let client = standard_tcp_conn(tcp_port.expect("no tcp port"), provider.clone());
let client = standard_tcp_conn(tcp_port.expect("no tcp port"));
let (client, bg) = io_loop.block_on(client);
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_all_dnssec_with_rfc6975(&mut io_loop, client, algorithm);
let client = standard_tcp_conn(tcp_port.expect("no tcp port"), provider.clone());
let client = standard_tcp_conn(tcp_port.expect("no tcp port"));
let (client, bg) = io_loop.block_on(client);
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_all_dnssec_wo_rfc6975(&mut io_loop, client, algorithm);
// test that request with Dnssec client is successful, i.e. validates chain
let trust_anchor = trust_anchor(&server_path.join(key_path), key_format, algorithm);
let client = standard_tcp_conn(tcp_port.expect("no tcp port"), provider);
let client = standard_tcp_conn(tcp_port.expect("no tcp port"));
let (client, bg) = io_loop.block_on(client);
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
let mut client = DnssecDnsHandle::with_trust_anchor(client, trust_anchor);
query_a(&mut io_loop, &mut client);

View File

@@ -5,46 +5,53 @@
// https://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
mod server_harness;
use std::io::Write;
use std::net::*;
use std::str::FromStr;
use tokio::net::TcpStream as TokioTcpStream;
use tokio::net::UdpSocket as TokioUdpSocket;
use tokio::runtime::Runtime;
use crate::server_harness::{named_test_harness, query_a, query_a_refused};
use hickory_client::client::{Client, ClientHandle};
use hickory_proto::op::ResponseCode;
use hickory_proto::rr::{DNSClass, Name, RecordType};
use hickory_proto::runtime::TokioRuntimeProvider;
use hickory_proto::tcp::TcpClientStream;
use hickory_proto::udp::UdpClientStream;
use hickory_proto::xfer::Protocol;
use test_support::subscribe;
use hickory_client::client::*;
use hickory_client::op::ResponseCode;
use hickory_client::rr::*;
use hickory_client::tcp::TcpClientStream;
use hickory_client::udp::UdpClientStream;
use hickory_server::server::Protocol;
use hickory_proto::iocompat::AsyncIoTokioAsStd;
use server_harness::{named_test_harness, query_a, query_a_refused};
#[test]
fn test_example_toml_startup() {
subscribe();
let provider = TokioRuntimeProvider::new();
named_test_harness("example.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
// just tests that multiple queries work
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
})
@@ -52,27 +59,32 @@ fn test_example_toml_startup() {
#[test]
fn test_ipv4_only_toml_startup() {
let provider = TokioRuntimeProvider::new();
named_test_harness("ipv4_only.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv4 should succeed
query_a(&mut io_loop, &mut client);
let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
assert!(io_loop.block_on(client).is_err());
//let (client, bg) = io_loop.block_on(client).expect("client failed to connect");
//hickory_proto::runtime::spawn_bg(&io_loop, bg);
//hickory_proto::spawn_bg(&io_loop, bg);
// ipv6 should fail
// FIXME: probably need to send something for proper test... maybe use JoinHandle in tokio 0.2
@@ -111,25 +123,30 @@ fn test_ipv4_only_toml_startup() {
#[test]
fn test_ipv4_and_ipv6_toml_startup() {
let provider = TokioRuntimeProvider::new();
named_test_harness("ipv4_and_ipv6.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv4 should succeed
query_a(&mut io_loop, &mut client);
let tcp_port = socket_ports.get_v6(Protocol::Tcp);
let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv6 should succeed
query_a(&mut io_loop, &mut client);
@@ -138,15 +155,17 @@ fn test_ipv4_and_ipv6_toml_startup() {
#[test]
fn test_nodata_where_name_exists() {
let provider = TokioRuntimeProvider::new();
named_test_harness("example.toml", |socket_ports| {
let io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
let msg = io_loop
.block_on(client.query(
@@ -162,15 +181,17 @@ fn test_nodata_where_name_exists() {
#[test]
fn test_nxdomain_where_no_name_exists() {
let provider = TokioRuntimeProvider::new();
named_test_harness("example.toml", |socket_ports| {
let io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
let msg = io_loop
.block_on(client.query(
@@ -186,21 +207,22 @@ fn test_nxdomain_where_no_name_exists() {
#[test]
fn test_server_continues_on_bad_data_udp() {
let provider = TokioRuntimeProvider::new();
named_test_harness("example.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let udp_port = socket_ports.get_v4(Protocol::Udp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, udp_port.expect("no udp_port")));
let stream = UdpClientStream::builder(addr, provider.clone()).build();
let client = Client::connect(stream);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
udp_port.expect("no udp_port"),
);
let stream = UdpClientStream::<TokioUdpSocket>::new(addr);
let client = AsyncClient::connect(stream);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
// Send a bad packet, this should get rejected by the server
let raw_socket = UdpSocket::bind(SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0))
let raw_socket = UdpSocket::bind(SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0))
.expect("couldn't bind raw");
raw_socket
@@ -208,12 +230,15 @@ fn test_server_continues_on_bad_data_udp() {
.expect("raw send failed");
// just tests that multiple queries work
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, udp_port.expect("no udp_port")));
let stream = UdpClientStream::builder(addr, provider).build();
let client = Client::connect(stream);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
udp_port.expect("no udp_port"),
);
let stream = UdpClientStream::<TokioUdpSocket>::new(addr);
let client = AsyncClient::connect(stream);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
})
@@ -221,16 +246,18 @@ fn test_server_continues_on_bad_data_udp() {
#[test]
fn test_server_continues_on_bad_data_tcp() {
let provider = TokioRuntimeProvider::new();
named_test_harness("example.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
@@ -242,11 +269,14 @@ fn test_server_continues_on_bad_data_tcp() {
.expect("raw send failed");
// just tests that multiple queries work
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
query_a(&mut io_loop, &mut client);
})
@@ -255,21 +285,23 @@ fn test_server_continues_on_bad_data_tcp() {
#[test]
#[cfg(feature = "resolver")]
fn test_forward() {
use crate::server_harness::query_message;
use hickory_proto::rr::rdata::A;
use server_harness::query_message;
subscribe();
let provider = TokioRuntimeProvider::new();
//env_logger::init();
named_test_harness("example_forwarder.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
let response = query_message(
&mut io_loop,
@@ -279,17 +311,20 @@ fn test_forward() {
)
.unwrap();
assert_eq!(
*response.answers()[0].data().as_a().unwrap(),
*response.answers()[0].data().and_then(RData::as_a).unwrap(),
A::new(93, 184, 215, 14)
);
// just tests that multiple queries work
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
let response = query_message(
&mut io_loop,
@@ -299,7 +334,7 @@ fn test_forward() {
)
.unwrap();
assert_eq!(
*response.answers()[0].data().as_a().unwrap(),
*response.answers()[0].data().and_then(RData::as_a).unwrap(),
A::new(93, 184, 215, 14)
);
assert!(!response.header().authoritative());
@@ -308,25 +343,30 @@ fn test_forward() {
#[test]
fn test_allow_networks_toml_startup() {
let provider = TokioRuntimeProvider::new();
named_test_harness("example_allow_networks.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv4 should succeed
query_a(&mut io_loop, &mut client);
let tcp_port = socket_ports.get_v6(Protocol::Tcp);
let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv6 should succeed
query_a(&mut io_loop, &mut client);
@@ -335,25 +375,30 @@ fn test_allow_networks_toml_startup() {
#[test]
fn test_deny_networks_toml_startup() {
let provider = TokioRuntimeProvider::new();
named_test_harness("example_deny_networks.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv4 should be refused
query_a_refused(&mut io_loop, &mut client);
let tcp_port = socket_ports.get_v6(Protocol::Tcp);
let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv6 should be refused
query_a_refused(&mut io_loop, &mut client);
@@ -362,25 +407,30 @@ fn test_deny_networks_toml_startup() {
#[test]
fn test_deny_allow_networks_toml_startup() {
let provider = TokioRuntimeProvider::new();
named_test_harness("example_deny_allow_networks.toml", |socket_ports| {
let mut io_loop = Runtime::new().unwrap();
let tcp_port = socket_ports.get_v4(Protocol::Tcp);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv4Addr::new(127, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv4 should succeed
query_a(&mut io_loop, &mut client);
let tcp_port = socket_ports.get_v6(Protocol::Tcp);
let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, tcp_port.expect("no tcp_port")));
let (stream, sender) = TcpClientStream::new(addr, None, None, provider.clone());
let client = Client::new(Box::new(stream), sender, None);
let addr: SocketAddr = SocketAddr::new(
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into(),
tcp_port.expect("no tcp_port"),
);
let (stream, sender) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(addr);
let client = AsyncClient::new(Box::new(stream), sender, None);
let (mut client, bg) = io_loop.block_on(client).expect("client failed to connect");
hickory_proto::runtime::spawn_bg(&io_loop, bg);
hickory_proto::spawn_bg(&io_loop, bg);
// ipv6 should be refused
query_a_refused(&mut io_loop, &mut client);

View File

@@ -13,13 +13,11 @@ use std::{
time::*,
};
use hickory_client::{client::*, error::ClientError, proto::xfer::DnsResponse};
#[cfg(feature = "dnssec")]
use hickory_client::client::Client;
use hickory_client::{client::ClientHandle, proto::xfer::DnsResponse, ClientError};
#[cfg(feature = "dnssec")]
use hickory_proto::dnssec::{Algorithm, SupportedAlgorithms};
use hickory_proto::rr::{rdata::A, DNSClass, Name, RData, RecordType};
use hickory_proto::xfer::Protocol;
use hickory_proto::rr::dnssec::*;
use hickory_proto::rr::{rdata::A, *};
use hickory_server::server::Protocol;
use regex::Regex;
use tokio::runtime::Runtime;
use tracing::{info, warn};
@@ -93,19 +91,16 @@ where
"hickory_dns=debug,hickory_client=debug,hickory_proto=debug,hickory_resolver=debug,hickory_server=debug",
)
.arg("-d")
.arg(format!(
.arg(&format!(
"--config={server_path}/tests/test-data/test_configs/{toml}"
))
.arg(format!(
.arg(&format!(
"--zonedir={server_path}/tests/test-data/test_configs"
))
.arg(format!("--port={}", 0));
#[cfg(feature = "dns-over-tls")]
command.arg(format!("--tls-port={}", 0));
#[cfg(feature = "dns-over-https-rustls")]
command.arg(format!("--https-port={}", 0));
#[cfg(feature = "dns-over-quic")]
command.arg(format!("--quic-port={}", 0));
.arg(&format!("--port={}", 0))
.arg(&format!("--tls-port={}", 0))
.arg(&format!("--https-port={}", 0))
.arg(&format!("--quic-port={}", 0));
println!("named cli options: {command:#?}");
@@ -188,9 +183,7 @@ where
"UDP" => socket_ports.put(Protocol::Udp, socket_addr),
"TCP" => socket_ports.put(Protocol::Tcp, socket_addr),
"TLS" => socket_ports.put(Protocol::Tls, socket_addr),
#[cfg(feature = "dns-over-https-rustls")]
"HTTPS" => socket_ports.put(Protocol::Https, socket_addr),
#[cfg(feature = "dns-over-quic")]
"QUIC" => socket_ports.put(Protocol::Quic, socket_addr),
_ => panic!("unsupported protocol: {proto}"),
}
@@ -255,7 +248,7 @@ pub fn query_a<C: ClientHandle>(io_loop: &mut Runtime, client: &mut C) {
let response = query_message(io_loop, client, name, RecordType::A).unwrap();
let record = &response.answers()[0];
if let RData::A(address) = record.data() {
if let Some(RData::A(ref address)) = record.data() {
assert_eq!(address, &A::new(127, 0, 0, 1))
} else {
panic!("wrong RDATA")
@@ -283,18 +276,15 @@ pub fn query_a_refused<C: ClientHandle>(io_loop: &mut Runtime, client: &mut C) {
#[cfg(feature = "dnssec")]
pub fn query_all_dnssec(
io_loop: &mut Runtime,
client: Client,
client: AsyncClient,
algorithm: Algorithm,
with_rfc6975: bool,
) {
use hickory_proto::{
dnssec::rdata::{DNSKEY, RRSIG},
rr::{Record, RecordData},
};
use hickory_client::rr::rdata::{DNSKEY, RRSIG};
let name = Name::from_str("example.com.").unwrap();
let mut client = MutMessageHandle::new(client);
client.lookup_options.set_dnssec_ok(true);
client.lookup_options.set_is_dnssec(true);
if with_rfc6975 {
client
.lookup_options
@@ -306,7 +296,7 @@ pub fn query_all_dnssec(
let dnskey = response
.answers()
.iter()
.map(Record::data)
.filter_map(Record::data)
.filter_map(DNSKEY::try_borrow)
.find(|d| d.algorithm() == algorithm);
assert!(dnskey.is_some(), "DNSKEY not found");
@@ -316,7 +306,7 @@ pub fn query_all_dnssec(
let rrsig = response
.answers()
.iter()
.map(Record::data)
.filter_map(Record::data)
.filter_map(RRSIG::try_borrow)
.filter(|rrsig| rrsig.algorithm() == algorithm)
.find(|rrsig| rrsig.type_covered() == RecordType::DNSKEY);
@@ -325,12 +315,20 @@ pub fn query_all_dnssec(
#[allow(dead_code)]
#[cfg(feature = "dnssec")]
pub fn query_all_dnssec_with_rfc6975(io_loop: &mut Runtime, client: Client, algorithm: Algorithm) {
pub fn query_all_dnssec_with_rfc6975(
io_loop: &mut Runtime,
client: AsyncClient,
algorithm: Algorithm,
) {
query_all_dnssec(io_loop, client, algorithm, true)
}
#[allow(dead_code)]
#[cfg(feature = "dnssec")]
pub fn query_all_dnssec_wo_rfc6975(io_loop: &mut Runtime, client: Client, algorithm: Algorithm) {
pub fn query_all_dnssec_wo_rfc6975(
io_loop: &mut Runtime,
client: AsyncClient,
algorithm: Algorithm,
) {
query_all_dnssec(io_loop, client, algorithm, false)
}

View File

@@ -1,14 +1,12 @@
use hickory_client::client::ClientHandle;
use hickory_proto::xfer::{DnsHandle, DnsRequest};
#[cfg(feature = "dnssec")]
use hickory_proto::{op::Edns, rr::rdata::opt::EdnsOption};
use hickory_client::client::*;
use hickory_client::proto::xfer::{DnsHandle, DnsRequest};
#[cfg(feature = "dnssec")]
use hickory_client::{op::Edns, rr::rdata::opt::EdnsOption};
use hickory_server::authority::LookupOptions;
#[derive(Clone)]
pub struct MutMessageHandle<C: ClientHandle + Unpin> {
client: C,
#[cfg(feature = "dnssec")]
pub lookup_options: LookupOptions,
}
@@ -17,7 +15,6 @@ impl<C: ClientHandle + Unpin> MutMessageHandle<C> {
pub fn new(client: C) -> Self {
MutMessageHandle {
client,
#[cfg(feature = "dnssec")]
lookup_options: Default::default(),
}
}

View File

@@ -1 +0,0 @@
/target

981
conformance/Cargo.lock generated
View File

@@ -1,981 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"serde",
"windows-targets",
]
[[package]]
name = "conformance-tests"
version = "0.1.0"
dependencies = [
"base64 0.21.7",
"dns-test",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "ctrlc"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b"
dependencies = [
"nix",
"windows-sys",
]
[[package]]
name = "darling"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dns-test"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"ctrlc",
"lazy_static",
"minijinja",
"pretty_assertions",
"serde",
"serde_json",
"serde_with",
"tempfile",
"url",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
"serde",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
version = "0.3.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "minijinja"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe0ff215195a22884d867b547c70a0c4815cbbcc70991f281dca604b20d10ce"
dependencies = [
"serde",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
"libc",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "rustix"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "ryu"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "serde"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_with"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270"
dependencies = [
"base64 0.21.7",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.2.3",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tempfile"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix",
"windows-sys",
]
[[package]]
name = "time"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "url"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "wasm-bindgen"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "yoke"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@@ -1,3 +0,0 @@
[workspace]
members = ["packages/*"]
resolver = "2"

View File

@@ -1,169 +0,0 @@
# Conformance tests
This workspace contains two packages:
- `dns-test`. This is a test framework (library) for testing DNS implementations.
- `conformance-tests`. This is a collection of DNS, mainly DNSSEC, tests.
## Requirements
To use the code in this workspace you need:
- a stable Rust toolchain to build the code
- a working Docker setup that can run *Linux* containers -- the host OS does not need to be Linux
## `dns-test`
This test framework was built with the following design goals and constraints in mind:
- Tests must work without access to the internet. That is, tests cannot rely on external services like `1.1.1.1`, `8.8.8.8`, `a.root-servers.net.`, etc. To this effect, each test runs into its own ephemeral network isolated from the internet and from the networks of other tests running concurrently.
- Test code must be decoupled from the API of any DNS implementation. That is, DNS implementation specific details (library/FFI calls, configuration files) must not appear in test code. To this end, interaction with DNS implementations is done at the network level using tools like `dig`, `delv` and `tshark`.
- It must be possible to switch the 'implementation under test' at runtime. In other words, one should not need to recompile the tests to switch the DNS implementation being tested. To this end, the `DNS_TEST_SUBJECT` environment variable is used to switch the DNS implementation that'll be tested.
### Test drive
To start a small DNS network using the `dns-test` framework run this command and follow the instructions to interact with the DNS network.
``` console
$ cargo run --example explore
```
By default, this will use `unbound` as the resolver. You can switch the resolver to `hickory-dns` using the `DNS_TEST_SUBJECT` environment variable:
``` shell
$ DNS_TEST_SUBJECT="hickory https://github.com/hickory-dns/hickory-dns" cargo run --example explore
```
### Environment variables
- `DNS_TEST_SUBJECT`. This variable controls what the `dns_test::subject` function returns. The variable can contain one of these values:
- `unbound`
- `hickory $REPOSITORY`. where `$REPOSITORY` is a placeholder for a git repository. Examples values for `$REPOSITORY`: `https://github.com/hickory-dns/hickory-dns`; `/home/user/git-repos/hickory-dns`. NOTE: when using a local repository, changes that have not been committed, regardless of whether they are staged or not, will **not** be included in the `hickory-dns` build.
- `DNS_TEST_VERBOSE_DOCKER_BUILD`. Setting this variable prints the output of the `docker build` invocations that the framework does to the console. This is useful to verify that image caching is working; for example if you set `DNS_TEST_SUBJECT` to a local `hickory-dns` repository then consecutively running the `explore` example and/or `conformance-tests` test suite **must** not rebuild `hickory-dns` provided that you have not *committed* any new change to the local repository.
- `DNS_TEST_SKIP_DOCKER_BUILD`. Setting this variable skips running `docker build`. This should only be used if containers have been built recently.
### Automatic clean-up
`dns-test` has been designed to clean up, that is remove, the Docker containers and Docker networks that it creates.
If you use `dns-test` and it does not clean up Docker resources, that's a bug that should be reported.
`dns-test` uses destructors (the `Drop` trait) to clean up resources.
If you forcefully terminate a process, e.g. using Ctrl+C or a signal like SIGINT, that uses `dns-test` then the destructors won't run and Docker resources won't be cleaned up.
Note that `cargo watch` terminates the last process using signals before starting a new instance of it.
Therefore we advise against using `cargo watch` to *run* tests that use the `dns-test` framework;
using `cargo-watch` to `check` such tests is perfectly fine, however.
### Writing tests
Here are some considerations when writing tests.
- Both `unbound` and BIND, in the resolver role, will initially query for the A record of their configured root server's FQDN as well as the A records of all the name servers covering the zones required to resolve the root server's FQDN. As of [49c89f7], All the name servers have a FQDN of the form `primaryNNN.nameservers.com.`, where `NNN` is a non-negative integer. These initial `A primaryNNN.nameservers.com.` queries will be sent to the name server that covers the `nameservers.com.` zone. What all this means in practice, is that you'll need to add these A records -- the root server's, `com.`'s name server and `nameservers.com.`'s name server -- to the `nameservers.com.` zone file; if you don't, most queries (expect perhaps for `SOA .`) will fail to resolve with return code SERVFAIL.
[49c89f7]: https://github.com/ferrous-systems/dnssec-tests/commit/49c89f764ede89aefe578b799e7766f051a600cc
``` rust
let root_ns: NameServer; // for `.` zone
let com_ns: NameServer; // for `com.` zone
let nameservers_ns: NameServer; // for `nameservers.com.` zone
nameservers_ns
.add(root_ns.a())
.add(com_ns.a());
// each `NameServer` will start out with an A record of its FQDN to its own IPv4 address in its
// zone file so NO need to add that one in the preceding statement
```
- To get resolution to work, you need referrals -- in the form of NS and A record pairs -- from parent zones to child zones. Check the [`dns::scenarios::can_resolve`] for an example of how to set up referrals.
[`dns::scenarios::can_resolve`]: https://github.com/ferrous-systems/dnssec-tests/blob/49c89f764ede89aefe578b799e7766f051a600cc/packages/conformance-tests/src/resolver/dns/scenarios.rs#L10
- To get DNSSEC validation to work, you need the DS record of the child zone in the parent zone. Furthermore, the DS record needs to be signed using parent zone's key. Check the [`dnssec::scenarios::secure::can_validate_with_delegation`] for an example of how to set up the DS records.
[`dnssec::scenarios::secure::can_validate_with_delegation`]: https://github.com/ferrous-systems/dnssec-tests/blob/49c89f764ede89aefe578b799e7766f051a600cc/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs#L48
- You can get the logs of both a `Resolver` and `NameServer` using the `terminate` method. This method terminates the server and returns all the logs. This can be useful when trying to figure out why a query is not producing the expected results.
``` rust
let resolver: Resolver;
let ans = client.dig(/* .. */);
let logs = resolver.terminate()?;
// print the logs to figure out ...
eprintln!("{logs}");
// ... why this assertion is not working
assert!(ans.status.is_noerror());
```
## `conformance-tests`
This is a collection of tests that check the conformance of a DNS implementation to the different RFCs around DNS and DNSSEC.
### Running the test suite
To run the conformance tests against `unbound` run:
``` console
$ cargo test -p conformance-tests -- --include-ignored
```
To run the conformance tests against `hickory-dns` run:
``` console
$ DNS_TEST_SUBJECT="hickory /path/to/repository" cargo test -p conformance-tests
```
### Test organization
The module organization is not yet set in stone but currently uses the following structure:
``` console
packages/conformance-tests/src
├── lib.rs
├── resolver
│ ├── dns
│ │ └── scenarios.rs
│ ├── dns.rs
│ ├── dnssec
│ │ ├── rfc4035
│ │ │ ├── section_4
│ │ │ │ └── section_4_1.rs
│ │ │ └── section_4.rs
│ │ ├── rfc4035.rs
│ │ └── scenarios.rs
│ └── dnssec.rs
└── resolver.rs
```
The modules in the root correspond to the *role* being tested: `resolver` (recursive resolver), `name-server` (authoritative-only name server), etc.
The next module level contains the *functionality* being tested: (plain) DNS, DNSSEC, NSEC3, etc.
The next module level contains the RFC documents, whose requirements are being tested: RFC4035, etc.
The next module levels contain sections, subsections and any other subdivision that may be relevant.
At the RFC module level there's a special module called `scenarios`. This module contains tests that map to representative use cases of the parent functionality. Each use case can be tested in successful and failure scenarios, hence the name. The organization within this module will be ad hoc.
### Adding tests and the use of `#[ignore]`
When adding a new test to the test suite, it must pass with the `unbound` implementation, which is treated as the *reference* implementation. The CI workflow will check that *all* tests, including the ones that have the `#[ignore]` attribute, pass with the `unbound` implementation.
New tests that don't pass with the `hickory-dns` implementation must be marked as `#[ignore]`-d. The CI workflow will check that non-`#[ignore]`-d tests pass with the `hickory-dns` implementation. Additionally, the CI workflow will check that all `#[ignore]`-d tests *fail* with the `hickory-dns` implementation; this is to ensure that fixed tests get un-`#[ignore]`-d.
## License
Licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <https://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
at your option.

View File

@@ -1,13 +0,0 @@
[package]
edition = "2021"
license = "MIT OR Apache-2.0"
name = "conformance-tests"
publish = false
version = "0.1.0"
[dependencies]
base64 = "0.21.7"
dns-test.path = "../dns-test"
[lib]
doctest = false

View File

@@ -1,4 +0,0 @@
#![cfg(test)]
mod name_server;
mod resolver;

View File

@@ -1,3 +0,0 @@
mod rfc4035;
mod rfc5155;
mod scenarios;

View File

@@ -1 +0,0 @@
mod section_3;

View File

@@ -1,63 +0,0 @@
use dns_test::client::{Client, DigSettings};
use dns_test::name_server::NameServer;
use dns_test::record::{Record, RecordType};
use dns_test::zone_file::SignSettings;
use dns_test::{Network, Result, FQDN};
#[test]
fn rrsig_in_answer_section() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)?
.sign(SignSettings::default())?
.start()?;
let client = Client::new(&network)?;
let ns_fqdn = ns.fqdn();
let ans = client.dig(
*DigSettings::default().dnssec(),
ns.ipv4_addr(),
RecordType::A,
ns_fqdn,
)?;
assert!(ans.status.is_noerror());
let [a, rrsig] = ans.answer.try_into().unwrap();
assert!(matches!(a, Record::A(..)));
let rrsig = rrsig.try_into_rrsig().unwrap();
assert_eq!(RecordType::A, rrsig.type_covered);
assert_eq!(ns_fqdn, &rrsig.fqdn);
Ok(())
}
#[test]
fn rrsig_in_authority_section() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)?
.sign(SignSettings::default())?
.start()?;
let client = Client::new(&network)?;
let ans = client.dig(
*DigSettings::default().dnssec(),
ns.ipv4_addr(),
RecordType::SOA,
&FQDN::ROOT,
)?;
assert!(ans.status.is_noerror());
let [ns, rrsig] = ans.authority.try_into().unwrap();
assert!(matches!(ns, Record::NS(..)));
let rrsig = rrsig.try_into_rrsig().unwrap();
assert_eq!(RecordType::NS, rrsig.type_covered);
assert_eq!(FQDN::ROOT, rrsig.fqdn);
Ok(())
}
// TODO Additional section
// TODO TC bit

View File

@@ -1,341 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::client::{Client, DigSettings, DigStatus};
use dns_test::name_server::NameServer;
use dns_test::nsec3::NSEC3Records;
use dns_test::record::{Record, RecordType, NSEC3};
use dns_test::zone_file::SignSettings;
use dns_test::{Network, Result, FQDN};
const TLD_FQDN: &str = "alice.com.";
const NON_EXISTENT_FQDN: &str = "charlie.alice.com.";
const WILDCARD_FQDN: &str = "*.alice.com.";
// These hashes are computed with 1 iteration of SHA-1 without salt and must be recomputed if
// those parameters were to change.
const TLD_HASH: &str = "LLKH4L6I60VHAPP6VRM3DFR9RI8AK9I0"; /* h(alice.com.) */
const NON_EXISTENT_HASH: &str = "99P1CCPQ2N64LIRMT2838O4HK0QFA51B"; /* h(charlie.alice.com.) */
const WILDCARD_HASH: &str = "19GBV5V1BO0P51H34JQDH1C8CIAA5RAQ"; /* h(*.alice.com.) */
// This test checks that name servers produce a name error response compliant with section 7.2.2.
// of RFC5155.
#[test]
fn name_error_response() -> Result<()> {
let alice_fqdn = FQDN(TLD_FQDN)?;
// The queried name
let qname = FQDN(NON_EXISTENT_FQDN)?;
let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::A,
)?;
assert!(status.is_nxdomain());
// Closest Encloser Proof
//
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
// existing RR.
//
// The next closer name of a name is the name one label longer than its closest encloser. In
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
// If this panics, it probably means that the precomputed hashes must be recomputed.
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_HASH)
.expect("Cannot find a closest encloser proof in the zonefile");
// Wildcard at the closet encloser RR: Must cover the wildcard at the closest encloser of
// QNAME.
//
// In this scenario, the closest encloser is `alice.com.`, so the wildcard at the closer
// encloser is `*.alice.com.`.
//
// This NSEC3 RR must cover the hash of the wildcard at the closests encloser.
// if this panics, it probably means that the precomputed hashes must be recomputed.
let wildcard_rr = nsec3_rrs
.find_cover(WILDCARD_HASH)
.expect("No RR in the zonefile covers the wildcard");
// Now we check that the response has the three NSEC3 RRs.
find_records(
&nsec3_rrs_response,
[
(
closest_encloser_rr,
"No RR in the response matches the closest encloser",
),
(
next_closer_name_rr,
"No RR in the response covers the next closer name",
),
(wildcard_rr, "No RR in the response covers the wildcard"),
],
);
Ok(())
}
// This test checks that name servers produce a no data response compliant with section 7.2.3.
// of RFC5155 when the query type is not DS.
#[test]
fn no_data_response_not_ds() -> Result<()> {
let alice_fqdn = FQDN(TLD_FQDN)?;
// The queried name
let qname = alice_fqdn.clone();
let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::MX,
)?;
assert!(status.is_noerror());
// The server MUST include the NSEC3 RR that matches QNAME.
// if this panics, it probably means that the precomputed hashes must be recomputed.
let qname_rr = nsec3_rrs
.find_match(TLD_HASH)
.expect("No RR in the zonefile matches QNAME");
find_records(
&nsec3_rrs_response,
[(qname_rr, "No RR in the response matches QNAME")],
);
Ok(())
}
// This test checks that name servers produce a no data response compliant with section 7.2.4.
// of RFC5155 when the query type is DS and there is an NSEC3 RR that matches the queried name.
#[test]
fn no_data_response_ds_match() -> Result<()> {
let alice_fqdn = FQDN(TLD_FQDN)?;
// The queried name
let qname = alice_fqdn.clone();
let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::DS,
)?;
assert!(status.is_noerror());
// If there is an NSEC3 RR that matches QNAME, the server MUST return it in the response.
// if this panics, it probably means that the precomputed hashes must be recomputed.
let qname_rr = nsec3_rrs
.find_match(TLD_HASH)
.expect("No RR in the zonefile matches QNAME");
find_records(
&nsec3_rrs_response,
[(qname_rr, "No RR in the response matches QNAME")],
);
Ok(())
}
// This test checks that name servers produce a no data response compliant with section 7.2.4.
// of RFC5155 when the query type is DS and no NSEC3 RR matches the queried name.
#[test]
fn no_data_response_ds_no_match() -> Result<()> {
let alice_fqdn = FQDN(TLD_FQDN)?;
// The queried name
let qname = FQDN(NON_EXISTENT_FQDN)?;
let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
[Record::a(alice_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::DS,
)?;
assert!(status.is_nxdomain());
// If no NSEC3 RR matches QNAME, the server MUST return a closest provable encloser proof for
// QNAME.
// Closest Encloser Proof
//
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
// existing RR.
//
// The next closer name of a name is the name one label longer than its closest encloser. In
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
// If this panics, it probably means that the precomputed hashes must be recomputed.
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_HASH)
.expect("Cannot find a closest encloser proof in the zonefile");
find_records(
&nsec3_rrs_response,
[
(
closest_encloser_rr,
"No RR in the response matches the closest encloser",
),
(
next_closer_name_rr,
"No RR in the response covers the next closer name",
),
],
);
Ok(())
}
// This test checks that name servers produce a wildcard no data response compliant with section 7.2.5.
#[test]
#[ignore]
fn wildcard_no_data_response() -> Result<()> {
let wildcard_fqdn = FQDN(WILDCARD_FQDN)?;
// The queried name
let qname = FQDN(NON_EXISTENT_FQDN)?;
let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
[Record::a(wildcard_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::MX,
)?;
assert!(status.is_noerror());
// If there is a wildcard match for QNAME, but QTYPE is not present at that name, the response MUST
// include a closest encloser proof for QNAME and MUST include the NSEC3 RR that matches the
// wildcard.
// Closest Encloser Proof
//
// The closest encloser of a name is its longest existing ancestor. In this scenario, the
// closest encloser of `charlie.alice.com.` is `alice.com.` as this is the longest ancestor with an
// existing RR.
//
// The next closer name of a name is the name one label longer than its closest encloser. In
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
// If this panics, it probably means that the precomputed hashes must be recomputed.
let (closest_encloser_rr, next_closer_name_rr) = nsec3_rrs
.closest_encloser_proof(TLD_HASH, NON_EXISTENT_HASH)
.expect("Cannot find a closest encloser proof in the zonefile");
// Wildcard RR: This NSEC3 RR must match `*.alice.com`.
// If this panics, it probably means that the precomputed hashes must be recomputed.
let wildcard_rr = nsec3_rrs
.find_match(WILDCARD_HASH)
.expect("No RR in the zonefile matches the wildcard");
find_records(
&nsec3_rrs_response,
[
(
closest_encloser_rr,
"No RR in the response matches the closest encloser",
),
(
next_closer_name_rr,
"No RR in the response covers the next closer name",
),
(wildcard_rr, "No RR in the response covers the wildcard"),
],
);
Ok(())
}
// This test checks that name servers produce a wildcard answer response compliant with section 7.2.6.
#[test]
fn wildcard_answer_response() -> Result<()> {
let wildcard_fqdn = FQDN(WILDCARD_FQDN)?;
// The queried name
let qname = FQDN(NON_EXISTENT_FQDN)?;
let (nsec3_rrs, status, nsec3_rrs_response) = query_nameserver(
[Record::a(wildcard_fqdn, Ipv4Addr::new(1, 2, 3, 4))],
&qname,
RecordType::A,
)?;
assert!(status.is_noerror());
// If there is a wildcard match for QNAME and QTYPE, then, in addition to the expanded wildcard
// RRSet returned in the answer section of the response, proof that the wildcard match was
// valid must be returned. ... To this end, the NSEC3 RR that covers the "next closer" name of the
// immediate ancestor of the wildcard MUST be returned.
// The next closer name of a name is the name one label longer than its closest encloser. In
// this scenario, the closest encloser is `alice.com.` which means that the next closer name is `charlie.alice.com.`
// If this panics, it probably means that the precomputed hashes must be recomputed.
let next_closer_name_rr = nsec3_rrs
.find_cover(NON_EXISTENT_HASH)
.expect("No RR in the zonefile covers the next closer name");
find_records(
&nsec3_rrs_response,
[(
next_closer_name_rr,
"No RR in the response covers the next closer name",
)],
);
Ok(())
}
fn query_nameserver(
records: impl IntoIterator<Item = Record>,
qname: &FQDN,
qtype: RecordType,
) -> Result<(NSEC3Records, DigStatus, Vec<NSEC3>)> {
let network = Network::new()?;
let mut ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)?;
for record in records {
ns.add(record);
}
let sign_settings = SignSettings::default();
let ns = ns.sign(sign_settings)?;
let nsec3_rrs = NSEC3Records::new(ns.signed_zone_file());
let ns = ns.start()?;
let client = Client::new(&network)?;
let output_res = client.dig(
*DigSettings::default().dnssec().authentic_data(),
ns.ipv4_addr(),
qtype,
qname,
);
if output_res.is_err() {
println!("{}", ns.logs().unwrap());
}
let output = output_res?;
let nsec3_rrs_response = output
.authority
.into_iter()
.filter_map(|rr| rr.try_into_nsec3().ok())
.collect::<Vec<_>>();
Ok((nsec3_rrs, output.status, nsec3_rrs_response))
}
#[track_caller]
fn find_records<'a>(
records: &[NSEC3],
records_and_err_msgs: impl IntoIterator<Item = (&'a NSEC3, &'a str)>,
) {
for (record, err_msg) in records_and_err_msgs {
records.iter().find(|&rr| rr == record).expect(err_msg);
}
}

View File

@@ -1,23 +0,0 @@
use dns_test::client::{Client, DigSettings};
use dns_test::name_server::NameServer;
use dns_test::record::RecordType;
use dns_test::{Network, Result, FQDN};
#[test]
fn authoritative_answer() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, network)?.start()?;
let client = Client::new(network)?;
let ans = client.dig(
DigSettings::default(),
ns.ipv4_addr(),
RecordType::SOA,
&FQDN::ROOT,
)?;
assert!(ans.status.is_noerror());
assert!(ans.flags.authoritative_answer);
Ok(())
}

View File

@@ -1,5 +0,0 @@
//! Recursive resolver role
mod dns;
mod dnssec;
mod nsec;

View File

@@ -1,4 +0,0 @@
//! plain DNS functionality
mod rfc1035;
mod scenarios;

View File

@@ -1 +0,0 @@
mod truncation;

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env python3
# This server replies with the truncated bit set over UDP, and changes the
# record set it returns each time. This allows tests to detect improper
# caching of truncated responses.
from dnslib import DNSLabel, DNSRecord, QTYPE, RCODE, RR, TXT
from dnslib.server import BaseResolver, DNSHandler, DNSServer
class Resolver(BaseResolver):
def __init__(self):
self.counter = 0
self.expected_name = DNSLabel("example.testing.")
def resolve(self, request: DNSRecord, _handler: DNSHandler) -> DNSRecord:
reply = request.reply()
if request.q.qname == self.expected_name:
reply.header.rcode = RCODE.NOERROR
if request.q.qtype == QTYPE.TXT:
rdata = TXT(f"counter={self.counter}".encode("ASCII"))
self.counter += 1
reply.add_answer(RR(
request.q.qname,
QTYPE.TXT,
ttl=86400,
rdata=rdata,
))
reply.header.tc = 1
else:
reply.header.rcode = RCODE.NXDOMAIN
return reply
if __name__ == "__main__":
resolver = Resolver()
server = DNSServer(resolver, address="0.0.0.0", port=53)
server.start()

View File

@@ -1,46 +0,0 @@
#!/usr/bin/env python3
# This server listens on both TCP and UDP ports. It always responds to UDP
# requests with the truncated bit set. The TCP server will allow clients to
# fall back and get a non-truncated response. The record set returned by the
# server changes each time, to allow tests to detect improper caching of
# truncated responses.
from dnslib import DNSLabel, DNSRecord, QTYPE, RCODE, RR, TXT
from dnslib.server import BaseResolver, DNSHandler, DNSServer
class Resolver(BaseResolver):
def __init__(self, tcp):
self.tcp = tcp
self.counter = 0
self.expected_name = DNSLabel("example.testing.")
def resolve(self, request: DNSRecord, _handler: DNSHandler) -> DNSRecord:
reply = request.reply()
if request.q.qname == self.expected_name:
reply.header.rcode = RCODE.NOERROR
if request.q.qtype == QTYPE.TXT:
counter_text = f"counter={self.counter}".encode("ASCII")
self.counter += 1
if self.tcp:
rdata = TXT([b"protocol=TCP", counter_text])
else:
reply.header.tc = 1
rdata = TXT([b"protocol=UDP", counter_text])
reply.add_answer(RR(
request.q.qname,
QTYPE.TXT,
ttl=86400,
rdata=rdata,
))
else:
reply.header.rcode = RCODE.NXDOMAIN
return reply
if __name__ == "__main__":
udp_resolver = Resolver(False)
udp_server = DNSServer(udp_resolver, address="0.0.0.0", port=53)
udp_server.start_thread()
tcp_resolver = Resolver(True)
tcp_server = DNSServer(tcp_resolver, address="0.0.0.0", port=53, tcp=True)
tcp_server.start()

View File

@@ -1,161 +0,0 @@
//! Section 7.4, "Using the cache" says in part, "When several RRs of the same type are available
//! for a particular owner name, the resolver should either cache them all or none at all. When a
//! response is truncated, and a resolver doesn't know whether it has a complete set, it should not
//! cache a possibly partial set of RRs."
use std::{fs, thread, time::Duration};
use dns_test::{
client::{Client, DigSettings, DigStatus},
name_server::{Graph, NameServer},
record::{Record, RecordType},
Implementation, Network, Resolver, Result, FQDN,
};
/// Verify that resolvers will retry a query over TCP if they get a truncated response via UDP, and
/// only cache the complete TCP response.
#[test]
fn truncated_response_caching_with_tcp_fallback() -> Result<()> {
let target_fqdn = FQDN("example.testing.")?;
let (resolver, client, _graph) =
setup("src/resolver/dns/rfc1035/truncated_with_tcp_fallback.py")?;
let dig_settings = *DigSettings::default().recurse().timeout(7);
let result_1 = client.dig(
dig_settings,
resolver.ipv4_addr(),
RecordType::TXT,
&target_fqdn,
);
let response_1 = result_1
.unwrap_or_else(|e| panic!("error {e:?} resolver logs: {}", resolver.logs().unwrap()));
println!("first response: {response_1:?}");
let (protocol_1, counter_1) = parse_txt_records(&response_1.answer)?;
assert_eq!(response_1.status, DigStatus::NOERROR);
// Check that the resolver fell back to TCP.
assert_eq!(protocol_1.as_deref(), Some("TCP"));
assert!(counter_1.is_some());
let result_2 = client.dig(
dig_settings,
resolver.ipv4_addr(),
RecordType::TXT,
&target_fqdn,
);
let response_2 = result_2
.unwrap_or_else(|e| panic!("error {e:?} resolver logs: {}", resolver.logs().unwrap()));
println!("second response: {response_2:?}");
let (protocol_2, counter_2) = parse_txt_records(&response_2.answer)?;
println!("{}", resolver.logs()?);
assert_eq!(response_2.status, DigStatus::NOERROR);
// Check that we got a cached response.
assert_eq!(protocol_2.as_deref(), Some("TCP"));
assert_eq!(counter_1, counter_2);
Ok(())
}
/// Verify that resolvers will not cache a truncated response received via UDP if the authoritative
/// server does not reply to TCP fallback queries.
#[test]
#[ignore = "hickory caches a truncated response if querying over TCP fails"]
fn truncated_response_caching_udp_only() -> Result<()> {
let target_fqdn = FQDN("example.testing.")?;
let (resolver, client, _graph) = setup("src/resolver/dns/rfc1035/truncated_udp_only.py")?;
let dig_settings = *DigSettings::default().recurse().timeout(7);
let result_1 = client.dig(
dig_settings,
resolver.ipv4_addr(),
RecordType::TXT,
&target_fqdn,
);
let response_1 = result_1
.unwrap_or_else(|e| panic!("error {e:?} resolver logs: {}", resolver.logs().unwrap()));
println!("first response: {response_1:?}");
let (_protocol_1, counter_1) = parse_txt_records(&response_1.answer)?;
if response_1.status == DigStatus::SERVFAIL {
// Unbound and BIND return an error instead of returning the truncated UDP response, if there's no
// reply via TCP.
return Ok(());
}
assert_eq!(response_1.status, DigStatus::NOERROR);
let result_2 = client.dig(
dig_settings,
resolver.ipv4_addr(),
RecordType::TXT,
&target_fqdn,
);
let response_2 = result_2
.unwrap_or_else(|e| panic!("error {e:?} resolver logs: {}", resolver.logs().unwrap()));
println!("second response: {response_2:?}");
let (_protocol_2, counter_2) = parse_txt_records(&response_2.answer)?;
println!("{}", resolver.logs()?);
assert_eq!(response_2.status, DigStatus::NOERROR);
// Check that the resolver did not cache the truncated response.
assert_ne!(counter_1, counter_2);
Ok(())
}
fn setup(script_path: &str) -> Result<(Resolver, Client, Graph)> {
let network = Network::new()?;
let mut root_ns = NameServer::new(&Implementation::test_peer(), FQDN::ROOT, &network)?;
let leaf_ns = NameServer::new(&Implementation::Dnslib, FQDN::TEST_TLD, &network)?;
let script = fs::read_to_string(script_path)?;
leaf_ns.cp("/script.py", &script)?;
root_ns.referral_nameserver(&leaf_ns);
let root_hint = root_ns.root_hint();
let resolver = Resolver::new(&network, root_hint.clone()).start()?;
let client = Client::new(resolver.network())?;
let root_ns = root_ns.start()?;
let leaf_ns = leaf_ns.start()?;
thread::sleep(Duration::from_secs(2));
let graph = Graph {
nameservers: vec![root_ns, leaf_ns],
root: root_hint,
trust_anchor: None,
};
Ok((resolver, client, graph))
}
/// Parse the protocol name and counter value from the dnslib-based name server's response.
fn parse_txt_records(records: &[Record]) -> Result<(Option<String>, Option<u64>)> {
let mut protocol = None;
let mut counter = None;
for record in records.iter() {
let Record::TXT(text) = record else {
continue;
};
for string in text.character_strings.iter() {
if let Some(protocol_str) = string.strip_prefix("protocol=") {
protocol = Some(protocol_str.to_string());
}
if let Some(counter_str) = string.strip_prefix("counter=") {
counter = Some(counter_str.parse::<u64>().unwrap());
}
}
}
Ok((protocol, counter))
}

View File

@@ -1,131 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::client::{Client, DigSettings};
use dns_test::name_server::{Graph, NameServer, Sign};
use dns_test::record::{Record, RecordType};
use dns_test::tshark::{Capture, Direction};
use dns_test::{Network, Resolver, Result, FQDN};
mod bad_referral;
mod packet_loss;
#[test]
fn can_resolve() -> Result<()> {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
let Graph {
nameservers: _nameservers,
root,
..
} = Graph::build(leaf_ns, Sign::No)?;
let resolver = Resolver::new(&network, root).start()?;
let resolver_ip_addr = resolver.ipv4_addr();
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse();
let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?;
assert!(output.status.is_noerror());
let [answer] = output.answer.try_into().unwrap();
let a = answer.try_into_a().unwrap();
assert_eq!(needle_fqdn, a.fqdn);
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
Ok(())
}
#[test]
fn nxdomain() -> Result<()> {
let needle_fqdn = FQDN::TEST_DOMAIN.push_label("unicorn");
let network = Network::new()?;
let leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let Graph {
nameservers: _nameservers,
root,
..
} = Graph::build(leaf_ns, Sign::No)?;
let resolver = Resolver::new(&network, root).start()?;
let resolver_ip_addr = resolver.ipv4_addr();
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse();
let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?;
assert!(dbg!(output).status.is_nxdomain());
Ok(())
}
#[test]
fn recursion_desired_flag() -> Result<()> {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
let Graph {
nameservers, root, ..
} = Graph::build(leaf_ns, Sign::No)?;
let resolver = Resolver::new(&network, root).start()?;
let resolver_ip_addr = resolver.ipv4_addr();
let client = Client::new(&network)?;
let mut tshark = resolver.eavesdrop()?;
let settings = *DigSettings::default().recurse();
let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?;
assert!(output.status.is_noerror());
let [answer] = output.answer.try_into().unwrap();
let a = answer.try_into_a().unwrap();
assert_eq!(needle_fqdn, a.fqdn);
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
// Query from client to resolver should have RD=1.
// Queries from resolver to nameservers should have RD=0.
let mut seen_incoming_query = false;
let mut seen_outgoing_query = false;
for Capture { message, direction } in captures.iter() {
match direction {
Direction::Incoming { source } if *source == client.ipv4_addr() => {
seen_incoming_query = true;
assert!(message.is_rd_flag_set(), "{message:#?}");
}
Direction::Outgoing { destination }
if nameservers.iter().any(|ns| *destination == ns.ipv4_addr()) =>
{
seen_outgoing_query = true;
assert!(!message.is_rd_flag_set(), "{message:#?}");
}
_ => {}
}
}
assert!(seen_incoming_query, "{captures:#?}");
assert!(seen_outgoing_query, "{captures:#?}");
Ok(())
}

View File

@@ -1,112 +0,0 @@
//! recursive resolution fails because a referral ("glue record") includes
//! a private IP address where no server is running
use std::net::Ipv4Addr;
use dns_test::client::{Client, DigOutput, DigSettings};
use dns_test::name_server::NameServer;
use dns_test::record::{Record, RecordType};
use dns_test::{Network, Resolver, Result, FQDN};
#[test]
fn v4_this_host() -> Result<()> {
if dns_test::SUBJECT.is_unbound() {
// unbound does not answer and `dig` times out
return Ok(());
}
let (output, logs) = fixture("v4-this-host", Ipv4Addr::UNSPECIFIED)?;
dbg!(&output);
assert!(output.status.is_servfail());
if dns_test::SUBJECT.is_hickory()
&& !logs.lines().any(|line| {
line.contains("ignoring address due to do_not_query") && line.contains("0.0.0.0")
})
{
panic!("did not find ignored referral to 0.0.0.0");
}
Ok(())
}
#[test]
fn v4_loopback() -> Result<()> {
let (output, logs) = fixture("v4-loopback", Ipv4Addr::LOCALHOST)?;
dbg!(&output);
assert!(output.status.is_servfail());
if dns_test::SUBJECT.is_hickory()
&& !logs.lines().any(|line| {
line.contains("ignoring address due to do_not_query") && line.contains("127.0.0.1")
})
{
panic!("did not find ignored referral to 127.0.0.1");
}
Ok(())
}
#[test]
fn v4_broadcast() -> Result<()> {
let (output, logs) = fixture("v4-broadcast", Ipv4Addr::BROADCAST)?;
dbg!(&output);
assert!(output.status.is_servfail());
if dns_test::SUBJECT.is_hickory()
&& !logs.lines().any(|line| {
line.contains("ignoring address due to do_not_query")
&& line.contains("255.255.255.255")
})
{
panic!("did not find ignored referral to 255.255.255.255");
}
Ok(())
}
fn fixture(label: &str, addr: Ipv4Addr) -> Result<(DigOutput, String)> {
let network = Network::new()?;
let leaf_zone = FQDN::TEST_TLD.push_label(label);
let needle_fqdn = leaf_zone.push_label("example");
let mut root_ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, &network)?;
let mut tld_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_TLD, &network)?;
let mut sibling_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, leaf_zone.clone(), &network)?;
sibling_ns.add(root_ns.a());
sibling_ns.add(tld_ns.a());
sibling_ns.add(leaf_ns.a());
leaf_ns.add(Record::a(needle_fqdn.clone(), Ipv4Addr::new(1, 2, 3, 4)));
root_ns.referral_nameserver(&tld_ns);
tld_ns.referral_nameserver(&sibling_ns);
// IMPORTANT! here we do a wrong/incorrect referral on purporse
tld_ns.referral(leaf_zone.clone(), leaf_ns.fqdn().clone(), addr);
let root_hint = root_ns.root_hint();
let _nameservers = [
root_ns.start()?,
tld_ns.start()?,
sibling_ns.start()?,
leaf_ns.start()?,
];
let mut resolver = Resolver::new(&network, root_hint);
if dns_test::SUBJECT.is_unbound() {
resolver.extended_dns_errors();
}
let resolver = resolver.start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().timeout(7);
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::A, &needle_fqdn)?;
Ok((output, resolver.logs().unwrap()))
}

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python3
# This server ignores the first query it receives, and replies to all
# subsequent queries, in order to simulate packet loss.
from dnslib import A, DNSError, DNSLabel, DNSRecord, QTYPE, RCODE, RR
from dnslib.server import BaseResolver, DNSHandler, DNSServer
class Resolver(BaseResolver):
def __init__(self):
self.first = True
self.expected_name = DNSLabel("example.testing.")
self.a = A("192.0.2.1") # in TEST-NET-1
def resolve(self, request: DNSRecord, _handler: DNSHandler) -> DNSRecord:
reply = request.reply()
if request.q.qname == self.expected_name:
reply.header.rcode = RCODE.NOERROR
if request.q.qtype == QTYPE.A:
if self.first:
self.first = False
# This will be caught by the try-except block in
# DNSHandler.handle(), which results in no response being
# sent.
raise DNSError("Ignoring first query")
reply.add_answer(RR(
request.q.qname,
QTYPE.A,
rdata=self.a,
))
else:
reply.header.rcode = RCODE.NXDOMAIN
return reply
if __name__ == "__main__":
resolver = Resolver()
server = DNSServer(resolver, address="0.0.0.0", port=53)
server.start()

View File

@@ -1,50 +0,0 @@
//! Test how resolvers respond to packet loss.
use std::{fs, net::Ipv4Addr};
use dns_test::{
client::{Client, DigSettings, DigStatus},
name_server::NameServer,
record::RecordType,
Implementation, Network, Resolver, Result, FQDN,
};
#[test]
#[ignore = "hickory-recursor does not have a retransmission policy"]
fn packet_loss_udp() -> Result<()> {
let target_fqdn = FQDN("example.testing.")?;
let network = Network::new()?;
let mut root_ns = NameServer::new(&Implementation::test_peer(), FQDN::ROOT, &network)?;
let leaf_ns = NameServer::new(&Implementation::Dnslib, FQDN::TEST_TLD, &network)?;
let script = fs::read_to_string("src/resolver/dns/scenarios/packet_loss.py")?;
leaf_ns.cp("/script.py", &script)?;
root_ns.referral_nameserver(&leaf_ns);
let root_hint = root_ns.root_hint();
let resolver = Resolver::new(&network, root_hint).start()?;
let client = Client::new(resolver.network())?;
let dig_settings = *DigSettings::default().recurse().timeout(10);
let _root_ns = root_ns.start()?;
let _leaf_ns = leaf_ns.start()?;
let result = client.dig(
dig_settings,
resolver.ipv4_addr(),
RecordType::A,
&target_fqdn,
);
let response = result
.unwrap_or_else(|e| panic!("error {e:?} resolver logs: {}", resolver.logs().unwrap()));
assert_eq!(response.status, DigStatus::NOERROR);
assert_eq!(response.answer.len(), 1, "{:?}", response.answer);
assert_eq!(
response.answer[0].clone().try_into_a().unwrap().ipv4_addr,
Ipv4Addr::new(192, 0, 2, 1)
);
Ok(())
}

View File

@@ -1,8 +0,0 @@
//! DNSSEC functionality
mod adhoc;
mod fixtures;
mod regression;
mod rfc4035;
mod rfc6975;
mod scenarios;

View File

@@ -1,36 +0,0 @@
//! sensible, ad-hoc behavior that other DNS servers implement but that do not map to a specific
//! RFC requirement
use std::net::Ipv4Addr;
use dns_test::{
client::{Client, DigSettings},
record::RecordType,
Result, FQDN,
};
use super::fixtures;
#[test]
fn empty_answer_section_on_failed_dnssec_validation_and_cd_flag_unset() -> Result<()> {
let leaf_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let leaf_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let (resolver, _graph) =
fixtures::bad_signature_in_leaf_nameserver(&leaf_fqdn, leaf_ipv4_addr)?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver_addr, RecordType::A, &leaf_fqdn)?;
assert!(output.status.is_servfail());
assert!(!output.flags.authenticated_data);
// the records that failed DNSSEC validation should not be returned so that the client does not
// use them by mistake, e.g. they forget to check the status (RCODE field) and the AD flag
assert!(output.answer.is_empty());
Ok(())
}

View File

@@ -1,85 +0,0 @@
use std::net::Ipv4Addr;
use base64::prelude::*;
use dns_test::{
name_server::{Graph, NameServer, Running, Sign},
record::Record,
zone_file::SignSettings,
Network, Resolver, Result, TrustAnchor, FQDN,
};
pub fn bad_signature_in_leaf_nameserver(
leaf_fqdn: &FQDN,
leaf_ipv4_addr: Ipv4Addr,
) -> Result<(Resolver, Graph)> {
assert_eq!(Some(FQDN::TEST_DOMAIN), leaf_fqdn.parent());
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
leaf_ns.add(Record::a(leaf_fqdn.clone(), leaf_ipv4_addr));
let graph = Graph::build(
leaf_ns,
Sign::AndAmend {
settings: SignSettings::default(),
mutate: &|zone, records| {
if zone == &FQDN::TEST_DOMAIN {
let mut modified = 0;
for record in records {
if let Record::RRSIG(rrsig) = record {
if rrsig.fqdn == *leaf_fqdn {
let mut signature =
BASE64_STANDARD.decode(&rrsig.signature).unwrap();
let last = signature.last_mut().expect("empty signature");
*last = !*last;
rrsig.signature = BASE64_STANDARD.encode(&signature);
modified += 1;
}
}
}
assert_eq!(modified, 1, "sanity check");
}
},
},
)?;
let trust_anchor = graph.trust_anchor.as_ref().unwrap();
let resolver = Resolver::new(&network, graph.root.clone())
.trust_anchor(trust_anchor)
.start()?;
Ok((resolver, graph))
}
pub fn minimally_secure(
leaf_fqdn: FQDN,
leaf_ipv4_addr: Ipv4Addr,
) -> Result<(Resolver, Vec<NameServer<Running>>, TrustAnchor)> {
assert_eq!(Some(FQDN::TEST_DOMAIN), leaf_fqdn.parent());
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
leaf_ns.add(Record::a(leaf_fqdn.clone(), leaf_ipv4_addr));
let Graph {
nameservers,
root,
trust_anchor,
} = Graph::build(
leaf_ns,
Sign::Yes {
settings: SignSettings::default(),
},
)?;
let trust_anchor = trust_anchor.unwrap();
let resolver = Resolver::new(&network, root)
.trust_anchor(&trust_anchor)
.start()?;
Ok((resolver, nameservers, trust_anchor))
}

View File

@@ -1,158 +0,0 @@
use dns_test::{
client::{Client, DigSettings},
name_server::{Graph, NameServer, Sign},
record::{Record, RecordType},
tshark::{Capture, Direction},
zone_file::SignSettings,
Implementation, Network, Resolver, Result, FQDN,
};
/// regression test for https://github.com/hickory-dns/hickory-dns/issues/2299
#[test]
fn includes_rrsig_record_in_ns_query() -> Result<()> {
let network = Network::new()?;
let leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let Graph {
nameservers: _nameservers,
root,
trust_anchor: _trust_anchor,
} = Graph::build(
leaf_ns,
Sign::Yes {
settings: SignSettings::default(),
},
)?;
// NOTE this is a security-aware, *non*-validating resolver
let resolver = Resolver::new(&network, root).start()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let output = client.dig(
*DigSettings::default().dnssec().recurse(),
resolver_addr,
RecordType::NS,
&FQDN::TEST_DOMAIN,
)?;
assert!(output.status.is_noerror());
// bug: this answer was missing the `rrsig` record
let [ns, rrsig] = output.answer.try_into().unwrap();
// check that we got the expected record types
assert!(matches!(ns, Record::NS(_)));
let rrsig = rrsig.try_into_rrsig().unwrap();
assert_eq!(RecordType::NS, rrsig.type_covered);
Ok(())
}
/// This is a regression test for https://github.com/hickory-dns/hickory-dns/issues/2285
#[test]
fn can_validate_ns_query() -> Result<()> {
let network = Network::new()?;
let leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(
leaf_ns,
Sign::Yes {
settings: SignSettings::default(),
},
)?;
// NOTE this is a security-aware, *validating* resolver
let resolver = Resolver::new(&network, root)
.trust_anchor(&trust_anchor.unwrap())
.start()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let output = client.dig(
*DigSettings::default().authentic_data().recurse(),
resolver_addr,
RecordType::NS,
&FQDN::TEST_DOMAIN,
)?;
// bug: this returned SERVFAIL instead of NOERROR with AD=1
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
// check that the record type is what we expect
let [ns] = output.answer.try_into().unwrap();
assert!(matches!(ns, Record::NS(_)));
Ok(())
}
/// regression test for https://github.com/hickory-dns/hickory-dns/issues/2306
#[test]
fn single_node_dns_graph_with_bind_as_peer() -> Result<()> {
let network = Network::new()?;
let peer = Implementation::Bind;
let nameserver = NameServer::new(&peer, FQDN::ROOT, &network)?
.sign(SignSettings::default())?
.start()?;
let client = Client::new(&network)?;
let nameserver_addr = nameserver.ipv4_addr();
let ans = client.dig(
DigSettings::default(),
nameserver_addr,
RecordType::NS,
&FQDN::ROOT,
)?;
// sanity check
assert!(ans.status.is_noerror());
let [ns] = ans.answer.try_into().unwrap();
assert!(matches!(ns, Record::NS(_)));
// pre-condition: BIND does NOT include a glue record (A record) in the additional section
assert!(ans.additional.is_empty());
let resolver = Resolver::new(&network, nameserver.root_hint()).start()?;
let mut tshark = resolver.eavesdrop()?;
let ans = client.dig(
*DigSettings::default().recurse(),
resolver.ipv4_addr(),
RecordType::SOA,
&FQDN::ROOT,
)?;
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
dbg!(captures.len());
assert!(ans.status.is_noerror());
let [soa] = ans.answer.try_into().unwrap();
assert!(matches!(soa, Record::SOA(_)));
// bug: hickory-dns goes into an infinite loop until it exhausts its network resources
assert!(captures.len() < 20);
for Capture { message, direction } in captures {
if let Direction::Outgoing { destination } = direction {
if destination == nameserver_addr {
eprintln!("{message:#?}\n");
}
}
}
Ok(())
}

View File

@@ -1,3 +0,0 @@
mod section_3;
mod section_4;
mod section_5;

View File

@@ -1,2 +0,0 @@
mod section_3_1;
mod section_3_2;

View File

@@ -1,86 +0,0 @@
use dns_test::{
client::{Client, DigSettings},
name_server::{Graph, NameServer, Sign},
record::RecordType,
tshark::{Capture, Direction},
zone_file::SignSettings,
Network, Resolver, Result, FQDN,
};
#[test]
fn on_clients_ds_query_it_queries_the_parent_zone() -> Result<()> {
let network = Network::new()?;
let leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let Graph {
nameservers,
root,
trust_anchor,
} = Graph::build(
leaf_ns,
Sign::Yes {
settings: SignSettings::default(),
},
)?;
let mut tld_ns_addr = None;
for nameserver in &nameservers {
if nameserver.zone() == &FQDN::TEST_TLD {
tld_ns_addr = Some(nameserver.ipv4_addr());
}
}
let tld_ns_addr = tld_ns_addr.expect("NS for TLD not found");
let trust_anchor = &trust_anchor.unwrap();
let resolver = Resolver::new(&network, root)
.trust_anchor(trust_anchor)
.start()?;
let mut tshark = resolver.eavesdrop()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse();
let output = client.dig(settings, resolver_addr, RecordType::DS, &FQDN::TEST_DOMAIN)?;
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
// check that we were able to retrieve the DS record
assert!(output.status.is_noerror());
let [record] = output.answer.try_into().unwrap();
let ds = record.try_into_ds().unwrap();
assert_eq!(ds.zone, FQDN::TEST_DOMAIN);
// check that DS query was forwarded to the `testing.` (parent zone) nameserver
let client_addr = client.ipv4_addr();
let mut outgoing_ds_query_count = 0;
for Capture { message, direction } in captures {
if let Direction::Outgoing { destination } = direction {
if destination != client_addr {
let queries = message.as_value()["Queries"]
.as_object()
.expect("expected Object");
for query in queries.keys() {
if query.contains("type DS") {
// the domain name in the query omits the last `.` so strip it from
// FQDN::TEST_DOMAIN
let test_domain = FQDN::TEST_DOMAIN.as_str();
let test_domain = test_domain.strip_suffix('.').unwrap_or(test_domain);
assert!(query.contains(test_domain));
assert_eq!(tld_ns_addr, destination);
outgoing_ds_query_count += 1;
}
}
}
}
}
assert_eq!(1, outgoing_ds_query_count);
Ok(())
}

View File

@@ -1,183 +0,0 @@
mod section_3_2_2;
use dns_test::{
client::{Client, DigSettings},
name_server::NameServer,
record::{Record, RecordType},
tshark::{Capture, Direction},
zone_file::SignSettings,
Network, Resolver, Result, FQDN,
};
#[test]
fn do_bit_not_set_in_request() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(SignSettings::default())?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let mut tshark = resolver.eavesdrop()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().recurse();
let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
// "the name server side MUST strip any authenticating DNSSEC RRs from the response"
let [answer] = ans.answer.try_into().unwrap();
assert!(matches!(answer, Record::SOA(_)));
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
let ns_addr = ns.ipv4_addr();
for Capture { message, direction } in captures {
if let Direction::Outgoing { destination } = direction {
if destination == client.ipv4_addr() {
continue;
}
// sanity check
assert_eq!(ns_addr, destination);
// "The resolver side of a security-aware recursive name server MUST set the DO bit
// when sending requests"
if destination == ns_addr {
assert_eq!(Some(true), message.is_do_bit_set());
}
}
}
Ok(())
}
// this ensures that even in the presence of a cached (answer+rrsig) we strip the dnssec records as
// per the RFC
#[test]
fn on_do_0_query_strips_dnssec_records_even_if_it_cached_a_previous_do_1_query() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(SignSettings::default())?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().dnssec().recurse();
let resolver_addr = resolver.ipv4_addr();
let ans = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?;
let [answer, rrsig] = ans.answer.try_into().unwrap();
assert!(matches!(answer, Record::SOA(_)));
assert!(matches!(rrsig, Record::RRSIG(_)));
let settings = *DigSettings::default().recurse();
let ans = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?;
let [answer] = ans.answer.try_into().unwrap();
assert!(matches!(answer, Record::SOA(_)));
Ok(())
}
// this ensures that in the presence of a cached entry (answer), the dnssec records (answer+rrsig) are still
// returned as per the RFC
#[test]
fn on_do_1_query_return_dnssec_records_even_if_it_cached_a_previous_do_0_query() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(SignSettings::default())?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().recurse();
let resolver_addr = resolver.ipv4_addr();
let ans = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?;
let [answer] = ans.answer.try_into().unwrap();
assert!(matches!(answer, Record::SOA(_)));
let settings = *DigSettings::default().dnssec().recurse();
let ans = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?;
let [answer, rrsig] = ans.answer.try_into().unwrap();
assert!(matches!(answer, Record::SOA(_)));
assert!(matches!(rrsig, Record::RRSIG(_)));
Ok(())
}
#[test]
fn if_do_bit_not_set_in_request_then_requested_dnssec_record_is_not_stripped() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(SignSettings::default())?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().recurse();
let ans = client.dig(
settings,
resolver.ipv4_addr(),
RecordType::DNSKEY,
&FQDN::ROOT,
)?;
// "MUST NOT strip any DNSSEC RR types that the initiating query explicitly requested"
for record in &ans.answer {
assert!(matches!(record, Record::DNSKEY(_)))
}
Ok(())
}
#[test]
fn do_bit_set_in_request() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(SignSettings::default())?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let mut tshark = resolver.eavesdrop()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().dnssec().recurse();
let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
let [answer, rrsig] = ans.answer.try_into().unwrap();
assert!(matches!(answer, Record::SOA(_)));
assert!(matches!(rrsig, Record::RRSIG(_)));
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
let ns_addr = ns.ipv4_addr();
for Capture { message, direction } in captures {
if let Direction::Outgoing { destination } = direction {
if destination == client.ipv4_addr() {
continue;
}
// sanity check
assert_eq!(ns_addr, destination);
// "The resolver side of a security-aware recursive name server MUST set the DO bit
// when sending requests"
if destination == ns_addr {
assert_eq!(Some(true), message.is_do_bit_set());
}
}
}
Ok(())
}

View File

@@ -1,75 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::{
client::{Client, DigSettings},
name_server::NameServer,
record::RecordType,
Network, Resolver, Result, FQDN,
};
use crate::resolver::dnssec::fixtures;
#[test]
fn copies_cd_bit_from_query_to_response() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().checking_disabled().recurse();
let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
assert!(ans.flags.checking_disabled);
Ok(())
}
#[test]
fn if_cd_bit_is_set_then_respond_with_data_that_fails_authentication() -> Result<()> {
let needle_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let needle_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let (resolver, _graph) =
fixtures::bad_signature_in_leaf_nameserver(&needle_fqdn, needle_ipv4_addr)?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default()
.recurse()
.authentic_data()
.checking_disabled();
let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?;
assert!(output.status.is_noerror());
assert!(!output.flags.authenticated_data);
let [record] = output.answer.try_into().unwrap();
let record = record.try_into_a().unwrap();
assert_eq!(needle_fqdn, record.fqdn);
assert_eq!(needle_ipv4_addr, record.ipv4_addr);
Ok(())
}
#[test]
fn if_cd_bit_is_clear_and_data_is_not_authentic_then_respond_with_servfail() -> Result<()> {
let needle_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let needle_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let (resolver, _graph) =
fixtures::bad_signature_in_leaf_nameserver(&needle_fqdn, needle_ipv4_addr)?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?;
assert!(output.status.is_servfail());
Ok(())
}

View File

@@ -1,3 +0,0 @@
mod section_4_1;
mod section_4_5;
mod section_4_6;

View File

@@ -1,46 +0,0 @@
use dns_test::client::{Client, DigSettings};
use dns_test::name_server::NameServer;
use dns_test::record::RecordType;
use dns_test::tshark::{Capture, Direction};
use dns_test::{Network, Resolver, Result, FQDN};
#[test]
fn edns_support() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let mut tshark = resolver.eavesdrop()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().authentic_data().recurse();
let _ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
// implementation-specific behavior
// unbound replies with SERVFAIL
// BIND replies with NOERROR
// assert!(_ans.status.is_servfail());
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
let ns_addr = ns.ipv4_addr();
for Capture { message, direction } in captures {
if let Direction::Outgoing { destination } = direction {
if destination == client.ipv4_addr() {
continue;
}
// sanity check
assert_eq!(ns_addr, destination);
if destination == ns_addr {
assert_eq!(Some(true), message.is_do_bit_set());
assert!(message.udp_payload_size().unwrap() >= 1220);
}
}
}
Ok(())
}

View File

@@ -1,141 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::{
client::{Client, DigSettings},
name_server::NameServer,
record::{Record, RecordType},
tshark::Capture,
zone_file::SignSettings,
Network, Resolver, Result, FQDN,
};
use crate::resolver::dnssec::fixtures;
/// Two queries are sent with DNSSEC enabled, the second query should take the answer from the cache.
#[test]
fn caches_dnssec_records() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(SignSettings::default())?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().dnssec().recurse();
// query twice; eavesdrop second query
let mut tshark = None;
for i in 0..2 {
if i == 1 {
tshark = Some(resolver.eavesdrop()?);
}
let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
let [answer, rrsig] = ans.answer.try_into().unwrap();
assert!(matches!(answer, Record::SOA(_)));
assert!(matches!(rrsig, Record::RRSIG(_)));
}
let mut tshark = tshark.unwrap();
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
// second query is cached so no communication between the resolver and the nameserver is
// expected
let ns_addr = ns.ipv4_addr();
for Capture { direction, .. } in captures {
assert_ne!(ns_addr, direction.peer_addr());
}
Ok(())
}
/// Two queries are sent, the first without DNSSEC enabled is put into the cache, the second query with
/// DNSSEC enabled will fetch its result from the cache
#[test]
fn caches_query_without_dnssec_to_return_all_dnssec_records_in_subsequent_query() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(SignSettings::default())?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start()?;
let client = Client::new(network)?;
// send first query without DNSSEC, fills cache
let settings = *DigSettings::default().recurse();
let dig = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
assert!(dig.status.is_noerror());
// send second query to fetch all DNSSEC records
let mut tshark = resolver.eavesdrop()?;
let settings = *DigSettings::default().dnssec().recurse();
let dig = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
assert!(dig.status.is_noerror());
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
// second query is cached so no communication between the resolver and the nameserver is
// expected
let ns_addr = ns.ipv4_addr();
for Capture { direction, .. } in captures {
assert_ne!(ns_addr, direction.peer_addr());
}
Ok(())
}
/// The chain of trust used to validate `A example.hickory-dns.testing.` includes records within the
/// `hickory-dns.testing.`, `testing` and `.` zones. Those records should be cached in the "Secure"
/// cache as part of the validation of `A example.hickory-dns.testing.`.
///
/// Therefore, a second query for a record like `DS testing.` should be a cache hit.
#[test]
fn caches_intermediate_records() -> Result<()> {
let leaf_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let leaf_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let (resolver, nameservers, _trust_anchor) =
fixtures::minimally_secure(leaf_fqdn.clone(), leaf_ipv4_addr)?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver_addr, RecordType::A, &leaf_fqdn)?;
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
let [a] = output.answer.try_into().unwrap();
let a = a.try_into_a().unwrap();
assert_eq!(leaf_fqdn, a.fqdn);
assert_eq!(leaf_ipv4_addr, a.ipv4_addr);
let mut tshark = resolver.eavesdrop()?;
let output = client.dig(settings, resolver_addr, RecordType::DS, &FQDN::TEST_TLD)?;
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
let ns_addrs = nameservers
.iter()
.map(|ns| ns.ipv4_addr())
.collect::<Vec<_>>();
for Capture { direction, .. } in captures {
assert!(!ns_addrs.contains(&direction.peer_addr()));
}
Ok(())
}
// TODO check expiration case

View File

@@ -1,70 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::{
client::{Client, DigSettings},
record::RecordType,
tshark::{Capture, Direction},
Result, FQDN,
};
use crate::resolver::dnssec::fixtures;
#[test]
fn clears_ad_bit_in_outgoing_queries() -> Result<()> {
let leaf_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let leaf_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let (resolver, nameservers, _trust_anchor) =
fixtures::minimally_secure(leaf_fqdn.clone(), leaf_ipv4_addr)?;
let mut tshark = resolver.eavesdrop()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default().recurse().authentic_data();
let _output = client.dig(settings, resolver_addr, RecordType::A, &leaf_fqdn)?;
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
let client_addr = client.ipv4_addr();
let mut ns_checks_count = 0;
let mut client_checks_count = 0;
let ns_addrs = nameservers
.iter()
.map(|ns| ns.ipv4_addr())
.collect::<Vec<_>>();
for Capture { message, direction } in captures {
match direction {
Direction::Incoming { source } => {
if source == client_addr {
// sanity check
assert!(message.is_ad_flag_set());
client_checks_count += 1;
}
}
Direction::Outgoing { destination } => {
if destination == client_addr {
// skip response to client
continue;
}
// sanity check
assert!(ns_addrs.contains(&destination));
assert!(!message.is_ad_flag_set());
ns_checks_count += 1;
}
}
}
// sanity checks
assert_eq!(1, client_checks_count);
assert_ne!(0, dbg!(ns_checks_count));
Ok(())
}

View File

@@ -1,218 +0,0 @@
//! Tests to check DNSSEC validation of the Resolver with invalid signed data.
//! According to RFC 4045 section 5.5 failed validations return `SERVFAIL` (RCODE 2) to the client.
//!
//! See RFC 4035 section 5.3.1 for more details: https://datatracker.ietf.org/doc/html/rfc4035#section-5.3.1
//!
use std::time::{Duration, SystemTime};
use dns_test::{
client::{Client, DigSettings},
name_server::NameServer,
record::{RecordType, RRSIG, SOA},
zone_file::SignSettings,
Network, Resolver, Result, FQDN,
};
const ONE_HOUR: Duration = Duration::from_secs(60 * 60);
/// Check that inception > current_time results in an invalid response.
#[test]
fn rrsig_rr_inception_time_is_set_in_the_future() -> Result<()> {
// `unbound` allows a skew / delta around inception time in `val-sig-skew-min` option
let inception = SystemTime::now() + 4 * ONE_HOUR;
let expiration = SystemTime::now() + 10 * ONE_HOUR;
let settings = SignSettings::default()
.inception(inception)
.expiration(expiration);
// Configure nameserver & sign zonefile
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(settings)?
.start()?;
// Set up Resolver with DNSSEC enabled
let resolver = Resolver::new(network, ns.root_hint())
.trust_anchor(ns.trust_anchor().expect("Failed to get trust anchor"))
.start()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().recurse();
let dig = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
// validation should fail
assert!(dig.status.is_servfail());
Ok(())
}
/// Check that expiration timestamp < current_time results in an invalid lookup.
#[test]
fn rrsig_rr_expiration_time_is_before_current_time() -> Result<()> {
let expiration = SystemTime::now() - 4 * ONE_HOUR;
let inception = SystemTime::now() - 10 * ONE_HOUR;
let settings = SignSettings::default()
.expiration(expiration)
.inception(inception);
// Configure nameserver & sign zonefile
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(settings)?
.start()?;
// Set up Resolver with DNSSEC enabled
let resolver = Resolver::new(network, ns.root_hint())
.trust_anchor(ns.trust_anchor().expect("Failed to get trust anchor"))
.start()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().recurse();
let dig = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
// validation should fail
assert!(dig.status.is_servfail());
Ok(())
}
/// Check that the validating resolver sets the TTL to a value between "now" and expiration time.
/// See Github issue: https://github.com/hickory-dns/hickory-dns/issues/2292
#[test]
fn rrsig_rr_ttl_is_not_greater_than_duration_between_current_time_and_signature_expiration_timestamp(
) -> Result<()> {
let ttl_delta = 4 * ONE_HOUR;
let settings = SignSettings::default().expiration(SystemTime::now() + ttl_delta);
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(settings)?
.start()?;
let resolver = Resolver::new(network, ns.root_hint())
.trust_anchor(ns.trust_anchor().expect("Failed to get trust anchor"))
.start()?;
let client = Client::new(network)?;
let settings = *DigSettings::default().recurse();
let dig = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
assert!(dig.status.is_noerror());
let [answer] = dig.answer.try_into().unwrap();
assert!(answer.is_soa());
// get TTL from record
let soa_ttl = answer.try_into_soa().unwrap().ttl as u64;
assert!(soa_ttl <= ttl_delta.as_secs());
Ok(())
}
/// Check that both RRSIG and RR use the same TTL, section 5.3.3 of RFC 4035 defines conditions how to adjust the TTL
/// while section 2.2 states "The RRSIG RR's TTL is equal to the TTL of the RRset."
#[test]
fn rrsig_and_rr_return_the_same_adjusted_ttl() -> Result<()> {
let ttl_delta = 4 * ONE_HOUR;
let settings = SignSettings::default().expiration(SystemTime::now() + ttl_delta);
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(settings)?
.start()?;
let resolver = Resolver::new(network, ns.root_hint())
.trust_anchor(ns.trust_anchor().expect("Failed to get trust anchor"))
.start()?;
// Fetch RRSIG + RR
let client = Client::new(network)?;
let settings = *DigSettings::default().dnssec().recurse();
let resolver_addr = resolver.ipv4_addr();
let dig = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?;
assert!(dig.status.is_noerror());
let [soa, rrsig] = dig.answer.try_into().unwrap();
let soa: SOA = soa.try_into_soa().unwrap();
let rrsig: RRSIG = rrsig.try_into_rrsig().unwrap();
assert_eq!(soa.ttl, rrsig.ttl);
assert!(soa.ttl <= ttl_delta.as_secs() as u32);
Ok(())
}
/// Check that Serial Number arithemtics support the case where the timesamp is `1 << 31` beyond UNIX_EPOCH.
#[test]
fn rrsig_rr_expiration_time_is_1_to_the_power_of_31_beyond_unix_epoch() -> Result<()> {
// The representation in the record uses format `YYYYMMDDhhmmss`
const MAX_UNIX_TIMESTAMP: u64 = 20380119031408;
let settings = SignSettings::default().expiration_from_u64(1 << 31);
let network = &Network::new()?;
let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?
.sign(settings)?
.start()?;
let resolver = Resolver::new(network, ns.root_hint())
.trust_anchor(ns.trust_anchor().expect("Failed to get trust anchor"))
.start()?;
// Fetch RRSIG + RR
let client = Client::new(network)?;
let settings = *DigSettings::default().dnssec().recurse();
let resolver_addr = resolver.ipv4_addr();
let dig = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?;
// Validation should succeed
assert!(dig.status.is_noerror());
let [soa, rrsig] = dig.answer.try_into().unwrap();
let soa: SOA = soa.try_into_soa().unwrap();
let rrsig: RRSIG = rrsig.try_into_rrsig().unwrap();
assert_eq!(soa.ttl, rrsig.ttl);
assert_eq!(MAX_UNIX_TIMESTAMP, rrsig.signature_expiration);
Ok(())
}
/// Check that Serial Number arithmetics invalidate the case where the timestamp is `1 << 32` beyond UNIX_EPOCH.
#[test]
fn rrsig_rr_expiration_time_is_1_to_the_power_of_32_beyond_unix_epoch() -> Result<()> {
let settings = SignSettings::default();
let network = &Network::new()?;
let mut ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?.sign(settings)?;
// `1 << 32` from Unix Epoch results in Sunday, February 7, 2106 6:28:16 AM
if let Some(rrsig) = ns.signed_zone_file_mut().rrsig_mut(RecordType::SOA) {
rrsig.signature_expiration = 21060207062816;
}
let ns = ns.start()?;
let resolver = Resolver::new(network, ns.root_hint())
.trust_anchor(ns.trust_anchor().expect("Failed to get trust anchor"))
.start()?;
// Fetch RRSIG + RR
let client = Client::new(network)?;
let settings = *DigSettings::default().dnssec().recurse();
let resolver_addr = resolver.ipv4_addr();
let dig = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?;
// Validation should fail
assert!(dig.status.is_servfail());
Ok(())
}

View File

@@ -1,52 +0,0 @@
use dns_test::{
client::{Client, DigSettings},
name_server::{Graph, NameServer, Sign},
record::RecordType,
zone_file::SignSettings,
Network, Resolver, Result, FQDN,
};
/// Section 4.2.1, last paragraph, says "Validating recursive resolvers MUST NOT set the DAU, DHU,
/// and/or N3U option(s) in the final response to the stub client."
#[test]
fn no_understood_options_in_response() -> Result<()> {
let network = Network::new()?;
let leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(
leaf_ns,
Sign::Yes {
settings: SignSettings::default(),
},
)?;
let trust_anchor = trust_anchor.unwrap();
let resolver = Resolver::new(&network, root)
.trust_anchor(&trust_anchor)
.start()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse();
let output = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::TEST_DOMAIN)?;
assert!(output.status.is_noerror());
// Disallow DAU, DHU, and N3U.
assert!(
output
.options
.iter()
.all(|(option_number, _)| *option_number != 5
&& *option_number != 6
&& *option_number != 7),
"{:?}",
output.options
);
Ok(())
}

View File

@@ -1,4 +0,0 @@
mod bogus;
mod ede;
mod insecure;
mod secure;

View File

@@ -1,227 +0,0 @@
mod no_rrsig_dnskey;
use dns_test::{
client::{Client, DigOutput, DigSettings, ExtendedDnsError},
name_server::{Graph, NameServer, Sign},
record::{Record, RecordType, DS},
zone_file::SignSettings,
Network, Resolver, Result, TrustAnchor, FQDN,
};
#[test]
fn ds_unassigned_key_algo() -> Result<()> {
let output =
malformed_ds_fixture(&FQDN::TEST_TLD.push_label("ds-unassigned-key-algo"), |ds| {
ds.algorithm = 100
})?;
dbg!(&output);
assert!(output.status.is_noerror() && !output.flags.authenticated_data);
if dns_test::SUBJECT.is_unbound() {
assert!(output.ede.is_empty());
}
Ok(())
}
#[test]
fn ds_reserved_key_algo() -> Result<()> {
let output = malformed_ds_fixture(&FQDN::TEST_TLD.push_label("ds-reserved-key-algo"), |ds| {
ds.algorithm = 200
})?;
dbg!(&output);
assert!(output.status.is_noerror() && !output.flags.authenticated_data);
if dns_test::SUBJECT.is_unbound() {
assert!(output.ede.is_empty());
}
Ok(())
}
// the key tag in the DS record does not match the key tag in the DNSKEY record
#[test]
fn ds_bad_tag() -> Result<()> {
let output = malformed_ds_fixture(&FQDN::TEST_TLD.push_label("ds-bad-tag"), |ds| {
ds.key_tag = !ds.key_tag;
})?;
dbg!(&output);
assert!(output.status.is_servfail());
if dns_test::SUBJECT.is_unbound() {
assert!(output.ede.iter().eq([&ExtendedDnsError::DnssecBogus]));
}
Ok(())
}
// the algorithm field in the DS record does not match the algorithm field in the DNSKEY record
#[test]
fn ds_bad_key_algo() -> Result<()> {
let output = malformed_ds_fixture(&FQDN::TEST_TLD.push_label("ds-bad-key-algo"), |ds| {
assert_eq!(8, ds.algorithm, "number below may need to change");
ds.algorithm = 7;
})?;
dbg!(&output);
assert!(output.status.is_servfail());
if dns_test::SUBJECT.is_unbound() {
assert!(output.ede.iter().eq([&ExtendedDnsError::DnssecBogus]));
}
Ok(())
}
// the RRSIG covering the DNSKEYs generated using the KSK has been removed
// but there's an RRSIG covering the DNSKEYs generated using the ZSK
#[test]
fn no_rrsig_ksk() -> Result<()> {
let network = Network::new()?;
let leaf_zone = FQDN::TEST_TLD.push_label("no-rrsig-ksk");
let leaf_ns = NameServer::new(&dns_test::PEER, leaf_zone.clone(), &network)?;
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(
leaf_ns,
Sign::AndAmend {
settings: SignSettings::default(),
mutate: &|zone, records| {
if zone == &leaf_zone {
let mut ksk_tag = None;
let mut zsk_tag = None;
for record in records.iter() {
if let Record::DNSKEY(dnskey) = record {
if dnskey.is_key_signing_key() {
assert!(ksk_tag.is_none(), "more than one KSK");
ksk_tag = Some(dnskey.rdata.calculate_key_tag());
} else {
assert!(zsk_tag.is_none(), "more than one ZSK");
zsk_tag = Some(dnskey.rdata.calculate_key_tag());
}
}
}
let ksk_tag = ksk_tag.expect("did not find the KSK");
let mut did_remove = false;
for (index, record) in records.iter().enumerate() {
if let Record::RRSIG(rrsig) = record {
if rrsig.type_covered == RecordType::DNSKEY && rrsig.key_tag == ksk_tag
{
records.remove(index);
did_remove = true;
break;
}
}
}
assert!(
did_remove,
"did not find an RRSIG covering DNSKEY generated using the KSK"
);
// PRE-CONDITION there must be a RRSIG covering DNSKEY but generated using
// the ZSK
let zsk_tag = zsk_tag.expect("did not find the ZSK");
let mut found = false;
for record in records.iter() {
if let Record::RRSIG(rrsig) = record {
if rrsig.type_covered == RecordType::DNSKEY && rrsig.key_tag == zsk_tag
{
found = true;
break;
}
}
}
assert!(
found,
"did not find an RRSIG covering DNSKEY generated using the ZSK"
);
}
},
},
)?;
let mut resolver = Resolver::new(&network, root);
let supports_ede = dns_test::SUBJECT.is_unbound();
if supports_ede {
resolver.extended_dns_errors();
}
let resolver = resolver.trust_anchor(&trust_anchor.unwrap()).start()?;
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::NS, &leaf_zone)?;
dbg!(&output);
assert!(output.status.is_servfail());
if supports_ede {
assert!(output.ede.iter().eq([&ExtendedDnsError::DnssecBogus]));
}
Ok(())
}
fn malformed_ds_fixture(leaf_zone: &FQDN, mutate: impl FnOnce(&mut DS)) -> Result<DigOutput> {
let network = Network::new()?;
let sign_settings = SignSettings::default();
let peer = &dns_test::PEER;
let mut root_ns = NameServer::new(peer, FQDN::ROOT, &network)?;
let mut tld_ns = NameServer::new(peer, FQDN::TEST_TLD, &network)?;
let mut nameservers_ns = NameServer::new(peer, FQDN::TEST_DOMAIN, &network)?;
let leaf_ns = NameServer::new(peer, leaf_zone.clone(), &network)?;
root_ns.referral_nameserver(&tld_ns);
tld_ns.referral_nameserver(&nameservers_ns);
tld_ns.referral_nameserver(&leaf_ns);
nameservers_ns.add(root_ns.a());
nameservers_ns.add(tld_ns.a());
let nameservers_ns = nameservers_ns.sign(sign_settings.clone())?;
let leaf_ns = leaf_ns.sign(sign_settings.clone())?;
tld_ns.add(nameservers_ns.ds().ksk.clone());
let mut ds = leaf_ns.ds().ksk.clone();
mutate(&mut ds);
tld_ns.add(ds);
let tld_ns = tld_ns.sign(sign_settings.clone())?;
root_ns.add(tld_ns.ds().ksk.clone());
let mut trust_anchor = TrustAnchor::empty();
let root_ns = root_ns.sign(sign_settings)?;
trust_anchor.add(root_ns.key_signing_key().clone());
trust_anchor.add(root_ns.zone_signing_key().clone());
let root_hint = root_ns.root_hint();
let _root_ns = root_ns.start()?;
let _tld_ns = tld_ns.start()?;
let _nameservers_ns = nameservers_ns.start()?;
let _leaf_ns = leaf_ns.start()?;
let mut resolver = Resolver::new(&network, root_hint);
if dns_test::SUBJECT.is_unbound() {
resolver.extended_dns_errors();
}
let resolver = resolver.trust_anchor(&trust_anchor).start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, leaf_zone)
}

View File

@@ -1,150 +0,0 @@
//! the RRSIGs that cover the DNSKEY have been removed
use std::net::Ipv4Addr;
use dns_test::{
client::{Client, DigSettings, ExtendedDnsError},
name_server::{Graph, NameServer, Sign},
record::{Record, RecordType},
zone_file::SignSettings,
Implementation, Network, Resolver, Result, FQDN,
};
#[test]
fn query_dnskey_record() -> Result<()> {
let network = Network::new()?;
let leaf_zone = FQDN::TEST_TLD.push_label("no-rrsig-dnskey");
let leaf_ns = NameServer::new(&dns_test::PEER, leaf_zone.clone(), &network)?;
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(
leaf_ns,
Sign::AndAmend {
settings: SignSettings::default(),
mutate: &|zone, records| {
if *zone == leaf_zone {
let mut remove_count = 0;
for index in (0..records.len()).rev() {
if let Record::RRSIG(rrsig) = &records[index] {
if rrsig.type_covered == RecordType::DNSKEY {
records.swap_remove(index);
remove_count += 1;
}
}
}
// sanity check
assert_ne!(0, remove_count);
}
},
},
)?;
let trust_anchor = trust_anchor.unwrap();
let mut resolver = Resolver::new(&network, root);
if dns_test::SUBJECT.is_unbound() {
resolver.extended_dns_errors();
}
let resolver = resolver.trust_anchor(&trust_anchor).start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(
settings,
resolver.ipv4_addr(),
RecordType::DNSKEY,
&leaf_zone,
)?;
dbg!(&output);
assert!(output.status.is_servfail());
if dns_test::SUBJECT.is_unbound() {
// check that this failed for the right reason
assert!(output.ede.iter().eq(&[ExtendedDnsError::RrsigsMissing]));
}
Ok(())
}
#[test]
fn query_other_record() -> Result<()> {
let network = Network::new()?;
let leaf_zone = FQDN::TEST_TLD.push_label("no-rrsig-dnskey");
// other implementations fail the PRE-CONDITION below
let peer = Implementation::Bind;
let mut leaf_ns = NameServer::new(&peer, leaf_zone.clone(), &network)?;
leaf_ns.add(Record::a(leaf_zone.clone(), Ipv4Addr::new(1, 2, 3, 4)));
let leaf_ns_addr = leaf_ns.ipv4_addr();
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(
leaf_ns,
Sign::AndAmend {
settings: SignSettings::default(),
mutate: &|zone, records| {
if *zone == leaf_zone {
let mut remove_count = 0;
for index in (0..records.len()).rev() {
if let Record::RRSIG(rrsig) = &records[index] {
if rrsig.type_covered == RecordType::DNSKEY {
records.swap_remove(index);
remove_count += 1;
}
}
}
// sanity check
assert_ne!(0, remove_count);
}
},
},
)?;
let trust_anchor = trust_anchor.unwrap();
let mut resolver = Resolver::new(&network, root);
if dns_test::SUBJECT.is_unbound() {
resolver.extended_dns_errors();
}
let resolver = resolver.trust_anchor(&trust_anchor).start()?;
let client = Client::new(&network)?;
// PRE-CONDITION the authoritative server must include the RRSIG records
let settings = *DigSettings::default().dnssec();
let output = client.dig(settings, leaf_ns_addr, RecordType::A, &leaf_zone)?;
assert!(output.status.is_noerror());
assert!(
output
.answer
.iter()
.any(|record| matches!(record, Record::RRSIG(_))),
"peer name server fails PRE-CONDITION"
);
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::A, &leaf_zone)?;
dbg!(&output);
assert!(output.status.is_servfail());
if dns_test::SUBJECT.is_unbound() {
// check that this failed for the right reason
assert!(output.ede.iter().eq(&[ExtendedDnsError::RrsigsMissing]));
}
Ok(())
}

View File

@@ -1,182 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::client::{Client, DigSettings, ExtendedDnsError};
use dns_test::name_server::{Graph, NameServer, Sign};
use dns_test::record::{Record, RecordType};
use dns_test::zone_file::SignSettings;
use dns_test::{Network, Resolver, Result, FQDN};
#[ignore]
#[test]
fn dnskey_missing() -> Result<()> {
fixture(
ExtendedDnsError::DnssecBogus,
|_needle_fqdn, zone, records| {
if zone == &FQDN::TEST_DOMAIN {
// remove the DNSKEY record that contains the ZSK
let mut remove_count = 0;
*records = records
.drain(..)
.filter(|record| {
let remove = if let Record::DNSKEY(dnskey) = record {
dnskey.is_zone_signing_key()
} else {
false
};
if remove {
remove_count += 1;
}
!remove
})
.collect();
assert_eq!(1, remove_count, "sanity check");
}
},
)
}
#[ignore]
#[test]
fn rrsigs_missing() -> Result<()> {
fixture(
ExtendedDnsError::RrsigsMissing,
|needle_fqdn, zone, records| {
if zone == &FQDN::TEST_DOMAIN {
// remove the RRSIG records that covers the needle record
let mut remove_count = 0;
*records = records
.drain(..)
.filter(|record| {
let remove = if let Record::RRSIG(rrsig) = record {
rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn
} else {
false
};
if remove {
remove_count += 1;
}
!remove
})
.collect();
assert_eq!(1, remove_count, "sanity check");
}
},
)
}
#[ignore]
#[test]
fn unsupported_dnskey_algorithm() -> Result<()> {
fixture(
ExtendedDnsError::UnsupportedDnskeyAlgorithm,
|needle_fqdn, zone, records| {
if zone == &FQDN::TEST_DOMAIN {
// lie about the algorithm that was used to sign the needle record
let mut modified_count = 0;
for record in records {
if let Record::RRSIG(rrsig) = record {
if rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn {
assert_ne!(1, rrsig.algorithm, "modify the value below");
rrsig.algorithm = 1;
modified_count += 1;
}
}
}
assert_eq!(1, modified_count, "sanity check");
}
},
)
}
#[ignore]
#[test]
fn dnssec_bogus() -> Result<()> {
fixture(
ExtendedDnsError::DnssecBogus,
|needle_fqdn, zone, records| {
if zone == &FQDN::TEST_DOMAIN {
// corrupt the RRSIG record that covers the needle record
let mut modified_count = 0;
for record in records {
if let Record::RRSIG(rrsig) = record {
if rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn {
let seconds = rrsig.signature_inception % 100;
let rest = rrsig.signature_inception - seconds;
let modified_seconds = (seconds + 59).rem_euclid(60);
rrsig.signature_expiration = rest + modified_seconds;
modified_count += 1;
}
}
}
assert_eq!(1, modified_count, "sanity check");
}
},
)
}
// Sets up a minimal, DNSSEC-enabled DNS graph where the leaf zone contains a "needle" A record
// that we'll search for
//
// `amend` can be used to modify zone files *after* they have been signed. it's used to introduce
// errors in the signed zone files
//
// the query for the needle record is expected to fail with the `expected` Extended DNS Error
fn fixture(
expected: ExtendedDnsError,
amend: fn(needle_fqdn: &FQDN, zone: &FQDN, records: &mut Vec<Record>),
) -> Result<()> {
let subject = &dns_test::SUBJECT;
let supports_ede = subject.supports_ede();
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(
leaf_ns,
Sign::AndAmend {
settings: SignSettings::default(),
mutate: &|zone, records| {
amend(&needle_fqdn, zone, records);
},
},
)?;
let mut resolver = Resolver::new(&network, root);
if supports_ede {
resolver.extended_dns_errors();
}
let trust_anchor = &trust_anchor.unwrap();
let resolver = resolver.trust_anchor(trust_anchor).start()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?;
assert!(output.status.is_servfail());
if supports_ede {
assert!(
output.ede.iter().eq([&expected]),
"output: {:?}, expected: {expected:?}",
&output.ede
);
}
Ok(())
}

View File

@@ -1,200 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::client::{Client, DigOutput, DigSettings};
use dns_test::name_server::NameServer;
use dns_test::record::{Record, RecordType};
use dns_test::zone_file::{Nsec, SignSettings};
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
mod deprecated_algorithm;
// in this DNS network all zones except one are signed. and importantly, the referral to the
// unsigned zone (the NS+A records in the parent zone) is also signed
//
// a validating resolver should not respond with SERVFAIL to queries about the unsigned zone because
// the security status of the whole zone is "Insecure", not "Bogus"
#[test]
fn unsigned_zone_nsec3() -> Result<()> {
unsigned_zone_fixture(Nsec::_3 {
opt_out: false,
salt: None,
})
}
#[test]
fn unsigned_zone_nsec() -> Result<()> {
unsigned_zone_fixture(Nsec::_1)
}
fn unsigned_zone_fixture(nsec: Nsec) -> Result<()> {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let unsigned_zone = FQDN::TEST_TLD.push_label("unsigned");
let needle_fqdn = unsigned_zone.push_label("example");
let mut sign_settings = SignSettings::default();
sign_settings = sign_settings.nsec(nsec);
let network = Network::new()?;
let mut unsigned_ns = NameServer::new(&dns_test::PEER, unsigned_zone.clone(), &network)?;
unsigned_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
let mut sibling_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let mut tld_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_TLD, &network)?;
let mut root_ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, &network)?;
sibling_ns.add(sibling_ns.a());
root_ns.referral_nameserver(&tld_ns);
tld_ns.referral_nameserver(&sibling_ns);
tld_ns.referral_nameserver(&unsigned_ns);
let sibling_ns = sibling_ns.sign(sign_settings.clone())?;
tld_ns.add(sibling_ns.ds().ksk.clone());
let tld_ns = tld_ns.sign(sign_settings.clone())?;
root_ns.add(tld_ns.ds().ksk.clone());
let mut trust_anchor = TrustAnchor::empty();
let root_ns = root_ns.sign(sign_settings)?;
trust_anchor.add(root_ns.key_signing_key().clone());
trust_anchor.add(root_ns.zone_signing_key().clone());
let root_hint = root_ns.root_hint();
let _nameservers = [
root_ns.start()?,
tld_ns.start()?,
sibling_ns.start()?,
unsigned_ns.start()?,
];
let resolver = Resolver::new(&network, root_hint)
.trust_anchor(&trust_anchor)
.start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
// sanity check: the other zones are correctly signed
for zone in [FQDN::ROOT, FQDN::TEST_TLD, FQDN::TEST_DOMAIN] {
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &zone)?;
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
}
let settings = *DigSettings::default().recurse();
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::A, &needle_fqdn)?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(!output.flags.authenticated_data);
Ok(())
}
#[test]
fn no_ds_record_nsec1() -> Result<()> {
let (output, _logs) = no_ds_record_fixture(SignSettings::default().nsec(Nsec::_1))?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(!output.flags.authenticated_data);
Ok(())
}
#[test]
fn no_ds_record_nsec3() -> Result<()> {
let (output, _logs) = no_ds_record_fixture(SignSettings::default().nsec(Nsec::_3 {
salt: None,
opt_out: false,
}))?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(!output.flags.authenticated_data);
Ok(())
}
#[test]
fn no_ds_record_nsec3_opt_out() -> Result<()> {
let (output, logs) = no_ds_record_fixture(SignSettings::rsasha256_nsec3_optout())?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(!output.flags.authenticated_data);
if dns_test::SUBJECT.is_hickory() {
assert!(logs.contains("DS query covered by opt-out proof"));
}
Ok(())
}
// the `no-ds.testing.` zone is signed but no DS record exists in the parent `testing.` zone.
// importantly, the `testing.` zone must contain NSEC/NSEC3 records to deny the existence of
// `no-ds.testing./DS` (which is why we cannot use `Graph::build` + `Sign::AndAmend` to produce
// this network)
fn no_ds_record_fixture(sign_settings: SignSettings) -> Result<(DigOutput, String)> {
let network = Network::new()?;
let no_ds_zone = FQDN::TEST_TLD.push_label("no-ds");
let needle_fqdn = no_ds_zone.push_label("example");
let needle_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let mut no_ds_ns = NameServer::new(&dns_test::PEER, no_ds_zone.clone(), &network)?;
no_ds_ns.add(Record::a(needle_fqdn.clone(), needle_ipv4_addr));
let mut sibling_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let mut tld_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_TLD, &network)?;
let mut root_ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, &network)?;
sibling_ns.add(root_ns.a());
sibling_ns.add(tld_ns.a());
sibling_ns.add(no_ds_ns.a());
sibling_ns.add(sibling_ns.a());
root_ns.referral_nameserver(&tld_ns);
tld_ns.referral_nameserver(&sibling_ns);
tld_ns.referral_nameserver(&no_ds_ns);
let no_ds_ns = no_ds_ns.sign(sign_settings.clone())?;
let sibling_ns = sibling_ns.sign(sign_settings.clone())?;
tld_ns.add(sibling_ns.ds().ksk.clone());
// IMPORTANT omit this! this is the DS that connects `testing.` to `no-ds.testing.` in
// the chain of trust. `no-ds.testing.` is correctly signed but the lack of the DS record turns
// it into an "island of security"
if false {
tld_ns.add(no_ds_ns.ds().ksk.clone());
}
let tld_ns = tld_ns.sign(sign_settings.clone())?;
root_ns.add(tld_ns.ds().ksk.clone());
let mut trust_anchor = TrustAnchor::empty();
let root_ns = root_ns.sign(sign_settings)?;
trust_anchor.add(root_ns.key_signing_key().clone());
trust_anchor.add(root_ns.zone_signing_key().clone());
let root_hint = root_ns.root_hint();
let _root_ns = root_ns.start()?;
let _com_ns = tld_ns.start()?;
let _sibling_ns = sibling_ns.start()?;
let _no_ds_ns = no_ds_ns.start()?;
let resolver = Resolver::new(&network, root_hint)
.trust_anchor(&trust_anchor)
.start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::A, &needle_fqdn)?;
Ok((output, resolver.logs()?))
}

View File

@@ -1,128 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::{
client::{Client, DigOutput, DigSettings},
name_server::NameServer,
record::{Record, RecordType},
zone_file::SignSettings,
Network, Resolver, Result, TrustAnchor, FQDN,
};
const EXPECTED: Ipv4Addr = Ipv4Addr::new(1, 2, 3, 4);
// check that the fixture works
#[test]
fn sanity_check() -> Result<()> {
let output = fixture("dsa", SignSettings::default())?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
let [record] = output.answer.try_into().unwrap();
let a = record.try_into_a().unwrap();
assert_eq!(EXPECTED, a.ipv4_addr);
Ok(())
}
#[test]
fn dsa() -> Result<()> {
let output = fixture("dsa", SignSettings::dsa())?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(!output.flags.authenticated_data);
let [record] = output.answer.try_into().unwrap();
let a = record.try_into_a().unwrap();
assert_eq!(EXPECTED, a.ipv4_addr);
Ok(())
}
#[test]
fn rsamd5() -> Result<()> {
let output = fixture("rsamd5", SignSettings::rsamd5())?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(!output.flags.authenticated_data);
let [record] = output.answer.try_into().unwrap();
let a = record.try_into_a().unwrap();
assert_eq!(EXPECTED, a.ipv4_addr);
Ok(())
}
fn fixture(label: &str, deprecated_settings: SignSettings) -> Result<DigOutput> {
let leaf_zone = FQDN::TEST_TLD.push_label(label);
let needle_fqdn = leaf_zone.push_label("example");
let good_settings = SignSettings::default();
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, leaf_zone.clone(), &network)?;
leaf_ns.add(Record::a(needle_fqdn.clone(), EXPECTED));
let mut sibling_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let mut tld_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_TLD, &network)?;
let mut root_ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, &network)?;
sibling_ns.add(root_ns.a());
sibling_ns.add(tld_ns.a());
sibling_ns.add(leaf_ns.a());
sibling_ns.add(sibling_ns.a());
root_ns.referral_nameserver(&tld_ns);
tld_ns.referral_nameserver(&sibling_ns);
tld_ns.referral_nameserver(&leaf_ns);
let sibling_ns = sibling_ns.sign(good_settings.clone())?;
// IMPORTANT! only this zone uses the deprecated algorithm
let leaf_ns = leaf_ns.sign(deprecated_settings.clone())?;
tld_ns.add(sibling_ns.ds().ksk.clone());
tld_ns.add(leaf_ns.ds().ksk.clone());
let tld_ns = tld_ns.sign(good_settings.clone())?;
root_ns.add(tld_ns.ds().ksk.clone());
let mut trust_anchor = TrustAnchor::empty();
let root_ns = root_ns.sign(good_settings)?;
trust_anchor.add(root_ns.key_signing_key().clone());
trust_anchor.add(root_ns.zone_signing_key().clone());
let root_hint = root_ns.root_hint();
let _nameservers = [
root_ns.start()?,
tld_ns.start()?,
sibling_ns.start()?,
leaf_ns.start()?,
];
let resolver = Resolver::new(&network, root_hint)
.trust_anchor(&trust_anchor)
.start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
// sanity check: the other zones are correctly signed
for zone in [FQDN::ROOT, FQDN::TEST_TLD, FQDN::TEST_DOMAIN] {
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &zone)?;
// XXX unclear why BIND & hickory fail this sanity check but that doesn't affect the
// main assertion below
if zone != FQDN::TEST_DOMAIN || dns_test::SUBJECT.is_unbound() {
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
}
}
let settings = *DigSettings::default().recurse().authentic_data();
client.dig(settings, resolver.ipv4_addr(), RecordType::A, &needle_fqdn)
}

View File

@@ -1,254 +0,0 @@
use std::net::Ipv4Addr;
use dns_test::client::{Client, DigSettings};
use dns_test::name_server::NameServer;
use dns_test::record::{Record, RecordType};
use dns_test::tshark::Capture;
use dns_test::zone_file::SignSettings;
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
use crate::resolver::dnssec::fixtures;
// no DS records are involved; this is a single-link chain of trust
#[test]
fn can_validate_without_delegation() -> Result<()> {
let network = Network::new()?;
let mut ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, &network)?;
ns.add(ns.a());
let ns = ns.sign(SignSettings::default())?;
let root_ksk = ns.key_signing_key().clone();
let root_zsk = ns.zone_signing_key().clone();
eprintln!("root.zone.signed:\n{}", ns.signed_zone_file());
let ns = ns.start()?;
eprintln!("root.zone:\n{}", ns.zone_file());
let trust_anchor = &TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]);
let resolver = Resolver::new(&network, ns.root_hint())
.trust_anchor(trust_anchor)
.start()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?;
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
Ok(())
}
#[test]
fn can_validate_with_delegation() -> Result<()> {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let (resolver, _nameservers, _trust_anchor) =
fixtures::minimally_secure(needle_fqdn.clone(), expected_ipv4_addr)?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?;
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
let [a] = output.answer.try_into().unwrap();
let a = a.try_into_a().unwrap();
assert_eq!(needle_fqdn, a.fqdn);
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
Ok(())
}
// the inclusion of RRSIGs records in the answer should not change the outcome of validation
// if the chain of trust was valid then the RRSIGs, which are part of the chain, must also be secure
#[test]
fn also_secure_when_do_is_set() -> Result<()> {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let (resolver, _nameservers, _trust_anchor) =
fixtures::minimally_secure(needle_fqdn.clone(), expected_ipv4_addr)?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default()
.recurse()
.dnssec() // DO = 1
.authentic_data();
let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?;
assert!(output.status.is_noerror());
// main assertion
assert!(output.flags.authenticated_data);
let [a, rrsig] = output.answer.try_into().unwrap();
let a = a.try_into_a().unwrap();
assert_eq!(needle_fqdn, a.fqdn);
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
// sanity check that the RRSIG makes sense
let rrsig = rrsig.try_into_rrsig().unwrap();
assert_eq!(RecordType::A, rrsig.type_covered);
Ok(())
}
#[test]
fn caches_answer() -> Result<()> {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle_fqdn = FQDN::EXAMPLE_SUBDOMAIN;
let (resolver, nameservers, _trust_anchor) =
fixtures::minimally_secure(needle_fqdn.clone(), expected_ipv4_addr)?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(resolver.network())?;
let settings = *DigSettings::default().recurse().authentic_data();
let mut tshark = None;
for i in 0..2 {
if i == 1 {
tshark = Some(resolver.eavesdrop()?);
}
let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?;
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
let [a] = output.answer.try_into().unwrap();
let a = a.try_into_a().unwrap();
assert_eq!(needle_fqdn, a.fqdn);
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
}
let mut tshark = tshark.unwrap();
tshark.wait_for_capture()?;
let captures = tshark.terminate()?;
// we validate caching behavior by eavesdropping on the second query and expecting no
// communication between the resolver and the nameservers
let ns_addrs = nameservers
.iter()
.map(|ns| ns.ipv4_addr())
.collect::<Vec<_>>();
for Capture { direction, .. } in captures {
assert!(!ns_addrs.contains(&direction.peer_addr()));
}
Ok(())
}
// all the zones are correctly signed but the parent of the leaf zone contains a DS record that
// corresponds to the child's ZSK. usually, the DS record contains the digest of the KSK and the
// KSK is used to sign the ZSK, which is the key used to sign the records in the child zone.
// however, it appears to also be fine to have the parent zone directly vouch for the child's ZSK,
// eliminating the need for a KSK, so long the ZSK is self-signed in the child zone
#[test]
#[ignore = "hickory responds with SERVFAIL"]
fn ds_of_zsk() -> Result<()> {
let sign_settings = SignSettings::default();
let network = Network::new()?;
let no_ds_zone = FQDN::TEST_TLD.push_label("ds-of-zsk");
let needle_fqdn = no_ds_zone.push_label("example");
let needle_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let mut leaf_ns = NameServer::new(&dns_test::PEER, no_ds_zone.clone(), &network)?;
leaf_ns.add(Record::a(needle_fqdn.clone(), needle_ipv4_addr));
let mut sibling_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_DOMAIN, &network)?;
let mut tld_ns = NameServer::new(&dns_test::PEER, FQDN::TEST_TLD, &network)?;
let mut root_ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, &network)?;
sibling_ns.add(root_ns.a());
sibling_ns.add(tld_ns.a());
sibling_ns.add(leaf_ns.a());
sibling_ns.add(sibling_ns.a());
root_ns.referral_nameserver(&tld_ns);
tld_ns.referral_nameserver(&sibling_ns);
tld_ns.referral_nameserver(&leaf_ns);
let mut leaf_ns = leaf_ns.sign(sign_settings.clone())?;
let sibling_ns = sibling_ns.sign(sign_settings.clone())?;
tld_ns.add(sibling_ns.ds().ksk.clone());
let ds2 = leaf_ns.ds();
let ksk_tag = ds2.ksk.key_tag;
let zsk_tag = ds2.zsk.key_tag;
dbg!(&ds2);
// sanity checks
assert_ne!(ds2.zsk.key_tag, ds2.ksk.key_tag, "DS records are equal");
assert_ne!(ds2.zsk.digest, ds2.ksk.digest, "DS records are equal");
// IMPORTANT here we use the DS that corresponds to the _Zone_ Signing Key (ZSK)
tld_ns.add(ds2.zsk.clone());
// remove the RRSIG over DNSKEY that was produced using the KSK
// check that there's a RRSIG over DNSKEY produced with the ZSK
let zone_file_records = &mut leaf_ns.signed_zone_file_mut().records;
let mut remove_count = 0;
let mut dnskey_signed_with_zsk = false;
for index in (0..zone_file_records.len()).rev() {
if let Record::RRSIG(rrsig) = &zone_file_records[index] {
if rrsig.key_tag == ksk_tag {
assert_eq!(RecordType::DNSKEY, rrsig.type_covered);
remove_count += 1;
zone_file_records.remove(index);
} else if rrsig.key_tag == zsk_tag && rrsig.type_covered == RecordType::DNSKEY {
dnskey_signed_with_zsk = true;
}
}
}
assert_eq!(1, remove_count);
assert!(dnskey_signed_with_zsk);
let tld_ns = tld_ns.sign(sign_settings.clone())?;
root_ns.add(tld_ns.ds().ksk.clone());
let mut trust_anchor = TrustAnchor::empty();
let root_ns = root_ns.sign(sign_settings)?;
trust_anchor.add(root_ns.key_signing_key().clone());
trust_anchor.add(root_ns.zone_signing_key().clone());
let root_hint = root_ns.root_hint();
let _root_ns = root_ns.start()?;
let _com_ns = tld_ns.start()?;
let _sibling_ns = sibling_ns.start()?;
let _no_ds_ns = leaf_ns.start()?;
let resolver = Resolver::new(&network, root_hint)
.trust_anchor(&trust_anchor)
.start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::A, &needle_fqdn)?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
Ok(())
}
// TODO nxdomain with NSEC records
// TODO nxdomain with NSEC3 records

View File

@@ -1,168 +0,0 @@
//! NSEC and NSEC3 denial of existence tests
use std::net::Ipv4Addr;
use dns_test::{
client::{Client, DigSettings},
name_server::{Graph, NameServer, Sign},
record::{Record, RecordType},
zone_file::{Nsec, SignSettings},
Network, Resolver, Result, FQDN,
};
#[test]
fn zone_exist_domain_does_not_nsec3() -> Result<()> {
zone_exist_domain_does_not(Nsec::_3 {
opt_out: false,
salt: None,
})
}
#[test]
fn zone_exist_domain_does_not_nsec() -> Result<()> {
zone_exist_domain_does_not(Nsec::_1)
}
#[test]
fn zone_does_not_exist_nsec3() -> Result<()> {
zone_does_not_exist(Nsec::_3 {
opt_out: false,
salt: None,
})
}
#[test]
fn zone_does_not_exist_nsec() -> Result<()> {
zone_does_not_exist(Nsec::_1)
}
#[test]
fn domain_exists_record_type_does_not_nsec3() -> Result<()> {
domain_exists_record_type_does_not(Nsec::_3 {
opt_out: false,
salt: None,
})
}
#[test]
fn domain_exists_record_type_does_not_nsec() -> Result<()> {
domain_exists_record_type_does_not(Nsec::_1)
}
fn zone_exist_domain_does_not(nsec: Nsec) -> Result<()> {
let leaf_zone = FQDN::TEST_TLD.push_label("exists");
let needle_fqdn = leaf_zone.push_label("unicorn");
let network = Network::new()?;
let leaf_ns = NameServer::new(&dns_test::PEER, leaf_zone.clone(), &network)?;
let mut settings = SignSettings::default();
settings = settings.nsec(nsec);
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(leaf_ns, Sign::Yes { settings })?;
let trust_anchor = trust_anchor.unwrap();
let resolver = Resolver::new(&network, root)
.trust_anchor(&trust_anchor)
.start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::A, &needle_fqdn)?;
dbg!(&output);
assert!(output.status.is_nxdomain());
assert!(output.flags.authenticated_data);
let [record] = output.authority.try_into().unwrap();
let soa = record.try_into_soa().unwrap();
assert_eq!(leaf_zone, soa.zone);
Ok(())
}
fn zone_does_not_exist(nsec: Nsec) -> Result<()> {
let parent_zone = FQDN::TEST_DOMAIN;
let leaf_zone = parent_zone.push_label("does-not-exist");
let needle_fqdn = leaf_zone.push_label("unicorn");
let network = Network::new()?;
let parent_ns = NameServer::new(&dns_test::PEER, parent_zone, &network)?;
let mut settings = SignSettings::default();
settings = settings.nsec(nsec);
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(parent_ns, Sign::Yes { settings })?;
let trust_anchor = trust_anchor.unwrap();
let resolver = Resolver::new(&network, root)
.trust_anchor(&trust_anchor)
.start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(settings, resolver.ipv4_addr(), RecordType::A, &needle_fqdn)?;
dbg!(&output);
assert!(output.status.is_nxdomain());
assert!(output.flags.authenticated_data);
let [record] = output.authority.try_into().unwrap();
let soa = record.try_into_soa().unwrap();
assert_eq!(FQDN::TEST_DOMAIN, soa.zone);
Ok(())
}
fn domain_exists_record_type_does_not(nsec: Nsec) -> Result<()> {
let leaf_zone = FQDN::TEST_TLD.push_label("exists");
let needle_fqdn = leaf_zone.push_label("example");
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, leaf_zone.clone(), &network)?;
leaf_ns.add(Record::a(needle_fqdn.clone(), Ipv4Addr::new(1, 2, 3, 4)));
let mut settings = SignSettings::default();
settings = settings.nsec(nsec);
let Graph {
nameservers: _nameservers,
root,
trust_anchor,
} = Graph::build(leaf_ns, Sign::Yes { settings })?;
let trust_anchor = trust_anchor.unwrap();
let resolver = Resolver::new(&network, root)
.trust_anchor(&trust_anchor)
.start()?;
let client = Client::new(&network)?;
let settings = *DigSettings::default().recurse().authentic_data();
let output = client.dig(
settings,
resolver.ipv4_addr(),
RecordType::AAAA,
&needle_fqdn,
)?;
dbg!(&output);
assert!(output.status.is_noerror());
assert!(output.flags.authenticated_data);
let [record] = output.authority.try_into().unwrap();
let soa = record.try_into_soa().unwrap();
assert_eq!(leaf_zone, soa.zone);
Ok(())
}

View File

@@ -1,23 +0,0 @@
[package]
edition = "2021"
license = "MIT OR Apache-2.0"
name = "dns-test"
publish = false
version = "0.1.0"
[dependencies]
base64 = "0.22.1"
lazy_static = "1.4.0"
minijinja = "1.0.12"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
serde_with = "3.6.1"
tempfile = "3.9.0"
url = "2.5.0"
[lib]
doctest = false
[dev-dependencies]
ctrlc = "3.4.2"
pretty_assertions = "1.4.0"

View File

@@ -1,139 +0,0 @@
use std::env;
use std::net::Ipv4Addr;
use std::sync::mpsc;
use dns_test::client::Client;
use dns_test::name_server::{Graph, NameServer, Sign};
use dns_test::record::RecordType;
use dns_test::zone_file::SignSettings;
use dns_test::{Network, Resolver, Result, FQDN};
fn main() -> Result<()> {
let args = Args::from_env()?;
let network = Network::new()?;
let peer = &dns_test::PEER;
println!("building docker image...");
let leaf_ns = NameServer::new(peer, FQDN::TEST_DOMAIN, &network)?;
println!("DONE");
println!("setting up name servers...");
let sign = if args.dnssec {
Sign::Yes {
settings: SignSettings::default(),
}
} else {
Sign::No
};
let Graph {
root,
trust_anchor,
nameservers,
} = Graph::build(leaf_ns, sign)?;
println!("DONE");
let client = Client::new(&network)?;
if args.dnssec {
// this will send queries to the loopback address and fail because there's no resolver
// but as a side-effect it will generate the `/etc/bind.keys` file we want
// ignore the expected error
let _ = client.delv(
Ipv4Addr::LOCALHOST,
RecordType::SOA,
&FQDN::ROOT,
trust_anchor.as_ref().unwrap(),
)?;
}
println!("building docker image...");
let mut builder = Resolver::new(&network, root);
if let Some(trust_anchor) = trust_anchor {
builder.trust_anchor(&trust_anchor);
}
let resolver = builder.start()?;
println!("DONE\n\n");
let (tx, rx) = mpsc::channel();
ctrlc::set_handler(move || tx.send(()).expect("could not forward signal"))?;
for ns in &nameservers {
println!("{} name server's IP address: {}", ns.zone(), ns.ipv4_addr());
println!(
"attach to this container with: `docker exec -it {} bash`\n",
ns.container_name()
);
}
let resolver_addr = resolver.ipv4_addr();
println!("resolver's IP address: {resolver_addr}",);
println!(
"attach to this container with: `docker exec -it {} bash`\n",
resolver.container_name()
);
println!("client's IP address: {}", client.ipv4_addr());
println!(
"attach to this container with: `docker exec -it {} bash`\n\n",
client.container_name()
);
println!("example queries (run these in the client container):\n");
let adflag = if args.dnssec { "+adflag" } else { "+noadflag" };
println!("`dig @{resolver_addr} {adflag} SOA .`\n");
if args.dnssec {
println!(
"`delv -a /etc/bind.keys @{resolver_addr} SOA .` (you MUST use the `-a` flag with delv)\n\n"
);
}
println!(
"to print the DNS traffic flowing through the resolver run this command in
the resolver container before performing queries:\n"
);
println!("`tshark -f 'udp port 53' -O dns`\n\n");
println!("press Ctrl+C to take down the network");
rx.recv()?;
println!("\ntaking down network...");
Ok(())
}
struct Args {
dnssec: bool,
}
impl Args {
fn from_env() -> Result<Self> {
let args: Vec<_> = env::args().skip(1).collect();
let num_args = args.len();
let dnssec = if num_args == 0 {
false
} else if num_args == 1 {
if args[0] == "--dnssec" {
true
} else {
return cli_error();
}
} else {
return cli_error();
};
Ok(Self { dnssec })
}
}
fn cli_error<T>() -> Result<T> {
eprintln!(
"usage: explore [--dnssec]
Options:
--dnssec sign zone files to enable DNSSEC"
);
Err("CLI error".into())
}

View File

@@ -1,593 +0,0 @@
use core::str::FromStr;
use std::collections::BTreeSet;
use std::net::Ipv4Addr;
use crate::container::{Container, Image, Network};
use crate::record::{Record, RecordType};
use crate::trust_anchor::TrustAnchor;
use crate::{Error, Result, FQDN};
pub struct Client {
inner: Container,
}
impl Client {
pub fn new(network: &Network) -> Result<Self> {
Ok(Self {
inner: Container::run(&Image::Client, network)?,
})
}
pub fn container_id(&self) -> &str {
self.inner.id()
}
pub fn container_name(&self) -> &str {
self.inner.name()
}
pub fn ipv4_addr(&self) -> Ipv4Addr {
self.inner.ipv4_addr()
}
pub fn delv(
&self,
server: Ipv4Addr,
record_type: RecordType,
fqdn: &FQDN,
trust_anchor: &TrustAnchor,
) -> Result<String> {
const TRUST_ANCHOR_PATH: &str = "/etc/bind.keys";
assert!(
!trust_anchor.is_empty(),
"`delv` cannot be used with an empty trust anchor"
);
self.inner.cp(TRUST_ANCHOR_PATH, &trust_anchor.delv())?;
self.inner.stdout(&[
"delv",
&format!("@{server}"),
"-a",
TRUST_ANCHOR_PATH,
fqdn.as_str(),
record_type.as_str(),
])
}
pub fn dig(
&self,
settings: DigSettings,
server: Ipv4Addr,
record_type: RecordType,
fqdn: &FQDN,
) -> Result<DigOutput> {
let output = self.inner.stdout(&[
"dig",
settings.rdflag(),
settings.do_bit(),
settings.adflag(),
settings.cdflag(),
settings.timeoutflag().as_str(),
&format!("@{server}"),
record_type.as_str(),
fqdn.as_str(),
])?;
output.parse()
}
}
#[derive(Clone, Copy, Default)]
pub struct DigSettings {
adflag: bool,
cdflag: bool,
dnssec: bool,
recurse: bool,
timeout: Option<u8>,
}
impl DigSettings {
/// Sets the AD bit in the query
pub fn authentic_data(&mut self) -> &mut Self {
self.adflag = true;
self
}
fn adflag(&self) -> &'static str {
if self.adflag {
"+adflag"
} else {
"+noadflag"
}
}
/// Sets the CD bit in the query
pub fn checking_disabled(&mut self) -> &mut Self {
self.cdflag = true;
self
}
fn cdflag(&self) -> &'static str {
if self.cdflag {
"+cdflag"
} else {
"+nocdflag"
}
}
/// Sets the DO bit in the query
pub fn dnssec(&mut self) -> &mut Self {
self.dnssec = true;
self
}
fn do_bit(&self) -> &'static str {
if self.dnssec {
"+dnssec"
} else {
"+nodnssec"
}
}
/// Sets the RD bit in the query
pub fn recurse(&mut self) -> &mut Self {
self.recurse = true;
self
}
fn rdflag(&self) -> &'static str {
if self.recurse {
"+recurse"
} else {
"+norecurse"
}
}
/// Sets the timeout for the query, specified in seconds
pub fn timeout(&mut self, timeout: u8) -> &mut Self {
self.timeout = Some(timeout);
self
}
fn timeoutflag(&self) -> String {
match self.timeout {
Some(timeout) => format!("+timeout={timeout}"),
None => "+timeout=5".into(),
}
}
}
#[derive(Debug)]
pub struct DigOutput {
pub ede: BTreeSet<ExtendedDnsError>,
pub flags: DigFlags,
pub status: DigStatus,
pub answer: Vec<Record>,
pub authority: Vec<Record>,
pub additional: Vec<Record>,
pub options: Vec<(u16, String)>,
// TODO(if needed) other sections
}
impl FromStr for DigOutput {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
const FLAGS_PREFIX: &str = ";; flags: ";
const STATUS_PREFIX: &str = ";; ->>HEADER<<- opcode: QUERY, status: ";
const EDE_PREFIX: &str = "; EDE: ";
const OPT_PREFIX: &str = "; OPT=";
const ANSWER_HEADER: &str = ";; ANSWER SECTION:";
const AUTHORITY_HEADER: &str = ";; AUTHORITY SECTION:";
const ADDITIONAL_HEADER: &str = ";; ADDITIONAL SECTION:";
fn not_found(prefix: &str) -> String {
format!("`{prefix}` line was not found")
}
fn more_than_once(prefix: &str) -> String {
format!("`{prefix}` line was found more than once")
}
fn missing(prefix: &str, delimiter: &str) -> String {
format!("`{prefix}` line is missing a {delimiter}")
}
let mut flags = None;
let mut status = None;
let mut answer = None;
let mut authority = None;
let mut additional = None;
let mut ede = BTreeSet::new();
let mut options = Vec::new();
let mut lines = input.lines();
while let Some(line) = lines.next() {
if let Some(unprefixed) = line.strip_prefix(FLAGS_PREFIX) {
let (flags_text, _rest) = unprefixed
.split_once(';')
.ok_or_else(|| missing(FLAGS_PREFIX, "semicolon (;)"))?;
if flags.is_some() {
return Err(more_than_once(FLAGS_PREFIX).into());
}
flags = Some(flags_text.parse()?);
} else if let Some(unprefixed) = line.strip_prefix(STATUS_PREFIX) {
let (status_text, _rest) = unprefixed
.split_once(',')
.ok_or_else(|| missing(STATUS_PREFIX, "comma (,)"))?;
if status.is_some() {
return Err(more_than_once(STATUS_PREFIX).into());
}
status = Some(status_text.parse()?);
} else if let Some(unprefixed) = line.strip_prefix(EDE_PREFIX) {
let code = unprefixed
.split_once(' ')
.map(|(code, _rest)| code)
.unwrap_or(unprefixed);
let code = code.parse()?;
let inserted = ede.insert(code);
assert!(inserted, "unexpected: duplicate EDE {code:?}");
} else if let Some(unprefixed) = line.strip_prefix(OPT_PREFIX) {
let Some((option_str, value)) = unprefixed.split_once(": ") else {
return Err("could not parse option".into());
};
let option_number = option_str.parse::<u16>()?;
options.push((option_number, value.to_string()));
} else if line.starts_with(ANSWER_HEADER) {
if answer.is_some() {
return Err(more_than_once(ANSWER_HEADER).into());
}
let mut records = vec![];
for line in lines.by_ref() {
if line.is_empty() {
break;
}
records.push(line.parse()?);
}
answer = Some(records);
} else if line.starts_with(AUTHORITY_HEADER) {
if authority.is_some() {
return Err(more_than_once(AUTHORITY_HEADER).into());
}
let mut records = vec![];
for line in lines.by_ref() {
if line.is_empty() {
break;
}
records.push(line.parse()?);
}
authority = Some(records);
} else if line.starts_with(ADDITIONAL_HEADER) {
if additional.is_some() {
return Err(more_than_once(ADDITIONAL_HEADER).into());
}
let mut records = vec![];
for line in lines.by_ref() {
if line.is_empty() {
break;
}
records.push(line.parse()?);
}
additional = Some(records);
}
}
Ok(Self {
answer: answer.unwrap_or_default(),
authority: authority.unwrap_or_default(),
additional: additional.unwrap_or_default(),
ede,
flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?,
status: status.ok_or_else(|| not_found(STATUS_PREFIX))?,
options,
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub enum ExtendedDnsError {
UnsupportedDnskeyAlgorithm = 1,
DnssecBogus = 6,
DnskeyMissing = 9,
RrsigsMissing = 10,
Prohibited = 18,
NoReachableAuthority = 22,
}
impl FromStr for ExtendedDnsError {
type Err = Error;
fn from_str(input: &str) -> std::prelude::v1::Result<Self, Self::Err> {
let code: u16 = input.parse()?;
let code = match code {
1 => Self::UnsupportedDnskeyAlgorithm,
6 => Self::DnssecBogus,
9 => Self::DnskeyMissing,
10 => Self::RrsigsMissing,
18 => Self::Prohibited,
22 => Self::NoReachableAuthority,
_ => todo!("EDE {code} has not yet been implemented"),
};
Ok(code)
}
}
#[derive(Debug, Default, PartialEq)]
pub struct DigFlags {
pub authenticated_data: bool,
pub authoritative_answer: bool,
pub checking_disabled: bool,
pub qr: bool,
pub recursion_available: bool,
pub recursion_desired: bool,
}
impl FromStr for DigFlags {
type Err = Error;
fn from_str(input: &str) -> std::prelude::v1::Result<Self, Self::Err> {
let mut qr = false;
let mut recursion_desired = false;
let mut recursion_available = false;
let mut authoritative_answer = false;
let mut authenticated_data = false;
let mut checking_disabled = false;
for flag in input.split_whitespace() {
match flag {
"qr" => qr = true,
"rd" => recursion_desired = true,
"ra" => recursion_available = true,
"aa" => authoritative_answer = true,
"ad" => authenticated_data = true,
"cd" => checking_disabled = true,
_ => return Err(format!("unknown flag: {flag}").into()),
}
}
Ok(Self {
authenticated_data,
authoritative_answer,
checking_disabled,
qr,
recursion_available,
recursion_desired,
})
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DigStatus {
NOERROR,
NXDOMAIN,
REFUSED,
SERVFAIL,
}
impl DigStatus {
#[must_use]
pub fn is_noerror(&self) -> bool {
matches!(self, Self::NOERROR)
}
#[must_use]
pub fn is_nxdomain(&self) -> bool {
matches!(self, Self::NXDOMAIN)
}
#[must_use]
pub fn is_servfail(&self) -> bool {
matches!(self, Self::SERVFAIL)
}
}
impl FromStr for DigStatus {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
let status = match input {
"NXDOMAIN" => Self::NXDOMAIN,
"NOERROR" => Self::NOERROR,
"REFUSED" => Self::REFUSED,
"SERVFAIL" => Self::SERVFAIL,
_ => return Err(format!("unknown status: {input}").into()),
};
Ok(status)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dig_nxdomain() -> Result<()> {
// $ dig nonexistent.domain.
let input = "
; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> nonexistent.domain.
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 45583
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;nonexistent.domain. IN A
;; Query time: 3 msec
;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP)
;; WHEN: Tue Feb 06 15:00:12 UTC 2024
;; MSG SIZE rcvd: 47
";
let output: DigOutput = input.parse()?;
assert_eq!(DigStatus::NXDOMAIN, output.status);
assert_eq!(
DigFlags {
qr: true,
recursion_desired: true,
recursion_available: true,
..DigFlags::default()
},
output.flags
);
assert!(output.answer.is_empty());
Ok(())
}
#[test]
fn authority_section() -> Result<()> {
// $ dig A .
let input = "
; <<>> DiG 9.18.24 <<>> A .
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39670
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;. IN A
;; AUTHORITY SECTION:
. 2910 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024022600 1800 900 604800 86400
;; Query time: 43 msec
;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP)
;; WHEN: Mon Feb 26 11:55:50 CET 2024
;; MSG SIZE rcvd: 103
";
let output: DigOutput = input.parse()?;
let [record] = output.authority.try_into().expect("exactly one record");
matches!(record, Record::SOA(..));
Ok(())
}
#[test]
fn additional_section() -> Result<()> {
// $ dig @a.root-servers.net. +norecurse NS .
// but with most records removed from each section to keep this short
let input =
"; <<>> DiG 9.18.24-0ubuntu0.22.04.1-Ubuntu <<>> @a.root-servers.net. +norecurse NS .
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3739
;; flags: qr aa; QUERY: 1, ANSWER: 13, AUTHORITY: 0, ADDITIONAL: 27
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;. IN NS
;; ANSWER SECTION:
. 518400 IN NS l.root-servers.net.
;; ADDITIONAL SECTION:
l.root-servers.net. 518400 IN A 199.7.83.42
;; Query time: 20 msec
;; SERVER: 198.41.0.4#53(a.root-servers.net.) (UDP)
;; WHEN: Fri Jul 12 18:14:04 CEST 2024
;; MSG SIZE rcvd: 811
";
let output: DigOutput = input.parse()?;
let [record] = output.additional.try_into().expect("exactly one record");
matches!(record, Record::A(..));
Ok(())
}
#[test]
fn ede() -> Result<()> {
let input = "; <<>> DiG 9.18.24-1-Debian <<>> +recurse +nodnssec +adflag +nocdflag @192.168.176.5 A example.nameservers.com.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 49801
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 9 (DNSKEY Missing)
;; QUESTION SECTION:
;example.nameservers.com. IN A
;; Query time: 26 msec
;; SERVER: 192.168.176.5#53(192.168.176.5) (UDP)
;; WHEN: Tue Mar 05 17:45:29 UTC 2024
;; MSG SIZE rcvd: 58
";
let output: DigOutput = input.parse()?;
assert!(output.ede.into_iter().eq([ExtendedDnsError::DnskeyMissing]));
Ok(())
}
#[test]
fn multiple_ede() -> Result<()> {
let input = "; <<>> DiG 9.18.28-1~deb12u2-Debian <<>> @1.1.1.1 allow-query-none.extended-dns-errors.com.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 57468
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 9 (DNSKEY Missing): (no SEP matching the DS found for allow-query-none.extended-dns-errors.com.)
; EDE: 18 (Prohibited)
; EDE: 22 (No Reachable Authority): (at delegation allow-query-none.extended-dns-errors.com.)
;; QUESTION SECTION:
;allow-query-none.extended-dns-errors.com. IN A
;; Query time: 98 msec
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
;; WHEN: Fri Aug 23 14:24:40 UTC 2024
;; MSG SIZE rcvd: 216";
let output: DigOutput = input.parse()?;
assert!(output.ede.into_iter().eq([
ExtendedDnsError::DnskeyMissing,
ExtendedDnsError::Prohibited,
ExtendedDnsError::NoReachableAuthority,
]));
Ok(())
}
}

View File

@@ -1,515 +0,0 @@
mod network;
use core::{fmt, str};
use std::ffi::OsStr;
use std::net::Ipv4Addr;
use std::process::{self, ChildStderr, ChildStdout, ExitStatus};
use std::process::{Command, Stdio};
use std::sync::atomic::AtomicUsize;
use std::sync::{atomic, Arc, Once};
use std::{env, fs};
use tempfile::{NamedTempFile, TempDir};
pub use crate::container::network::Network;
use crate::{Error, HickoryDnssecFeature, Implementation, Repository, Result};
#[derive(Clone)]
pub struct Container {
inner: Arc<Inner>,
}
const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
#[derive(Clone)]
pub enum Image {
Bind,
Dnslib,
Client,
Hickory {
repo: Repository<'static>,
dnssec_feature: Option<HickoryDnssecFeature>,
},
Unbound,
}
impl Image {
pub fn hickory() -> Self {
Self::Hickory {
repo: Repository(crate::repo_root()),
dnssec_feature: None,
}
}
fn dockerfile(&self) -> &'static str {
match self {
Self::Bind => include_str!("docker/bind.Dockerfile"),
Self::Dnslib => include_str!("docker/dnslib.Dockerfile"),
Self::Client => include_str!("docker/client.Dockerfile"),
Self::Hickory { .. } => include_str!("docker/hickory.Dockerfile"),
Self::Unbound => include_str!("docker/unbound.Dockerfile"),
}
}
fn once(&self) -> &'static Once {
match self {
Self::Bind => {
static BIND_ONCE: Once = Once::new();
&BIND_ONCE
}
Self::Dnslib => {
static DNSLIB_ONCE: Once = Once::new();
&DNSLIB_ONCE
}
Self::Client => {
static CLIENT_ONCE: Once = Once::new();
&CLIENT_ONCE
}
Self::Hickory { .. } => {
static HICKORY_ONCE: Once = Once::new();
&HICKORY_ONCE
}
Self::Unbound => {
static UNBOUND_ONCE: Once = Once::new();
&UNBOUND_ONCE
}
}
}
}
impl From<Implementation> for Image {
fn from(implementation: Implementation) -> Self {
match implementation {
Implementation::Bind => Self::Bind,
Implementation::Dnslib => Self::Dnslib,
Implementation::Unbound => Self::Unbound,
Implementation::Hickory {
repo,
dnssec_feature,
} => Self::Hickory {
repo,
dnssec_feature,
},
}
}
}
impl fmt::Display for Image {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Client => f.write_str("client"),
Self::Bind => f.write_str("bind"),
Self::Dnslib => f.write_str("dnslib"),
Self::Hickory {
repo: _,
dnssec_feature: None,
} => f.write_str("hickory"),
Self::Hickory {
repo: _,
dnssec_feature: Some(dnssec_feature),
} => write!(f, "hickory-{dnssec_feature}"),
Self::Unbound => f.write_str("unbound"),
}
}
}
impl Container {
/// Starts the container in a "parked" state
pub fn run(image: &Image, network: &Network) -> Result<Self> {
// TODO make this configurable and support hickory & bind
let dockerfile = image.dockerfile();
let docker_build_dir = TempDir::new()?;
let docker_build_dir = docker_build_dir.path();
fs::write(docker_build_dir.join("Dockerfile"), dockerfile)?;
let image_tag = format!("{PACKAGE_NAME}-{image}");
let mut command = Command::new("docker");
command
.args(["build", "-t"])
.arg(&image_tag)
.arg(docker_build_dir);
if let Image::Hickory {
dnssec_feature: Some(dnssec_feature),
..
} = image
{
command.arg(format!("--build-arg=DNSSEC_FEATURE={dnssec_feature}"));
};
let repo = if let Image::Hickory { repo, .. } = image {
Some(repo)
} else {
None
};
if !skip_docker_build() {
image.once().call_once(|| {
if let Some(repo) = repo {
let mut cp_r = Command::new("git");
cp_r.args([
"clone",
"--depth",
"1",
repo.as_str(),
&docker_build_dir.join("src").display().to_string(),
]);
exec_or_panic(&mut cp_r, false);
}
fs::write(docker_build_dir.join(".dockerignore"), "src/.git")
.expect("could not create .dockerignore file");
exec_or_panic(&mut command, verbose_docker_build());
});
}
let mut command = Command::new("docker");
let pid = process::id();
let count = container_count();
let name = format!("{PACKAGE_NAME}-{image}-{pid}-{count}");
command
.args([
"run",
"--rm",
"--detach",
"--cap-add=NET_RAW",
"--cap-add=NET_ADMIN",
"--network",
network.name(),
"--name",
&name,
])
.arg(image_tag)
.args(["sleep", "infinity"]);
let output: Output = checked_output(&mut command)?.try_into()?;
let id = output.stdout;
let ipv4_addr = get_ipv4_addr(&id)?;
let inner = Inner {
id,
name,
ipv4_addr,
network: network.clone(),
};
Ok(Self {
inner: Arc::new(inner),
})
}
pub fn cp(&self, path_in_container: &str, file_contents: &str) -> Result<()> {
const CHMOD_RW_EVERYONE: &str = "666";
let mut temp_file = NamedTempFile::new()?;
fs::write(&mut temp_file, file_contents)?;
let src_path = temp_file.path().display().to_string();
let dest_path = format!("{}:{path_in_container}", self.inner.id);
let mut command = Command::new("docker");
command.args(["cp", &src_path, &dest_path]);
checked_output(&mut command)?;
self.status_ok(&["chmod", CHMOD_RW_EVERYONE, path_in_container])?;
Ok(())
}
/// Similar to `std::process::Command::output` but runs `command_and_args` in the container
pub fn output(&self, command_and_args: &[&str]) -> Result<Output> {
let mut command = Command::new("docker");
command
.args(["exec", &self.inner.id])
.args(command_and_args);
command.output()?.try_into()
}
/// Similar to `Self::output` but checks `command_and_args` ran successfully and only
/// returns the stdout
pub fn stdout(&self, command_and_args: &[&str]) -> Result<String> {
let Output {
status,
stderr,
stdout,
} = self.output(command_and_args)?;
if status.success() {
Ok(stdout)
} else {
eprintln!("STDOUT:\n{stdout}\nSTDERR:\n{stderr}");
Err(format!("[{}] `{command_and_args:?}` failed", self.inner.name).into())
}
}
/// Similar to `std::process::Command::status` but runs `command_and_args` in the container
pub fn status(&self, command_and_args: &[&str]) -> Result<ExitStatus> {
let mut command = Command::new("docker");
command
.args(["exec", &self.inner.id])
.args(command_and_args)
.stdout(Stdio::null())
.stderr(Stdio::null());
Ok(command.status()?)
}
/// Like `Self::status` but checks that `command_and_args` executed successfully
pub fn status_ok(&self, command_and_args: &[&str]) -> Result<()> {
let status = self.status(command_and_args)?;
if status.success() {
Ok(())
} else {
Err(format!("[{}] `{command_and_args:?}` failed", self.inner.name).into())
}
}
pub fn spawn(&self, cmd: &[impl AsRef<OsStr>]) -> Result<Child> {
let mut command = Command::new("docker");
command.stdout(Stdio::piped()).stderr(Stdio::piped());
command.args(["exec", &self.inner.id]).args(cmd);
let inner = command.spawn()?;
Ok(Child {
inner: Some(inner),
_container: self.inner.clone(),
})
}
pub fn ipv4_addr(&self) -> Ipv4Addr {
self.inner.ipv4_addr
}
pub fn id(&self) -> &str {
&self.inner.id
}
pub(crate) fn network(&self) -> &Network {
&self.inner.network
}
pub fn name(&self) -> &str {
&self.inner.name
}
}
fn verbose_docker_build() -> bool {
env::var("DNS_TEST_VERBOSE_DOCKER_BUILD").as_deref().is_ok()
}
fn skip_docker_build() -> bool {
env::var("DNS_TEST_SKIP_DOCKER_BUILD").is_ok()
}
fn exec_or_panic(command: &mut Command, verbose: bool) {
if verbose {
let status = command.status().unwrap();
assert!(status.success());
} else {
let output = command.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"--- STDOUT ---\n{stdout}\n--- STDERR ---\n{stderr}"
);
}
}
fn container_count() -> usize {
static COUNT: AtomicUsize = AtomicUsize::new(0);
COUNT.fetch_add(1, atomic::Ordering::Relaxed)
}
struct Inner {
name: String,
id: String,
// TODO probably also want the IPv6 address
ipv4_addr: Ipv4Addr,
network: Network,
}
/// NOTE unlike `std::process::Child`, the drop implementation of this type will `kill` the
/// child process
// this wrapper over `std::process::Child` stores a reference to the container the child process
// runs inside of, to prevent the scenario of the container being destroyed _before_
// the child is killed
pub struct Child {
inner: Option<process::Child>,
_container: Arc<Inner>,
}
impl Child {
/// Returns a handle to the child's stdout
///
/// This method will succeed at most once
pub fn stdout(&mut self) -> Result<ChildStdout> {
Ok(self
.inner
.as_mut()
.and_then(|child| child.stdout.take())
.ok_or("could not retrieve child's stdout")?)
}
/// Returns a handle to the child's stderr
///
/// This method will succeed at most once
pub fn stderr(&mut self) -> Result<ChildStderr> {
Ok(self
.inner
.as_mut()
.and_then(|child| child.stderr.take())
.ok_or("could not retrieve child's stderr")?)
}
/// Returns the child's exit status, if the child process has exited. try_wait will not block
/// on a running process.
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
match self.inner.as_mut() {
Some(ref mut child) => Ok(child.try_wait()?),
_ => Err("can't borrow child as mut for try_wait".into()),
}
}
pub fn wait(mut self) -> Result<Output> {
let output = self.inner.take().expect("unreachable").wait_with_output()?;
output.try_into()
}
}
impl Drop for Child {
fn drop(&mut self) {
if let Some(mut inner) = self.inner.take() {
let _ = inner.kill();
}
}
}
#[derive(Debug)]
pub struct Output {
pub status: ExitStatus,
pub stderr: String,
pub stdout: String,
}
impl TryFrom<process::Output> for Output {
type Error = Error;
fn try_from(output: process::Output) -> Result<Self> {
let mut stderr = String::from_utf8(output.stderr)?;
while stderr.ends_with(['\n', '\r']) {
stderr.pop();
}
let mut stdout = String::from_utf8(output.stdout)?;
while stdout.ends_with(['\n', '\r']) {
stdout.pop();
}
Ok(Self {
status: output.status,
stderr,
stdout,
})
}
}
fn checked_output(command: &mut Command) -> Result<process::Output> {
let output = command.output()?;
if output.status.success() {
Ok(output)
} else {
Err(format!("`{command:?}` failed").into())
}
}
fn get_ipv4_addr(container_id: &str) -> Result<Ipv4Addr> {
let mut command = Command::new("docker");
command
.args([
"inspect",
"-f",
"{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}",
])
.arg(container_id);
let output = command.output()?;
if !output.status.success() {
return Err(format!("`{command:?}` failed").into());
}
let ipv4_addr = str::from_utf8(&output.stdout)?.trim().to_string();
Ok(ipv4_addr.parse()?)
}
// this ensures the container gets deleted and does not linger after the test runner process ends
impl Drop for Inner {
fn drop(&mut self) {
// running this to completion would block the current thread for several seconds so just
// fire and forget
let _ = Command::new("docker")
.args(["rm", "-f", &self.id])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_works() -> Result<()> {
let network = Network::new()?;
let container = Container::run(&Image::Client, &network)?;
let output = container.output(&["true"])?;
assert!(output.status.success());
Ok(())
}
#[test]
fn ipv4_addr_works() -> Result<()> {
let network = Network::new()?;
let container = Container::run(&Image::Client, &network)?;
let ipv4_addr = container.ipv4_addr();
let output = container.output(&["ping", "-c1", &format!("{ipv4_addr}")])?;
assert!(output.status.success());
Ok(())
}
#[test]
fn cp_works() -> Result<()> {
let network = Network::new()?;
let container = Container::run(&Image::Client, &network)?;
let path = "/tmp/somefile";
let contents = "hello";
container.cp(path, contents)?;
let output = container.output(&["cat", path])?;
dbg!(&output);
assert!(output.status.success());
assert_eq!(contents, output.stdout);
Ok(())
}
}

View File

@@ -1,171 +0,0 @@
use std::{
process::{self, Command, Stdio},
sync::{
atomic::{self, AtomicUsize},
Arc, Mutex,
},
};
use crate::Result;
/// Represents a network in which to put containers into.
#[derive(Clone)]
pub struct Network(Arc<NetworkInner>);
impl Network {
/// Returns the name of the network.
pub fn name(&self) -> &str {
self.0.name.as_str()
}
/// Returns the subnet mask
pub fn netmask(&self) -> &str {
&self.0.config.subnet
}
}
struct NetworkInner {
name: String,
config: NetworkConfig,
}
impl Network {
pub fn new() -> Result<Self> {
let pid = process::id();
let network_name = env!("CARGO_PKG_NAME");
Ok(Self(Arc::new(NetworkInner::new(pid, network_name, true)?)))
}
pub fn with_internet_access() -> Result<Self> {
let pid = process::id();
let network_name = env!("CARGO_PKG_NAME");
Ok(Self(Arc::new(NetworkInner::new(pid, network_name, false)?)))
}
}
/// This ensure the Docker network is deleted after the test runner process ends.
impl Drop for NetworkInner {
fn drop(&mut self) {
let _ = Command::new("docker")
.args(["network", "rm", "--force", self.name.as_str()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
impl NetworkInner {
pub fn new(pid: u32, network_name: &str, internal: bool) -> Result<Self> {
static CRITICAL_SECTION: Mutex<()> = Mutex::new(());
let count = network_count();
let network_name = format!("{network_name}-{pid}-{count}");
let mut command = Command::new("docker");
command.args(["network", "create"]);
if internal {
command.arg("--internal");
}
command.arg("--attachable").arg(&network_name);
// create network
let output = {
// `docker network create` is racy in some versions of Docker. this `Mutex` ensure that
// multiple test threads do not run the command in parallel
let _guard = CRITICAL_SECTION.lock()?;
command.output()?
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
return Err(format!("--- STDOUT ---\n{stdout}\n--- STDERR ---\n{stderr}").into());
}
// inspect & parse network details
let config = get_network_config(&network_name)?;
Ok(Self {
name: network_name,
config,
})
}
}
/// Collects all important configs.
pub struct NetworkConfig {
/// The CIDR subnet mask, e.g. "172.21.0.0/16"
subnet: String,
}
/// Return network config
fn get_network_config(network_name: &str) -> Result<NetworkConfig> {
let mut command = Command::new("docker");
command
.args([
"network",
"inspect",
"-f",
"{{range .IPAM.Config}}{{.Subnet}}{{end}}",
])
.arg(network_name);
let output = command.output()?;
if !output.status.success() {
return Err(format!("{command:?} failed").into());
}
let subnet = std::str::from_utf8(&output.stdout)?.trim().to_string();
Ok(NetworkConfig { subnet })
}
fn network_count() -> usize {
static COUNT: AtomicUsize = AtomicUsize::new(1);
COUNT.fetch_add(1, atomic::Ordering::Relaxed)
}
#[cfg(test)]
mod tests {
use crate::container::{Container, Image};
use super::*;
fn exists_network(network_name: &str) -> bool {
let mut command = Command::new("docker");
command.args(["network", "ls", "--format={{ .Name }}"]);
let output = command.output().expect("Failed to get output");
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.trim().lines().any(|line| line == network_name)
}
#[test]
fn create_works() -> Result<()> {
let network = Network::new();
assert!(network.is_ok());
let network = network.expect("Failed to construct network");
assert!(exists_network(network.name()));
Ok(())
}
#[test]
fn remove_network_works() -> Result<()> {
let network = Network::new().expect("Failed to create network");
let network_name = network.name().to_string();
let container =
Container::run(&Image::Client, &network).expect("Failed to start container");
assert!(exists_network(&network_name));
drop(network);
assert!(exists_network(&network_name));
drop(container);
assert!(!exists_network(&network_name));
Ok(())
}
}

View File

@@ -1,11 +0,0 @@
FROM debian:bookworm-slim
# ldns-utils = ldns-{key2ds,keygen,signzone}
# rm = remove default configuration files
RUN apt-get update && \
apt-get install -y \
bind9 \
ldnsutils \
bind9-utils \
tshark && \
rm -f /etc/bind/*

View File

@@ -1,9 +0,0 @@
FROM debian:bookworm-slim
# dnsutils = dig & delv
# iputils-ping = ping
RUN apt-get update && \
apt-get install -y \
dnsutils \
iputils-ping \
netcat-openbsd

View File

@@ -1,6 +0,0 @@
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y \
python3 \
python3-dnslib

View File

@@ -1,23 +0,0 @@
FROM rust:1-slim-bookworm
ARG DNSSEC_FEATURE=dnssec-ring
# ldns-utils = ldns-{key2ds,keygen,signzone}
RUN apt-get update && \
apt-get install -y \
ldnsutils \
bind9-utils \
tshark \
libssl-dev \
pkg-config
# `dns-test` will invoke `docker build` from a temporary directory that contains
# a clone of the hickory repository. `./src` here refers to that clone; not to
# any directory inside the `hickory-dns` repository
COPY ./src /usr/src/hickory
RUN --mount=type=cache,target=/usr/src/hickory/target \
cargo build --manifest-path /usr/src/hickory/Cargo.toml -p hickory-dns --features recursor,$DNSSEC_FEATURE && \
cargo build --manifest-path /usr/src/hickory/Cargo.toml --bin dns --features dns-over-h3,dns-over-https-rustls,dns-over-quic && \
cp /usr/src/hickory/target/debug/hickory-dns /usr/bin/ && \
cp /usr/src/hickory/target/debug/dns /usr/bin/
ENV RUST_LOG=debug

View File

@@ -1,30 +0,0 @@
FROM debian:bookworm-slim
# ldns-utils = ldns-{key2ds,keygen,signzone}
# curl, etc. are used to build unbound from source
RUN apt-get update && \
apt-get install -y \
ldnsutils \
bind9-utils \
nsd \
tshark \
curl \
gcc \
bison \
flex \
libssl-dev \
libexpat-dev \
make
ENV UNBOUND_VERSION=1.21.0
RUN curl -L https://github.com/NLnetLabs/unbound/archive/refs/tags/release-$UNBOUND_VERSION.tar.gz | tar xvz -C /tmp/ && \
cd /tmp/unbound-release-$UNBOUND_VERSION && \
./configure \
--prefix=/usr \
--sysconfdir=/etc \
--localstatedir=/var \
--with-chroot-dir= && \
make -j$(nproc) && make install && \
rm -rf /tmp/unbound-release-$UNBOUND_VERSION
RUN useradd --shell /usr/sbin/nologin --system --create-home --home-dir /var/lib/unbound unbound

View File

@@ -1,149 +0,0 @@
use core::fmt;
use core::str::FromStr;
use std::borrow::Cow;
use crate::{Error, Result};
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct FQDN {
inner: Cow<'static, str>,
}
// TODO likely needs further validation
#[allow(non_snake_case)]
pub fn FQDN(input: impl Into<Cow<'static, str>>) -> Result<FQDN> {
let input = input.into();
if !input.ends_with('.') {
return Err("FQDN must end with a `.`".into());
}
if input != "." && input.starts_with('.') {
return Err("non-root FQDN cannot start with a `.`".into());
}
Ok(FQDN { inner: input })
}
impl FQDN {
pub const ROOT: FQDN = FQDN {
inner: Cow::Borrowed("."),
};
pub const TEST_TLD: FQDN = FQDN {
inner: Cow::Borrowed("testing."),
};
pub const TEST_DOMAIN: FQDN = FQDN {
inner: Cow::Borrowed("hickory-dns.testing."),
};
pub const EXAMPLE_SUBDOMAIN: FQDN = FQDN {
inner: Cow::Borrowed("example.hickory-dns.testing."),
};
pub fn is_root(&self) -> bool {
self.inner == "."
}
pub fn as_str(&self) -> &str {
&self.inner
}
pub fn push_label(&self, label: &str) -> Self {
assert!(!label.is_empty());
assert!(!label.contains('.'));
Self {
inner: format!("{label}.{}", self.inner).into(),
}
}
pub fn into_owned(self) -> FQDN {
let owned = match self.inner {
Cow::Borrowed(borrowed) => borrowed.to_string(),
Cow::Owned(owned) => owned,
};
FQDN {
inner: Cow::Owned(owned),
}
}
pub fn parent(&self) -> Option<FQDN> {
let (fragment, parent) = self.inner.split_once('.').unwrap();
if fragment.is_empty() {
None
} else {
let parent = if parent.is_empty() {
FQDN::ROOT
} else {
FQDN(parent.to_string()).unwrap()
};
Some(parent)
}
}
pub fn num_labels(&self) -> usize {
self.inner
.split('.')
.filter(|label| !label.is_empty())
.count()
}
pub fn last_label(&self) -> &str {
self.inner.split_once('.').map(|(label, _)| label).unwrap()
}
}
impl FromStr for FQDN {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
FQDN(input.to_string())
}
}
impl fmt::Debug for FQDN {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for FQDN {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parent() -> Result<()> {
let mut fqdn = FQDN::EXAMPLE_SUBDOMAIN;
assert_eq!(3, fqdn.num_labels());
let parent = fqdn.parent();
assert_eq!(Some(FQDN::TEST_DOMAIN), parent);
fqdn = parent.unwrap();
assert_eq!(2, fqdn.num_labels());
let parent = fqdn.parent();
assert_eq!(Some(FQDN::TEST_TLD), parent);
fqdn = parent.unwrap();
assert_eq!(1, fqdn.num_labels());
let parent = fqdn.parent();
assert_eq!(Some(FQDN::ROOT), parent);
fqdn = parent.unwrap();
assert_eq!(0, fqdn.num_labels());
let parent = fqdn.parent();
assert!(parent.is_none());
Ok(())
}
}

View File

@@ -1,330 +0,0 @@
use core::fmt;
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::Path;
use std::str::FromStr;
use url::Url;
use crate::zone_file::ZoneFile;
use crate::{Error, FQDN};
#[derive(Clone)]
pub enum Config<'a> {
NameServer {
origin: &'a FQDN,
use_dnssec: bool,
additional_zones: HashMap<FQDN, ZoneFile>,
},
Resolver {
use_dnssec: bool,
netmask: &'a str,
/// Extended DNS error (RFC8914)
ede: bool,
},
}
impl Config<'_> {
pub fn role(&self) -> Role {
match self {
Config::NameServer { .. } => Role::NameServer,
Config::Resolver { .. } => Role::Resolver,
}
}
}
#[derive(Clone, Copy)]
pub enum Role {
NameServer,
Resolver,
}
#[derive(Clone, Debug)]
pub enum Implementation {
Bind,
Dnslib,
Hickory {
repo: Repository<'static>,
dnssec_feature: Option<HickoryDnssecFeature>,
},
Unbound,
}
impl Implementation {
pub fn supports_ede(&self) -> bool {
match self {
Implementation::Bind => false,
Implementation::Dnslib => true,
Implementation::Hickory { .. } => true,
Implementation::Unbound => true,
}
}
/// Returns the latest hickory-dns local revision
pub fn hickory() -> Self {
Self::Hickory {
repo: Repository(crate::repo_root()),
dnssec_feature: None,
}
}
/// A test peer that cannot be changed using the `DNS_TEST_PEER` env variable
pub const fn test_peer() -> Implementation {
Implementation::Unbound
}
#[must_use]
pub fn is_bind(&self) -> bool {
matches!(self, Self::Bind)
}
#[must_use]
pub fn is_dnslib(&self) -> bool {
matches!(self, Self::Dnslib)
}
#[must_use]
pub fn is_hickory(&self) -> bool {
matches!(self, Self::Hickory { .. })
}
#[must_use]
pub fn is_unbound(&self) -> bool {
matches!(self, Self::Unbound)
}
pub(crate) fn format_config(&self, config: Config) -> String {
match config {
Config::Resolver {
use_dnssec,
netmask,
ede,
} => match self {
Self::Bind => {
assert!(!ede, "the BIND resolver does not support EDE (RFC8914)");
minijinja::render!(
include_str!("templates/named.resolver.conf.jinja"),
use_dnssec => use_dnssec,
netmask => netmask,
)
}
Self::Dnslib => {
// Dnslib resolvers don't have a config
"".into()
}
Self::Hickory { .. } => {
// TODO enable EDE in Hickory when supported
minijinja::render!(
include_str!("templates/hickory.resolver.toml.jinja"),
use_dnssec => use_dnssec,
)
}
Self::Unbound => {
minijinja::render!(
include_str!("templates/unbound.conf.jinja"),
use_dnssec => use_dnssec,
netmask => netmask,
ede => ede,
)
}
},
Config::NameServer {
origin,
use_dnssec,
additional_zones,
} => match self {
Self::Bind => {
minijinja::render!(
include_str!("templates/named.name-server.conf.jinja"),
fqdn => origin.as_str(),
additional_zones => additional_zones.keys().map(|x| x.as_str()).collect::<Vec<&str>>(),
)
}
Self::Dnslib => {
// Dnslib name servers don't have a config
"".into()
}
Self::Unbound => {
minijinja::render!(
include_str!("templates/nsd.conf.jinja"),
fqdn => origin.as_str(),
additional_zones => additional_zones.keys().map(|x| x.as_str()).collect::<Vec<&str>>(),
)
}
Self::Hickory { dnssec_feature, .. } => {
let use_pkcs8 = matches!(dnssec_feature, Some(HickoryDnssecFeature::Ring));
minijinja::render!(
include_str!("templates/hickory.name-server.toml.jinja"),
fqdn => origin.as_str(),
use_dnssec => use_dnssec,
additional_zones => additional_zones.keys().map(|x| x.as_str()).collect::<Vec<&str>>(),
use_pkcs8 => use_pkcs8,
)
}
},
}
}
pub(crate) fn conf_file_path(&self, role: Role) -> Option<&'static str> {
match self {
Self::Bind => Some("/etc/bind/named.conf"),
Self::Dnslib => None,
Self::Hickory { .. } => Some("/etc/named.toml"),
Self::Unbound => match role {
Role::NameServer => Some("/etc/nsd/nsd.conf"),
Role::Resolver => Some("/etc/unbound/unbound.conf"),
},
}
}
pub(crate) fn cmd_args(&self, role: Role) -> Vec<String> {
let base = match self {
Implementation::Bind => "named -g -d5",
Implementation::Dnslib => "python3 /script.py",
Implementation::Hickory { .. } => "hickory-dns -d",
Implementation::Unbound => match role {
Role::NameServer => "nsd -d",
Role::Resolver => "unbound -d",
},
};
vec![
"sh".into(),
"-c".into(),
format!(
"{base} >{} 2>{}",
self.stdout_logfile(role),
self.stderr_logfile(role)
),
]
}
pub(crate) fn stdout_logfile(&self, role: Role) -> String {
self.logfile(role, Stream::Stdout)
}
pub(crate) fn stderr_logfile(&self, role: Role) -> String {
self.logfile(role, Stream::Stderr)
}
fn logfile(&self, role: Role, stream: Stream) -> String {
let suffix = stream.as_str();
let path = match self {
Implementation::Bind => "/tmp/named",
Implementation::Dnslib => "/tmp/dnslib",
Implementation::Hickory { .. } => "/tmp/hickory",
Implementation::Unbound => match role {
Role::NameServer => "/tmp/nsd",
Role::Resolver => "/tmp/unbound",
},
};
format!("{path}.{suffix}")
}
}
/// A Hickory DNS Cargo feature used to enable DNSSEC with a particular cryptography library.
#[derive(Debug, Clone, Copy)]
pub enum HickoryDnssecFeature {
Openssl,
Ring,
}
impl fmt::Display for HickoryDnssecFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Openssl => "dnssec-openssl",
Self::Ring => "dnssec-ring",
})
}
}
impl FromStr for HickoryDnssecFeature {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"dnssec-openssl" => Ok(Self::Openssl),
"dnssec-ring" => Ok(Self::Ring),
_ => Err(format!(
"invalid value for DNSSEC_FEATURE: {s}, expected dnssec-openssl or dnssec-ring"
)
.into()),
}
}
}
#[derive(Clone, Copy)]
enum Stream {
Stdout,
Stderr,
}
impl Stream {
fn as_str(&self) -> &'static str {
match self {
Self::Stdout => "stdout",
Self::Stderr => "stderr",
}
}
}
impl fmt::Display for Implementation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Implementation::Bind => "bind",
Implementation::Dnslib => "dnslib",
Implementation::Hickory { .. } => "hickory",
Implementation::Unbound => "unbound",
};
f.write_str(s)
}
}
#[derive(Clone, Debug)]
pub struct Repository<'a> {
inner: Cow<'a, str>,
}
impl Repository<'_> {
pub(crate) fn as_str(&self) -> &str {
&self.inner
}
}
/// checks that `input` looks like a valid repository which can be either local or remote
///
/// # Panics
///
/// this function panics if `input` is not a local `Path` that exists or a well-formed URL
#[allow(non_snake_case)]
pub fn Repository(input: impl Into<Cow<'static, str>>) -> Repository<'static> {
let input = input.into();
assert!(
Path::new(&*input).exists() || Url::parse(&input).is_ok(),
"{input} is not a valid repository"
);
Repository { inner: input }
}
impl Default for Implementation {
fn default() -> Self {
Self::Unbound
}
}

View File

@@ -1,189 +0,0 @@
//! A test framework for all things DNS
use std::io::{Read as _, Write as _};
use std::{env, io};
use client::Client;
use lazy_static::lazy_static;
use name_server::{NameServer, Running};
pub use crate::container::Network;
pub use crate::fqdn::FQDN;
pub use crate::implementation::{HickoryDnssecFeature, Implementation, Repository};
pub use crate::resolver::Resolver;
pub use crate::trust_anchor::TrustAnchor;
pub mod client;
pub mod container;
mod fqdn;
mod implementation;
pub mod name_server;
pub mod nsec3;
pub mod record;
mod resolver;
mod trust_anchor;
pub mod tshark;
pub mod zone_file;
pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = core::result::Result<T, Error>;
// TODO maybe this should be a TLS variable that each unit test (thread) can override
const DEFAULT_TTL: u32 = 24 * 60 * 60; // 1 day
lazy_static! {
pub static ref SUBJECT: Implementation = parse_subject();
pub static ref PEER: Implementation = parse_peer();
}
/// Helper to prevent a unit test from immediately terminating so its associated containers can be
/// manually inspected
pub fn inspect(clients: &[Client], resolvers: &[Resolver], nameservers: &[NameServer<Running>]) {
use core::fmt::Write as _;
let mut output = String::new();
if !clients.is_empty() {
output.push_str("\n\nCLIENTS");
}
for client in clients {
write!(output, "\n{} {}", client.container_id(), client.ipv4_addr()).unwrap();
}
if !resolvers.is_empty() {
output.push_str("\n\nRESOLVERS");
}
for resolver in resolvers {
write!(
output,
"\n{} {}",
resolver.container_id(),
resolver.ipv4_addr()
)
.unwrap();
}
if !nameservers.is_empty() {
output.push_str("\n\nNAME SERVERS");
}
for nameserver in nameservers {
write!(
output,
"\n{} {} {}",
nameserver.container_id(),
nameserver.ipv4_addr(),
nameserver.zone(),
)
.unwrap();
}
output.push_str("\n\ntest paused. press ENTER to continue\n\n");
// try to write everything in a single system call to avoid this output interleaving with the
// output of other tests (when `--nocapture` is used)
io::stdout().write_all(output.as_bytes()).unwrap();
// block this thread until user provides some input
let mut buf = [0];
let _ = io::stdin().read(&mut buf).unwrap();
}
fn parse_subject() -> Implementation {
if let Ok(subject) = env::var("DNS_TEST_SUBJECT") {
if subject == "unbound" {
return Implementation::Unbound;
}
if subject == "bind" {
return Implementation::Bind;
}
if subject.starts_with("hickory") {
let Some(rest) = subject.strip_prefix("hickory ") else {
panic!("the syntax of DNS_TEST_SUBJECT is 'hickory $URL' or 'hickory $URL $DNSSEC_FEATURE', e.g. 'hickory /tmp/hickory' or 'hickory https://github.com/owner/repo'")
};
let (url, dnssec_feature) = if let Some((url, dnssec_feature)) = rest.split_once(' ') {
(url, Some(dnssec_feature.parse().unwrap()))
} else {
(rest, None)
};
Implementation::Hickory {
repo: Repository(url.to_string()),
dnssec_feature,
}
} else {
panic!("unknown implementation: {subject}")
}
} else {
Implementation::default()
}
}
fn parse_peer() -> Implementation {
if let Ok(peer) = env::var("DNS_TEST_PEER") {
match peer.as_str() {
"unbound" => Implementation::Unbound,
"bind" => Implementation::Bind,
_ => panic!("`{peer}` is not supported as a test peer implementation"),
}
} else {
Implementation::default()
}
}
fn repo_root() -> String {
use std::path::PathBuf;
let mut repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); // /conformance/packages/dns-test
repo_root.pop(); // /conformance/packages/
repo_root.pop(); // /conformance
repo_root.pop(); // /
repo_root.display().to_string()
}
#[cfg(test)]
mod tests {
use std::env;
use super::*;
impl PartialEq for Implementation {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Hickory { .. }, Self::Hickory { .. }) => true,
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
#[test]
fn immutable_subject() {
let before = super::SUBJECT.clone();
let newval = if before == Implementation::Unbound {
"bind"
} else {
"unbound"
};
env::set_var("DNS_TEST_SUBJECT", newval);
let after = super::SUBJECT.clone();
assert_eq!(before, after);
}
#[test]
fn immutable_peer() {
let before = super::PEER.clone();
let newval = if before == Implementation::Unbound {
"bind"
} else {
"unbound"
};
env::set_var("DNS_TEST_PEER", newval);
let after = super::PEER.clone();
assert_eq!(before, after);
}
}

View File

@@ -1,803 +0,0 @@
use core::sync::atomic::{self, AtomicUsize};
use std::{collections::HashMap, net::Ipv4Addr, thread, time::Duration};
use crate::container::{Child, Container, Network};
use crate::implementation::{Config, Role};
use crate::record::{self, Record, SoaSettings, DS, SOA};
use crate::tshark::Tshark;
use crate::zone_file::{self, Root, ZoneFile};
use crate::zone_file::{SignSettings, Signer};
use crate::{Implementation, Result, TrustAnchor, DEFAULT_TTL, FQDN};
pub struct Graph {
pub nameservers: Vec<NameServer<Running>>,
pub root: Root,
pub trust_anchor: Option<TrustAnchor>,
}
/// Whether to sign the zone files
pub enum Sign<'a> {
No,
Yes {
settings: SignSettings,
},
/// Signs the zone files and then modifies the records produced by the signing process
AndAmend {
settings: SignSettings,
mutate: &'a dyn Fn(&FQDN, &mut Vec<Record>),
},
}
impl Graph {
/// Builds up a minimal DNS graph from `leaf` up to a root name server and returns all the
/// name servers in the graph
///
/// All new name servers will share the `Implementation` of `leaf`.
///
/// The returned name servers are sorted from leaf zone to root zone.
///
/// both `Sign::Yes` and `Sign::AndAmend` will add a DS record with the hash of the child's
/// key to the parent's zone file
///
/// a non-empty `TrustAnchor` is returned only when `Sign::Yes` or `Sign::AndAmend` is used
pub fn build(leaf: NameServer<Stopped>, sign: Sign) -> Result<Self> {
assert_eq!(2, leaf.zone().num_labels(), "not yet implemented");
assert_eq!(
Some(FQDN::TEST_TLD),
leaf.zone().parent(),
"not yet implemented"
);
// first pass: create nameservers for parent zones
let mut zone = leaf.zone().clone();
let network = leaf.container.network().clone();
let implementation = leaf.implementation.clone();
let (mut nameservers_ns, leaf) = if leaf.zone() != &FQDN::TEST_DOMAIN {
let nameservers_ns = NameServer::new(&implementation, FQDN::TEST_DOMAIN, &network)?;
(nameservers_ns, Some(leaf))
} else {
(leaf, None)
};
// the nameserver covering `FQDN::NAMESERVERS` needs A records about all the nameservers in the graph
let mut nameservers = vec![];
while let Some(parent) = zone.parent() {
let nameserver = NameServer::new(&implementation, parent.clone(), &network)?;
nameservers_ns.add(nameserver.a());
nameservers.push(nameserver);
zone = parent;
}
drop((network, implementation));
if let Some(leaf) = leaf {
nameservers.insert(0, leaf);
}
nameservers.insert(0, nameservers_ns);
// second pass: add referrals from parent to child
// the nameservers are sorted leaf-most zone first but siblings may be next to each other
// for each child (e.g. `nameservers.com.`), do a linear search for its parent (`com.`)
for index in 1..nameservers.len() {
let (left, right) = nameservers.split_at_mut(index);
let child = left.last_mut().unwrap();
for maybe_parent in right {
if Some(maybe_parent.zone()) == child.zone().parent().as_ref() {
let parent = maybe_parent;
parent.referral_nameserver(child);
break;
}
}
}
let root = nameservers.last().unwrap().root_hint();
// start name servers
let (nameservers, trust_anchor) = match sign {
Sign::No => (
nameservers
.into_iter()
.map(|nameserver| nameserver.start())
.collect::<Result<_>>()?,
None,
),
_ => {
let mut trust_anchor = TrustAnchor::empty();
let (settings, maybe_mutate) = match sign {
Sign::No => unreachable!(),
Sign::Yes { settings } => (settings, None),
Sign::AndAmend { settings, mutate } => (settings, Some(mutate)),
};
let mut running = vec![];
let mut children_ds = vec![];
let mut children_num_labels = 0;
let len = nameservers.len();
for (index, mut nameserver) in nameservers.into_iter().enumerate() {
if !children_ds.is_empty() {
let is_parent = nameserver.zone().num_labels() + 1 == children_num_labels;
if is_parent {
for ds in children_ds.drain(..) {
nameserver.add(ds);
}
}
}
let mut nameserver = nameserver.sign(settings.clone())?;
children_ds.push(nameserver.ds().ksk.clone());
children_num_labels = nameserver.zone().num_labels();
if let Some(mutate) = maybe_mutate {
let zone = nameserver.zone().clone();
mutate(&zone, &mut nameserver.signed_zone_file_mut().records);
}
if index == len - 1 {
// the last nameserver covers `.`
trust_anchor.add(nameserver.key_signing_key().clone());
trust_anchor.add(nameserver.zone_signing_key().clone());
}
running.push(nameserver.start()?);
}
(running, Some(trust_anchor))
}
};
Ok(Graph {
nameservers,
root,
trust_anchor,
})
}
}
pub struct NameServer<State> {
container: Container,
implementation: Implementation,
state: State,
zone_file: ZoneFile,
additional_zones: HashMap<FQDN, ZoneFile>,
}
impl NameServer<Stopped> {
/// Spins up a primary name server that has authority over the given `zone`
///
/// The initial state of the server is the "Stopped" state where it won't answer any query.
///
/// The FQDN of the name server will have the form `primary{count}.nameservers.com.` where
/// `{count}` is a (process-wide) unique, monotonically increasing integer
///
/// The zone file will contain these records
///
/// - one SOA record, with the primary name server field set to this name server's FQDN
/// - one NS record, with this name server's FQDN set as the only available name server for
/// the zone
pub fn new(implementation: &Implementation, zone: FQDN, network: &Network) -> Result<Self> {
let ns_count = ns_count();
let nameserver = primary_ns(ns_count, &zone);
let image = implementation.clone().into();
let container = Container::run(&image, network)?;
let soa = SOA {
zone: zone.clone(),
ttl: DEFAULT_TTL,
nameserver: nameserver.clone(),
admin: admin_ns(ns_count, &zone),
settings: SoaSettings::default(),
};
let mut zone_file = ZoneFile::new(soa);
zone_file.add(Record::ns(zone, nameserver.clone()));
// BIND requires that `nameserver` has an A record
zone_file.add(Record::a(nameserver.clone(), container.ipv4_addr()));
Ok(Self {
container,
implementation: implementation.clone(),
zone_file,
additional_zones: HashMap::new(),
state: Stopped,
})
}
/// Adds a NS + A record pair to the zone file
pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) -> &mut Self {
self.zone_file.referral(zone, nameserver, ipv4_addr);
self
}
/// Adds a NS + A record pair to the zone file from another NameServer
pub fn referral_nameserver<T>(&mut self, nameserver: &NameServer<T>) -> &mut Self {
self.referral(
nameserver.zone().clone(),
nameserver.fqdn().clone(),
nameserver.ipv4_addr(),
)
}
/// Adds a record to the name server's zone file
pub fn add(&mut self, record: impl Into<Record>) -> &mut Self {
self.zone_file.add(record);
self
}
/// Copy a file to the name server's filesystem
pub fn cp(&self, path: &str, contents: &str) -> Result<()> {
self.container.cp(path, contents)?;
Ok(())
}
/// Adds an additional zone to the nameserver
pub fn add_zone(&mut self, name: FQDN, zone: ZoneFile) {
self.additional_zones.insert(name, zone);
}
/// Freezes and signs the name server's zone file
pub fn sign(self, settings: SignSettings) -> Result<NameServer<Signed>> {
let Self {
container,
zone_file,
implementation,
additional_zones,
state: _,
} = self;
let state = Signer::new(&container, settings)?.sign_zone(&zone_file)?;
Ok(NameServer {
container,
implementation,
zone_file,
state,
additional_zones,
})
}
/// Moves the server to the "Start" state where it can answer client queries
pub fn start(self) -> Result<NameServer<Running>> {
let Self {
container,
zone_file,
implementation,
additional_zones,
state: _,
} = self;
let config = Config::NameServer {
origin: zone_file.origin(),
use_dnssec: false,
additional_zones: additional_zones.clone(),
};
if let Some(conf_file_path) = implementation.conf_file_path(config.role()) {
container.cp(
conf_file_path,
&implementation.format_config(config.clone()),
)?;
}
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
container.cp(&zone_file_path(), &zone_file.to_string())?;
for (key, zone_file) in &additional_zones {
container.cp(&format!("{ZONES_DIR}/{key}zone"), &zone_file.to_string())?;
}
let mut child = container.spawn(&implementation.cmd_args(config.role()))?;
// For Dnslib, make sure the python interpreter is still running after two seconds
if let Implementation::Dnslib = implementation {
thread::sleep(Duration::from_secs(2));
match child.try_wait() {
Ok(None) => {} // the process is still running
Ok(Some(status)) => {
return Err(format!(
"unable to start dnslib server: {status:?}; logs: {:?}",
container
.stdout(&["cat", &implementation.stderr_logfile(Role::NameServer)]),
)
.into())
}
Err(e) => println!("unable to determine if dnslib started: {e}"),
}
}
Ok(NameServer {
container,
implementation,
zone_file,
additional_zones,
state: Running {
_child: child,
trust_anchor: None,
},
})
}
}
const ZONES_DIR: &str = "/etc/zones";
const ZONE_FILENAME: &str = "main.zone";
const ZSK_PRIVATE_FILENAME: &str = "zsk.key";
const ZSK_PKCS8_FILENAME: &str = "zsk.pk8";
fn zone_file_path() -> String {
format!("{ZONES_DIR}/{ZONE_FILENAME}")
}
fn zsk_private_path() -> String {
format!("{ZONES_DIR}/{ZSK_PRIVATE_FILENAME}")
}
fn zsk_pkcs8_path() -> String {
format!("{ZONES_DIR}/{ZSK_PKCS8_FILENAME}")
}
fn ns_count() -> usize {
thread_local! {
static COUNT: AtomicUsize = const { AtomicUsize::new(0) };
}
COUNT.with(|count| count.fetch_add(1, atomic::Ordering::Relaxed))
}
impl NameServer<Signed> {
/// Moves the server to the "Start" state where it can answer client queries
pub fn start(self) -> Result<NameServer<Running>> {
let Self {
container,
zone_file,
implementation,
additional_zones,
state,
} = self;
let config = Config::NameServer {
origin: zone_file.origin(),
use_dnssec: state.use_dnssec,
additional_zones: additional_zones.clone(),
};
if let Some(conf_file_path) = implementation.conf_file_path(config.role()) {
container.cp(
conf_file_path,
&implementation.format_config(config.clone()),
)?;
}
if implementation.is_hickory() && state.use_dnssec {
// FIXME: Hickory does not support pre-signed zonefiles. We copy the unsigned
// zonefile so hickory can sign the zonefile itself.
container.cp(&zone_file_path(), &zone_file.to_string())?;
// FIXME: Given that hickory doesn't support the key format produced by
// `ldns-keygen` we generate a new zsk from scratch. This is fine as long as we
// don't compare signatures in any of the conformance tests.
let zsk = container.stdout(&["openssl", "genpkey", "-algorithm", "RSA"])?;
container.cp(&zsk_private_path(), &zsk)?;
container.status_ok(&[
"openssl",
"pkcs8",
"-topk8",
"-nocrypt",
"-inform",
"pem",
"-in",
&zsk_private_path(),
"-outform",
"der",
"-out",
&zsk_pkcs8_path(),
])?;
} else {
container.cp(&zone_file_path(), &state.signed.to_string())?;
}
let child = container.spawn(&implementation.cmd_args(config.role()))?;
Ok(NameServer {
container,
implementation,
zone_file,
additional_zones,
state: Running {
_child: child,
trust_anchor: Some(state.trust_anchor()),
},
})
}
pub fn key_signing_key(&self) -> &record::DNSKEY {
&self.state.ksk
}
pub fn zone_signing_key(&self) -> &record::DNSKEY {
&self.state.zsk
}
pub fn signed_zone_file(&self) -> &ZoneFile {
&self.state.signed
}
pub fn signed_zone_file_mut(&mut self) -> &mut ZoneFile {
&mut self.state.signed
}
pub fn trust_anchor(&self) -> TrustAnchor {
self.state.trust_anchor()
}
pub fn ds(&self) -> &DS2 {
&self.state.ds
}
}
impl NameServer<Running> {
/// Starts a `tshark` instance that captures DNS messages flowing through this network node
pub fn eavesdrop(&self) -> Result<Tshark> {
self.container.eavesdrop()
}
pub fn trust_anchor(&self) -> Option<&TrustAnchor> {
self.state.trust_anchor.as_ref()
}
/// Returns the logs collected so far
pub fn logs(&self) -> Result<String> {
if self.implementation.is_hickory() {
Ok(format!(
"STDOUT:\n{}\nSTDERR:\n{}",
self.stdout()?,
self.stderr()?,
))
} else {
self.stderr()
}
}
fn stdout(&self) -> Result<String> {
self.container
.stdout(&["cat", &self.implementation.stdout_logfile(Role::NameServer)])
}
fn stderr(&self) -> Result<String> {
self.container
.stdout(&["cat", &self.implementation.stderr_logfile(Role::NameServer)])
}
}
impl<S> NameServer<S> {
pub fn container_id(&self) -> &str {
self.container.id()
}
pub fn container_name(&self) -> &str {
self.container.name()
}
pub fn ipv4_addr(&self) -> Ipv4Addr {
self.container.ipv4_addr()
}
/// Zone file BEFORE signing
pub fn zone_file(&self) -> &ZoneFile {
&self.zone_file
}
pub fn zone(&self) -> &FQDN {
self.zone_file.origin()
}
pub fn fqdn(&self) -> &FQDN {
&self.zone_file.soa.nameserver
}
/// Returns the [`Record::A`] record for this server.
pub fn a(&self) -> Record {
Record::a(self.fqdn().clone(), self.ipv4_addr())
}
/// Returns the [`Root`] hint for this server.
pub fn root_hint(&self) -> Root {
Root::new(self.fqdn().clone(), self.ipv4_addr())
}
}
pub struct Stopped;
/// DS records for both the KSK and the ZSK
#[derive(Debug)]
pub struct DS2 {
pub ksk: DS,
pub zsk: DS,
}
impl DS2 {
pub(crate) fn classify(dses: Vec<DS>, zsk: &zone_file::DNSKEY, ksk: &zone_file::DNSKEY) -> DS2 {
let mut ksk_ds = None;
let mut zsk_ds = None;
let zsk_tag = zsk.rdata().calculate_key_tag();
let ksk_tag = ksk.rdata().calculate_key_tag();
for ds in dses {
if ds.key_tag == zsk_tag {
assert!(zsk_ds.is_none());
zsk_ds = Some(ds);
} else if ds.key_tag == ksk_tag {
assert!(ksk_ds.is_none());
ksk_ds = Some(ds);
}
}
DS2 {
ksk: ksk_ds.expect("DS for KSK not found"),
zsk: zsk_ds.expect("DS for ZSK not found"),
}
}
}
pub struct Signed {
pub(crate) ds: DS2,
pub(crate) zsk: record::DNSKEY,
pub(crate) ksk: record::DNSKEY,
pub(crate) signed: ZoneFile,
pub(crate) use_dnssec: bool,
}
impl Signed {
/// Return a [`TrustAnchor`] active on this NameServer.
pub fn trust_anchor(&self) -> TrustAnchor {
let mut trust_anchor = TrustAnchor::empty();
trust_anchor.add(self.ksk.clone());
trust_anchor.add(self.zsk.clone());
trust_anchor
}
}
pub struct Running {
_child: Child,
trust_anchor: Option<TrustAnchor>,
}
fn primary_ns(ns_count: usize, zone: &FQDN) -> FQDN {
FQDN(format!("primary{ns_count}.{}", expand_zone(zone))).unwrap()
}
fn admin_ns(ns_count: usize, zone: &FQDN) -> FQDN {
FQDN(format!("admin{ns_count}.{}", expand_zone(zone))).unwrap()
}
fn expand_zone(zone: &FQDN) -> String {
if zone == &FQDN::ROOT {
FQDN::TEST_DOMAIN.as_str().to_string()
} else if zone.num_labels() == 1 {
if *zone == FQDN::TEST_TLD {
FQDN::TEST_DOMAIN.as_str().to_string()
} else {
unimplemented!()
}
} else {
zone.to_string()
}
}
#[cfg(test)]
mod tests {
use std::thread;
use std::time::Duration;
use crate::client::{Client, DigSettings};
use crate::record::{RecordType, A, NS};
use super::*;
#[test]
fn simplest() -> Result<()> {
let network = Network::new()?;
let tld_ns =
NameServer::new(&Implementation::Unbound, FQDN::TEST_TLD, &network)?.start()?;
let ip_addr = tld_ns.ipv4_addr();
let client = Client::new(&network)?;
let output = client.dig(
DigSettings::default(),
ip_addr,
RecordType::SOA,
&FQDN::TEST_TLD,
)?;
assert!(output.status.is_noerror());
Ok(())
}
#[test]
fn with_referral() -> Result<()> {
let network = Network::new()?;
let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1);
let mut root_ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?;
root_ns.referral(
FQDN::TEST_TLD,
FQDN("primary.tld-server.com.")?,
expected_ip_addr,
);
let root_ns = root_ns.start()?;
eprintln!("root.zone:\n{}", root_ns.zone_file());
let ipv4_addr = root_ns.ipv4_addr();
let client = Client::new(&network)?;
let output = client.dig(
DigSettings::default(),
ipv4_addr,
RecordType::NS,
&FQDN::TEST_TLD,
)?;
assert!(output.status.is_noerror());
Ok(())
}
#[test]
fn signed() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?
.sign(SignSettings::default())?;
eprintln!("KSK:\n{}", ns.key_signing_key());
eprintln!("ZSK:\n{}", ns.zone_signing_key());
eprintln!("root.zone.signed:\n{}", ns.signed_zone_file());
let tld_ns = ns.start()?;
let ns_addr = tld_ns.ipv4_addr();
let client = Client::new(&network)?;
let settings = *DigSettings::default().dnssec();
let output = client.dig(settings, ns_addr, RecordType::SOA, &FQDN::ROOT)?;
assert!(output.status.is_noerror());
let [soa, rrsig] = output
.answer
.try_into()
.expect("two records in answer section");
assert!(soa.is_soa());
let rrsig = rrsig.try_into_rrsig().unwrap();
assert_eq!(RecordType::SOA, rrsig.type_covered);
Ok(())
}
#[test]
fn nsd_logs_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?;
// no way to block until the server has finished starting up so we just give it some
// arbitrary amount of time
thread::sleep(Duration::from_secs(1));
let logs = ns.logs()?;
assert!(logs.contains("nsd starting"));
Ok(())
}
#[test]
fn named_logs_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Bind, FQDN::ROOT, &network)?.start()?;
// no way to block until the server has finished starting up so we just give it some
// arbitrary amount of time
thread::sleep(Duration::from_secs(1));
let logs = ns.logs()?;
eprintln!("{logs}");
assert!(logs.contains("starting BIND"));
Ok(())
}
#[test]
fn hickory_logs_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::hickory(), FQDN::ROOT, &network)?.start()?;
// no way to block until the server has finished starting up so we just give it some
// arbitrary amount of time
thread::sleep(Duration::from_secs(1));
let logs = ns.logs()?;
eprintln!("{logs}");
let mut found = false;
for line in logs.lines() {
if line.contains("Hickory DNS") && line.contains("starting") {
found = true;
}
}
assert!(found);
Ok(())
}
#[test]
fn bind_multizone_works() -> Result<()> {
multizone_test(&Implementation::Bind)?;
Ok(())
}
#[test]
fn hickory_multizone_works() -> Result<()> {
multizone_test(&Implementation::hickory())?;
Ok(())
}
#[test]
fn unbound_multizone_works() -> Result<()> {
multizone_test(&Implementation::Unbound)?;
Ok(())
}
#[cfg(test)]
fn multizone_test(implementation: &Implementation) -> Result<()> {
let network = Network::new()?;
let mut ns = NameServer::new(implementation, FQDN::ROOT, &network)?;
let mut zone_file = ZoneFile::new(SOA {
zone: FQDN("domain.testing.")?,
ttl: 86400,
nameserver: FQDN("ns.domain.testing.")?,
admin: FQDN("admin.domain.testing.")?,
settings: SoaSettings::default(),
});
zone_file.add(Record::NS(NS {
zone: FQDN("domain.testing.")?,
ttl: 86400,
nameserver: FQDN("ns.domain.testing.")?,
}));
zone_file.add(Record::A(A {
fqdn: FQDN("ns.domain.testing.")?,
ipv4_addr: Ipv4Addr::new(192, 0, 2, 1),
ttl: 86400,
}));
zone_file.add(Record::A(A {
fqdn: FQDN("host.domain.testing.")?,
ipv4_addr: Ipv4Addr::new(192, 0, 2, 1),
ttl: 86400,
}));
ns.add_zone(FQDN("domain.testing.")?, zone_file);
let ns = ns.start()?;
thread::sleep(Duration::from_secs(2));
let client = Client::new(&network)?;
let dig_settings = DigSettings::default();
let res = client.dig(
dig_settings,
ns.ipv4_addr(),
RecordType::A,
&FQDN("host.domain.testing.")?,
);
if let Ok(res) = &res {
assert!(res.status.is_noerror());
assert_eq!(res.answer.len(), 1);
if let Record::A(rec) = res.answer.first().unwrap() {
assert_eq!(rec.fqdn, FQDN("host.domain.testing.")?);
assert_eq!(rec.ipv4_addr, Ipv4Addr::new(192, 0, 2, 1));
} else {
panic!("error");
}
} else {
panic!("error");
}
Ok(())
}
}

View File

@@ -1,66 +0,0 @@
use std::collections::BTreeMap;
use crate::{record::NSEC3, zone_file::ZoneFile};
pub struct NSEC3Records {
records: BTreeMap<String, NSEC3>,
}
impl NSEC3Records {
/// Extract the NSEC3 RRs from the signed zonefile and sort them by the hash embedded in the
/// last label of each record's owner.
pub fn new(signed_zf: &ZoneFile) -> Self {
Self {
records: signed_zf
.records
.iter()
.cloned()
.filter_map(|rr| {
let mut nsec3_rr = rr.try_into_nsec3().ok()?;
nsec3_rr.next_hashed_owner_name =
nsec3_rr.next_hashed_owner_name.to_uppercase();
Some((nsec3_rr.fqdn.last_label().to_uppercase(), nsec3_rr))
})
.collect(),
}
}
/// An NSEC3 RR is said to "match" a name if the owner name of the NSEC3 RR is the same as the
/// hashed owner name of that name.
pub fn find_match<'a>(&'a self, name_hash: &str) -> Option<&'a NSEC3> {
self.records.get(name_hash)
}
/// An NSEC3 RR is said to cover a name if the hash of the name or "next closer" name falls between
/// the owner name and the next hashed owner name of the NSEC3. In other words, if it proves the
/// nonexistence of the name, either directly or by proving the nonexistence of an ancestor of the
/// name.
pub fn find_cover<'a>(&'a self, name_hash: &str) -> Option<&'a NSEC3> {
let (hash, candidate) = self
.records
// Find the greater hash that is less or equal than the name's hash.
.range(..=name_hash.to_owned())
.last()
// If no value is less or equal than the name's hash, it means that the name's hash is out
// of range and the last record covers it.
.or_else(|| self.records.last_key_value())?;
// If the found hash is exactly the name's hash, return None as it wouldn't be proving its
// nonexistence. Otherwise return the RR with that hash.
(hash != name_hash).then_some(candidate)
}
/// This proof consists of (up to) two different NSEC3 RRs:
/// - An NSEC3 RR that matches the closest (provable) encloser.
/// - An NSEC3 RR that covers the "next closer" name to the closest encloser.
pub fn closest_encloser_proof<'a>(
&'a self,
closest_encloser_hash: &str,
next_closer_name_hash: &str,
) -> Option<(&'a NSEC3, &'a NSEC3)> {
Some((
self.find_match(closest_encloser_hash)?,
self.find_cover(next_closer_name_hash)?,
))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,237 +0,0 @@
use core::fmt::Write;
use std::net::Ipv4Addr;
use crate::container::{Child, Container, Network};
use crate::implementation::{Config, Role};
use crate::record::DNSKEY;
use crate::trust_anchor::TrustAnchor;
use crate::tshark::Tshark;
use crate::zone_file::Root;
use crate::{Implementation, Result};
pub struct Resolver {
container: Container,
_child: Child,
implementation: Implementation,
}
impl Resolver {
#[allow(clippy::new_ret_no_self)]
pub fn new(network: &Network, root: Root) -> ResolverSettings {
ResolverSettings {
ede: false,
network: network.clone(),
roots: vec![root],
trust_anchor: TrustAnchor::empty(),
custom_config: None,
}
}
pub fn eavesdrop(&self) -> Result<Tshark> {
self.container.eavesdrop()
}
pub fn network(&self) -> &Network {
self.container.network()
}
pub fn container_id(&self) -> &str {
self.container.id()
}
pub fn container_name(&self) -> &str {
self.container.name()
}
pub fn ipv4_addr(&self) -> Ipv4Addr {
self.container.ipv4_addr()
}
/// Returns the logs collected so far
pub fn logs(&self) -> Result<String> {
if self.implementation.is_hickory() {
self.stdout()
} else {
self.stderr()
}
}
fn stdout(&self) -> Result<String> {
self.container
.stdout(&["cat", &self.implementation.stdout_logfile(Role::Resolver)])
}
fn stderr(&self) -> Result<String> {
self.container
.stdout(&["cat", &self.implementation.stderr_logfile(Role::Resolver)])
}
}
pub struct ResolverSettings {
/// Extended DNS Errors (RFC8914)
ede: bool,
network: Network,
roots: Vec<Root>,
trust_anchor: TrustAnchor,
custom_config: Option<String>,
}
impl ResolverSettings {
/// Starts a DNS server in the recursive resolver role
///
/// The server uses the implementation based on `$DNS_TEST_SUBJECT` env var.
pub fn start(&self) -> Result<Resolver> {
self.start_with_subject(&crate::SUBJECT)
}
/// Starts a DNS server in the recursive resolver role
///
/// This server is not an authoritative name server; it does not serve a zone file to clients
pub fn start_with_subject(&self, implementation: &Implementation) -> Result<Resolver> {
let image = implementation.clone().into();
let container = Container::run(&image, &self.network)?;
let mut hints = String::new();
for root in &self.roots {
writeln!(hints, "{root}").unwrap();
}
container.cp("/etc/root.hints", &hints)?;
let use_dnssec = !self.trust_anchor.is_empty();
let config_contents = if let Some(custom_config) = &self.custom_config {
custom_config
} else {
let config = Config::Resolver {
use_dnssec,
netmask: self.network.netmask(),
ede: self.ede,
};
&implementation.format_config(config)
};
if let Some(conf_file_path) = implementation.conf_file_path(Role::Resolver) {
container.cp(conf_file_path, config_contents)?;
}
if use_dnssec {
let path = if implementation.is_bind() {
"/etc/bind/bind.keys"
} else {
"/etc/trusted-key.key"
};
let contents = if implementation.is_bind() {
self.trust_anchor.delv()
} else {
self.trust_anchor.to_string()
};
container.cp(path, &contents)?;
}
let child = container.spawn(&implementation.cmd_args(Role::Resolver))?;
Ok(Resolver {
_child: child,
container,
implementation: implementation.clone(),
})
}
/// Enables the Extended DNS Errors (RFC8914) feature
pub fn extended_dns_errors(&mut self) -> &mut Self {
self.ede = true;
self
}
/// Adds a root hint
pub fn root(&mut self, root: Root) -> &mut Self {
self.roots.push(root);
self
}
/// Adds a DNSKEY record to the trust anchor
pub fn trust_anchor_key(&mut self, key: DNSKEY) -> &mut Self {
self.trust_anchor.add(key.clone());
self
}
/// Adds all the keys in the `other` trust anchor to ours
pub fn trust_anchor(&mut self, other: &TrustAnchor) -> &mut Self {
for key in other.keys() {
self.trust_anchor.add(key.clone());
}
self
}
/// Overrides the automatically-generated configuration file.
pub fn custom_config(&mut self, config: String) -> &mut Self {
self.custom_config = Some(config);
self
}
}
#[cfg(test)]
mod tests {
use std::{thread, time::Duration};
use crate::{name_server::NameServer, FQDN};
use super::*;
#[test]
fn unbound_logs_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?;
let resolver =
Resolver::new(&network, ns.root_hint()).start_with_subject(&Implementation::Unbound)?;
// no way to block until the server has finished starting up so we just give it some
// arbitrary amount of time
thread::sleep(Duration::from_secs(1));
let logs = resolver.logs()?;
eprintln!("{logs}");
assert!(logs.contains("start of service"));
Ok(())
}
#[test]
fn bind_logs_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?;
let resolver =
Resolver::new(&network, ns.root_hint()).start_with_subject(&Implementation::Bind)?;
// no way to block until the server has finished starting up so we just give it some
// arbitrary amount of time
thread::sleep(Duration::from_secs(1));
let logs = resolver.logs()?;
eprintln!("{logs}");
assert!(logs.contains("starting BIND"));
Ok(())
}
#[test]
fn hickory_logs_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?;
let resolver = Resolver::new(&network, ns.root_hint())
.start_with_subject(&Implementation::hickory())?;
// no way to block until the server has finished starting up so we just give it some
// arbitrary amount of time
thread::sleep(Duration::from_secs(1));
let logs = resolver.logs()?;
eprintln!("{logs}");
let mut found = false;
for line in logs.lines() {
if line.contains("Hickory DNS") && line.contains("starting") {
found = true;
}
}
assert!(found);
Ok(())
}
}

View File

@@ -1,25 +0,0 @@
user = "nobody"
group = "nogroup"
[[zones]]
zone = "{{ fqdn }}"
zone_type = "Primary"
file = "/etc/zones/main.zone"
enable_dnssec = {{ use_dnssec }}
nx_proof_kind = { nsec3 = { iterations = 1 } }
[[zones.keys]]
{% if use_pkcs8 %}
key_path = "/etc/zones/zsk.pk8"
{% else %}
key_path = "/etc/zones/zsk.key"
{% endif %}
algorithm = "RSASHA256"
is_zone_signing_key = true
{% for zone in additional_zones -%}
[[zones]]
zone = "{{ zone }}"
zone_type = "Primary"
file = "/etc/zones/{{ zone }}zone"
{% endfor -%}

View File

@@ -1,7 +0,0 @@
user = "nobody"
group = "nogroup"
[[zones]]
zone = "."
zone_type = "Hint"
stores = { type = "recursor" , roots = "/etc/root.hints" {% if use_dnssec %}, dnssec_policy.ValidateWithStaticKey.path = "/etc/trusted-key.key" {% else %}, dnssec_policy = "ValidationDisabled" {% endif %} , allow_server = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] }

View File

@@ -1,21 +0,0 @@
options {
directory "/var/cache/bind";
pid-file "/tmp/named.pid";
recursion no;
dnssec-validation no;
allow-transfer { none; };
# significantly reduces noise in logs
empty-zones-enable no;
};
zone "{{ fqdn }}" IN {
type primary;
file "/etc/zones/main.zone";
};
{% for zone in additional_zones -%}
zone "{{ zone }}" IN {
type primary;
file "/etc/zones/{{ zone }}zone";
};
{% endfor -%}

View File

@@ -1,14 +0,0 @@
options {
directory "/var/cache/bind";
pid-file "/tmp/named.pid";
recursion yes;
dnssec-validation {% if use_dnssec %} auto {% else %} no {% endif %};
allow-transfer { none; };
# significantly reduces noise in logs
empty-zones-enable no;
};
zone "." {
type hint;
file "/etc/root.hints";
};

View File

@@ -1,15 +0,0 @@
server:
pidfile: /tmp/nsd.pid
remote-control:
control-enable: no
zone:
name: {{ fqdn }}
zonefile: /etc/zones/main.zone
{% for zone in additional_zones -%}
zone:
name: {{ zone }}
zonefile: /etc/zones/{{ zone }}zone
{% endfor -%}

View File

@@ -1,21 +0,0 @@
server:
verbosity: 4
use-syslog: no
interface: 0.0.0.0
access-control: {{ netmask }} allow
root-hints: /etc/root.hints
pidfile: /tmp/unbound.pid
cache-max-ttl: 60
{% if ede %}
# For details check https://blog.nlnetlabs.nl/extended-dns-error-support-for-unbound/
ede: yes
val-log-level: 2
{% endif %}
{% if use_dnssec %}
val-sig-skew-min: 3600
trust-anchor-file: /etc/trusted-key.key
{% endif %}
remote-control:
control-enable: no

View File

@@ -1,83 +0,0 @@
use core::fmt;
use crate::{
record::{DNSKEYRData, DNSKEY},
DEFAULT_TTL, FQDN,
};
pub struct TrustAnchor {
keys: Vec<DNSKEY>,
}
impl TrustAnchor {
pub fn empty() -> Self {
Self { keys: Vec::new() }
}
pub fn public_dns() -> Self {
let mut anchors = Self::empty();
anchors.add(DNSKEY {
zone: FQDN::ROOT,
ttl: DEFAULT_TTL,
rdata: DNSKEYRData {
flags: 256,
protocol: 3,
algorithm: 8,
public_key: "AwEAAbPwrxwtOMENWvblQbUFwBllR7ZtXsu9rg/LdyklKs9gU2GQTeOc59XjhuAPZ4WrT09z6YPL+vzIIJqnG3Hiru7hFUQ4pH0qsLNxrsuZrZYmXAKoVa9SXL1Ap0LygwrIugEk1G4v7Rk/Alt1jLUIE+ZymGtSEhIuGQdXrEmj3ffzXY13H42X4Ja3vJTn/WIQOXY7vwHXGDypSh9j0Tt0hknF1yVJCrIpfkhFWihMKNdMzMprD4bV+PDLRA5YSn3OPIeUnRn9qBUCN11LXQKb+W3Jg+m/5xQRQJzJ/qXgDh1+aN+Mc9AstP29Y/ZLFmF6cKtL2zoUMN5I5QymeSkJJzc=".to_string(),
}
});
anchors.add(DNSKEY {
zone: FQDN::ROOT,
ttl: DEFAULT_TTL,
rdata: DNSKEYRData {
flags: 257,
protocol: 3,
algorithm: 8,
public_key: "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=".to_string(),
}
});
anchors
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn add(&mut self, key: DNSKEY) -> &mut Self {
self.keys.push(key);
self
}
pub(crate) fn keys(&self) -> &[DNSKEY] {
&self.keys
}
/// formats the `TrustAnchor` in the format `delv` expects
pub(super) fn delv(&self) -> String {
let mut buf = "trust-anchors {".to_string();
for key in &self.keys {
buf.push_str(&key.delv());
}
buf.push_str("};");
buf
}
}
impl fmt::Display for TrustAnchor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for key in &self.keys {
writeln!(f, "{key}")?;
}
Ok(())
}
}
impl FromIterator<DNSKEY> for TrustAnchor {
fn from_iter<T: IntoIterator<Item = DNSKEY>>(iter: T) -> Self {
Self {
keys: iter.into_iter().collect(),
}
}
}

View File

@@ -1,435 +0,0 @@
//! `tshark` JSON output parser
use core::result::Result as CoreResult;
use std::io::{self, BufRead, BufReader, Lines, Read};
use std::net::Ipv4Addr;
use std::process::ChildStderr;
use std::sync::atomic::{self, AtomicUsize};
use std::thread::{self, JoinHandle};
use serde::Deserialize;
use serde_with::{serde_as, DisplayFromStr};
use crate::container::{Child, Container};
use crate::Result;
static ID: AtomicUsize = AtomicUsize::new(0);
const UDP_PORT: u16 = 53;
impl Container {
pub fn eavesdrop(&self) -> Result<Tshark> {
let id = ID.fetch_add(1, atomic::Ordering::Relaxed);
let pidfile = pid_file(id);
let tshark = format!(
"echo $$ > {pidfile}
exec tshark --log-level debug --log-domain main,capture -l -i eth0 -T json -O dns -f 'udp port {UDP_PORT}'"
);
let mut child = self.spawn(&["sh", "-c", &tshark])?;
let stderr = child.stderr()?;
let mut stderr = BufReader::new(stderr).lines();
// Read from stdout on another thread. This ensures the subprocess won't block when writing
// to stdout due to full pipe buffers, and means we can read from stderr in
// `wait_for_capture()`, `wait_for_new_packets()`, or `terminate()` at our own pace without
// causing a deadlock.
let mut stdout = child.stdout()?;
let stdout_handle = thread::spawn(move || -> CoreResult<String, io::Error> {
let mut buf = String::new();
stdout.read_to_string(&mut buf)?;
Ok(buf)
});
for res in stderr.by_ref() {
let line = res?;
if line.contains("Capture started") {
break;
}
}
Ok(Tshark {
container: self.clone(),
child,
stdout_handle,
stderr,
id,
})
}
}
fn pid_file(id: usize) -> String {
format!("/tmp/tshark{id}.pid")
}
pub struct Tshark {
child: Child,
container: Container,
id: usize,
stdout_handle: JoinHandle<CoreResult<String, io::Error>>,
stderr: Lines<BufReader<ChildStderr>>,
}
impl Tshark {
/// Blocks until `tshark` reports the number of expected captured packets.
pub fn wait_for_new_packets(&mut self, expected: usize) -> Result<usize> {
let mut captured = 0;
loop {
captured += self.wait_for_capture()?;
if captured >= expected {
break;
}
}
Ok(captured)
}
/// Blocks until `tshark` reports that it has captured new DNS messages
///
/// This method returns the number of newly captured messages
// XXX maybe do this automatically / always in `terminate`?
pub fn wait_for_capture(&mut self) -> Result<usize> {
// sync_pipe_input_cb(): new packets NN
for res in self.stderr.by_ref() {
let line = res?;
if line.contains(": new packets ") {
let (_rest, count) = line.rsplit_once(' ').unwrap();
return Ok(count.parse()?);
}
}
Err("unexpected EOF".into())
}
pub fn terminate(self) -> Result<Vec<Capture>> {
let pidfile = pid_file(self.id);
let kill = format!("test -f {pidfile} || sleep 1; kill $(cat {pidfile})");
self.container.status_ok(&["sh", "-c", &kill])?;
let output = self.child.wait()?;
if !output.status.success() {
return Err("could not terminate the `tshark` process".into());
}
// wait until the message "NN packets captured" appears
// wireshark will close stderr after printing that so exhausting
// the file descriptor produces the same result
for res in self.stderr {
res?;
}
let output = self
.stdout_handle
.join()
.map_err(|_| "stdout thread panicked")??;
let mut messages = vec![];
let entries: Vec<Entry> = serde_json::from_str(&output)?;
let own_addr = self.container.ipv4_addr();
for entry in entries {
let Layers { ip, dns } = entry._source.layers;
let direction = if ip.dst == own_addr {
Direction::Incoming { source: ip.src }
} else if ip.src == own_addr {
Direction::Outgoing {
destination: ip.dst,
}
} else {
return Err(
format!("unexpected IP packet found in wireshark trace: {ip:?}").into(),
);
};
messages.push(Capture {
message: Message { inner: dns },
direction,
});
}
Ok(messages)
}
}
#[derive(Debug)]
pub struct Capture {
pub message: Message,
pub direction: Direction,
}
#[derive(Debug)]
pub struct Message {
// TODO this should be more "cooked", i.e. be deserialized into a `struct`
inner: serde_json::Value,
}
impl Message {
/// Returns `true` if the DO bit is set
///
/// Returns `None` if there's no OPT pseudo-RR
pub fn is_do_bit_set(&self) -> Option<bool> {
let do_bit = match self
.opt_record()?
.get("dns.resp.z_tree")?
.get("dns.resp.z.do")?
.as_str()?
{
"1" => true,
"0" => false,
_ => return None,
};
Some(do_bit)
}
/// Returns the "sender's UDP payload size" field in the OPT pseudo-RR
///
/// Returns `None` if there's no OPT record present
pub fn udp_payload_size(&self) -> Option<u16> {
self.opt_record()?
.get("dns.rr.udp_payload_size")?
.as_str()?
.parse()
.ok()
}
pub fn as_value(&self) -> &serde_json::Value {
&self.inner
}
pub fn is_ad_flag_set(&self) -> bool {
let Some(authenticated) = self.inner["dns.flags_tree"]
.as_object()
.unwrap()
.get("dns.flags.authenticated")
else {
return false;
};
let authenticated = authenticated.as_str().unwrap();
assert_eq!("1", authenticated);
true
}
fn opt_record(&self) -> Option<&serde_json::Value> {
for (key, value) in self.inner.get("Additional records")?.as_object()? {
if key.ends_with(": type OPT") {
return Some(value);
}
}
None
}
pub fn is_rd_flag_set(&self) -> bool {
let Some(recursion_desired) = self.inner["dns.flags_tree"]
.as_object()
.unwrap()
.get("dns.flags.recdesired")
else {
return false;
};
let recursion_desired = recursion_desired.as_str().unwrap();
match recursion_desired {
"1" => true,
"0" => false,
_ => panic!("unexpected value for dns.flags.recdesired: {recursion_desired}"),
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum Direction {
Incoming { source: Ipv4Addr },
Outgoing { destination: Ipv4Addr },
}
impl Direction {
/// The address of the peer, independent of the direction of the packet
pub fn peer_addr(&self) -> Ipv4Addr {
match self {
Direction::Incoming { source } => *source,
Direction::Outgoing { destination } => *destination,
}
}
pub fn try_into_incoming(self) -> CoreResult<Ipv4Addr, Self> {
if let Self::Incoming { source } = self {
Ok(source)
} else {
Err(self)
}
}
pub fn try_into_outgoing(self) -> CoreResult<Ipv4Addr, Self> {
if let Self::Outgoing { destination } = self {
Ok(destination)
} else {
Err(self)
}
}
}
#[derive(Deserialize)]
struct Entry {
_source: Source,
}
#[derive(Deserialize)]
struct Source {
layers: Layers,
}
#[derive(Deserialize)]
struct Layers {
ip: Ip,
dns: serde_json::Value,
}
#[serde_as]
#[derive(Debug, Deserialize)]
struct Ip {
#[serde(rename = "ip.src")]
#[serde_as(as = "DisplayFromStr")]
src: Ipv4Addr,
#[serde(rename = "ip.dst")]
#[serde_as(as = "DisplayFromStr")]
dst: Ipv4Addr,
}
#[cfg(test)]
mod tests {
use crate::client::{Client, DigSettings};
use crate::name_server::NameServer;
use crate::record::RecordType;
use crate::{Implementation, Network, Resolver, FQDN};
use super::*;
#[test]
fn nameserver() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, network)?.start()?;
let mut tshark = ns.eavesdrop()?;
let client = Client::new(network)?;
let resp = client.dig(
DigSettings::default(),
ns.ipv4_addr(),
RecordType::SOA,
&FQDN::ROOT,
)?;
assert!(resp.status.is_noerror());
tshark.wait_for_new_packets(2)?;
let messages = tshark.terminate()?;
let [first, second] = messages.try_into().expect("2 DNS messages");
assert_eq!(
client.ipv4_addr(),
first.direction.try_into_incoming().unwrap()
);
assert_eq!(
client.ipv4_addr(),
second.direction.try_into_outgoing().unwrap()
);
Ok(())
}
#[test]
fn resolver() -> Result<()> {
let network = &Network::new()?;
let mut root_ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, network)?;
let mut com_ns = NameServer::new(&Implementation::Unbound, FQDN::TEST_TLD, network)?;
let mut nameservers_ns =
NameServer::new(&Implementation::Unbound, FQDN::TEST_DOMAIN, network)?;
nameservers_ns.add(root_ns.a()).add(com_ns.a());
let nameservers_ns = nameservers_ns.start()?;
com_ns.referral_nameserver(&nameservers_ns);
let com_ns = com_ns.start()?;
root_ns.referral_nameserver(&com_ns);
let root_ns = root_ns.start()?;
let resolver = Resolver::new(network, root_ns.root_hint())
.start_with_subject(&Implementation::Unbound)?;
let mut tshark = resolver.eavesdrop()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(network)?;
let settings = *DigSettings::default().recurse();
let output = client.dig(settings, dbg!(resolver_addr), RecordType::A, root_ns.fqdn())?;
assert!(output.status.is_noerror());
let count = tshark.wait_for_capture()?;
dbg!(count);
let messages = tshark.terminate()?;
assert!(messages.len() > 2);
let ns_addrs = dbg!([
root_ns.ipv4_addr(),
com_ns.ipv4_addr(),
nameservers_ns.ipv4_addr(),
]);
let client_addr = dbg!(client.ipv4_addr());
let mut from_client_count = 0;
let mut to_client_count = 0;
let mut to_ns_count = 0;
let mut from_ns_count = 0;
for message in messages {
match message.direction {
Direction::Incoming { source } => {
if source == client_addr {
from_client_count += 1;
} else if ns_addrs.contains(&source) {
from_ns_count += 1;
} else {
panic!(
"found packet coming from {source} which is outside the network graph"
)
}
}
Direction::Outgoing { destination } => {
if destination == client_addr {
to_client_count += 1;
} else if ns_addrs.contains(&destination) {
to_ns_count += 1;
} else {
panic!(
"found packet going to {destination} which is outside the network graph"
)
}
}
}
}
// query from client (dig)
assert_eq!(1, from_client_count);
// answer to client (dig)
assert_eq!(1, to_client_count);
// check that all queries sent to nameservers were answered
assert_eq!(to_ns_count, from_ns_count);
Ok(())
}
}

View File

@@ -1,246 +0,0 @@
//! BIND-style zone files
//!
//! Note that
//! - the `@` syntax is not used to avoid relying on the order of the entries
//! - relative domain names are not used; all domain names must be in fully-qualified form
use core::fmt;
use std::array;
use std::net::Ipv4Addr;
use std::str::FromStr;
use crate::record::{self, DNSKEYRData, Record, RecordType, RRSIG, SOA};
use crate::{Error, Result, DEFAULT_TTL, FQDN};
mod signer;
pub use signer::{Nsec, SignSettings, Signer};
#[derive(Clone)]
pub struct ZoneFile {
origin: FQDN,
pub soa: SOA,
pub records: Vec<Record>,
}
impl ZoneFile {
/// Convenience constructor that uses "reasonable" defaults
pub fn new(soa: SOA) -> Self {
Self {
origin: soa.zone.clone(),
soa,
records: Vec::new(),
}
}
/// Adds the given `record` to the zone file
pub fn add(&mut self, record: impl Into<Record>) {
self.records.push(record.into())
}
/// Modify the RRSIG for the covered record type.
pub fn rrsig_mut(&mut self, covered_type: RecordType) -> Option<&mut RRSIG> {
self.records
.iter_mut()
.filter_map(|r| r.as_rrsig_mut())
.find(|rrsig| rrsig.type_covered == covered_type)
}
/// Shortcut method for adding a referral (NS + A record pair)
pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) {
self.add(Record::ns(zone, nameserver.clone()));
self.add(Record::a(nameserver, ipv4_addr));
}
pub(crate) fn origin(&self) -> &FQDN {
&self.origin
}
}
impl fmt::Display for ZoneFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { soa, records, .. } = self;
writeln!(f, "{soa}")?;
for record in records {
writeln!(f, "{record}")?;
}
Ok(())
}
}
impl FromStr for ZoneFile {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
let mut records = vec![];
let mut maybe_soa = None;
for line in input.lines() {
let mut line = line.trim();
// When using dnssec-signzone, comments are inserted; remove them.
if let Some((item, _)) = line.split_once(';') {
line = item.trim_matches('\n');
}
if line.is_empty() {
continue;
}
let record: Record = line.parse()?;
if let Record::SOA(soa) = record {
if maybe_soa.is_some() {
return Err("found more than one SOA record".into());
}
maybe_soa = Some(soa);
} else {
records.push(record)
}
}
let soa = maybe_soa.ok_or("no SOA record found in zone file")?;
Ok(Self {
origin: soa.zone.clone(),
soa,
records,
})
}
}
/// A root (server) hint
#[derive(Clone)]
pub struct Root {
pub ipv4_addr: Ipv4Addr,
pub ns: FQDN,
pub ttl: u32,
}
impl Root {
/// Convenience constructor that uses "reasonable" defaults
pub fn new(ns: FQDN, ipv4_addr: Ipv4Addr) -> Self {
Self {
ipv4_addr,
ns,
ttl: DEFAULT_TTL,
}
}
pub fn public_dns() -> Root {
Root {
ipv4_addr: Ipv4Addr::new(198, 41, 0, 4),
ns: FQDN("a.root-servers.net.").unwrap(),
ttl: DEFAULT_TTL,
}
}
}
impl fmt::Display for Root {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { ipv4_addr, ns, ttl } = self;
writeln!(f, ".\t{ttl}\tNS\t{ns}")?;
write!(f, "{ns}\t{ttl}\tA\t{ipv4_addr}")
}
}
// NOTE compared to `record::DNSKEY`, this zone file entry lacks the TTL field
#[allow(clippy::upper_case_acronyms)]
pub(crate) struct DNSKEY {
zone: FQDN,
rdata: DNSKEYRData,
}
impl DNSKEY {
pub fn with_ttl(self, ttl: u32) -> record::DNSKEY {
let Self { zone, rdata } = self;
record::DNSKEY { zone, ttl, rdata }
}
pub(crate) fn rdata(&self) -> &DNSKEYRData {
&self.rdata
}
}
impl FromStr for DNSKEY {
type Err = Error;
fn from_str(mut input: &str) -> Result<Self> {
// discard trailing comment
if let Some((before, _after)) = input.split_once(';') {
input = before.trim();
}
let mut columns = input.split_whitespace();
let [Some(zone), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm), Some(public_key), None] =
array::from_fn(|_| columns.next())
else {
return Err("expected 7 columns".into());
};
if record_type != "DNSKEY" {
return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into());
}
if class != "IN" {
return Err(format!("unknown class: {class}").into());
}
Ok(Self {
zone: zone.parse()?,
rdata: DNSKEYRData {
flags: flags.parse()?,
protocol: protocol.parse()?,
algorithm: algorithm.parse()?,
public_key: public_key.to_string(),
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn dnskey() -> Result<()> {
let input = ". IN DNSKEY 256 3 7 AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB ;{id = 42933 (zsk), size = 1024b}";
let DNSKEY {
zone,
rdata:
DNSKEYRData {
flags,
protocol,
algorithm,
public_key,
},
} = input.parse()?;
assert_eq!(FQDN::ROOT, zone);
assert_eq!(256, flags);
assert_eq!(3, protocol);
assert_eq!(7, algorithm);
let expected = "AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB";
assert_eq!(expected, public_key);
Ok(())
}
#[test]
fn roundtrip() -> Result<()> {
// `ldns-signzone`'s output minus trailing comments; long trailing fields have been split as well
let input = include_str!("muster.zone");
let zone: ZoneFile = input.parse()?;
let output = zone.to_string();
assert_eq!(input, output);
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More