Compare commits

...

203 Commits

Author SHA1 Message Date
Saber Haj Rabiee
66dbc73d2b formatted just file 2024-06-23 11:37:07 +02:00
Saber Haj Rabiee
b9009e770c fixed just file to compile bind 2024-06-23 11:37:07 +02:00
Daniel McCarney
02a8668fd5 docs: add content from 2183 to changelog 2024-06-17 12:12:57 -07:00
Christian Poveda
1ebc11c1d9 Remove unnecessary derives 2024-06-17 12:06:37 -07:00
Christian Poveda
b7e4f9a984 Rename fqdns 2024-06-17 12:06:37 -07:00
Christian Poveda
9f8c19cb71 Factor NSEC3 records logic to its own module 2024-06-17 12:06:37 -07:00
Christian Poveda
5d078ab765 Remove bob 2024-06-17 12:06:37 -07:00
Christian Poveda
da5423cb06 Add conformance tests for NSEC3 2024-06-17 12:06:37 -07:00
Jorge Aparicio
198128ca48 DnsLru: cache RRSIG records together with the record they cover
this simplifies the DNSSEC_OK (DO) bit handling logic in `Recursor`

as a side effect `Recursor` will now cache queries that have the DO bit
set whereas before it wasn't
2024-06-17 12:23:00 +02:00
Benjamin Fry
3129a31386 update version for each package in workspace to 0.25... 2024-06-16 08:28:56 -07:00
Benjamin Fry
522220321f update changelog for 0.25-alpha 2024-06-16 08:28:56 -07:00
Dirkjan Ochtman
c505f84db8 Apply clippy suggestions for Rust 1.79 2024-06-14 16:45:26 -07:00
Jorge Aparicio
e585bf0a41 dns-test: make NameServer's FQDN more stable
previously, the FQDN of a NameServer was generated using a globally
shared, monotonically increasing counter. that resulted in a
unpredictable FQDN in the presence other tests running concurrently /
in parallel

with this change, the presence of other test threads does not affect the
FQDN automatically chosen for a NameServer. the FQDN becomes only a
function of the order in which the NameServers are created *within a
thread*
2024-06-14 06:45:40 -07:00
Jorge Aparicio
ed192864f3 refactor the Resource data structure
the `rr_type` field has been removed. this eliminates the possibility of
setting `rdata` to a type that does not match `rr_type`. as a
consequence, the `set_record_type` has also been removed

the `record_type` of the `Resource` is now derived from what's stored in
the `rdata` field

`rdata` is no longer an `Option`. `rdata = None` was being used to
represent update records. an `Update` variant has been added to the
`RData` enum. this variant is used to represent update records, which
have RDLENGTH set to 0.

the `Resource::{default,new}` constructors have been removed. they felt
error prone as in most cases one wants to set the `rdata` and `name`
fields since they have no sensible defaults. all uses of those
constructors now use the pre-existing `from_rdata` constructor

the `Resource::with` constructor has also been removed. it was pretty
similar to `from_rdata` but initialized `rr_type` and not `rdata`. all
uses of `Record::with` has been changed to `from_rdata`
2024-06-10 20:00:48 -07:00
Christian Poveda
76f717d10f Add just recipes to clean leftover containers and networks 2024-06-08 14:42:07 +02:00
Jorge Aparicio
383a4e72dd remove dnssec-tests' GHA workflow
it has been deprecated by the new `/.github/workflows/conformance.yml`
workflow
2024-06-07 11:04:53 +02:00
Jorge Aparicio
74a3d69eea add new CI workflow 2024-06-07 11:04:41 +02:00
Jorge Aparicio
f581eb0483 add conformance tasks to justfile 2024-06-07 11:04:34 +02:00
Jorge Aparicio
cd7e898ff3 add CODEOWNERS 2024-06-07 11:04:29 +02:00
Jorge Aparicio
4b6c032e17 remove redundant license files 2024-06-07 11:04:26 +02:00
Jorge Aparicio
2a04299fbc Add 'conformance/' from commit '2a86c07974b27af6de6cdb0cc87074aaf57b8c39'
git-subtree-dir: conformance
git-subtree-mainline: f1489da675
git-subtree-split: 2a86c07974
2024-06-07 11:03:57 +02:00
Jorge Aparicio
f1489da675 ci: pin nightly version
the past-future job fails with latest nightly toolchain:
1.80.0-nightly (da159eb33 2024-05-28)

this commit pins the nightly version to a known to work version

closes #2223
2024-05-30 12:05:04 +02:00
Christian Poveda Ruiz
2a86c07974
Merge pull request #67 from ferrous-systems/ja-bump-hickory
bump hickory-dns and unignore fixed tests
2024-05-29 10:17:31 -05:00
Jorge Aparicio
0d529d4f41 bump hickory-dns and unignore fixed tests
also build hickory-dns with dnssec support and enable security
awareness
2024-05-29 12:46:32 +02:00
Андрей Листочкин (Andrei Listochkin)
568c75ec6f
Merge pull request #65 from ferrous-systems/ja-graph-two-point-o
tweak `Graph::build` to support anydomain.com. as leaf zone
2024-05-23 12:22:37 +01:00
Jorge Aparicio
f87d98eaaf explore: use Graph API and use mydomain.com. as leaf zone 2024-05-23 13:11:04 +02:00
Jorge Aparicio
a0909f3c04 tweak Graph::build to support anydomain.com. as leaf zone 2024-05-23 13:10:53 +02:00
Jorge Aparicio
ab2aa39b06 NameServer: fix generation of SOA record
`unbound` requires that the MNAME lies underneath the zone. That is
`primaryNN.nameservers.com.` is not a valid MNAME for a nameserver
authoritative over `mydomain.com.`. For that zone,
`primaryNN.mydomain.com.` would be a valid MNAME.
2024-05-23 13:10:53 +02:00
Jorge Aparicio
0ba63a167d
Merge pull request #58 from ferrous-systems/refactor-test-helpers
Refactor test helpers
2024-05-22 19:07:58 +02:00
Sebastian Ziebell
0ea1397480 Add function to return root hint
This adds a function to `NameServer` to return root hints.
2024-05-22 19:00:55 +02:00
Sebastian Ziebell
0bcf749617 Add function to set referral via other nameserver
* fix clippy lints
2024-05-22 18:51:44 +02:00
Sebastian Ziebell
96b3b6650d Add function to return A record
A NameServer can now return the `Record::A` entry for itself.
2024-05-22 18:49:12 +02:00
Jorge Aparicio
14ee8fd6cc
Merge pull request #57 from ferrous-systems/ja-clear-ad-bit-on-queries
resolver: check that out queries don't have the AD bit
2024-05-22 18:06:26 +02:00
Jorge Aparicio
2de7139bc6 resolver: check that out queries don't have the AD bit 2024-05-22 17:56:57 +02:00
Jorge Aparicio
a58fb71eeb
Merge pull request #56 from ferrous-systems/ja-cd-bit-handling
resolver: add CD bit handling tests
2024-05-22 17:49:11 +02:00
Jorge Aparicio
b6e97bf3b6 move existing test into RFC4035 section 3.2.2 2024-05-22 17:44:08 +02:00
Jorge Aparicio
8f414879b8 test CD bit forwarding 2024-05-22 17:39:13 +02:00
Jorge Aparicio
c50b28262e
Merge pull request #53 from ferrous-systems/ja-do-bit-tests
resolver: test DO bit handling
2024-05-22 14:56:14 +02:00
Jorge Aparicio
8b582baffc resolver: test DO bit handling
this corresponds to section 3.2.1 of RFC4035
2024-05-22 14:47:06 +02:00
Jorge Aparicio
b3b214317a
Merge pull request #54 from ferrous-systems/ja-ds-query
resolver: test that DS query is sent to parent zone
2024-05-22 14:44:38 +02:00
Jorge Aparicio
261b9f4428 resolver: test that DS query is sent to parent zone 2024-05-22 14:38:50 +02:00
Jorge Aparicio
fef26b7139 dns-test: add getters & make some fields public 2024-05-22 14:32:02 +02:00
Jorge Aparicio
3a54e693fa
Merge pull request #31 from ferrous-systems/ja-explore-opt-in-dnssec
`explore`: make DNSSEC opt-in
2024-05-22 14:31:40 +02:00
Jorge Aparicio
5d15aa2228 explore: generate bind.keys w/o querying resolver
this avoids the resolver caching any query. that way `tshark` can
observe all the messages involved in DNSSEC validating a query "from
scratch"
2024-05-22 14:26:55 +02:00
Jorge Aparicio
1aab8812df explore: make DNSSEC opt-in 2024-05-22 14:26:55 +02:00
Jorge Aparicio
a9c6e42715
Merge pull request #52 from ferrous-systems/ja-use-default-cache-size
hickory/resolver: drop _cache_size settings
2024-05-22 14:26:23 +02:00
Jorge Aparicio
b840819998 hickory/resolver: drop _cache_size settings
they are not required as default values exist
2024-05-22 14:21:38 +02:00
Jorge Aparicio
e12a1f4d92
Merge pull request #44 from ferrous-systems/ja-clippier
CI: make clippy check cfg(test) code, tests & examples
2024-05-22 14:21:17 +02:00
Jorge Aparicio
49990d2530 fix previously undetected clippy warning 2024-05-22 14:16:53 +02:00
Jorge Aparicio
cd2895a168 CI: make clippy check cfg(test) code, tests & examples 2024-05-22 14:16:53 +02:00
Jorge Aparicio
76d308c984
Merge pull request #43 from ferrous-systems/ja-immutable-subject-peer
turn `dns_test::{subject,peer}` into immutable statics
2024-05-22 14:16:27 +02:00
Jorge Aparicio
58239028f4 turn dns_test::{subject,peer} into immutable statics
using `std::env::set_var` to set or change the value of either
DNS_TEST_SUBJECT or DNS_TEST_PEER  is A Bad Idea, specially so when
tests  are running in parallel

we can't forbid the use of `env::set_var` _but_ at least we can ensure
that even in its presence the return value of `dns_test::{subject,peer}`
will not change

this is accomplished using a "lazy" static variable that gets
initialized at most once during the lifetime of the process instead of
reading the env var each time `{subject,peer}` is called

to better convey the fact that the return value of `{subject,peer}`
won't change, we present them as static variables instead
2024-05-22 14:03:18 +02:00
Dirkjan Ochtman
107635c6c5 Update dependencies 2024-05-21 10:24:01 -07:00
Dirkjan Ochtman
cde6f908c2 Remove broken mtls code 2024-05-21 10:24:01 -07:00
Sebastian Ziebell
2e46421927 Check hickory-dns is fully started
When starting `hickory-dns` there is no easy way to check the start
sequence has finished & its fully ready to accept connections. Other
tools, e.g. unbound, are designed as services, they will correctly
manage their `pidfile`. They also could be queried by the `servicectl`
inside the Docker container.
2024-05-21 15:05:29 +01:00
Benjamin Fry
978235f7cd add RFC2931 SIG(0) as supported 2024-05-21 11:33:19 +02:00
Jonathan Davies
dbb48114ce cargo: Enable LTO on release build. 2024-05-19 07:30:40 -07:00
Adrian Kappel
ede83dc7d6
Adds deref call in assertion for hickory-client README example (#2173)
* Adds deref call in assertion

* Adds inner struct to pattern match

---------

Co-authored-by: Benjamin Fry <benjaminfry@me.com>
2024-05-18 11:40:03 -07:00
Jorge Aparicio
f868f8e27d recursor: send DS query to the parent zone 2024-05-18 11:33:35 -07:00
Benjamin Fry
202c2304da retry all tcp on all IO errors recieved from UDP requests 2024-05-18 09:54:28 +02:00
Benjamin Fry
27018620b7 remove IO error from ResolveError and only use ProtoError(Io) 2024-05-18 09:54:28 +02:00
Dirkjan Ochtman
872ada02ef recursor: rename Recursor::new() to builder() 2024-05-18 09:38:36 +02:00
Dirkjan Ochtman
5db65e336b recursor: make security awareness depend on config 2024-05-18 09:38:36 +02:00
Jorge Aparicio
97e1f43456 expose security-aware setting in named.toml 2024-05-10 18:25:18 -07:00
Jorge Aparicio
36258a8a03 make Recursor configurable via a "builder"
and make security-awareness opt-in
2024-05-10 18:25:18 -07:00
Jorge Aparicio
f3a012cc36 recursor: honor DO bit in client's query 2024-05-10 18:25:18 -07:00
Jorge Aparicio
e558fcc43c recursor: preserve DNSSEC records 2024-05-10 18:25:18 -07:00
Jorge Aparicio
cc81d5636e recursor: set DO in outgoing queries
when the recursor is "security-aware" -- that is the "dnssec" feature is
enabled -- as per RFC 4035 section 3.2.1
2024-05-10 18:25:18 -07:00
Jorge Aparicio
fe3961ffe1
Merge pull request #42 from ferrous-systems/ja-terminate-hickory
implement terminate for Hickory
2024-05-08 12:44:14 +02:00
0xffffharry
cffc3fac2a Make H3ClientStream Clonable 2024-05-04 11:13:01 -07:00
Divma
c1f2e9b4de
address new clippy lint assigning-clones (#2205) 2024-05-02 21:35:29 +02:00
Diva M
3799e13f7b fix copy-pasted sentence 2024-04-30 08:11:57 -07:00
Diva M
03b0daca71 fix misc typos in md top files 2024-04-30 08:11:57 -07:00
dependabot[bot]
9f27fcc0dc Bump parking_lot from 0.12.1 to 0.12.2
Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.12.1 to 0.12.2.
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/0.12.1...0.12.2)

---
updated-dependencies:
- dependency-name: parking_lot
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 16:54:57 +02:00
Jorge Aparicio
4f277c1dbb implement terminate for Hickory
both in the NameServer and Resolver roles
2024-04-22 17:14:11 +02:00
Jorge Aparicio
6189787d9f
Merge pull request #51 from ferrous-systems/ja-hickory-name-server-take-2
support Hickory in the NameServer role
2024-04-22 16:51:29 +02:00
Jorge Aparicio
74e4797061 add a DNSSEC-disabled NameServer scenario test
to ensure that hickory-dns is correctly configured to work in NameServer
mode
2024-04-22 16:40:56 +02:00
Jorge Aparicio
ce222b3de1 support Hickory in NameServer role
note that because Hickory does not support pre-signed zone files all the
DNSSEC tests fail with it
2024-04-22 16:40:56 +02:00
Jorge Aparicio
05ffecec45 add a few NameServer role DNSSEC tests 2024-04-22 16:40:56 +02:00
Jorge Aparicio
4ce9ec9937
Merge pull request #50 from ferrous-systems/ja-ede-support-take-2
add support for Extended DNS Error (EDE)
2024-04-22 16:40:38 +02:00
Jorge Aparicio
b96aa89da9 add more EDE tests 2024-04-22 16:23:03 +02:00
Jorge Aparicio
4d31eca533 use Graph to simplify tests 2024-04-22 16:23:03 +02:00
Jorge Aparicio
7bb6b9439c add name_server::Graph 2024-04-22 16:23:03 +02:00
Jorge Aparicio
95f94e2c7b add first EDE test 2024-04-22 16:23:03 +02:00
Jorge Aparicio
31048f5cd0 extend DNSKEY API 2024-04-22 16:23:03 +02:00
Jorge Aparicio
dc19776107 parse EDE info from dig's output 2024-04-22 16:23:03 +02:00
Jorge Aparicio
166863bcc4 allow enabling Extended DNS Errors (EDE) 2024-04-22 16:23:03 +02:00
Jorge Aparicio
75de211a06
Merge pull request #49 from ferrous-systems/ja-resolver-builder-take-2
refactor: use builder pattern in Resolver ctor
2024-04-22 16:22:38 +02:00
Jorge Aparicio
70245e7ff8 refactor: use builder pattern in Resolver ctor
the `start` constructor's parameter list was getting long and we want to
add even more configuration options, like EDE, in the future.

using the builder pattern lets us introduce new settings without
breaking changes
2024-04-22 16:18:47 +02:00
Jorge Aparicio
63c95fd0db
Merge pull request #48 from ferrous-systems/ja-refactor-impl-take-2
refactor Implementation branching into its own module
2024-04-22 16:18:24 +02:00
Jorge Aparicio
9689568974 refactor Implementation branching into its own module 2024-04-22 16:07:36 +02:00
Jorge Aparicio
4f024887e0 CI: test unbound (subject) against BIND (peers) 2024-03-28 21:41:31 +00:00
Jorge Aparicio
7aa9d543b4 support BIND in the NameServer role 2024-03-28 21:41:31 +00:00
Jorge Aparicio
d25cc923ec
Merge pull request #27 from ferrous-systems/ja-bind-impl
support using BIND in the Resolver role
2024-03-11 12:04:58 +01:00
Jorge Aparicio
90ee7b30f6 CI: run conformance tests against BIND 2024-03-11 11:59:56 +01:00
Jorge Aparicio
2c4ef88a98 support using BIND in the Resolver role 2024-03-11 11:59:56 +01:00
Jorge Aparicio
b8605f7944
Merge pull request #36 from ferrous-systems/ja-contraindications
README: cover dns-test automatic clean-up
2024-03-11 11:58:34 +01:00
Jorge Aparicio
f3bd5a1c55 README: cover dns-test automatic clean-up
and advise against running tests with cargo-watch
2024-03-07 16:04:18 +01:00
Андрей Листочкин (Andrei Listochkin)
6e303831a2
Merge pull request #32 from ferrous-systems/ja-readme-up
README: add some tips on writing tests
2024-03-01 21:31:12 +00:00
Jorge Aparicio
30ffd3882b README: add some tips on writing tests 2024-03-01 19:49:15 +01:00
Jorge Aparicio
49c89f764e
Merge pull request #25 from japaric/ja-impl-by-ref
take `Implementation` by reference
2024-02-26 14:53:39 +01:00
Jorge Aparicio
ab9b1e68cc also take Impl by ref in Resolver::new
mainly for symmetry. the existing tests don't clone the subject so the
code does not become any shorter
2024-02-26 14:48:48 +01:00
Jorge Aparicio
49b2abc6be take Impl by ref in NameServer::new
this eliminates the need for cloning in tests that use more than one
name server, making the code more succint / less noisy
2024-02-26 14:48:48 +01:00
Jorge Aparicio
19ae3d9083
Merge pull request #24 from japaric/ja-authority-section
parse authority section from dig's output
2024-02-26 14:44:44 +01:00
Jorge Aparicio
6fda01af00 parse authority section from dig's output 2024-02-26 14:40:33 +01:00
Андрей Листочкин (Andrei Listochkin)
72683a969d
Merge pull request #26 from japaric/ja-explicit-license
make license more visible
2024-02-26 13:27:12 +00:00
Андрей Листочкин (Andrei Listochkin)
eda8fd6ec3
Merge branch 'main' into ja-explicit-license 2024-02-26 13:13:45 +00:00
Андрей Листочкин (Andrei Listochkin)
a182db1a09
Merge pull request #16 from japaric/ja-signed-zone-mutation
allow signed zone file mutation
2024-02-26 11:58:58 +00:00
Jorge Aparicio
5f26698a51 make license more visible
it was already in the Cargo.toml metadata but add the files and mention
it in the README
2024-02-26 12:28:42 +01:00
Jorge Aparicio
a39afe6412 test a bogus DNSSEC scenario 2024-02-26 11:50:48 +01:00
Jorge Aparicio
b87ae21d2a make dig queries more configurable
switch from enum arguments like `Recurse` and `Dnssec` to a
build-pattern-based `Settings` struct
2024-02-26 11:50:48 +01:00
Jorge Aparicio
df344e57b1 fix integer types in RRSIG 2024-02-26 11:50:48 +01:00
Jorge Aparicio
16e83b1d6b move secure DNSSEC scenarios into a module 2024-02-26 11:50:48 +01:00
Jorge Aparicio
1592454395 allow mutation of signed zone file 2024-02-26 11:50:48 +01:00
Jorge Aparicio
57a1fc9231 parse more record types 2024-02-26 11:50:48 +01:00
Jorge Aparicio
66d6061ffc drop most zone_file entry types
instead use the record types in zone files

the main difference between e.g. zone_file::A and record::A was that the
latter had a TTL filed and the former didn't

to eliminate code duplication we make the `ZoneFile` API use the
`record` types and discard the zone_file entry types
2024-02-26 11:50:43 +01:00
Jorge Aparicio
a83b6629a3 drop generic lifetime parameters from types
none of the parsing we are doing requires a non-static lifetime
2024-02-26 11:43:25 +01:00
Jorge Aparicio
795adc32c2
Merge pull request #21 from japaric/ja-fix-ci
fix the explore example
2024-02-23 15:42:55 +01:00
Jorge Aparicio
56a9613615 fix the explore example 2024-02-23 15:35:51 +01:00
Андрей Листочкин (Andrei Listochkin)
9f03274589
Merge pull request #19 from japaric/ja-dns-test-peer
add `dns_test::peer` and use it to initialize `NameServer`
2024-02-23 13:59:25 +00:00
Андрей Листочкин (Andrei Listochkin)
ace04090ef
Merge pull request #18 from japaric/readme-up
add `explore` example and update the README
2024-02-23 13:56:15 +00:00
Jorge Aparicio
98cb9ddaae add a Client image 2024-02-23 13:21:07 +01:00
Jorge Aparicio
c0b681e0a3 add an Implementation parameter to NameServer::new
use `dns_test::peer` for name servers in conformance tests
2024-02-23 12:48:34 +01:00
Jorge Aparicio
acca2d4f0f update README 2024-02-20 17:00:30 +01:00
Jorge Aparicio
59dc60c5c4 enable verbose docker build with any value of DNS_TEST_VERBOSE_DOCKER_BUILD 2024-02-20 16:36:52 +01:00
Jorge Aparicio
86284cce4b add explore example 2024-02-20 16:30:41 +01:00
Jorge Aparicio
9d3d405af5
Merge pull request #14 from japaric/ja-hickory-source
support building hickory from a local/remote git source
2024-02-20 10:34:03 +01:00
Jorge Aparicio
709a8406b1 document what ./src refers to in hickory.Dockerfile 2024-02-20 10:28:13 +01:00
Jorge Aparicio
1cb7ee40fe perform some validation of the repository argument 2024-02-20 10:28:11 +01:00
Jorge Aparicio
1429b6bedf ci: fix syntax of DNS_TEST_SUBJECT 2024-02-20 10:26:33 +01:00
Jorge Aparicio
0afae4f042 don't include the .git directory in the context send to docker build
without this `DNS_TEST_SUBJECT="hickory $URL" cargo t` was re-building
hickory from scratch even when its source had not change

not including the `.git` directory in the hickory repository lets Docker
see that the source code has not changed
2024-02-20 10:26:33 +01:00
Jorge Aparicio
9bfa4c5c71 ci: make docker build verbose 2024-02-20 10:26:33 +01:00
Jorge Aparicio
10351a7021 ci: build hickory from a pinned git commit 2024-02-20 10:26:33 +01:00
Jorge Aparicio
55184172e3 build hickory from a local/remote git source
DNS_TEST_SUBJECT now needs to contain the URL to the hickory source code
2024-02-20 10:26:33 +01:00
Jorge Aparicio
0e2b35699c add mechanism to make docker build visible
set the DNS_TEST_VERBOSE_DOCKER_BUILD env var to 1 to make the `docker
build` command print its output to the console
2024-02-20 10:26:33 +01:00
Jorge Aparicio
3707bdfd3f
Merge pull request #11 from japaric/dns-shark
add eavesdrop API & test a RFC requirement with it
2024-02-20 10:23:32 +01:00
Jorge Aparicio
156e005ff2 RFC4035: test EDNS support 2024-02-19 13:22:15 +01:00
Jorge Aparicio
438af31340 Tshark: newtype the json::Value & add some getters 2024-02-19 12:56:40 +01:00
Jorge Aparicio
e77fd41635 add eavesdrop API
closes #9
2024-02-19 12:56:38 +01:00
Jorge Aparicio
2abb8268f9
Merge pull request #13 from japaric/sebastian/gh7-docker-network
Use network to group all running containers
2024-02-16 15:00:25 +01:00
Sebastian Ziebell
014662d218
Refactor tests to check network state 2024-02-16 14:38:30 +01:00
Sebastian Ziebell
a4ca3d6423
Incorporate feedback
* add new type `Network` that holds `Arc`
* adjust network name to use `CARGO_PKG_NAME` env var, process id and
  counter
* remove function to remove network
* clone Network in container
* refactor Network tests
2024-02-16 14:38:30 +01:00
Sebastian Ziebell
2289567998
Disconnect all containers before removing network
The list of attached containers is determined, all of them are
disconnected from the network, then the network is deleted.

* set net mask in unbound conf template
* expose container id
2024-02-16 14:38:30 +01:00
Sebastian Ziebell
820f1c3447
Pass in Network to containers 2024-02-16 14:38:28 +01:00
Sebastian Ziebell
5630dd79e9
Add Network types
Creates & removes the Docker network & reads in the allocated subnet mask.
2024-02-16 14:36:42 +01:00
Jorge Aparicio
f690522111
Merge pull request #12 from japaric/ja-deny-empty-roots
resolver: check that the root servers list is not empty
2024-02-14 13:20:04 +01:00
Jorge Aparicio
36f93252a2 resolver: check that the root servers list is not empty 2024-02-12 19:11:59 +01:00
Jorge Aparicio
77150bbca6
Merge pull request #6 from japaric/reorg
reorganize tests in modules
2024-02-09 18:22:27 +01:00
Jorge Aparicio
33509f4484 check that ignored tests fail with hickory 2024-02-09 16:48:59 +01:00
Jorge Aparicio
952c346e9e build hickory in debug mode 2024-02-09 16:42:52 +01:00
Jorge Aparicio
3d73fe3da1 fix CI workflow 2024-02-09 16:38:45 +01:00
Jorge Aparicio
d17e42a679 update CI to run tests against unbound & hickory 2024-02-09 16:37:53 +01:00
Jorge Aparicio
e52980a82f test NXDOMAIN scenario 2024-02-09 16:35:29 +01:00
Jorge Aparicio
9526338ca7 shorten RecursiveResolver -> Resolver 2024-02-09 16:03:09 +01:00
Jorge Aparicio
04a7190e61 supporty hickory-based Resolver 2024-02-09 15:37:58 +01:00
Jorge Aparicio
3c95b85150 introduce Implementation enum 2024-02-09 14:55:03 +01:00
Jorge Aparicio
362838b41f update CI config 2024-02-08 19:28:05 +01:00
Jorge Aparicio
5c53ba0899 make Client::delv work & use it in dnssec tests 2024-02-08 19:23:50 +01:00
Jorge Aparicio
edd6eebe1a mv tests into conformance-tests package 2024-02-08 18:28:05 +01:00
Jorge Aparicio
1c2d9ec4dc restructure into a workspace 2024-02-08 18:15:41 +01:00
Jorge Aparicio
1e5aac55e7 bake Dockerfile into the crate
to avoid the problem of changes in the `docker` directory affecting the
outcomes of tests. IOW, so that all downstream uses of the crate use the
same docker image
2024-02-08 18:10:49 +01:00
Jorge Aparicio
095b68b887 add API to gracefully terminate name server & resolver 2024-02-08 17:54:35 +01:00
Jorge Aparicio
3e78cfa30e ensure child process does not outlive its container 2024-02-08 15:30:32 +01:00
Jorge Aparicio
02f5307056 make docker build less noisy 2024-02-08 15:14:54 +01:00
Jorge Aparicio
11e9c43e19
Merge pull request #5 from japaric/ci
enable CI
2024-02-08 15:11:47 +01:00
Jorge Aparicio
c50568c709 enable CI 2024-02-08 15:09:03 +01:00
Jorge Aparicio
b21875b963
Merge pull request #4 from japaric/container-api
minimal local network with working resolution
2024-02-08 15:02:08 +01:00
Jorge Aparicio
306ce7a32b set up DS records and trust anchor to make DNSSEC work 2024-02-07 20:11:09 +01:00
Jorge Aparicio
2bcad2a25c parse RRSIG record & complete signed NS test 2024-02-07 14:59:15 +01:00
Jorge Aparicio
037cf4f698 ns: sign zone file 2024-02-06 20:05:21 +01:00
Jorge Aparicio
a527ed6218 hardcode chmod used in Container::cp
everything uses the same value
2024-02-06 18:53:40 +01:00
Jorge Aparicio
5858309bfa revise names and module organization 2024-02-06 18:52:30 +01:00
Jorge Aparicio
7f7d9f7ccf rename Domain -> FQDN 2024-02-06 18:15:05 +01:00
Jorge Aparicio
e29b901bc1 shorten AuthoritativeNameServer -> NameServer 2024-02-06 18:12:22 +01:00
Jorge Aparicio
3e5ef300ce refactor/ns: build pattern + type state 2024-02-06 18:11:31 +01:00
Jorge Aparicio
7ad5bacbdc parse dig's output 2024-02-06 16:47:18 +01:00
Jorge Aparicio
fc7cf970a5 fix nameserver's A record & add some docs 2024-02-05 19:51:02 +01:00
Jorge Aparicio
d13186e404 make resolution test work 2024-02-05 19:21:52 +01:00
Jorge Aparicio
984a05e873 revamp zone file generation 2024-02-05 18:33:04 +01:00
Jorge Aparicio
7e9f63d85e fix non-fatal NSD error about PID file 2024-02-05 15:55:01 +01:00
Jorge Aparicio
1b0f1ef59f move validation to Domain ctor 2024-02-05 15:53:48 +01:00
Jorge Aparicio
bab595a412 simplify code with minijinja::render! 2024-02-05 15:44:25 +01:00
Jorge Aparicio
cbbb12b3b5 refactor Container methods 2024-02-05 15:03:57 +01:00
Jorge Aparicio
c7e0580c7a use Ipv4Addr type for IP addresses 2024-02-05 14:42:20 +01:00
Jorge Aparicio
d79581bdcc fix warnings 2024-02-05 14:27:10 +01:00
Jorge Aparicio
60ecfeca5e initial RecursiveResolver API 2024-02-05 14:24:01 +01:00
Jorge Aparicio
9101bb1046 only build image once per test runner run 2024-02-02 15:45:12 +01:00
Jorge Aparicio
1d616e822d rename nsd -> AuthoritativeNameServer 2024-02-02 15:40:43 +01:00
Jorge Aparicio
f4ded488ce merge all docker images into one 2024-02-02 15:39:38 +01:00
Jorge Aparicio
42de7c3a92 move NsdContainer into its own module 2024-02-02 15:19:29 +01:00
Jorge Aparicio
e997c8cff5 move Container into its own module 2024-02-02 15:15:42 +01:00
Jorge Aparicio
61bb4bb315 test nameserver with dig 2024-02-02 15:09:45 +01:00
Jorge Aparicio
cc753de77c add NsdContainer 2024-02-02 14:59:13 +01:00
Jorge Aparicio
6026caf25d make nameserver setup work 2024-02-02 14:48:26 +01:00
Jorge Aparicio
bc10cda9cc WIP root & tld name server setup 2024-02-01 18:07:00 +01:00
Jorge Aparicio
3c50ca911a initial Container API 2024-02-01 17:19:01 +01:00
Jorge Aparicio
e6691ffc40
Merge pull request #3 from japaric/expand-dockerfiles-install-tools
Prepare containers to work on OSX
2024-02-01 16:31:30 +01:00
Jorge Aparicio
97beb1c083
Merge pull request #2 from japaric/readme-up
add TLD name server instructions
2024-02-01 16:25:40 +01:00
Sebastian Ziebell
907f40923d
Prepare containers to work on OSX
* copy config files into unbound container
* install vim & tshark in all containers
2024-02-01 16:24:39 +01:00
Jorge Aparicio
88afa403a4 add TLD name server instructions 2024-02-01 16:24:34 +01:00
Jorge Aparicio
ecc73a5255
Merge pull request #1 from japaric/expand-example-setup
Expand test setup to run on OSX as well
2024-02-01 15:36:38 +01:00
Sebastian Ziebell
e5c373b6da
Expand test setup to run on OSX as well
A client based `Dockerfile` is added to run `dig` & `delv` in, to make
the setup work on OSX.

* set up client container
* install additional tools
* expand Readme with setup instructions
2024-02-01 15:34:30 +01:00
Jorge Aparicio
a438d7c5e1 initial commit 2024-01-29 17:08:18 +01:00
154 changed files with 8345 additions and 2017 deletions

8
.github/CODEOWNERS vendored Normal file
View File

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

42
.github/workflows/conformance.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: conformance
on:
push:
branches:
- main
pull_request:
branches:
- main
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: lint code
run: just conformance-clippy
- name: check that code is formatted
run: just conformance-fmt

View File

@ -6,10 +6,14 @@ on:
- main
- release/**
- "*_dev"
paths-ignore:
- "conformance/**"
pull_request:
branches:
- main
- release/**
paths-ignore:
- "conformance/**"
schedule:
- cron: "0 3 * * 4"
@ -117,7 +121,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
version: ["1.67.0", beta, nightly]
version: ["1.67.0", beta, "nightly-2024-05-23"]
steps:
- uses: actions/checkout@v4
@ -137,7 +141,7 @@ jobs:
run: just no-default-features
- name: just build-bench
if: matrix.version == 'nightly'
if: contains( matrix.version, 'nightly' )
run: just build-bench
## Execute the clippy checks

View File

@ -5,6 +5,68 @@ 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
### Fixed
- (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
- (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) 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

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 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.
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.
### Test policy

395
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.24.1"
version = "0.25.0-alpha.1"
authors = ["The contributors to Hickory DNS"]
edition = "2021"
rust-version = "1.67.0"
@ -28,11 +28,11 @@ license = "MIT OR Apache-2.0"
[workspace.dependencies]
# hickory
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 }
hickory-client = { version = "0.25.0-alpha.1", path = "crates/client", default-features = false }
hickory-recursor = { version = "0.25.0-alpha.1", path = "crates/recursor", default-features = false }
hickory-resolver = { version = "0.25.0-alpha.1", path = "crates/resolver", default-features = false }
hickory-server = { version = "0.25.0-alpha.1", path = "crates/server", default-features = false }
hickory-proto = { version = "0.25.0-alpha.1", path = "crates/proto", default-features = false }
# logging
@ -111,3 +111,9 @@ 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"

View File

@ -97,7 +97,7 @@ 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 only 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 valid DoT or DoH resolvers being registered in order to be used.
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.
@ -136,6 +136,7 @@ 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

View File

@ -90,9 +90,6 @@ dns-over-tls = []
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"]

View File

@ -22,7 +22,7 @@ This a named implementation for DNS zone hosting. It is capable of performing si
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 only 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 valid DoT or DoH resolvers being registered in order to be used.
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.

View File

@ -311,7 +311,7 @@ fn test_forward() {
)
.unwrap();
assert_eq!(
*response.answers()[0].data().and_then(RData::as_a).unwrap(),
*response.answers()[0].data().as_a().unwrap(),
A::new(93, 184, 215, 14)
);
@ -334,7 +334,7 @@ fn test_forward() {
)
.unwrap();
assert_eq!(
*response.answers()[0].data().and_then(RData::as_a).unwrap(),
*response.answers()[0].data().as_a().unwrap(),
A::new(93, 184, 215, 14)
);
assert!(!response.header().authoritative());

View File

@ -248,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 Some(RData::A(ref address)) = record.data() {
if let RData::A(ref address) = record.data() {
assert_eq!(address, &A::new(127, 0, 0, 1))
} else {
panic!("wrong RDATA")
@ -296,7 +296,7 @@ pub fn query_all_dnssec(
let dnskey = response
.answers()
.iter()
.filter_map(Record::data)
.map(Record::data)
.filter_map(DNSKEY::try_borrow)
.find(|d| d.algorithm() == algorithm);
assert!(dnskey.is_some(), "DNSKEY not found");
@ -306,7 +306,7 @@ pub fn query_all_dnssec(
let rrsig = response
.answers()
.iter()
.filter_map(Record::data)
.map(Record::data)
.filter_map(RRSIG::try_borrow)
.filter(|rrsig| rrsig.algorithm() == algorithm)
.find(|rrsig| rrsig.type_covered() == RecordType::DNSKEY);

View File

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

1
conformance/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

734
conformance/Cargo.lock generated Normal file
View File

@ -0,0 +1,734 @@
# 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 = "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",
"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 = "dns-test"
version = "0.1.0"
dependencies = [
"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 = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[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 = "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",
"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 = "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 = "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 = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "url"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[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 = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"

3
conformance/Cargo.toml Normal file
View File

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

167
conformance/README.md Normal file
View File

@ -0,0 +1,167 @@
# `dnssec-tests`
This repository 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 repository 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 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.
### 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

@ -0,0 +1,13 @@
[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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,64 @@
use dns_test::client::{Client, DigSettings};
use dns_test::name_server::NameServer;
use dns_test::record::{Record, RecordType};
use dns_test::{Network, Result, FQDN};
#[test]
#[ignore]
fn rrsig_in_answer_section() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)?
.sign()?
.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]
#[ignore]
fn rrsig_in_authority_section() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)?
.sign()?
.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

@ -0,0 +1,329 @@
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::{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]
#[ignore]
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]
#[ignore]
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,
)?;
// 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]
#[ignore]
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,
)?;
// 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]
#[ignore]
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,
)?;
// 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,
)?;
// 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]
#[ignore]
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,
)?;
// 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 ns = ns.sign()?;
let nsec3_rrs = NSEC3Records::new(ns.signed_zone_file());
let ns = ns.start()?;
let client = Client::new(&network)?;
let output = client.dig(
*DigSettings::default().dnssec().authentic_data(),
ns.ipv4_addr(),
qtype,
qname,
)?;
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

@ -0,0 +1,23 @@
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

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

View File

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

View File

@ -0,0 +1,68 @@
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::{Network, Resolver, Result, FQDN};
#[test]
fn can_resolve() -> Result<()> {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle_fqdn = FQDN("example.nameservers.com.")?;
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &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(&dns_test::SUBJECT)?;
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(())
}
#[ignore]
#[test]
fn nxdomain() -> Result<()> {
let needle_fqdn = FQDN("unicorn.nameservers.com.")?;
let network = Network::new()?;
let leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?;
let Graph {
nameservers: _nameservers,
root,
..
} = Graph::build(leaf_ns, Sign::No)?;
let resolver = Resolver::new(&network, root).start(&dns_test::SUBJECT)?;
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(())
}

View File

@ -0,0 +1,5 @@
//! DNSSEC functionality
mod fixtures;
mod rfc4035;
mod scenarios;

View File

@ -0,0 +1,75 @@
use std::net::Ipv4Addr;
use base64::prelude::*;
use dns_test::{
name_server::{Graph, NameServer, Running, Sign},
record::Record,
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::NAMESERVERS), leaf_fqdn.parent());
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?;
leaf_ns.add(Record::a(leaf_fqdn.clone(), leaf_ipv4_addr));
let graph = Graph::build(
leaf_ns,
Sign::AndAmend(&|zone, records| {
if zone == &FQDN::NAMESERVERS {
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(&dns_test::SUBJECT)?;
Ok((resolver, graph))
}
pub fn minimally_secure(
leaf_fqdn: FQDN,
leaf_ipv4_addr: Ipv4Addr,
) -> Result<(Resolver, Vec<NameServer<Running>>, TrustAnchor)> {
assert_eq!(Some(FQDN::NAMESERVERS), leaf_fqdn.parent());
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?;
leaf_ns.add(Record::a(leaf_fqdn.clone(), leaf_ipv4_addr));
let Graph {
nameservers,
root,
trust_anchor,
} = Graph::build(leaf_ns, Sign::Yes)?;
let trust_anchor = trust_anchor.unwrap();
let resolver = Resolver::new(&network, root)
.trust_anchor(&trust_anchor)
.start(&dns_test::SUBJECT)?;
Ok((resolver, nameservers, trust_anchor))
}

View File

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

View File

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

View File

@ -0,0 +1,76 @@
use dns_test::{
client::{Client, DigSettings},
name_server::{Graph, NameServer, Sign},
record::RecordType,
tshark::{Capture, Direction},
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::NAMESERVERS, &network)?;
let Graph {
nameservers,
root,
trust_anchor,
} = Graph::build(leaf_ns, Sign::Yes)?;
let mut com_ns_addr = None;
for nameserver in &nameservers {
if nameserver.zone() == &FQDN::COM {
com_ns_addr = Some(nameserver.ipv4_addr());
}
}
let com_ns_addr = com_ns_addr.expect("com. NS not found");
let trust_anchor = &trust_anchor.unwrap();
let resolver = Resolver::new(&network, root)
.trust_anchor(trust_anchor)
.start(&dns_test::SUBJECT)?;
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::NAMESERVERS)?;
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::NAMESERVERS);
// check that DS query was forwarded to the `com.` (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") {
assert!(query.contains("nameservers.com"));
assert_eq!(com_ns_addr, destination);
outgoing_ds_query_count += 1;
}
}
}
}
}
assert_eq!(1, outgoing_ds_query_count);
Ok(())
}

View File

@ -0,0 +1,122 @@
mod section_3_2_2;
use dns_test::{
client::{Client, DigSettings},
name_server::NameServer,
record::{Record, RecordType},
tshark::{Capture, Direction},
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()?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start(&dns_test::SUBJECT)?;
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(())
}
#[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()?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start(&dns_test::SUBJECT)?;
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()?
.start()?;
let resolver = Resolver::new(network, ns.root_hint()).start(&dns_test::SUBJECT)?;
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

@ -0,0 +1,55 @@
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(&dns_test::SUBJECT)?;
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.nameservers.com.")?;
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(())
}

View File

@ -0,0 +1,2 @@
mod section_4_1;
mod section_4_6;

View File

@ -0,0 +1,46 @@
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(&dns_test::SUBJECT)?;
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

@ -0,0 +1,70 @@
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.nameservers.com.")?;
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

@ -0,0 +1,3 @@
mod bogus;
mod ede;
mod secure;

View File

@ -0,0 +1,29 @@
use std::net::Ipv4Addr;
use dns_test::client::{Client, DigSettings};
use dns_test::record::RecordType;
use dns_test::{Result, FQDN};
use crate::resolver::dnssec::fixtures;
// TODO find out which RFC section states this
#[ignore]
#[test]
fn if_cd_bit_is_clear_and_data_is_not_authentic_then_respond_with_servfail() -> Result<()> {
let needle_fqdn = FQDN("example.nameservers.com.")?;
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

@ -0,0 +1,171 @@
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::{Network, Resolver, Result, FQDN};
#[ignore]
#[test]
fn dnskey_missing() -> Result<()> {
fixture(
ExtendedDnsError::DnskeyMissing,
|_needle_fqdn, zone, records| {
if zone == &FQDN::NAMESERVERS {
// 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::NAMESERVERS {
// 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::NAMESERVERS {
// 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::NAMESERVERS {
// 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 {
rrsig.signature_expiration = rrsig.signature_inception - 1;
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.nameservers.com.")?;
let network = Network::new()?;
let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &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(&|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(subject)?;
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_eq!(Some(expected), output.ede);
}
Ok(())
}

View File

@ -0,0 +1,79 @@
use std::net::Ipv4Addr;
use dns_test::client::{Client, DigSettings};
use dns_test::name_server::NameServer;
use dns_test::record::RecordType;
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
#[ignore]
#[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()?;
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(&dns_test::SUBJECT)?;
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);
let output = client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, trust_anchor)?;
assert!(output.starts_with("; fully validated"));
Ok(())
}
#[ignore]
#[test]
fn can_validate_with_delegation() -> Result<()> {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle_fqdn = FQDN("example.nameservers.com.")?;
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);
let output = client.delv(resolver_addr, RecordType::A, &needle_fqdn, &trust_anchor)?;
assert!(output.starts_with("; fully validated"));
Ok(())
}
// TODO nxdomain with NSEC records
// TODO nxdomain with NSEC3 records

View File

@ -0,0 +1,22 @@
[package]
edition = "2021"
license = "MIT OR Apache-2.0"
name = "dns-test"
publish = false
version = "0.1.0"
[dependencies]
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

@ -0,0 +1,132 @@
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::{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("mydomain.com.")?, &network)?;
println!("DONE");
println!("setting up name servers...");
let sign = if args.dnssec { Sign::Yes } 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::new(127, 0, 0, 1),
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(&dns_test::SUBJECT)?;
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_id()
);
}
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_id()
);
println!("client's IP address: {}", client.ipv4_addr());
println!(
"attach to this container with: `docker exec -it {} bash`\n\n",
client.container_id()
);
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

@ -0,0 +1,470 @@
use core::str::FromStr;
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 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(),
&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,
}
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"
}
}
}
#[derive(Debug)]
pub struct DigOutput {
pub ede: Option<ExtendedDnsError>,
pub flags: DigFlags,
pub status: DigStatus,
pub answer: Vec<Record>,
pub authority: Vec<Record>,
// 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 ANSWER_HEADER: &str = ";; ANSWER SECTION:";
const AUTHORITY_HEADER: &str = ";; AUTHORITY 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 ede = None;
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);
if ede.is_some() {
return Err(more_than_once(EDE_PREFIX).into());
}
ede = Some(code.parse()?);
} 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);
}
}
Ok(Self {
answer: answer.unwrap_or_default(),
authority: authority.unwrap_or_default(),
ede,
flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?,
status: status.ok_or_else(|| not_found(STATUS_PREFIX))?,
})
}
}
#[derive(Debug, PartialEq)]
pub enum ExtendedDnsError {
DnskeyMissing,
DnssecBogus,
RrsigsMissing,
UnsupportedDnskeyAlgorithm,
}
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,
_ => 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 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_eq!(Some(ExtendedDnsError::DnskeyMissing), output.ede);
Ok(())
}
}

View File

@ -0,0 +1,443 @@
mod network;
use core::{fmt, str};
use std::net::Ipv4Addr;
use std::process::{self, 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, 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,
Client,
Hickory(Repository<'static>),
Unbound,
}
impl Image {
fn dockerfile(&self) -> &'static str {
match self {
Self::Bind => include_str!("docker/bind.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::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::Unbound => Self::Unbound,
Implementation::Hickory(repo) => Self::Hickory(repo),
}
}
}
impl fmt::Display for Image {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Client => "client",
Self::Bind => "bind",
Self::Hickory { .. } => "hickory",
Self::Unbound => "unbound",
};
f.write_str(s)
}
}
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);
let repo = if let Image::Hickory(repo) = image {
Some(repo)
} else {
None
};
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,
"-it",
])
.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", "-t", &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", "-t", &self.inner.id])
.args(command_and_args);
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: &[&str]) -> Result<Child> {
let mut command = Command::new("docker");
command.stdout(Stdio::piped()).stderr(Stdio::piped());
command.args(["exec", "-t", &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
}
}
fn verbose_docker_build() -> bool {
env::var("DNS_TEST_VERBOSE_DOCKER_BUILD").as_deref().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")?)
}
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(|c| matches!(c, '\n' | '\r')) {
stderr.pop();
}
let mut stdout = String::from_utf8(output.stdout)?;
while stdout.ends_with(|c| matches!(c, '\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

@ -0,0 +1,156 @@
use std::{
process::{self, Command, Stdio},
sync::{
atomic::{self, AtomicUsize},
Arc,
},
};
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)?)))
}
}
/// 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) -> Result<Self> {
let count = network_count();
let network_name = format!("{network_name}-{pid}-{count}");
let mut command = Command::new("docker");
command
.args(["network", "create"])
.args(["--internal", "--attachable"])
.arg(&network_name);
// create network
let output = 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

@ -0,0 +1,10 @@
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 \
tshark && \
rm -f /etc/bind/*

View File

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

View File

@ -0,0 +1,15 @@
FROM rust:1-slim-bookworm
# ldns-utils = ldns-{key2ds,keygen,signzone}
RUN apt-get update && \
apt-get install -y \
ldnsutils \
tshark
# `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 `dns-test` repository
COPY ./src /usr/src/hickory
RUN cargo install --path /usr/src/hickory/bin --features recursor,dnssec-ring --debug && \
mkdir /etc/hickory
env RUST_LOG=debug

View File

@ -0,0 +1,9 @@
FROM debian:bookworm-slim
# ldns-utils = ldns-{key2ds,keygen,signzone}
RUN apt-get update && \
apt-get install -y \
ldnsutils \
nsd \
tshark \
unbound

View File

@ -0,0 +1,139 @@
use core::fmt;
use core::str::FromStr;
use std::borrow::Cow;
use crate::{Error, Result};
#[derive(Clone, 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 COM: FQDN = FQDN {
inner: Cow::Borrowed("com."),
};
pub const NAMESERVERS: FQDN = FQDN {
inner: Cow::Borrowed("nameservers.com."),
};
pub fn is_root(&self) -> bool {
self.inner == "."
}
pub fn as_str(&self) -> &str {
&self.inner
}
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.nameservers.com.")?;
assert_eq!(3, fqdn.num_labels());
let parent = fqdn.parent();
assert_eq!(
Some("nameservers.com."),
parent.as_ref().map(|fqdn| fqdn.as_str())
);
fqdn = parent.unwrap();
assert_eq!(2, fqdn.num_labels());
let parent = fqdn.parent();
assert_eq!(Some(FQDN::COM), 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

@ -0,0 +1,211 @@
use core::fmt;
use std::borrow::Cow;
use std::path::Path;
use url::Url;
use crate::FQDN;
#[derive(Clone, Copy)]
pub enum Config<'a> {
NameServer {
origin: &'a FQDN,
},
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,
Hickory(Repository<'static>),
Unbound,
}
impl Implementation {
pub fn supports_ede(&self) -> bool {
match self {
Implementation::Bind => false,
Implementation::Hickory(_) => true,
Implementation::Unbound => true,
}
}
#[must_use]
pub fn is_bind(&self) -> bool {
matches!(self, Self::Bind)
}
#[must_use]
pub fn is_hickory(&self) -> bool {
matches!(self, Self::Hickory(_))
}
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::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 } => match self {
Self::Bind => {
minijinja::render!(
include_str!("templates/named.name-server.conf.jinja"),
fqdn => origin.as_str()
)
}
Self::Unbound => {
minijinja::render!(
include_str!("templates/nsd.conf.jinja"),
fqdn => origin.as_str()
)
}
Self::Hickory(_) => {
minijinja::render!(
include_str!("templates/hickory.name-server.toml.jinja"),
fqdn => origin.as_str()
)
}
},
}
}
pub(crate) fn conf_file_path(&self, role: Role) -> &'static str {
match self {
Self::Bind => "/etc/bind/named.conf",
Self::Hickory(_) => "/etc/named.toml",
Self::Unbound => match role {
Role::NameServer => "/etc/nsd/nsd.conf",
Role::Resolver => "/etc/unbound/unbound.conf",
},
}
}
pub(crate) fn cmd_args(&self, role: Role) -> &[&'static str] {
match self {
Implementation::Bind => &["named", "-g", "-d5"],
Implementation::Hickory(_) => &[
"sh",
"-c",
"echo $$ > /tmp/hickory.pid
exec hickory-dns -d",
],
Implementation::Unbound => match role {
Role::NameServer => &["nsd", "-d"],
Role::Resolver => &["unbound", "-d"],
},
}
}
pub(crate) fn pidfile(&self, role: Role) -> &'static str {
match self {
Implementation::Bind => "/tmp/named.pid",
Implementation::Hickory(_) => "/tmp/hickory.pid",
Implementation::Unbound => match role {
Role::NameServer => "/tmp/nsd.pid",
Role::Resolver => "/tmp/unbound.pid",
},
}
}
}
impl fmt::Display for Implementation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Implementation::Bind => "bind",
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

@ -0,0 +1,114 @@
//! A test framework for all things DNS
use std::env;
use lazy_static::lazy_static;
pub use crate::container::Network;
pub use crate::fqdn::FQDN;
pub use crate::implementation::{Implementation, Repository};
pub use crate::resolver::Resolver;
pub use crate::trust_anchor::TrustAnchor;
pub mod client;
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();
}
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") {
if let Some(url) = subject.strip_prefix("hickory ") {
Implementation::Hickory(Repository(url.to_string()))
} else {
panic!("the syntax of DNS_TEST_SUBJECT is 'hickory $URL', e.g. 'hickory /tmp/hickory' or 'hickory https://github.com/owner/repo'")
}
} 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()
}
}
#[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

@ -0,0 +1,611 @@
use core::sync::atomic::{self, AtomicUsize};
use std::net::Ipv4Addr;
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::{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,
/// Signs the zone files and then modifies the records produced by the signing process
AndAmend(&'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::COM), 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::NAMESERVERS {
let nameservers_ns = NameServer::new(&implementation, FQDN::NAMESERVERS, &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 maybe_mutate = match sign {
Sign::No => unreachable!(),
Sign::Yes => None,
Sign::AndAmend(f) => Some(f),
};
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()?;
children_ds.push(nameserver.ds().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,
}
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,
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
}
/// Freezes and signs the name server's zone file
pub fn sign(self) -> Result<NameServer<Signed>> {
// TODO do we want to make these settings configurable?
const ZSK_BITS: usize = 1024;
const KSK_BITS: usize = 2048;
const ALGORITHM: &str = "RSASHA1-NSEC3-SHA1";
let Self {
container,
zone_file,
implementation,
state: _,
} = self;
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
let zone_file_path = zone_file_path();
container.cp(&zone_file_path, &zone_file.to_string())?;
let zone = zone_file.origin();
let zsk_keygen =
format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}");
let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?;
let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key");
let zsk: zone_file::DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?;
let ksk_keygen =
format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}");
let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?;
let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key");
let ksk: zone_file::DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?;
// -n = use NSEC3 instead of NSEC
// -p = set the opt-out flag on all nsec3 rrs
let signzone = format!(
"cd {ZONES_DIR} && ldns-signzone -n -p {ZONE_FILENAME} {zsk_filename} {ksk_filename}"
);
container.status_ok(&["sh", "-c", &signzone])?;
// TODO do we want to make the hashing algorithm configurable?
// -2 = use SHA256 for the DS hash
let key2ds = format!("cd {ZONES_DIR} && ldns-key2ds -n -2 {ZONE_FILENAME}.signed");
let ds: DS = container.stdout(&["sh", "-c", &key2ds])?.parse()?;
let signed: ZoneFile = container
.stdout(&["cat", &format!("{zone_file_path}.signed")])?
.parse()?;
let ttl = zone_file.soa.ttl;
Ok(NameServer {
container,
implementation,
zone_file,
state: Signed {
ds,
signed,
// inherit SOA's TTL value
ksk: ksk.with_ttl(ttl),
zsk: zsk.with_ttl(ttl),
},
})
}
/// 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,
state: _,
} = self;
let config = Config::NameServer {
origin: zone_file.origin(),
};
container.cp(
implementation.conf_file_path(config.role()),
&implementation.format_config(config),
)?;
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
container.cp(&zone_file_path(), &zone_file.to_string())?;
let child = container.spawn(implementation.cmd_args(config.role()))?;
Ok(NameServer {
container,
implementation,
zone_file,
state: Running { child },
})
}
}
const ZONES_DIR: &str = "/etc/zones";
const ZONE_FILENAME: &str = "main.zone";
fn zone_file_path() -> String {
format!("{ZONES_DIR}/{ZONE_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,
state,
} = self;
let config = Config::NameServer {
origin: zone_file.origin(),
};
container.cp(
implementation.conf_file_path(config.role()),
&implementation.format_config(config),
)?;
container.cp(&zone_file_path(), &state.signed.to_string())?;
let child = container.spawn(implementation.cmd_args(config.role()))?;
Ok(NameServer {
container,
implementation,
zone_file,
state: Running { child },
})
}
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 ds(&self) -> &DS {
&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()
}
/// gracefully terminates the name server collecting all logs
pub fn terminate(self) -> Result<String> {
let pidfile = self.implementation.pidfile(Role::NameServer);
// if `terminate` is called right after `start` NSD may not have had the chance to create
// the PID file so if it doesn't exist wait for a bit before invoking `kill`
let kill = format!(
"test -f {pidfile} || sleep 1
kill -TERM $(cat {pidfile})"
);
self.container.status_ok(&["sh", "-c", &kill])?;
let output = self.state.child.wait()?;
// the hickory-dns binary does not do signal handling so it won't shut down gracefully; we
// will still get some logs so we'll ignore the fact that it fails to shut down ...
let is_hickory = matches!(self.implementation, Implementation::Hickory(_));
if !is_hickory && !output.status.success() {
return Err(
format!("could not terminate the `{}` process", self.implementation).into(),
);
}
assert!(
output.stderr.is_empty(),
"stderr should be returned if not empty"
);
Ok(output.stdout)
}
}
impl<S> NameServer<S> {
pub fn container_id(&self) -> &str {
self.container.id()
}
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;
pub struct Signed {
ds: DS,
zsk: record::DNSKEY,
ksk: record::DNSKEY,
signed: ZoneFile,
}
pub struct Running {
child: Child,
}
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 {
"nameservers.com.".to_string()
} else if zone.num_labels() == 1 {
format!("nameservers.{}", zone.as_str())
} else {
zone.to_string()
}
}
#[cfg(test)]
mod tests {
use std::thread;
use std::time::Duration;
use crate::client::{Client, DigSettings};
use crate::record::RecordType;
use crate::Repository;
use super::*;
#[test]
fn simplest() -> Result<()> {
let network = Network::new()?;
let tld_ns = NameServer::new(&Implementation::Unbound, FQDN::COM, &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::COM)?;
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::COM,
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::COM,
)?;
assert!(output.status.is_noerror());
Ok(())
}
#[test]
fn signed() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.sign()?;
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 terminate_nsd_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?;
let logs = ns.terminate()?;
assert!(logs.contains("nsd starting"));
Ok(())
}
#[test]
fn terminate_named_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Bind, FQDN::ROOT, &network)?.start()?;
let logs = ns.terminate()?;
eprintln!("{logs}");
assert!(logs.contains("starting BIND"));
Ok(())
}
#[test]
fn terminate_hickory_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(
&Implementation::Hickory(Repository("https://github.com/hickory-dns/hickory-dns")),
FQDN::ROOT,
&network,
)?
.start()?;
// hickory-dns does not do signal handling so we need to wait until it prints something to
// the console
thread::sleep(Duration::from_millis(500));
let logs = ns.terminate()?;
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

@ -0,0 +1,66 @@
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

@ -0,0 +1,222 @@
use core::fmt::Write;
use std::io::{BufRead, BufReader};
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(),
}
}
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 ipv4_addr(&self) -> Ipv4Addr {
self.container.ipv4_addr()
}
/// Gracefully terminates the name server collecting all logs
pub fn terminate(self) -> Result<String> {
let Resolver {
implementation,
container,
child,
} = self;
let pidfile = implementation.pidfile(Role::Resolver);
let kill = format!(
"test -f {pidfile} || sleep 1
kill -TERM $(cat {pidfile})"
);
container.status_ok(&["sh", "-c", &kill])?;
let output = child.wait()?;
// the hickory-dns binary does not do signal handling so it won't shut down gracefully; we
// will still get some logs so we'll ignore the fact that it fails to shut down ...
if !implementation.is_hickory() && !output.status.success() {
return Err(format!("could not terminate the `{}` process", implementation).into());
}
assert!(
output.stderr.is_empty(),
"stderr should be returned if not empty"
);
Ok(output.stdout)
}
}
pub struct ResolverSettings {
/// Extended DNS Errors (RFC8914)
ede: bool,
network: Network,
roots: Vec<Root>,
trust_anchor: TrustAnchor,
}
impl ResolverSettings {
/// 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(&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 = Config::Resolver {
use_dnssec,
netmask: self.network.netmask(),
ede: self.ede,
};
container.cp(
implementation.conf_file_path(config.role()),
&implementation.format_config(config),
)?;
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 mut child = container.spawn(implementation.cmd_args(config.role()))?;
// For HickoryDNS we need to wait until its start sequence finished. Only then the server is able
// to accept connections. The start sequence logs are consumed here.
if implementation.is_hickory() {
let stdout = child.stdout()?;
let lines = BufReader::new(stdout).lines();
for line in lines {
let line = line?;
if line.contains("Server starting up") {
break;
}
}
}
Ok(Resolver {
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
}
}
#[cfg(test)]
mod tests {
use crate::{name_server::NameServer, Repository, FQDN};
use super::*;
#[test]
fn terminate_unbound_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(&Implementation::Unbound)?;
let logs = resolver.terminate()?;
eprintln!("{logs}");
assert!(logs.contains("start of service"));
Ok(())
}
#[test]
fn terminate_bind_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(&Implementation::Bind)?;
let logs = resolver.terminate()?;
eprintln!("{logs}");
assert!(logs.contains("starting BIND"));
Ok(())
}
#[test]
fn terminate_hickory_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(&Implementation::Hickory(
Repository("https://github.com/hickory-dns/hickory-dns"),
))?;
let logs = resolver.terminate()?;
// Hickory-DNS start sequence log has been consumed in `ResolverSettings.start`.
assert!(logs.is_empty());
Ok(())
}
}

View File

@ -0,0 +1,4 @@
[[zones]]
zone = "{{ fqdn }}"
zone_type = "Primary"
file = "/etc/zones/main.zone"

View File

@ -0,0 +1,5 @@
[[zones]]
zone = "."
zone_type = "Hint"
stores = { type = "recursor", roots = "/etc/root.hints", security_aware = true }
enable_dnssec = {{ use_dnssec }}

View File

@ -0,0 +1,14 @@
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";
};

View File

@ -0,0 +1,14 @@
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

@ -0,0 +1,9 @@
server:
pidfile: /tmp/nsd.pid
remote-control:
control-enable: no
zone:
name: {{ fqdn }}
zonefile: /etc/zones/main.zone

View File

@ -0,0 +1,14 @@
server:
verbosity: 4
use-syslog: no
interface: 0.0.0.0
access-control: {{ netmask }} allow
root-hints: /etc/root.hints
pidfile: /tmp/unbound.pid
ede: {% if ede %} yes {% else %} no {% endif %}
{% if use_dnssec %}
trust-anchor-file: /etc/trusted-key.key
{% endif %}
remote-control:
control-enable: no

View File

@ -0,0 +1,55 @@
use core::fmt;
use crate::record::DNSKEY;
pub struct TrustAnchor {
keys: Vec<DNSKEY>,
}
impl TrustAnchor {
pub fn empty() -> Self {
Self { keys: Vec::new() }
}
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

@ -0,0 +1,391 @@
//! `tshark` JSON output parser
use core::result::Result as CoreResult;
use std::io::{BufRead, BufReader, Lines};
use std::net::Ipv4Addr;
use std::process::ChildStdout;
use std::sync::atomic::{self, AtomicUsize};
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 capture_file = capture_file(id);
// `docker exec $child` merges the child's stderr and stdout streams and pipes them into
// its stdout. as we cannot tell stdout (JSON) from stderr (log message) from the host side,
// we'll redirect the JSON output to a file inside the container and read the log messages
// from the host side
// --log-level info --log-domain main
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}' > {capture_file}"
);
let mut child = self.spawn(&["sh", "-c", &tshark])?;
let stdout = child.stdout()?;
let mut stdout = BufReader::new(stdout).lines();
for res in stdout.by_ref() {
let line = res?;
if line.contains("Capture started") {
break;
}
}
Ok(Tshark {
container: self.clone(),
child,
stdout,
id,
})
}
}
fn pid_file(id: usize) -> String {
format!("/tmp/tshark{id}.pid")
}
fn capture_file(id: usize) -> String {
format!("/tmp/tshark{id}.json")
}
pub struct Tshark {
child: Child,
container: Container,
id: usize,
stdout: Lines<BufReader<ChildStdout>>,
}
impl Tshark {
/// 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.stdout.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.stdout {
res?;
}
let capture_file = capture_file(self.id);
let output = self.container.stdout(&["cat", &capture_file])?;
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
}
}
#[derive(Clone, Copy, Debug)]
pub enum Direction {
Incoming { source: Ipv4Addr },
Outgoing { destination: Ipv4Addr },
}
impl Direction {
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());
let captured = tshark.wait_for_capture()?;
assert_eq!(2, captured);
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::COM, network)?;
let mut nameservers_ns =
NameServer::new(&Implementation::Unbound, FQDN("nameservers.com.")?, 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(&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

@ -0,0 +1,227 @@
//! 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, Record, SOA};
use crate::{Error, Result, DEFAULT_TTL, FQDN};
#[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())
}
/// 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 line = line.trim();
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,
}
}
}
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,
flags: u16,
protocol: u8,
algorithm: u8,
public_key: String,
}
impl DNSKEY {
pub fn with_ttl(self, ttl: u32) -> record::DNSKEY {
let Self {
zone,
flags,
protocol,
algorithm,
public_key,
} = self;
record::DNSKEY {
zone,
ttl,
flags,
protocol,
algorithm,
public_key,
}
}
}
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()?,
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,
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(())
}
}

View File

@ -0,0 +1,11 @@
. 86400 IN SOA primary0.nameservers.com. admin0.nameservers.com. 2024022028 1800 900 604800 86400
. 86400 IN RRSIG SOA 7 0 86400 20240319104519 20240220104519 11387 . Ks9b5tMyNxxrvw3JkgGkR2H5NPqTDwAwmwh3B7iNC0UHAYGU4B01ZJHj DIsJqDoJ2hsKG5oq0hQuwBSKv2nSBA1oSQcNrBDzOk105gu6tsXg2O8V ZCpAtEColco5ziOX8AWRqRMM5adSfA4xyj5H3NToMjRVDLpVpZsU4BAa 4dU=
. 86400 IN NS primary0.nameservers.com.
. 86400 IN RRSIG NS 7 0 86400 20240319104519 20240220104519 11387 . rZpACeVX3m2CwI/gY/rVYNOAs6ge4h+M74yV+CoAZYJaJLjeHd+jY0YV ixU3hap9bbFCZqhXKU5WSpJSsc/9PrgxEt2XycpbvAJwvIwdqWLUW741 /AOwnyrgv+7PLp4vkDdeLI9tcsY5V/ABpQrYW2i8Gtz90OEpvEEd5+4C LyU=
. 86400 IN DNSKEY 256 3 7 AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQ iakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJP QtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nu hfWE4w15
. 86400 IN DNSKEY 257 3 7 AwEAAco2Ck4XM5M4RO+QiwZhMFW9Hf8s0cOWH6QZ8OUQisjP6n+gYsbE pOOHhRiABN+QuVhRK9BN+Mt0LqMSBjSy53t5P3NerckqUTQ4HlZkn2QK bhc+TOgvHN5iDj0RBMkTaJ09y5vYmeNv5npFk6hV+VsbBoFRLXTuPSms 8LsH72W6y1HEHNzvAd5H3ro1d2awp66CXRTOcbXbFAIELpTgAU6ZJjEo RBMASZ3Ug4oZ96yvegy2OZnAyFsxBGdOvecs+zoYKeezqaq21YMpnZkf eE7RYexGPm1p8/7smQjBph/uoVDp5k5DuPkTmzpafVOn2YHGB395vT37 uLi9B5Oef9c=
. 86400 IN RRSIG DNSKEY 7 0 86400 20240319104519 20240220104519 11245 . yH/aEcWQhgfmf8RjByMYDDuglaquWsHECA+nRmedIA4Kz7Vc74f77JLi QrhvFFSIFkQNyixNsTugLmTZunphbLrbQNKTWw8gpgd/8u6Oc9OdTYJu T+ADL+Rrgge7mDkPjDRKNhQ6VkIiRzwLBFhoYTA1LZF98CAnJGQcpw4W 1YCkqPbXIzsa3hq2OajC8NzZMEgeI95N1CJ/o5AmhwLtWVuv04q+seGX roSiTlWIQRKGsbCR2v97UjMG4l9XIbijzZbY4dK2/4WrCIw9mjp/cSE8 r/AdfegTi1oqOM9i4QebKvyU9c3rnJRVbFMhEXL1e0M/5bZNytXp43ex VTTHcA==
. 3600 IN NSEC3PARAM 1 0 1 -
. 3600 IN RRSIG NSEC3PARAM 7 0 3600 20240319104519 20240220104519 11387 . IhM+g5s6DwlFKbj6+zd+f/CqN1I4/QtF0aTOMvf0c+s5l+emx/yZEVCT 8LdX4cmz72eYeC4w/dM2btrhhHohhb/hdK1v7ukxtBVgvk6pOmuye2/E cuGkll7B59l+wlRaSmeXAQjiCUX6gyg9tlvmtcnomWVgtjIgMKJpggy8 B6k=
fasdp12mo9fh69ahu5bseugoh3np33tc. 86400 IN NSEC3 1 1 1 - fasdp12mo9fh69ahu5bseugoh3np33tc NS SOA RRSIG DNSKEY NSEC3PARAM
fasdp12mo9fh69ahu5bseugoh3np33tc. 86400 IN RRSIG NSEC3 7 1 86400 20240319104519 20240220104519 11387 . dsdwsTOGL5BvrC1v/5bmDy5Bz8wnN/IG3XRAg6RqKVMK0fLPMsd5uhXm U2gPJ5xUg1RkBQ5+etlBRm2p7vSDjbMa/hjRbvUJgP+c4dL68g+FcHv4 v9fb1Jaao9Goy/ZxZ1dbwXAdxhi+pyvikCdNcKsdiCtFD9pX7V5Nh2Cc GQQ=

View File

@ -27,7 +27,7 @@ use std::str::FromStr;
use hickory_client::client::{Client, SyncClient};
use hickory_client::udp::UdpClientConnection;
use hickory_client::op::DnsResponse;
use hickory_client::rr::{DNSClass, Name, RData, Record, RecordType};
use hickory_client::rr::{rdata::A, DNSClass, Name, RData, Record, RecordType};
let address = "8.8.8.8:53".parse().unwrap();
let conn = UdpClientConnection::new(address).unwrap();
@ -49,7 +49,7 @@ let answers: &[Record] = response.answers();
// Records are generic objects which can contain any data.
// In order to access it we need to first check what type of record it is
// In this case we are interested in A, IPv4 address
if let Some(RData::A(ref ip)) = answers[0].data() {
if let Some(RData::A(A(ref ip))) = answers[0].data() {
assert_eq!(*ip, Ipv4Addr::new(93, 184, 215, 14))
} else {
assert!(false, "unexpected result")

View File

@ -34,7 +34,7 @@ use crate::{
},
TokioTime,
},
rr::{rdata::SOA, DNSClass, Name, RData, Record, RecordSet, RecordType},
rr::{rdata::SOA, DNSClass, Name, Record, RecordSet, RecordType},
};
/// A DNS Client implemented over futures-rs.
@ -709,7 +709,7 @@ impl<R> ClientStreamXfrState<R> {
fn process(&mut self, answers: &[Record]) -> Result<(), ClientError> {
use ClientStreamXfrState::*;
fn get_serial(r: &Record) -> Option<u32> {
r.data().and_then(RData::as_soa).map(SOA::serial)
r.data().as_soa().map(SOA::serial)
}
if answers.is_empty() {
@ -864,6 +864,7 @@ mod tests {
use crate::rr::rdata::{A, SOA};
use futures_util::stream::iter;
use hickory_proto::rr::RData;
use ClientStreamXfrState::*;
fn soa_record(serial: u32) -> Record {
@ -1116,7 +1117,7 @@ mod tests {
let (message_returned, buffer) = query.await.unwrap().into_parts();
// validate it's what we expected
if let Some(RData::A(addr)) = message_returned.answers()[0].data() {
if let RData::A(addr) = message_returned.answers()[0].data() {
assert_eq!(*addr, A::new(93, 184, 215, 14));
}
@ -1124,7 +1125,7 @@ mod tests {
.expect("buffer was parsed already by AsyncClient so we should be able to do it again");
// validate it's what we expected
if let Some(RData::A(addr)) = message_parsed.answers()[0].data() {
if let RData::A(addr) = message_parsed.answers()[0].data() {
assert_eq!(*addr, A::new(93, 184, 215, 14));
}
}

View File

@ -4,7 +4,6 @@
// 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.
#![cfg(feature = "dnssec")]
use std::future::Future;
use std::pin::Pin;
@ -108,11 +107,7 @@ where
pub async fn build(
mut self,
) -> Result<(AsyncDnssecClient, DnsExchangeBackground<S, TokioTime>), ProtoError> {
let trust_anchor = if let Some(trust_anchor) = self.trust_anchor.take() {
trust_anchor
} else {
TrustAnchor::default()
};
let trust_anchor = self.trust_anchor.take().unwrap_or_default();
let result = AsyncClient::connect(self.connect_future).await;
result.map(|(client, bg)| (AsyncDnssecClient::from_client(client, trust_anchor), bg))

View File

@ -128,7 +128,7 @@
//! // Records are generic objects which can contain any data.
//! // In order to access it we need to first check what type of record it is
//! // In this case we are interested in A, IPv4 address
//! if let Some(RData::A(ref ip)) = answers[0].data() {
//! if let RData::A(ref ip) = answers[0].data() {
//! assert_eq!(*ip, A::new(93, 184, 215, 14))
//! } else {
//! assert!(false, "unexpected result")
@ -263,7 +263,7 @@
//! let response = query.await.unwrap();
//!
//! // validate it's what we expected
//! if let Some(RData::A(addr)) = response.answers()[0].data() {
//! if let RData::A(addr) = response.answers()[0].data() {
//! assert_eq!(*addr, A::new(93, 184, 215, 14));
//! }
//! }

View File

@ -73,9 +73,6 @@ serde-config = ["serde", "url/serde"]
# enables experimental the mDNS (multicast) feature
mdns = ["socket2/all"]
# WARNING: there is a bug in the mutual tls auth code at the moment see issue #100
# mtls = ["tls"]
wasm-bindgen = ["wasm-bindgen-crate", "js-sys"]
backtrace = ["dep:backtrace"]

View File

@ -381,6 +381,12 @@ impl ProtoError {
matches!(*self.kind, ProtoErrorKind::NoConnections)
}
/// Returns true if this is a std::io::Error
#[inline]
pub fn is_io(&self) -> bool {
matches!(*self.kind, ProtoErrorKind::Io(..))
}
pub(crate) fn as_dyn(&self) -> &(dyn std::error::Error + 'static) {
self
}

View File

@ -540,7 +540,7 @@ mod tests {
use crate::iocompat::AsyncIoTokioAsStd;
use crate::op::{Message, Query, ResponseCode};
use crate::rr::rdata::{A, AAAA};
use crate::rr::{Name, RData, RecordType};
use crate::rr::{Name, RecordType};
use crate::xfer::{DnsRequestOptions, FirstAnswer};
use super::*;
@ -572,10 +572,7 @@ mod tests {
.expect("send_message failed");
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_a)
.expect("Expected A record");
let addr = record.data().as_a().expect("Expected A record");
assert_eq!(addr, &A::new(93, 184, 215, 14));
@ -600,7 +597,7 @@ mod tests {
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_aaaa)
.as_aaaa()
.expect("invalid response, expected A record");
assert_eq!(
@ -637,10 +634,7 @@ mod tests {
.expect("send_message failed");
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_a)
.expect("Expected A record");
let addr = record.data().as_a().expect("Expected A record");
assert_eq!(addr, &A::new(93, 184, 215, 14));
@ -665,7 +659,7 @@ mod tests {
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_aaaa)
.as_aaaa()
.expect("invalid response, expected A record");
assert_eq!(
@ -705,7 +699,7 @@ mod tests {
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_a)
.as_a()
.expect("invalid response, expected A record");
assert_eq!(addr, &A::new(93, 184, 215, 14));
@ -727,7 +721,7 @@ mod tests {
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_aaaa)
.as_aaaa()
.expect("invalid response, expected A record");
assert_eq!(

View File

@ -6,7 +6,7 @@
// copied, modified, or distributed except according to those terms.
use std::fmt::{self, Display};
use std::future::Future;
use std::future::{self, Future};
use std::net::SocketAddr;
use std::pin::Pin;
use std::str::FromStr;
@ -16,12 +16,13 @@ use std::task::{Context, Poll};
use bytes::{Buf, BufMut, Bytes, BytesMut};
use futures_util::future::FutureExt;
use futures_util::stream::Stream;
use h3::client::{Connection, SendRequest};
use h3::client::SendRequest;
use h3_quinn::OpenStreams;
use http::header::{self, CONTENT_LENGTH};
use quinn::{ClientConfig, Endpoint, EndpointConfig, TransportConfig};
use rustls::ClientConfig as TlsClientConfig;
use tracing::debug;
use tokio::sync::mpsc;
use tracing::{debug, warn};
use crate::error::ProtoError;
use crate::http::Version;
@ -34,13 +35,14 @@ use crate::xfer::{DnsRequest, DnsRequestSender, DnsResponse, DnsResponseStream};
use super::ALPN_H3;
/// A DNS client connection for DNS-over-HTTP/3
#[derive(Clone)]
#[must_use = "futures do nothing unless polled"]
pub struct H3ClientStream {
// Corresponds to the dns-name of the HTTP/3 server
name_server_name: Arc<str>,
name_server: SocketAddr,
driver: Connection<h3_quinn::Connection, Bytes>,
send_request: SendRequest<OpenStreams, Bytes>,
shutdown_tx: mpsc::Sender<()>,
is_shutdown: bool,
}
@ -264,19 +266,19 @@ impl DnsRequestSender for H3ClientStream {
impl Stream for H3ClientStream {
type Item = Result<(), ProtoError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.is_shutdown {
return Poll::Ready(None);
}
// just checking if the connection is ok
match self.driver.poll_close(cx) {
Poll::Ready(Ok(())) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(ProtoError::from(format!(
"h3 stream errored: {e}",
))))),
if self.shutdown_tx.is_closed() {
return Poll::Ready(Some(Err(ProtoError::from(
"h3 connection is already shutdown",
))));
}
Poll::Ready(Some(Ok(())))
}
}
@ -398,15 +400,31 @@ impl H3ClientStreamBuilder {
};
let h3_connection = h3_quinn::Connection::new(quic_connection);
let (driver, send_request) = h3::client::new(h3_connection)
let (mut driver, send_request) = h3::client::new(h3_connection)
.await
.map_err(|e| ProtoError::from(format!("h3 connection failed: {e}")))?;
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
// TODO: hand this back for others to run rather than spawning here?
debug!("h3 connection is ready: {}", name_server);
tokio::spawn(async move {
tokio::select! {
res = future::poll_fn(|cx| driver.poll_close(cx)) => {
res.map_err(|e| warn!("h3 connection failed: {e}"))
}
_ = shutdown_rx.recv() => {
debug!("h3 connection is shutting down: {}", name_server);
Ok(())
}
}
});
Ok(H3ClientStream {
name_server_name: Arc::from(dns_name),
name_server,
driver,
send_request,
shutdown_tx,
is_shutdown: false,
})
}
@ -453,10 +471,11 @@ mod tests {
use rustls::KeyLogFile;
use tokio::runtime::Runtime;
use tokio::task::JoinSet;
use crate::op::{Message, Query, ResponseCode};
use crate::rr::rdata::{A, AAAA};
use crate::rr::{Name, RData, RecordType};
use crate::rr::{Name, RecordType};
use crate::xfer::{DnsRequestOptions, FirstAnswer};
use super::*;
@ -488,10 +507,7 @@ mod tests {
.expect("send_message failed");
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_a)
.expect("Expected A record");
let addr = record.data().as_a().expect("Expected A record");
assert_eq!(addr, &A::new(93, 184, 215, 14));
@ -516,7 +532,7 @@ mod tests {
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_aaaa)
.as_aaaa()
.expect("invalid response, expected A record");
assert_eq!(
@ -553,10 +569,7 @@ mod tests {
.expect("send_message failed");
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_a)
.expect("Expected A record");
let addr = record.data().as_a().expect("Expected A record");
assert_eq!(addr, &A::new(93, 184, 215, 14));
@ -581,7 +594,7 @@ mod tests {
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_aaaa)
.as_aaaa()
.expect("invalid response, expected A record");
assert_eq!(
@ -622,7 +635,7 @@ mod tests {
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_a)
.as_a()
.expect("invalid response, expected A record");
assert_eq!(addr, &A::new(93, 184, 215, 14));
@ -644,7 +657,7 @@ mod tests {
let record = &response.answers()[0];
let addr = record
.data()
.and_then(RData::as_aaaa)
.as_aaaa()
.expect("invalid response, expected A record");
assert_eq!(
@ -652,4 +665,56 @@ mod tests {
&AAAA::new(0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c)
);
}
#[test]
#[allow(clippy::print_stdout)]
fn test_h3_client_stream_clonable() {
// use google
let google = SocketAddr::from(([8, 8, 8, 8], 443));
let mut client_config = super::super::client_config_tls13().unwrap();
client_config.key_log = Arc::new(KeyLogFile::new());
let mut h3_builder = H3ClientStream::builder();
h3_builder.crypto_config(client_config);
let connect = h3_builder.build(google, "dns.google".to_string());
// tokio runtime stuff...
let runtime = Runtime::new().expect("could not start runtime");
let h3 = runtime.block_on(connect).expect("h3 connect failed");
// prepare request
let mut request = Message::new();
let query = Query::query(
Name::from_str("www.example.com.").unwrap(),
RecordType::AAAA,
);
request.add_query(query);
let request = DnsRequest::new(request, DnsRequestOptions::default());
runtime.block_on(async move {
let mut join_set = JoinSet::new();
for i in 0..50 {
let mut h3 = h3.clone();
let request = request.clone();
join_set.spawn(async move {
let start = std::time::Instant::now();
h3.send_message(request)
.first_answer()
.await
.expect("send_message failed");
println!("request[{i}] completed: {:?}", start.elapsed());
});
}
let total = join_set.len();
let mut idx = 0usize;
while join_set.join_next().await.is_some() {
println!("join_set completed {idx}/{total}");
idx += 1;
}
});
}
}

View File

@ -387,7 +387,7 @@ impl Future for NextRandomUdpSocket {
//
// The dynamic port range defined by IANA consists of the 49152-65535
// range, and is meant for the selection of ephemeral ports.
let rand_port_range = Uniform::new_inclusive(49152_u16, u16::max_value());
let rand_port_range = Uniform::new_inclusive(49152_u16, u16::MAX);
let mut rand = rand::thread_rng();
for attempt in 0..10 {

View File

@ -42,15 +42,7 @@ use crate::{iocompat::AsyncIoTokioAsStd, DnsStreamHandle};
#[test]
#[cfg_attr(target_os = "macos", ignore)] // TODO: add back once https://github.com/sfackler/rust-native-tls/issues/143 is fixed
fn test_tls_client_stream_ipv4() {
tls_client_stream_test(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), false)
}
// FIXME: mtls is disabled at the moment, it causes a hang on Linux, and is currently not supported on macOS
#[cfg(feature = "mtls")]
#[test]
#[cfg(not(target_os = "macos"))]
fn test_tls_client_stream_ipv4_mtls() {
tls_client_stream_test(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), true)
tls_client_stream_test(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))
}
#[test]
@ -74,7 +66,7 @@ fn read_file(path: &str) -> Vec<u8> {
}
#[allow(unused, unused_mut)]
fn tls_client_stream_test(server_addr: IpAddr, mtls: bool) {
fn tls_client_stream_test(server_addr: IpAddr) {
let succeeded = Arc::new(atomic::AtomicBool::new(false));
let succeeded_clone = succeeded.clone();
thread::Builder::new()
@ -117,28 +109,6 @@ fn tls_client_stream_test(server_addr: IpAddr, mtls: bool) {
.spawn(move || {
let mut tls = TlsAcceptor::builder(identity);
// #[cfg(target_os = "linux")]
// {
// let mut openssl_builder = tls.builder_mut();
// let mut openssl_ctx_builder = openssl_builder.builder_mut();
// let mut mode = openssl::ssl::SslVerifyMode::empty();
// // TODO: mtls tests hang on Linux...
// if mtls {
// // mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
// // let mut store = X509StoreBuilder::new().unwrap();
// // let root_ca = X509::from_der(&root_cert_der_copy).unwrap();
// // store.add_cert(root_ca).unwrap();
// // openssl_ctx_builder.set_verify_cert_store(store.build()).unwrap();
// } else {
// mode.insert(SSL_VERIFY_NONE);
// }
// openssl_ctx_builder.set_verify(mode);
// }
// TODO: add CA on macOS
let tls = tls.build().expect("tls build failed");
@ -199,11 +169,6 @@ fn tls_client_stream_test(server_addr: IpAddr, mtls: bool) {
let mut builder = TlsStreamBuilder::<AsyncIoTokioAsStd<TokioTcpStream>>::new();
builder.add_ca(trust_chain);
// fix MTLS
// if mtls {
// config_mtls(&root_pkey, &root_name, &root_cert, &mut builder);
// }
let (stream, mut sender) = builder.build(server_addr, dns_name.to_string());
// TODO: there is a race failure here... a race with the server thread most likely...
@ -226,20 +191,3 @@ fn tls_client_stream_test(server_addr: IpAddr, mtls: bool) {
succeeded.store(true, std::sync::atomic::Ordering::Relaxed);
server_handle.join().expect("server thread failed");
}
// TODO: fix MTLS
// #[allow(unused_variables)]
// fn config_mtls(root_pkey: &PKey,
// root_name: &X509Name,
// root_cert: &X509,
// builder: &mut TlsStreamBuilder) {
// // signed by the same root cert
// let client_name = "resolv.example.com";
// let (_ /*client_pkey*/, _ /*client_cert*/, client_identity) =
// cert(client_name, root_pkey, root_name, root_cert);
// let client_identity =
// native_tls::Pkcs12::from_der(&client_identity.to_der().unwrap(), "mypass").unwrap();
// #[cfg(feature = "mtls")]
// builder.identity(client_identity);
// }

View File

@ -13,8 +13,6 @@ use std::pin::Pin;
use futures_util::TryFutureExt;
use native_tls::Certificate;
#[cfg(feature = "mtls")]
use native_tls::Pkcs12;
use tokio_native_tls::TlsStream as TokioTlsStream;
use crate::error::ProtoError;
@ -46,12 +44,6 @@ impl<S: DnsTcpStream> TlsClientStreamBuilder<S> {
self.0.add_ca(ca);
}
/// Client side identity for client auth in TLS (aka mutual TLS auth)
#[cfg(feature = "mtls")]
pub fn identity(&mut self, pkcs12: Pkcs12) {
self.0.identity(pkcs12);
}
/// Sets the address to connect from.
pub fn bind_addr(&mut self, bind_addr: SocketAddr) {
self.0.bind_addr(bind_addr);

View File

@ -86,12 +86,6 @@ impl<S: DnsTcpStream> TlsStreamBuilder<S> {
self.ca_chain.push(ca);
}
/// Client side identity for client auth in TLS (aka mutual TLS auth)
#[cfg(feature = "mtls")]
pub fn identity(&mut self, identity: Identity) {
self.identity = Some(identity);
}
/// Sets the address to connect from.
pub fn bind_addr(&mut self, bind_addr: SocketAddr) {
self.bind_addr = Some(bind_addr);

View File

@ -156,11 +156,11 @@ impl<'a> From<&'a Record> for Edns {
let max_payload: u16 = u16::from(value.dns_class());
let options: OPT = match value.data() {
Some(RData::NULL(..)) | None => {
RData::Update0(..) | RData::NULL(..) => {
// NULL, there was no data in the OPT
OPT::default()
}
Some(RData::OPT(ref option_data)) => {
RData::OPT(ref option_data) => {
option_data.clone() // TODO: Edns should just refer to this, have the same lifetime as the Record
}
_ => {
@ -183,24 +183,18 @@ impl<'a> From<&'a Edns> for Record {
/// This returns a Resource Record that is formatted for Edns(0).
/// Note: the rcode_high value is only part of the rcode, the rest is part of the base
fn from(value: &'a Edns) -> Self {
let mut record = Self::new();
record.set_name(Name::root());
record.set_record_type(RecordType::OPT);
record.set_dns_class(DNSClass::for_opt(value.max_payload()));
// rebuild the TTL field
let mut ttl: u32 = u32::from(value.rcode_high()) << 24;
ttl |= u32::from(value.version()) << 16;
ttl |= u32::from(value.flags);
record.set_ttl(ttl);
// now for each option, write out the option array
// also, since this is a hash, there is no guarantee that ordering will be preserved from
// the original binary format.
// maybe switch to: https://crates.io/crates/linked-hash-map/
record.set_data(Some(RData::OPT(value.options().clone())));
let mut record = Self::from_rdata(Name::root(), ttl, RData::OPT(value.options().clone()));
record.set_dns_class(DNSClass::for_opt(value.max_payload()));
record
}
@ -223,7 +217,7 @@ impl BinEncodable for Edns {
let place = encoder.place::<u16>()?;
self.options.emit(encoder)?;
let len = encoder.len_since_place(&place);
assert!(len <= u16::max_value() as usize);
assert!(len <= u16::MAX as usize);
place.replace(encoder, len as u16)?;
Ok(())

View File

@ -78,10 +78,10 @@ pub fn update_header_counts(
is_truncated: bool,
counts: HeaderCounts,
) -> Header {
assert!(counts.query_count <= u16::max_value() as usize);
assert!(counts.answer_count <= u16::max_value() as usize);
assert!(counts.nameserver_count <= u16::max_value() as usize);
assert!(counts.additional_count <= u16::max_value() as usize);
assert!(counts.query_count <= u16::MAX as usize);
assert!(counts.answer_count <= u16::MAX as usize);
assert!(counts.nameserver_count <= u16::MAX as usize);
assert!(counts.additional_count <= u16::MAX as usize);
// TODO: should the function just take by value?
let mut header = *current_header;
@ -1191,9 +1191,9 @@ mod tests {
.set_checking_disabled(true)
.set_response_code(ResponseCode::ServFail);
message.add_answer(Record::new());
message.add_name_server(Record::new());
message.add_additional(Record::new());
message.add_answer(Record::stub());
message.add_name_server(Record::stub());
message.add_additional(Record::stub());
message.update_counts();
test_emit_and_read(message);
@ -1229,9 +1229,9 @@ mod tests {
.set_checking_disabled(true)
.set_response_code(ResponseCode::ServFail);
message.add_answer(Record::new());
message.add_name_server(Record::new());
message.add_additional(Record::new());
message.add_answer(Record::stub());
message.add_name_server(Record::stub());
message.add_additional(Record::stub());
// at here, we don't call update_counts and we even set wrong count,
// because we are trying to test whether the counts in the header

View File

@ -11,10 +11,7 @@ use std::fmt::Debug;
use crate::{
op::{Edns, Message, MessageType, OpCode, Query},
rr::{
rdata::{NULL, SOA},
DNSClass, Name, RData, Record, RecordSet, RecordType,
},
rr::{rdata::SOA, DNSClass, Name, RData, Record, RecordSet, RecordType},
};
/// To reduce errors in using the Message struct as an Update, this will do the call throughs
@ -181,9 +178,9 @@ pub fn create(rrset: RecordSet, zone_origin: Name, use_edns: bool) -> Message {
.set_recursion_desired(false);
message.add_zone(zone);
let mut prerequisite = Record::with(rrset.name().clone(), rrset.record_type(), 0);
let mut prerequisite = Record::update0(rrset.name().clone(), 0, rrset.record_type());
prerequisite.set_dns_class(DNSClass::NONE);
message.add_pre_requisite(prerequisite);
message.add_pre_requisite(prerequisite.into_record_of_rdata());
message.add_updates(rrset);
// Extended dns
@ -251,9 +248,9 @@ pub fn append(rrset: RecordSet, zone_origin: Name, must_exist: bool, use_edns: b
message.add_zone(zone);
if must_exist {
let mut prerequisite = Record::with(rrset.name().clone(), rrset.record_type(), 0);
let mut prerequisite = Record::update0(rrset.name().clone(), 0, rrset.record_type());
prerequisite.set_dns_class(DNSClass::ANY);
message.add_pre_requisite(prerequisite);
message.add_pre_requisite(prerequisite.into_record_of_rdata());
}
message.add_updates(rrset);
@ -492,7 +489,7 @@ pub fn delete_rrset(mut record: Record, zone_origin: Name, use_edns: bool) -> Me
// the TTL should be 0
record.set_ttl(0);
// the rdata must be null to delete all rrsets
record.set_data(Some(RData::NULL(NULL::new())));
record.set_data(RData::Update0(record.record_type()));
message.add_update(record);
// Extended dns
@ -557,12 +554,12 @@ pub fn delete_all(
// the TTL should be 0
// the rdata must be null to delete all rrsets
// the record type must be any
let mut record = Record::with(name_of_records, RecordType::ANY, 0);
let mut record = Record::update0(name_of_records, 0, RecordType::ANY);
// the class must be none for an rrset delete
record.set_dns_class(DNSClass::ANY);
message.add_update(record);
message.add_update(record.into_record_of_rdata());
// Extended dns
if use_edns {

View File

@ -11,8 +11,6 @@ use std::net::SocketAddr;
use std::pin::Pin;
use futures_util::TryFutureExt;
#[cfg(feature = "mtls")]
use openssl::pkcs12::Pkcs12;
use openssl::x509::X509;
use tokio_openssl::SslStream as TokioTlsStream;
@ -54,12 +52,6 @@ impl<S: DnsTcpStream> TlsClientStreamBuilder<S> {
Ok(())
}
/// Client side identity for client auth in TLS (aka mutual TLS auth)
#[cfg(feature = "mtls")]
pub fn identity(&mut self, pkcs12: Pkcs12) {
self.0.identity(pkcs12);
}
/// Sets the address to connect from.
pub fn bind_addr(&mut self, bind_addr: SocketAddr) {
self.0.bind_addr(bind_addr);

View File

@ -170,12 +170,6 @@ impl<S: DnsTcpStream> TlsStreamBuilder<S> {
self.ca_chain.push(ca);
}
/// Client side identity for client auth in TLS (aka mutual TLS auth)
#[cfg(feature = "mtls")]
pub fn identity(&mut self, pkcs12: ParsedPkcs12) {
self.identity = Some(pkcs12);
}
/// Sets the address to connect from.
pub fn bind_addr(&mut self, bind_addr: SocketAddr) {
self.bind_addr = Some(bind_addr);

View File

@ -756,7 +756,7 @@ pub fn signed_bitmessage_to_buf(
// parse a tsig record
let sig = Record::read(&mut decoder)?;
let tsig = if let (RecordType::TSIG, Some(RData::DNSSEC(DNSSECRData::TSIG(tsig_data)))) =
let tsig = if let (RecordType::TSIG, RData::DNSSEC(DNSSECRData::TSIG(tsig_data))) =
(sig.record_type(), sig.data())
{
tsig_data
@ -795,17 +795,17 @@ pub fn signed_bitmessage_to_buf(
pub fn make_tsig_record(name: Name, rdata: TSIG) -> Record {
// https://tools.ietf.org/html/rfc8945#section-4.2
let mut tsig = Record::new();
// NAME: The name of the key used, in domain name syntax
tsig.set_name(name)
// TYPE: This MUST be TSIG (250: Transaction SIGnature).
.set_record_type(RecordType::TSIG)
// CLASS: This MUST be ANY.
.set_dns_class(DNSClass::ANY)
let mut tsig = Record::from_rdata(
// NAME: The name of the key used, in domain name syntax
name,
// TTL: This MUST be 0.
.set_ttl(0)
.set_data(Some(DNSSECRData::TSIG(rdata).into()));
0,
// TYPE: This MUST be TSIG (250: Transaction SIGnature).
DNSSECRData::TSIG(rdata).into(),
);
// CLASS: This MUST be ANY.
tsig.set_dns_class(DNSClass::ANY);
tsig
}
@ -864,7 +864,7 @@ mod tests {
#[test]
fn test_sign_encode() {
let mut message = Message::new();
message.add_answer(Record::new());
message.add_answer(Record::stub());
let key_name = Name::from_ascii("some.name").unwrap();
@ -899,7 +899,7 @@ mod tests {
#[test]
fn test_sign_encode_id_changed() {
let mut message = Message::new();
message.set_id(123).add_answer(Record::new());
message.set_id(123).add_answer(Record::stub());
let key_name = Name::from_ascii("some.name").unwrap();

View File

@ -13,7 +13,7 @@ pub(crate) struct RSAPublicKey<'a> {
}
impl<'a> RSAPublicKey<'a> {
pub(crate) fn try_from(encoded: &'a [u8]) -> ProtoResult<RSAPublicKey<'a>> {
pub(crate) fn try_from(encoded: &'a [u8]) -> ProtoResult<Self> {
let (e_len_len, e_len) = match encoded.first() {
Some(&0) if encoded.len() >= 3 => {
(3, (usize::from(encoded[1]) << 8) | usize::from(encoded[2]))

View File

@ -530,23 +530,15 @@ impl MessageFinalizer for SigSigner {
// this is based on RFCs 2535, 2931 and 3007
// 'For all SIG(0) RRs, the owner name, class, TTL, and original TTL, are
// meaningless.' - 2931
let mut sig0 = Record::new();
// The TTL fields SHOULD be zero
sig0.set_ttl(0);
// The CLASS field SHOULD be ANY
sig0.set_dns_class(DNSClass::ANY);
// The owner name SHOULD be root (a single zero octet).
sig0.set_name(Name::root());
let num_labels = sig0.name().num_labels();
let name = Name::root();
// The TTL fields SHOULD be zero
let ttl = 0;
let num_labels = name.num_labels();
let expiration_time: u32 = current_time + (5 * 60); // +5 minutes in seconds
sig0.set_record_type(RecordType::SIG);
let pre_sig0 = SIG::new(
// type covered in SIG(0) is 0 which is what makes this SIG0 vs a standard SIG
RecordType::ZERO,
@ -565,9 +557,14 @@ impl MessageFinalizer for SigSigner {
Vec::new(),
);
let signature: Vec<u8> = self.sign_message(message, &pre_sig0)?;
sig0.set_data(Some(RData::DNSSEC(DNSSECRData::SIG(
pre_sig0.set_sig(signature),
))));
let rdata = RData::DNSSEC(DNSSECRData::SIG(pre_sig0.set_sig(signature)));
// 'For all SIG(0) RRs, the owner name, class, TTL, and original TTL, are
// meaningless.' - 2931
let mut sig0 = Record::from_rdata(name, ttl, rdata);
// The CLASS field SHOULD be ANY
sig0.set_dns_class(DNSClass::ANY);
Ok((vec![sig0], None))
}
@ -659,7 +656,7 @@ mod tests {
let sig = signer.sign_message(&question, &pre_sig0);
println!("sig after sign: {sig:?}");
if let Some(RData::DNSSEC(DNSSECRData::SIG(ref sig))) = question.sig0()[0].data() {
if let RData::DNSSEC(DNSSECRData::SIG(ref sig)) = question.sig0()[0].data() {
assert!(sig0key.verify_message(&question, sig.sig(), sig).is_ok());
}
}
@ -692,28 +689,20 @@ mod tests {
);
let rrset = vec![
Record::new()
.set_name(origin.clone())
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(Name::parse(
"a.iana-servers.net.",
None,
)
.unwrap()))))
.clone(),
Record::new()
.set_name(origin)
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(Name::parse(
"b.iana-servers.net.",
None,
)
.unwrap()))))
.clone(),
Record::from_rdata(
origin.clone(),
86400,
RData::NS(NS(Name::parse("a.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone(),
Record::from_rdata(
origin,
86400,
RData::NS(NS(Name::parse("b.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone(),
];
let tbs = tbs::rrset_tbs_with_rrsig(&rrsig, &rrset).unwrap();
@ -829,87 +818,61 @@ MC0CAQACBQC+L6pNAgMBAAECBQCYj0ZNAgMA9CsCAwDHZwICeEUCAnE/AgMA3u0=
),
);
let rrset = vec![
Record::new()
.set_name(origin.clone())
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(Name::parse(
"a.iana-servers.net.",
None,
)
.unwrap()))))
.clone(),
Record::new()
.set_name(origin.clone())
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(Name::parse(
"b.iana-servers.net.",
None,
)
.unwrap()))))
.clone(),
Record::from_rdata(
origin.clone(),
86400,
RData::NS(NS(Name::parse("a.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone(),
Record::from_rdata(
origin.clone(),
86400,
RData::NS(NS(Name::parse("b.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone(),
];
let tbs = rrset_tbs_with_rrsig(&rrsig, &rrset).unwrap();
assert!(!tbs.as_ref().is_empty());
let rrset = vec![
Record::new()
.set_name(origin.clone())
.set_ttl(86400)
.set_record_type(RecordType::CNAME)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::CNAME(CNAME(
Name::parse("a.iana-servers.net.", None).unwrap(),
))))
.clone(), // different type
Record::new()
.set_name(Name::parse("www.example.com.", None).unwrap())
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(Name::parse(
"a.iana-servers.net.",
None,
)
.unwrap()))))
.clone(), // different name
Record::new()
.set_name(origin.clone())
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::CH)
.set_data(Some(RData::NS(NS(Name::parse(
"a.iana-servers.net.",
None,
)
.unwrap()))))
.clone(), // different class
Record::new()
.set_name(origin.clone())
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(Name::parse(
"a.iana-servers.net.",
None,
)
.unwrap()))))
.clone(),
Record::new()
.set_name(origin)
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(Name::parse(
"b.iana-servers.net.",
None,
)
.unwrap()))))
.clone(),
Record::from_rdata(
origin.clone(),
86400,
RData::CNAME(CNAME(Name::parse("a.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone(), // different type
Record::from_rdata(
Name::parse("www.example.com.", None).unwrap(),
86400,
RData::NS(NS(Name::parse("a.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone(), // different name
Record::from_rdata(
origin.clone(),
86400,
RData::NS(NS(Name::parse("a.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::CH)
.clone(), // different class
Record::from_rdata(
origin.clone(),
86400,
RData::NS(NS(Name::parse("a.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone(),
Record::from_rdata(
origin,
86400,
RData::NS(NS(Name::parse("b.iana-servers.net.", None).unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone(),
];
let filtered_tbs = rrset_tbs_with_rrsig(&rrsig, &rrset).unwrap();

View File

@ -114,7 +114,7 @@ impl SupportedAlgorithms {
/// Return the count of supported algorithms
pub fn len(self) -> u16 {
// this is pretty much guaranteed to be less that u16::max_value()
// this is pretty much guaranteed to be less that u16::MAX
self.iter().count() as u16
}
@ -193,7 +193,7 @@ impl<'a> Iterator for SupportedAlgorithmsIter<'a> {
type Item = Algorithm;
fn next(&mut self) -> Option<Self::Item> {
// some quick bounds checking
if self.current > u8::max_value() as usize {
if self.current > u8::MAX as usize {
return None;
}

View File

@ -170,9 +170,7 @@ pub fn rrset_tbs<B: Borrow<Record>>(
{
let mut rdata_encoder = BinEncoder::new(&mut rdata_buf);
rdata_encoder.set_canonical_names(true);
if let Some(rdata) = record.data() {
assert!(rdata.emit(&mut rdata_encoder).is_ok());
}
assert!(record.data().emit(&mut rdata_encoder).is_ok());
}
assert!(encoder.emit_u16(rdata_buf.len() as u16).is_ok());
//
@ -199,11 +197,7 @@ pub fn rrset_tbs_with_rrsig<B: Borrow<Record>>(
rrsig: &Record<RRSIG>,
records: &[B],
) -> ProtoResult<TBS> {
if let Some(sig) = rrsig.data() {
rrset_tbs_with_sig(rrsig.name(), rrsig.dns_class(), sig, records)
} else {
Err(format!("could not determine name from {}", rrsig.name()).into())
}
rrset_tbs_with_sig(rrsig.name(), rrsig.dns_class(), rrsig.data(), records)
}
/// Returns the to-be-signed serialization of the given record set using the information

View File

@ -128,7 +128,7 @@ impl TSigner {
first_message: bool,
) -> ProtoResult<(Vec<u8>, Range<u64>, u64)> {
let (tbv, record) = signed_bitmessage_to_buf(previous_hash, message, first_message)?;
let tsig = if let Some(RData::DNSSEC(DNSSECRData::TSIG(tsig))) = record.data() {
let tsig = if let RData::DNSSEC(DNSSECRData::TSIG(tsig)) = record.data() {
tsig
} else {
unreachable!("tsig::signed_message_to_buff always returns a TSIG record")

View File

@ -1770,7 +1770,7 @@ mod tests {
use crate::error::ProtoErrorKind;
// u16 max value is where issues start being tickled...
let mut buf = Vec::with_capacity(u16::max_value() as usize);
let mut buf = Vec::with_capacity(u16::MAX as usize);
let mut encoder = BinEncoder::new(&mut buf);
let mut result = Ok(());

View File

@ -57,6 +57,11 @@ pub trait RecordData: Clone + Sized + PartialEq + Eq + Display + Debug + BinEnco
/// Converts this RecordData into generic RecordData
fn into_rdata(self) -> RData;
/// RDLENGTH = 0
fn is_update(&self) -> bool {
false
}
}
trait RecordDataDecodable<'r>: Sized {

View File

@ -684,7 +684,7 @@ fn emit_tag(buf: &mut [u8], tag: &Property) -> ProtoResult<u8> {
let property = property.as_bytes();
let len = property.len();
if len > ::std::u8::MAX as usize {
if len > u8::MAX as usize {
return Err(format!("CAA property too long: {len}").into());
}
if buf.len() < len {
@ -713,7 +713,7 @@ impl BinEncodable for CAA {
encoder.emit(flags)?;
// TODO: it might be interesting to use the new place semantics here to output all the data, then place the length back to the beginning...
let mut tag_buf = [0_u8; ::std::u8::MAX as usize];
let mut tag_buf = [0_u8; u8::MAX as usize];
let len = emit_tag(&mut tag_buf, &self.tag)?;
// now write to the encoder

View File

@ -0,0 +1 @@
pub struct UpdateRdata(pub RecordType);

View File

@ -691,6 +691,9 @@ pub enum RData {
rdata: NULL,
},
/// Update record with RDLENGTH = 0 (RFC2136)
Update0(RecordType),
/// This corresponds to a record type of 0, unspecified
#[deprecated(note = "Use None for the RData in the resource record instead")]
ZERO,
@ -735,6 +738,7 @@ impl RData {
#[cfg(feature = "dnssec")]
Self::DNSSEC(ref rdata) => DNSSECRData::to_record_type(rdata),
Self::Unknown { code, .. } => code,
Self::Update0(record_type) => record_type,
Self::ZERO => RecordType::ZERO,
}
}
@ -975,6 +979,7 @@ impl BinEncodable for RData {
#[cfg(feature = "dnssec")]
Self::DNSSEC(ref rdata) => encoder.with_canonical_names(|encoder| rdata.emit(encoder)),
Self::Unknown { ref rdata, .. } => rdata.emit(encoder),
Self::Update0(_) => Ok(()),
}
}
}
@ -995,6 +1000,10 @@ impl RecordData for RData {
fn into_rdata(self) -> RData {
self
}
fn is_update(&self) -> bool {
matches!(self, RData::Update0(_))
}
}
impl fmt::Display for RData {
@ -1034,6 +1043,7 @@ impl fmt::Display for RData {
#[cfg(feature = "dnssec")]
Self::DNSSEC(ref rdata) => w(f, rdata),
Self::Unknown { ref rdata, .. } => w(f, rdata),
Self::Update0(_) => w(f, "UPDATE"),
}
}
}
@ -1115,11 +1125,11 @@ mod tests {
RData::SOA(SOA::new(
Name::from_str("www.example.com").unwrap(),
Name::from_str("xxx.example.com").unwrap(),
u32::max_value(),
u32::MAX,
-1,
-1,
-1,
u32::max_value(),
u32::MAX,
)),
vec![
3, b'w', b'w', b'w', 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c',
@ -1182,11 +1192,11 @@ mod tests {
RData::SOA(SOA::new(
Name::from_str("www.example.com").unwrap(),
Name::from_str("xxx.example.com").unwrap(),
u32::max_value(),
u32::MAX,
-1,
-1,
-1,
u32::max_value(),
u32::MAX,
)),
RData::TXT(TXT::new(vec![
"abcdef".to_string(),
@ -1203,11 +1213,11 @@ mod tests {
RData::SOA(SOA::new(
Name::from_str("www.example.com").unwrap(),
Name::from_str("xxx.example.com").unwrap(),
u32::max_value(),
u32::MAX,
-1,
-1,
-1,
u32::max_value(),
u32::MAX,
)),
RData::TXT(TXT::new(vec![
"abcdef".to_string(),
@ -1274,6 +1284,7 @@ mod tests {
#[cfg(feature = "dnssec")]
RData::DNSSEC(ref rdata) => rdata.to_record_type(),
RData::Unknown { code, .. } => code,
RData::Update0(record_type) => record_type,
RData::ZERO => RecordType::ZERO,
}
}

View File

@ -75,25 +75,23 @@ const MDNS_ENABLE_CACHE_FLUSH: u16 = 1 << 15;
// TODO: make Record carry a lifetime for more efficient storage options in the future
pub struct Record<R: RecordData = RData> {
name_labels: Name,
rr_type: RecordType,
dns_class: DNSClass,
ttl: u32,
rdata: Option<R>,
rdata: R,
#[cfg(feature = "mdns")]
mdns_cache_flush: bool,
#[cfg(feature = "dnssec")]
proof: Proof,
}
impl Default for Record<RData> {
fn default() -> Self {
impl Record {
#[cfg(test)]
pub fn stub() -> Self {
Self {
// TODO: make these part of a Builder instead to cleanup Records at runtime?
name_labels: Name::new(),
rr_type: RecordType::NULL,
dns_class: DNSClass::IN,
ttl: 0,
rdata: None,
rdata: RData::Update0(RecordType::NULL),
#[cfg(feature = "mdns")]
mdns_cache_flush: false,
#[cfg(feature = "dnssec")]
@ -102,62 +100,20 @@ impl Default for Record<RData> {
}
}
impl Record<RData> {
/// Creates a default record, use the setters to build a more useful object.
///
/// There are no optional elements in this object, defaults are an empty name, type A, class IN,
/// ttl of 0 and the 0.0.0.0 ip address.
pub fn new() -> Self {
Self::default()
}
/// Create a record with the specified initial values.
///
/// # Arguments
///
/// * `name` - name of the resource records
/// * `rr_type` - the record type
/// * `ttl` - time-to-live is the amount of time this record should be cached before refreshing
// TODO: maybe deprecate in the future, there are valid use cases for null data...
// #[deprecated = "consider using the typed variant `from_rdata`"]
pub fn with(name: Name, rr_type: RecordType, ttl: u32) -> Self {
impl Record {
/// Creates an update record with RDLENGTH=0
pub fn update0(name: Name, ttl: u32, rr_type: RecordType) -> Self {
Self {
name_labels: name,
rr_type,
dns_class: DNSClass::IN,
ttl,
rdata: None,
rdata: RData::Update0(rr_type),
#[cfg(feature = "mdns")]
mdns_cache_flush: false,
#[cfg(feature = "dnssec")]
proof: Proof::default(),
}
}
/// ```text
/// TYPE two octets containing one of the RR type codes. This
/// field specifies the meaning of the data in the RDATA
/// field.
/// ```
#[deprecated(note = "use `set_record_type`")]
pub fn set_rr_type(&mut self, rr_type: RecordType) -> &mut Self {
self.rr_type = rr_type;
self
}
/// Generally Speaking, this is redundant to the RecordType stored in the associated RData and not recommended
/// to set this separately. Exceptions to this are for Update Messages, where the RecordType is used distinctly
/// as a means to express certain Update instructions. For queries and responses, it will always match the RData
///
/// ```text
/// TYPE two octets containing one of the RR type codes. This
/// field specifies the meaning of the data in the RDATA
/// field.
/// ```
pub fn set_record_type(&mut self, rr_type: RecordType) -> &mut Self {
self.rr_type = rr_type;
self
}
}
impl<R: RecordData> Record<R> {
@ -171,10 +127,9 @@ impl<R: RecordData> Record<R> {
pub fn from_rdata(name: Name, ttl: u32, rdata: R) -> Self {
Self {
name_labels: name,
rr_type: rdata.record_type(),
dns_class: DNSClass::IN,
ttl,
rdata: Some(rdata),
rdata,
#[cfg(feature = "mdns")]
mdns_cache_flush: false,
#[cfg(feature = "dnssec")]
@ -187,7 +142,6 @@ impl<R: RecordData> Record<R> {
pub fn try_from(record: Record<RData>) -> Result<Self, Record<RData>> {
let Record {
name_labels,
rr_type,
dns_class,
ttl,
rdata,
@ -197,35 +151,22 @@ impl<R: RecordData> Record<R> {
proof,
} = record;
match rdata.map(R::try_from_rdata) {
None => Err(Record {
match R::try_from_rdata(rdata) {
Ok(rdata) => Ok(Self {
name_labels,
rr_type,
dns_class,
ttl,
rdata: None,
rdata,
#[cfg(feature = "mdns")]
mdns_cache_flush,
#[cfg(feature = "dnssec")]
proof,
}),
Some(Ok(rdata)) => Ok(Self {
Err(rdata) => Err(Record {
name_labels,
rr_type: rdata.record_type(),
dns_class,
ttl,
rdata: Some(rdata),
#[cfg(feature = "mdns")]
mdns_cache_flush,
#[cfg(feature = "dnssec")]
proof,
}),
Some(Err(rdata)) => Err(Record {
name_labels,
rr_type,
dns_class,
ttl,
rdata: Some(rdata),
rdata,
#[cfg(feature = "mdns")]
mdns_cache_flush,
#[cfg(feature = "dnssec")]
@ -238,7 +179,6 @@ impl<R: RecordData> Record<R> {
pub fn into_record_of_rdata(self) -> Record<RData> {
let Self {
name_labels,
rr_type,
dns_class,
ttl,
rdata,
@ -248,11 +188,10 @@ impl<R: RecordData> Record<R> {
proof,
} = self;
let rdata: Option<RData> = rdata.map(RecordData::into_rdata);
let rdata: RData = RecordData::into_rdata(rdata);
Record {
name_labels,
rr_type,
dns_class,
ttl,
rdata,
@ -300,18 +239,7 @@ impl<R: RecordData> Record<R> {
/// the RDATA field is a 4 octet ARPA Internet address.
/// ```
#[track_caller]
pub fn set_data(&mut self, rdata: Option<R>) -> &mut Self {
debug_assert!(
if let Some(rdata) = &rdata {
rdata.record_type() == self.rr_type || rdata.record_type() == RecordType::NULL
} else {
true
},
"record types do not match, {} <> {:?}",
self.rr_type,
rdata.map(|r| r.record_type())
);
pub fn set_data(&mut self, rdata: R) -> &mut Self {
self.rdata = rdata;
self
}
@ -342,7 +270,7 @@ impl<R: RecordData> Record<R> {
/// Returns the type of the RecordData in the record
#[inline]
pub fn record_type(&self) -> RecordType {
self.rr_type
self.rdata.record_type()
}
/// Returns the DNSClass of the Record, generally IN fro internet
@ -359,19 +287,19 @@ impl<R: RecordData> Record<R> {
/// Returns the Record Data, i.e. the record information
#[inline]
pub fn data(&self) -> Option<&R> {
self.rdata.as_ref()
pub fn data(&self) -> &R {
&self.rdata
}
/// Returns a mutable reference to the Record Data
#[inline]
pub fn data_mut(&mut self) -> Option<&mut R> {
self.rdata.as_mut()
pub fn data_mut(&mut self) -> &mut R {
&mut self.rdata
}
/// Returns the RData consuming the Record
#[inline]
pub fn into_data(self) -> Option<R> {
pub fn into_data(self) -> R {
self.rdata
}
@ -405,14 +333,12 @@ impl<R: RecordData> Record<R> {
pub struct RecordParts<R: RecordData = RData> {
/// label names
pub name_labels: Name,
/// record type
pub rr_type: RecordType,
/// dns class
pub dns_class: DNSClass,
/// time to live
pub ttl: u32,
/// rdata
pub rdata: Option<R>,
pub rdata: R,
/// mDNS cache flush
#[cfg(feature = "mdns")]
#[cfg_attr(docsrs, doc(cfg(feature = "mdns")))]
@ -427,7 +353,6 @@ impl<R: RecordData> From<Record<R>> for RecordParts<R> {
fn from(record: Record<R>) -> Self {
let Record {
name_labels,
rr_type,
dns_class,
ttl,
rdata,
@ -439,7 +364,6 @@ impl<R: RecordData> From<Record<R>> for RecordParts<R> {
Self {
name_labels,
rr_type,
dns_class,
ttl,
rdata,
@ -461,7 +385,7 @@ impl IntoRecordSet for Record {
impl<R: RecordData> BinEncodable for Record<R> {
fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> {
self.name_labels.emit(encoder)?;
self.rr_type.emit(encoder)?;
self.record_type().emit(encoder)?;
#[cfg(not(feature = "mdns"))]
self.dns_class.emit(encoder)?;
@ -483,13 +407,13 @@ impl<R: RecordData> BinEncodable for Record<R> {
// write the RData
// the None case is handled below by writing `0` for the length of the RData
// this is in turn read as `None` during the `read` operation.
if let Some(rdata) = &self.rdata {
rdata.emit(encoder)?;
if !self.rdata.is_update() {
self.rdata.emit(encoder)?;
}
// get the length written
let len = encoder.len_since_place(&place);
assert!(len <= u16::max_value() as usize);
assert!(len <= u16::MAX as usize);
// replace the location with the length
place.replace(encoder, len as u16)?;
@ -568,30 +492,18 @@ impl<'r> BinDecodable<'r> for Record<RData> {
// this is to handle updates, RFC 2136, which uses 0 to indicate certain aspects of pre-requisites
// Null represents any data.
let rdata = if rd_length == 0 {
None
RData::Update0(record_type)
} else {
// RDATA a variable length string of octets that describes the
// resource. The format of this information varies
// according to the TYPE and CLASS of the resource record.
// Adding restrict to the rdata length because it's used for many calculations later
// and must be validated before hand
Some(RData::read(decoder, record_type, Restrict::new(rd_length))?)
RData::read(decoder, record_type, Restrict::new(rd_length))?
};
debug_assert!(
if let Some(rdata) = &rdata {
rdata.record_type() == record_type
} else {
true
},
"record types do not match, {} <> {:?}",
record_type,
rdata.map(|r| r.record_type())
);
Ok(Self {
name_labels,
rr_type: record_type,
dns_class: class,
ttl,
rdata,
@ -649,17 +561,14 @@ impl<R: RecordData> fmt::Display for Record<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"{name} {ttl} {class} {ty}",
"{name} {ttl} {class} {ty} {rdata}",
name = self.name_labels,
ttl = self.ttl,
class = self.dns_class,
ty = self.rr_type,
ty = self.record_type(),
rdata = self.rdata,
)?;
if let Some(rdata) = &self.rdata {
write!(f, " {rdata}")?;
}
Ok(())
}
}
@ -679,7 +588,6 @@ impl<R: RecordData> PartialEq for Record<R> {
fn eq(&self, other: &Self) -> bool {
// self == other && // the same pointer
self.name_labels == other.name_labels
&& self.rr_type == other.rr_type
&& self.dns_class == other.dns_class
&& self.rdata == other.rdata
}
@ -732,7 +640,10 @@ impl Ord for Record {
// resource records be maintained in binary?
compare_or_equal!(self, other, name_labels);
compare_or_equal!(self, other, rr_type);
match self.record_type().cmp(&other.record_type()) {
o @ Ordering::Less | o @ Ordering::Greater => return o,
Ordering::Equal => {}
}
compare_or_equal!(self, other, dns_class);
compare_or_equal!(self, other, ttl);
compare_or_equal!(self, other, rdata);
@ -794,10 +705,9 @@ impl<'a> From<&'a Record> for Proven<&'a Record> {
/// A Record where the RecordData type is already known
pub struct RecordRef<'a, R: RecordData> {
name_labels: &'a Name,
rr_type: RecordType,
dns_class: DNSClass,
ttl: u32,
rdata: Option<&'a R>,
rdata: &'a R,
#[cfg(feature = "mdns")]
mdns_cache_flush: bool,
#[cfg(feature = "dnssec")]
@ -809,10 +719,9 @@ impl<'a, R: RecordData> RecordRef<'a, R> {
pub fn to_owned(&self) -> Record<R> {
Record {
name_labels: self.name_labels.to_owned(),
rr_type: self.rr_type,
dns_class: self.dns_class,
ttl: self.ttl,
rdata: self.rdata.cloned(),
rdata: self.rdata.clone(),
#[cfg(feature = "mdns")]
mdns_cache_flush: self.mdns_cache_flush,
#[cfg(feature = "dnssec")]
@ -829,7 +738,7 @@ impl<'a, R: RecordData> RecordRef<'a, R> {
/// Returns the type of the RecordData in the record
#[inline]
pub fn record_type(&self) -> RecordType {
self.rr_type
self.rdata.record_type()
}
/// Returns the DNSClass of the Record, generally IN fro internet
@ -846,7 +755,7 @@ impl<'a, R: RecordData> RecordRef<'a, R> {
/// Returns the Record Data, i.e. the record information
#[inline]
pub fn data(&self) -> Option<&R> {
pub fn data(&self) -> &R {
self.rdata
}
@ -874,7 +783,6 @@ impl<'a, R: RecordData> TryFrom<&'a Record> for RecordRef<'a, R> {
fn try_from(record: &'a Record) -> Result<Self, Self::Error> {
let Record {
name_labels,
rr_type,
dns_class,
ttl,
rdata,
@ -884,24 +792,13 @@ impl<'a, R: RecordData> TryFrom<&'a Record> for RecordRef<'a, R> {
proof,
} = record;
match rdata.as_ref().and_then(R::try_borrow) {
None => Ok(Self {
name_labels,
rr_type: *rr_type,
dns_class: *dns_class,
ttl: *ttl,
rdata: None,
#[cfg(feature = "mdns")]
mdns_cache_flush: *mdns_cache_flush,
#[cfg(feature = "dnssec")]
proof: *proof,
}),
match R::try_borrow(rdata) {
None => Err(record),
Some(rdata) => Ok(Self {
name_labels,
rr_type: *rr_type,
dns_class: *dns_class,
ttl: *ttl,
rdata: Some(rdata),
rdata,
#[cfg(feature = "mdns")]
mdns_cache_flush: *mdns_cache_flush,
#[cfg(feature = "dnssec")]
@ -920,9 +817,8 @@ mod tests {
use super::*;
use crate::rr::dns_class::DNSClass;
use crate::rr::rdata::A;
use crate::rr::rdata::{A, AAAA};
use crate::rr::record_data::RData;
use crate::rr::record_type::RecordType;
use crate::rr::Name;
#[allow(clippy::useless_attribute)]
#[allow(unused)]
@ -930,13 +826,11 @@ mod tests {
#[test]
fn test_emit_and_read() {
let mut record = Record::new();
record
.set_name(Name::from_str("www.example.com").unwrap())
.set_record_type(RecordType::A)
.set_dns_class(DNSClass::IN)
.set_ttl(5)
.set_data(Some(RData::A(A::new(192, 168, 0, 1))));
let record = Record::from_rdata(
Name::from_str("www.example.com").unwrap(),
5,
RData::A(A::new(192, 168, 0, 1)),
);
let mut vec_bytes: Vec<u8> = Vec::with_capacity(512);
{
@ -953,25 +847,24 @@ mod tests {
#[test]
fn test_order() {
let mut record = Record::new();
record
.set_name(Name::from_str("www.example.com").unwrap())
.set_record_type(RecordType::A)
.set_dns_class(DNSClass::IN)
.set_ttl(5)
.set_data(Some(RData::A(A::new(192, 168, 0, 1))));
let mut record = Record::from_rdata(
Name::from_str("www.example.com").unwrap(),
5,
RData::A(A::new(192, 168, 0, 1)),
);
record.set_dns_class(DNSClass::IN);
let mut greater_name = record.clone();
greater_name.set_name(Name::from_str("zzz.example.com").unwrap());
let mut greater_type = record.clone().into_record_of_rdata();
greater_type.set_record_type(RecordType::AAAA);
greater_type.set_data(RData::AAAA(AAAA::new(0, 0, 0, 0, 0, 0, 0, 0)));
let mut greater_class = record.clone();
greater_class.set_dns_class(DNSClass::NONE);
let mut greater_rdata = record.clone();
greater_rdata.set_data(Some(RData::A(A::new(192, 168, 0, 255))));
greater_rdata.set_data(RData::A(A::new(192, 168, 0, 255)));
let compares = vec![
(&record, &greater_name),
@ -993,7 +886,7 @@ mod tests {
const RR_CLASS_OFFSET: usize = 1 /* empty name */ +
std::mem::size_of::<u16>() /* rr_type */;
let mut record = Record::<RData>::new();
let mut record = Record::<RData>::stub();
record.set_mdns_cache_flush(true);
let mut vec_bytes: Vec<u8> = Vec::with_capacity(512);

View File

@ -231,7 +231,7 @@ impl RecordSet {
self.records
.iter()
.find(|r| r.data().map(|r| r == rdata).unwrap_or(false))
.find(|r| r.data() == rdata)
.expect("insert failed")
}
@ -239,8 +239,7 @@ impl RecordSet {
pub fn add_rdata(&mut self, rdata: RData) -> bool {
debug_assert_eq!(self.record_type, rdata.record_type());
let mut record = Record::with(self.name.clone(), self.record_type, self.ttl);
record.set_data(Some(rdata));
let record = Record::from_rdata(self.name.clone(), self.ttl, rdata);
self.insert(record, 0)
}
@ -295,8 +294,8 @@ impl RecordSet {
if let Some(soa_record) = self.records.first() {
match soa_record.data() {
Some(RData::SOA(ref existing_soa)) => {
if let Some(RData::SOA(ref new_soa)) = record.data() {
RData::SOA(ref existing_soa) => {
if let RData::SOA(ref new_soa) = record.data() {
if new_soa.serial() <= existing_soa.serial() {
info!(
"update ignored serial out of data: {:?} <= {:?}",
@ -561,14 +560,14 @@ impl<'r> Iterator for RrsigsByAlgorithms<'r> {
self.rrsigs
.by_ref()
.filter(|record| {
if let Some(RData::DNSSEC(DNSSECRData::RRSIG(ref rrsig))) = record.data() {
if let RData::DNSSEC(DNSSECRData::RRSIG(ref rrsig)) = record.data() {
supported_algorithms.has(rrsig.algorithm())
} else {
false
}
})
.max_by_key(|record| {
if let Some(RData::DNSSEC(DNSSECRData::RRSIG(ref rrsig))) = record.data() {
if let RData::DNSSEC(DNSSECRData::RRSIG(ref rrsig)) = record.data() {
rrsig.algorithm()
} else {
#[allow(deprecated)]
@ -626,13 +625,13 @@ mod test {
let record_type = RecordType::A;
let mut rr_set = RecordSet::new(&name, record_type, 0);
let insert = Record::new()
.set_name(name.clone())
.set_ttl(86400)
.set_record_type(record_type)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::A(Ipv4Addr::new(93, 184, 216, 24).into())))
.clone();
let insert = Record::from_rdata(
name.clone(),
86400,
RData::A(Ipv4Addr::new(93, 184, 216, 24).into()),
)
.set_dns_class(DNSClass::IN)
.clone();
assert!(rr_set.insert(insert.clone(), 0));
assert_eq!(rr_set.records_without_rrsigs().count(), 1);
@ -644,13 +643,13 @@ mod test {
assert!(rr_set.records_without_rrsigs().any(|ref x| x == &&insert));
// add one
let insert1 = Record::new()
.set_name(name)
.set_ttl(86400)
.set_record_type(record_type)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::A(Ipv4Addr::new(93, 184, 216, 25).into())))
.clone();
let insert1 = Record::from_rdata(
name,
86400,
RData::A(Ipv4Addr::new(93, 184, 216, 25).into()),
)
.set_dns_class(DNSClass::IN)
.clone();
assert!(rr_set.insert(insert1.clone(), 0));
assert_eq!(rr_set.records_without_rrsigs().count(), 2);
assert!(rr_set.records_without_rrsigs().any(|ref x| x == &&insert));
@ -664,12 +663,10 @@ mod test {
let record_type = RecordType::SOA;
let mut rr_set = RecordSet::new(&name, record_type, 0);
let insert = Record::new()
.set_name(name.clone())
.set_ttl(3600)
.set_record_type(RecordType::SOA)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::SOA(SOA::new(
let insert = Record::from_rdata(
name.clone(),
3600,
RData::SOA(SOA::new(
Name::from_str("sns.dns.icann.org.").unwrap(),
Name::from_str("noc.dns.icann.org.").unwrap(),
2015082403,
@ -677,14 +674,14 @@ mod test {
3600,
1209600,
3600,
))))
.clone();
let same_serial = Record::new()
.set_name(name.clone())
.set_ttl(3600)
.set_record_type(RecordType::SOA)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::SOA(SOA::new(
)),
)
.set_dns_class(DNSClass::IN)
.clone();
let same_serial = Record::from_rdata(
name.clone(),
3600,
RData::SOA(SOA::new(
Name::from_str("sns.dns.icann.net.").unwrap(),
Name::from_str("noc.dns.icann.net.").unwrap(),
2015082403,
@ -692,14 +689,14 @@ mod test {
3600,
1209600,
3600,
))))
.clone();
let new_serial = Record::new()
.set_name(name)
.set_ttl(3600)
.set_record_type(RecordType::SOA)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::SOA(SOA::new(
)),
)
.set_dns_class(DNSClass::IN)
.clone();
let new_serial = Record::from_rdata(
name,
3600,
RData::SOA(SOA::new(
Name::from_str("sns.dns.icann.net.").unwrap(),
Name::from_str("noc.dns.icann.net.").unwrap(),
2015082404,
@ -707,8 +704,10 @@ mod test {
3600,
1209600,
3600,
))))
.clone();
)),
)
.set_dns_class(DNSClass::IN)
.clone();
assert!(rr_set.insert(insert.clone(), 0));
assert!(rr_set.records_without_rrsigs().any(|ref x| x == &&insert));
@ -741,19 +740,11 @@ mod test {
let record_type = RecordType::CNAME;
let mut rr_set = RecordSet::new(&name, record_type, 0);
let insert = Record::new()
.set_name(name.clone())
.set_ttl(3600)
.set_record_type(RecordType::CNAME)
let insert = Record::from_rdata(name.clone(), 3600, RData::CNAME(CNAME(cname)))
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::CNAME(CNAME(cname))))
.clone();
let new_record = Record::new()
.set_name(name)
.set_ttl(3600)
.set_record_type(RecordType::CNAME)
let new_record = Record::from_rdata(name, 3600, RData::CNAME(CNAME(new_cname)))
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::CNAME(CNAME(new_cname))))
.clone();
assert!(rr_set.insert(insert.clone(), 0));
@ -773,20 +764,20 @@ mod test {
let record_type = RecordType::A;
let mut rr_set = RecordSet::new(&name, record_type, 0);
let insert = Record::new()
.set_name(name.clone())
.set_ttl(86400)
.set_record_type(record_type)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::A(Ipv4Addr::new(93, 184, 216, 24).into())))
.clone();
let insert1 = Record::new()
.set_name(name)
.set_ttl(86400)
.set_record_type(record_type)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::A(Ipv4Addr::new(93, 184, 216, 25).into())))
.clone();
let insert = Record::from_rdata(
name.clone(),
86400,
RData::A(Ipv4Addr::new(93, 184, 216, 24).into()),
)
.set_dns_class(DNSClass::IN)
.clone();
let insert1 = Record::from_rdata(
name,
86400,
RData::A(Ipv4Addr::new(93, 184, 216, 25).into()),
)
.set_dns_class(DNSClass::IN)
.clone();
assert!(rr_set.insert(insert.clone(), 0));
assert!(rr_set.insert(insert1.clone(), 0));
@ -804,12 +795,10 @@ mod test {
let record_type = RecordType::SOA;
let mut rr_set = RecordSet::new(&name, record_type, 0);
let insert = Record::new()
.set_name(name)
.set_ttl(3600)
.set_record_type(RecordType::SOA)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::SOA(SOA::new(
let insert = Record::from_rdata(
name,
3600,
RData::SOA(SOA::new(
Name::from_str("sns.dns.icann.org.").unwrap(),
Name::from_str("noc.dns.icann.org.").unwrap(),
2015082403,
@ -817,8 +806,10 @@ mod test {
3600,
1209600,
3600,
))))
.clone();
)),
)
.set_dns_class(DNSClass::IN)
.clone();
assert!(rr_set.insert(insert.clone(), 0));
assert!(!rr_set.remove(&insert, 0));
@ -831,24 +822,20 @@ mod test {
let record_type = RecordType::NS;
let mut rr_set = RecordSet::new(&name, record_type, 0);
let ns1 = Record::new()
.set_name(name.clone())
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(
Name::from_str("a.iana-servers.net.").unwrap()
))))
.clone();
let ns2 = Record::new()
.set_name(name)
.set_ttl(86400)
.set_record_type(RecordType::NS)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::NS(NS(
Name::from_str("b.iana-servers.net.").unwrap()
))))
.clone();
let ns1 = Record::from_rdata(
name.clone(),
86400,
RData::NS(NS(Name::from_str("a.iana-servers.net.").unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone();
let ns2 = Record::from_rdata(
name,
86400,
RData::NS(NS(Name::from_str("b.iana-servers.net.").unwrap())),
)
.set_dns_class(DNSClass::IN)
.clone();
assert!(rr_set.insert(ns1.clone(), 0));
assert!(rr_set.insert(ns2.clone(), 0));
@ -918,41 +905,37 @@ mod test {
vec![],
);
let rrsig_rsa = Record::new()
.set_name(name.clone())
.set_ttl(3600)
.set_record_type(RecordType::RRSIG)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::DNSSEC(DNSSECRData::RRSIG(rsasha256))))
.clone();
let rrsig_ecp256 = Record::new()
.set_name(name.clone())
.set_ttl(3600)
.set_record_type(RecordType::RRSIG)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::DNSSEC(DNSSECRData::RRSIG(ecp256))))
.clone();
let rrsig_ecp384 = Record::new()
.set_name(name.clone())
.set_ttl(3600)
.set_record_type(RecordType::RRSIG)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::DNSSEC(DNSSECRData::RRSIG(ecp384))))
.clone();
let rrsig_ed25519 = Record::new()
.set_name(name.clone())
.set_ttl(3600)
.set_record_type(RecordType::RRSIG)
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::DNSSEC(DNSSECRData::RRSIG(ed25519))))
.clone();
let rrsig_rsa = Record::from_rdata(
name.clone(),
3600,
RData::DNSSEC(DNSSECRData::RRSIG(rsasha256)),
)
.set_dns_class(DNSClass::IN)
.clone();
let rrsig_ecp256 = Record::from_rdata(
name.clone(),
3600,
RData::DNSSEC(DNSSECRData::RRSIG(ecp256)),
)
.set_dns_class(DNSClass::IN)
.clone();
let rrsig_ecp384 = Record::from_rdata(
name.clone(),
3600,
RData::DNSSEC(DNSSECRData::RRSIG(ecp384)),
)
.set_dns_class(DNSClass::IN)
.clone();
let rrsig_ed25519 = Record::from_rdata(
name.clone(),
3600,
RData::DNSSEC(DNSSECRData::RRSIG(ed25519)),
)
.set_dns_class(DNSClass::IN)
.clone();
let a = Record::new()
.set_name(name)
.set_ttl(3600)
.set_record_type(RecordType::A)
let a = Record::from_rdata(name, 3600, RData::A(Ipv4Addr::new(93, 184, 216, 24).into()))
.set_dns_class(DNSClass::IN)
.set_data(Some(RData::A(Ipv4Addr::new(93, 184, 216, 24).into())))
.clone();
let mut rrset = RecordSet::from(a);
@ -964,7 +947,7 @@ mod test {
assert!(rrset
.records_with_rrsigs(SupportedAlgorithms::all(),)
.any(
|r| if let Some(RData::DNSSEC(DNSSECRData::RRSIG(ref sig))) = r.data() {
|r| if let RData::DNSSEC(DNSSECRData::RRSIG(ref sig)) = r.data() {
sig.algorithm() == Algorithm::ED25519
} else {
false
@ -974,7 +957,7 @@ mod test {
let mut supported_algorithms = SupportedAlgorithms::new();
supported_algorithms.set(Algorithm::ECDSAP384SHA384);
assert!(rrset.records_with_rrsigs(supported_algorithms).any(|r| {
if let Some(RData::DNSSEC(DNSSECRData::RRSIG(ref sig))) = r.data() {
if let RData::DNSSEC(DNSSECRData::RRSIG(ref sig)) = r.data() {
sig.algorithm() == Algorithm::ECDSAP384SHA384
} else {
false
@ -984,7 +967,7 @@ mod test {
let mut supported_algorithms = SupportedAlgorithms::new();
supported_algorithms.set(Algorithm::ED25519);
assert!(rrset.records_with_rrsigs(supported_algorithms).any(|r| {
if let Some(RData::DNSSEC(DNSSECRData::RRSIG(ref sig))) = r.data() {
if let RData::DNSSEC(DNSSECRData::RRSIG(ref sig)) = r.data() {
sig.algorithm() == Algorithm::ED25519
} else {
false

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