Skip to content
This repository has been archived by the owner on Jun 7, 2024. It is now read-only.

add explore example and update the README #18

Merged
merged 3 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

174 changes: 65 additions & 109 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,145 +1,101 @@
# `dnssec-tests`

Test infrastructure for DNSSEC conformance tests.
This repository contains two packages:

## Design goals
- `dns-test`. This is a test framework (library) for testing DNS implementations.
- `conformance-tests`. This is a collection of DNS, mainly DNSSEC, tests.

- Test MUST not depend on external services like `1.1.1.1` or `8.8.8.8`
- rationale: it must be possible to run tests locally, without internet access
- All nodes in the network must not be the subject under test.
- rationale: test inter-operability with other software like `unbound` and `nsd`
- All test input must be local files or constants
- rationale: tests are self-contained
-
## Requirements

## Minimally working DNSSEC-enabled network
To use the code in this repository you need:

- `.` domain
- name server: `nsd` (`my.root-server.com`)
- TLD domain (`com.`)
- name server: `nsd` (`ns.com`)
- target domain (`example.com.`)
- name server: `nsd` (`ns.example.com`)
- recursive resolver: `unbound`
- configured to use `my.root-server.com` as root server
- configured with a trust anchor: the public key of `my.root-server.com`
- 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

each name server has
- a zone signing key pair
- a key signing key pair
- signed zone files
## `dns-test`

### exploration
This test framework was built with the following design goals and constraints in mind:

Notes:
- 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.

- run all containers with ` --cap-add=NET_RAW --cap-add=NET_ADMIN`
- use `docker exec` to run `tshark` on network nodes ( containers ) of interest
- 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`.

#### `nsd` for root name server
- 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.

run: `nsd -d`
### Test drive

- `/etc/nsd/nsd.conf`
To start a small DNS network using the `dns-test` framework run this command and follow the instructions to interact with the DNS network.

``` text
remote-control:
control-enable: no

zone:
name: .
zonefile: /etc/nsd/zones/main.zone
```

- `/etc/nsd/zones/main.zone`

``` text
$ORIGIN .
$TTL 1800
@ IN SOA primary.root-server.com. admin.root-server.com. (
2014080301
3600
900
1209600
1800
)
@ IN NS primary.root-server.com.

; referral
com. IN NS primary.tld-server.com.
primary.tld-server.com. IN A 172.17.0.$TLD_NS_IP_ADDRESS
``` console
$ cargo run --example explore
```

#### `nsd` for the TLD name server
By default, this will use `unbound` as the resolver. You can switch the resolver to `hickory-dns` using the `DNS_TEST_SUBJECT` environment variable:

run: `nsd -d`
``` shell
$ DNS_TEST_SUBJECT="hickory https://github.com/hickory-dns/hickory-dns" cargo run --example explore
```

- `/etc/nsd/nsd.conf`
### Environment variables

``` text
remote-control:
control-enable: no
- `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.

zone:
name: main
zonefile: /etc/nsd/zones/main.zone
```
## `conformance-tests`

- `/etc/nsd/zones/main.zone`

``` text
$ORIGIN com.
$TTL 1800
@ IN SOA primary.tld-server.com. admin.tld-server.com. (
2014010100 ; Serial
10800 ; Refresh (3 hours)
900 ; Retry (15 minutes)
604800 ; Expire (1 week)
86400 ; Minimum (1 day)
)
@ IN NS primary.tld-server.com.
```
#### `unbound`
This is a collection of tests that check the conformance of a DNS implementation to the different RFCs around DNS and DNSSEC.

run `unbound -d`
### Running the test suite

- `/etc/unbound/unbound.conf`
To run the conformance tests against `unbound` run:

ideally instead of `0.0.0.0`, it should only cover the `docker0` network interface. or disable docker containers' access to the internet
``` console
$ cargo test -p conformance-tests -- --include-ignored
```

``` text
server:
verbosity: 4
use-syslog: no
interface: 0.0.0.0
access-control: 172.17.0.0/16 allow
root-hints: /etc/unbound/root.hints
To run the conformance tests against `hickory-dns` run:

remote-control:
control-enable: no
``` console
$ DNS_TEST_SUBJECT="hickory /path/to/repository" cargo test -p conformance-tests
```

- `/etc/unbound/root.hints`. NOTE IP address of docker container

``` text
. 3600000 NS primary.root-server.com.
primary.root-server.com. 3600000 A 172.17.0.$ROOT_NS_IP_ADDRESS
### 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
```

#### `client`
The modules in the root correspond to the *role* being tested: `resolver` (recursive resolver), `name-server` (authoritative-only name server), etc.

Container is `docker/client.Dockerfile`, build with: `docker build -t dnssec-tests-client -f docker/client.Dockerfile docker`, with `tshark`.
The next module level contains the *functionality* being tested: (plain) DNS, DNSSEC, NSEC3, etc.

Run the client container with extra capabilities
The next module level contains the RFC documents, whose requirements are being tested: RFC4035, etc.

```shell
docker run --rm -it --cap-add=NET_RAW --cap-add=NET_ADMIN dnssec-tests-client /bin/bash
```
The next module levels contain sections, subsections and any other subdivision that may be relevant.

Then run `tshark` inside the container:
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.

```shell
tshark -f 'host 172.17.0.3' -O dns
```
### 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.

to filter DNS messages for host `172.17.0.3` (`unbound`).
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.
3 changes: 3 additions & 0 deletions packages/dns-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ url = "2.5.0"

[lib]
doctest = false

[dev-dependencies]
ctrlc = "3.4.2"
116 changes: 116 additions & 0 deletions packages/dns-test/examples/explore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::sync::mpsc;

use dns_test::client::Client;
use dns_test::name_server::NameServer;
use dns_test::record::RecordType;
use dns_test::zone_file::Root;
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};

fn main() -> Result<()> {
let network = Network::new()?;

println!("building docker image...");
let mut root_ns = NameServer::new(FQDN::ROOT, &network)?;
println!("DONE");

println!("setting up name servers...");
let mut com_ns = NameServer::new(FQDN::COM, &network)?;

let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?;
nameservers_ns
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr());
let nameservers_ns = nameservers_ns.sign()?;
let nameservers_ds = nameservers_ns.ds().clone();
let nameservers_ns = nameservers_ns.start()?;

com_ns
.referral(
nameservers_ns.zone().clone(),
nameservers_ns.fqdn().clone(),
nameservers_ns.ipv4_addr(),
)
.ds(nameservers_ds);
let com_ns = com_ns.sign()?;
let com_ds = com_ns.ds().clone();
let com_ns = com_ns.start()?;

root_ns
.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr())
.ds(com_ds);
let root_ns = root_ns.sign()?;
let root_ksk = root_ns.key_signing_key().clone();
let root_zsk = root_ns.zone_signing_key().clone();

let root_ns = root_ns.start()?;

let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
println!("DONE");

let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]);
println!("building docker image...");
let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?;
println!("DONE\n\n");

let resolver_addr = resolver.ipv4_addr();
let client = Client::new(&network)?;
// generate `/etc/bind.keys`
client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?;

let (tx, rx) = mpsc::channel();

ctrlc::set_handler(move || tx.send(()).expect("could not forward signal"))?;

println!(". (root) name server's IP address: {}", root_ns.ipv4_addr());
println!(
"attach to this container with: `docker exec -it {} bash`\n",
root_ns.container_id()
);

println!("com. name server's IP address: {}", com_ns.ipv4_addr());
println!(
"attach to this container with: `docker exec -it {} bash`\n",
com_ns.container_id()
);

println!(
"nameservers.com. name server's IP address: {}",
nameservers_ns.ipv4_addr()
);
println!(
"attach to this container with: `docker exec -it {} bash`\n",
nameservers_ns.container_id()
);

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");
println!("`dig @{resolver_addr} SOA .`\n");
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(())
}
4 changes: 4 additions & 0 deletions packages/dns-test/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ impl Client {
})
}

pub fn container_id(&self) -> &str {
self.inner.id()
}

pub fn ipv4_addr(&self) -> Ipv4Addr {
self.inner.ipv4_addr()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/dns-test/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ impl Container {
}

fn verbose_docker_build() -> bool {
env::var("DNS_TEST_VERBOSE_DOCKER_BUILD").as_deref() == Ok("1")
env::var("DNS_TEST_VERBOSE_DOCKER_BUILD").as_deref().is_ok()
}

fn exec_or_panic(command: &mut Command, verbose: bool) {
Expand Down
Loading