Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document & refactor Stelline a bit #433

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,5 @@ pub mod validate;
pub mod validator;
pub mod zonefile;
pub mod zonetree;

mod logging;
21 changes: 21 additions & 0 deletions src/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! Common logging functions

/// Setup logging of events reported by domain and the test suite.
///
/// Use the RUST_LOG environment variable to override the defaults.
///
/// E.g. To enable debug level logging:
///
/// ```bash
/// RUST_LOG=DEBUG
/// ```
#[cfg(feature = "tracing-subscriber")]
pub fn init_logging() {

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (1.78.0)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (1.78.0)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (1.78.0)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (1.78.0)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (stable)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (stable)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (stable)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (stable)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (beta)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (beta)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (beta)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (beta)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (nightly)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (nightly)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (nightly)

function `init_logging` is never used

Check warning on line 13 in src/logging.rs

View workflow job for this annotation

GitHub Actions / Build examples (nightly)

function `init_logging` is never used
use tracing_subscriber::EnvFilter;
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_thread_ids(true)
.without_time()
.try_init()
.ok();
}
2 changes: 1 addition & 1 deletion src/net/client/validator_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ fn parse_server_config(config: &Config) -> TrustAnchors {
ta.add_u8(a.trim_matches('"').as_bytes()).unwrap();
}
_ => {
eprintln!("Ignoring unknown server setting '{setting}' with value: {value}");
eprintln!("Ignoring unknown server setting '{setting}' with value: {value:?}");
}
}
}
Expand Down
71 changes: 27 additions & 44 deletions src/net/server/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::base::net::IpAddr;
use crate::base::wire::Composer;
use crate::base::Name;
use crate::base::Rtype;
use crate::logging::init_logging;
use crate::net::client::request::{RequestMessage, RequestMessageMulti};
use crate::net::client::{dgram, stream, tsig};
use crate::net::server;
Expand Down Expand Up @@ -70,15 +71,7 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) {
// which responses will be expected, and how the server that answers them
// should be configured.

// Initialize tracing based logging. Override with env var RUST_LOG, e.g.
// RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step
// numbers and types as they are being executed.
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_thread_ids(true)
.without_time()
.try_init()
.ok();
init_logging();

// Load the test .rpl file that determines which queries will be sent
// and which responses will be expected, and how the server that
Expand Down Expand Up @@ -278,7 +271,7 @@ fn mk_client_factory(
// query, and (b) if the query specifies "MATCHES TCP". Clients created by
// this factory connect to the TCP server created above.
let only_for_tcp_queries = |entry: &parse_stelline::Entry| {
matches!(entry.matches, Some(Matches { tcp: true, .. }))
matches!(entry.matches, Matches { tcp: true, .. })
};

let tcp_key_store = key_store.clone();
Expand All @@ -304,11 +297,9 @@ fn mk_client_factory(

let conn = Box::new(tsig::Connection::new(key, conn));

if let Some(sections) = &entry.sections {
if let Some(q) = sections.question.first() {
if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) {
return Client::Multi(conn);
}
if let Some(q) = entry.sections.question.first() {
if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) {
return Client::Multi(conn);
}
}
Client::Single(conn)
Expand All @@ -322,11 +313,9 @@ fn mk_client_factory(

let conn = Box::new(conn);

if let Some(sections) = &entry.sections {
if let Some(q) = sections.question.first() {
if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) {
return Client::Multi(conn);
}
if let Some(q) = entry.sections.question.first() {
if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) {
return Client::Multi(conn);
}
}
Client::Single(conn)
Expand All @@ -350,33 +339,27 @@ fn mk_client_factory(
});

if let Some(key) = key {
match entry.matches.as_ref().map(|v| v.mock_client) {
Some(true) => {
Client::Single(Box::new(tsig::Connection::new(
key,
simple_dgram_client::Connection::new(connect),
)))
}

_ => Client::Single(Box::new(tsig::Connection::new(
if entry.matches.mock_client {
Client::Single(Box::new(tsig::Connection::new(
key,
simple_dgram_client::Connection::new(connect),
)))
} else {
Client::Single(Box::new(tsig::Connection::new(
key,
dgram::Connection::new(connect),
))),
)))
}
} else if entry.matches.mock_client {
Client::Single(Box::new(
simple_dgram_client::Connection::new(connect),
))
} else {
match entry.matches.as_ref().map(|v| v.mock_client) {
Some(true) => Client::Single(Box::new(
simple_dgram_client::Connection::new(connect),
)),

_ => {
let mut config = dgram::Config::new();
config.set_max_retries(0);
Client::Single(Box::new(
dgram::Connection::with_config(connect, config),
))
}
}
let mut config = dgram::Config::new();
config.set_max_retries(0);
Client::Single(Box::new(dgram::Connection::with_config(
connect, config,
)))
}
},
for_all_other_queries,
Expand Down Expand Up @@ -552,7 +535,7 @@ fn parse_server_config(config: &Config) -> ServerConfig {
zone_name = Some(v.to_string());
}
_ => {
eprintln!("Ignoring unknown server setting '{setting}' with value: {value}");
eprintln!("Ignoring unknown server setting '{setting}' with value: {value:?}");
}
}
}
Expand Down
20 changes: 3 additions & 17 deletions src/net/server/tests/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ use tokio::io::{AsyncRead, AsyncWrite};
use tokio::time::sleep;
use tokio::time::Instant;
use tracing::trace;
use tracing_subscriber::EnvFilter;

use crate::base::MessageBuilder;
use crate::base::Name;
use crate::base::Rtype;
use crate::base::StaticCompressor;
use crate::base::StreamTarget;
use crate::logging::init_logging;
use crate::net::server::buf::BufSource;
use crate::net::server::message::Request;
use crate::net::server::middleware::mandatory::MandatoryMiddlewareSvc;
Expand Down Expand Up @@ -378,14 +378,7 @@ fn mk_query() -> StreamTarget<Vec<u8>> {
// waiting to allow time to elapse.
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn tcp_service_test() {
// Initialize tracing based logging. Override with env var RUST_LOG, e.g.
// RUST_LOG=trace.
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_thread_ids(true)
.without_time()
.try_init()
.ok();
init_logging();

let (srv_handle, server_status_printer_handle) = {
let fast_client = MockClientConfig {
Expand Down Expand Up @@ -477,14 +470,7 @@ async fn tcp_service_test() {

#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn tcp_client_disconnect_test() {
// Initialize tracing based logging. Override with env var RUST_LOG, e.g.
// RUST_LOG=trace.
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_thread_ids(true)
.without_time()
.try_init()
.ok();
init_logging();

let (srv_handle, server_status_printer_handle) = {
let fast_client = MockClientConfig {
Expand Down
13 changes: 1 addition & 12 deletions src/net/xfr/protocol/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::base::{
Message, MessageBuilder, ParsedName, Record, Rtype, Serial, Ttl,
};
use crate::base::{Name, ToName};
use crate::logging::init_logging;
use crate::rdata::{Aaaa, Soa, ZoneRecordData, A};
use crate::zonetree::types::{ZoneUpdate, ZoneUpdate as ZU};

Expand Down Expand Up @@ -449,18 +450,6 @@ fn mk_second_ixfr_response(

//------------ Helper functions -------------------------------------------

fn init_logging() {
// Initialize tracing based logging. Override with env var RUST_LOG, e.g.
// RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step
// numbers and types as they are being executed.
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_thread_ids(true)
.without_time()
.try_init()
.ok();
}

fn mk_request(qname: &str, qtype: Rtype) -> QuestionBuilder<BytesMut> {
let req = MessageBuilder::new_bytes();
let mut req = req.question();
Expand Down
114 changes: 114 additions & 0 deletions src/stelline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,117 @@ In both cases real `net::client` instances handle interaction with the server.
When using a mock server the replay file should define both test requests and test responses and the mock server replies with the test responses.

When using a real server the replay file defines DNS requests and server configuration and real `net::server` instances reply based on their configuration.

## Syntax of an `.rpl` file


The replay format used by Stelline is a line based configuration. The format supports line comments starting with `;`.

The format contains two sections `CONFIG` and `SCENARIO`. The format of the `CONFIG` section depends on how Stelline is used (e.g. as a client or as a server). The `CONFIG` section extends from the start of the file until the line containing the `CONFIG_END` option.

The `SCENARIO` section contains the steps to perform in the test. It starts with `SCENARIO_BEGIN` and ends with `SCENARIO_END`. Note that all `.rpl` files must end with `SCENARIO_END`.

### Steps
A scenario consists of steps. Each step is something that is executed in the test. A step can be one of the following types:

- `QUERY`
- `CHECK_ANSWER`
- `TIME_PASSES`
- `TRAFFIC` (TODO)
- `CHECK_TEMP_FILE` (TODO)
- `ASSIGN` (TODO)

In general, the syntax looks like:

```rpl
STEP id type data
```

where `id` is a positive integer, `type` on of the step types mentioned above, and `data` is the data for the step.

Steps of types `QUERY` and `CHECK_ANSWER` have entries associated with them, which are textual representations of DNS messages. These entries are simply put aafter the `STEP` declaration.

A `QUERY` step sends a query to the tested program. It can optionally have data declaring its `ADDRESS` and `KEY`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be worded differently. A query is "sent" (sort of, more on that in a minute) but I'm not sure if it goes to the "tested program", that depends on what you mean by "tested program".

Stelline tests consists of several parts:

  • A test recipe/replay script (stored in /test-data/ as .rpl files)
  • A test that defines:
    • A runner.
    • A client.
    • A network connection.
    • (optional) A server.

Stelline tests broadly fall into two categories, real client with mock responses, or real/simple client with real responses, or even a mix of both (see https://github.com/NLnetLabs/domain/blob/xfr/test-data/server/secondary_zone_lifecycle.rpl).

Examples of real clients with mock responses can be seen in the /tests/net-client*.rs tests. Each test creates a real client and connects it to the Stelline mock response "server" by using a Stelline mock Dgram connection that allows the Stelline runner to capture the requests and reply using responses from the .rpl script. These tests use the "do_client_simple()" "runner".

Examples of real clients with real responses can be seen in src/net/server/tests/integration.rs. The server_tests() fn runs tests using test-data/server/*.rpl recipe/replay scripts using the "do_client()" "runner". This runner dispatches requests using a factory supplied by the test function. The factory helps it select the right kind of client for the test, e.g. UDP, TCP, using a TSIG key or not, etc. This runner also has logic for handling large mutli-response answers such as occur with XFR. The test also does much more setup work than the client only tests as it sets up a real server that will receive requests via a mock network connection and respond to them (based on how it was configured by the test based on the .rpl file config block). The server tests and "do_client" runer also support mixing real and mock servers which is just a matter of using the right mock network connection (a connection to a real server, or a connection to the Stelline mock response "server"). Some of these tests also use a different "simple" client (defined in src/stelline/simple_dgram_client.rs) which is a real client but interferes as little as possible with the request and the response so that Stelline tests can test additional things.

So, back to the start. Does a request get "sent". Yes as far as the client code but no actual network activiity occurs, not even localhost. And what is the "tested program"? It's either the Stelline framework itself which receives the request and replies per the .rpl script, or it's a real server created by the test function.

So, your comment isn't wrong, but I'm wondering how and where we can capture these details, and how to hint in higher level docs at these different use cases and supporting logic.


```rpl
STEP 1 QUERY
STEP 1 QUERY ADDRESS <ip_address> KEY <key_name>
```

A `CHECK_ANSWER` step checks an incoming answer. It has no data, only entries.

```rpl
STEP 1 CHECK_ANSWER
```

A `TIME_PASSES` step increments the fake system clock by the specified number of seconds. The time is given after the `ELAPSE` token.

```rpl
STEP 1 TIME_PASSES ELAPSE
```

### Entries

An entry represents a DNS message (or an expected DNS message). It starts with `ENTRY_BEGIN` and ends with `ENTRY_END`. There are several parts to an entry, which depend on the use of the entry. The simplest form is the form used the `QUERY` step:

```rpl
ENTRY_BEGIN
REPLY <opcode> <rcode> <flags>
SECTION <section_type>
...
SECTION <section_type>
...
...
ENTRY_END
```

The `REPLY` part specifies the header information of the outgoing message. The supported values are:

- RCODES: `NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, `NOTIMP`, `REFUSED`, `YXDOMAIN`, `YXRRSET`, `NXRRSET`, `NOTAUTH`, `NOTZONE`, `BADVERS`, `BADCOOKIE`.
- Flags: `AA`, `AD`, `CD`, `DO`, `QR`, `RA`, `RD`, `TC`, `NOTIFY`.

The `SECTION` directives specify which sections to fill with each section with. The content of a section is exactly like a zonefile, except for the question section, which does not require rdata.

There are two other sections in an entry, which are only relevant to ranges (see below), which are `MATCH` and `ADJUST`. The `MATCH` directive specifies which parts of the incoming message must match the reply to match the entry. The following values are allowed:

- `all`: same as `opcode qtype qname rcode flags answer authority additional`
- `opcode`
- `qname`
- `rcode`
- Flags: `AD`, `CD`, `DO`, `RD`, `flags`
- `question`: same as `qtype qname`
- Sections: `answer`, `authority`, `additional`
- `subdomain`
- `ttl`
- Protocol: `TCP`, `UDP`
- `server_cookie`
- `ednsdata`
- `MOCK_CLIENT`
- `CONNECTION_CLOSED`
- `EXTRA_PACKETS`
- `ANY_ANSWER`

The `ADJUST` directive specifies which parts of the incoming message should be changed before sending. The possible values are `copy_id` and `copy_query`.

### Ranges

Mock responses from are defined by a `RANGE`, which specifies the reponses that are given to a "range" of queries. A range is delimited by the `RANGE_BEGIN` and `RANGE_END` tokens.

The `RANGE_BEGIN` directive takes two positive integers: a start and end value. A `QUERY` step with an id within that range can match this range.

After the numeric range, a range has a list of addresses that match on this range, with each address on its own line prefixed with `ADDRESS`.

Lastly, a range contains a list of entries, following the syntax described above.

```rpl
RANGE_BEGIN 0 100 ; begin and end of the range
ADDRESS 1.1.1.1 ; an address to match
ADDRESS 2.2.2.2 ; a second address, any number of address is allowed

ENTRY_BEGIN
; ...
ENTRY_END

; more entries may be added
RANGE_END
```
Loading
Loading