Skip to content

Commit

Permalink
Add a queryable zone tree (#286)
Browse files Browse the repository at this point in the history
This PR adds zonefile/parsed.rs, zonetree/ and related examples/, which
together enable, and show a library user how, to go from a zone file in
presentation format to an in-memory tree of zones which can be queried to
provide an answer, or walked (iterated over).

The tree also supports versioned write operations and a trait based zone
implementation allowing for the in-memory tree for a zone to instead be some
other (a)synchornous backing store (as demonstrated by the mysql-zone.rs
example).

The zonetree module is feature-gated behind the unstable-zonetree feature.
  • Loading branch information
ximon18 authored Apr 12, 2024
1 parent 3a36b61 commit b5b411e
Show file tree
Hide file tree
Showing 58 changed files with 4,842 additions and 562 deletions.
36 changes: 30 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ futures = { version = "0.3.22", optional = true } # Force futures to at l
futures-util = { version = "0.3", optional = true }
heapless = { version = "0.8", optional = true }
hex = { version = "0.4", optional = true }
libc = { version = "0.2.79", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT
libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT
parking_lot = { version = "0.11.2", optional = true }
moka = { version = "0.12.3", optional = true, features = ["future"] }
proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to at least 1.0.69 for minimal-version build
ring = { version = "0.17", optional = true }
Expand All @@ -45,7 +46,7 @@ mock_instant = { version = "0.3.2", optional = true, features = ["sync"] }

[target.'cfg(macos)'.dependencies]
# specifying this overrides minimum-version mio's 0.2.69 libc dependency, which allows the build to work
libc = { version = "0.2.71", default-features = false, optional = true }
libc = { version = "0.2.153", default-features = false, optional = true }

[features]
default = ["std", "rand"]
Expand All @@ -65,6 +66,7 @@ zonefile = ["bytes", "serde", "std"]
# Unstable features
unstable-client-transport = [ "moka", "tracing" ]
unstable-server-transport = ["arc-swap", "chrono/clock", "hex", "libc", "tracing"]
unstable-zonetree = ["futures", "parking_lot", "serde", "tokio", "tracing"]

# Test features
# Commented out as using --all-features to build would cause mock time to also
Expand Down Expand Up @@ -92,14 +94,13 @@ tokio-tfo = { version = "0.2.0" }
lazy_static = { version = "1.4.0" } # Force lazy_static to > 1.0.0 for https://github.com/rust-lang-nursery/lazy-static.rs/pull/107
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

# For the "mysql-zone" example
#sqlx = { version = "0.6", features = [ "runtime-tokio-native-tls", "mysql" ] }

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[[example]]
name = "readzone"
required-features = ["zonefile"]

[[example]]
name = "download-rust-lang"
required-features = ["resolv"]
Expand All @@ -123,3 +124,26 @@ required-features = ["net", "unstable-client-transport"]
[[example]]
name = "server-transports"
required-features = ["net", "unstable-server-transport"]

[[example]]
name = "read-zone"
required-features = ["zonefile"]

[[example]]
name = "query-zone"
required-features = ["zonefile", "unstable-zonetree"]

[[example]]
name = "serve-zone"
required-features = ["zonefile", "net", "unstable-server-transport", "unstable-zonetree"]

# This example is commented out because it is difficult, if not impossible,
# when including the sqlx dependency, to make the dependency tree compatible
# with both `cargo +nightly update -Z minimal versions` and the crate minimum
# supported Rust version (1.67 at the time of writing), both of which are
# tested by our CI setup. To try this example, uncomment the lines below and
# the sqlx dependency above, then run `cargo run --example mysql-zone`.
#[[example]]
#name = "mysql-zone"
#path = "examples/other/mysql-zone.rs"
#required-features = ["zonefile", "net", "unstable-server-transport", "unstable-zonetree"]
19 changes: 9 additions & 10 deletions examples/client-transports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async fn main() {
// Get the reply
println!("Wating for UDP+TCP reply");
let reply = request.get_response().await;
println!("UDP+TCP reply: {:?}", reply);
println!("UDP+TCP reply: {reply:?}");

// The query may have a reference to the connection. Drop the query
// when it is no longer needed.
Expand All @@ -94,15 +94,15 @@ async fn main() {
// Get the reply
println!("Wating for cache reply");
let reply = request.get_response().await;
println!("Cache reply: {:?}", reply);
println!("Cache reply: {reply:?}");

// Send the request message again.
let mut request = cache.send_request(req.clone());

// Get the reply
println!("Wating for cached reply");
let reply = request.get_response().await;
println!("Cached reply: {:?}", reply);
println!("Cached reply: {reply:?}");

// Create a new TCP connections object. Pass the destination address and
// port as parameter.
Expand Down Expand Up @@ -130,7 +130,7 @@ async fn main() {
println!("Wating for multi TCP reply");
let reply =
timeout(Duration::from_millis(500), request.get_response()).await;
println!("multi TCP reply: {:?}", reply);
println!("multi TCP reply: {reply:?}");

drop(request);

Expand Down Expand Up @@ -181,7 +181,7 @@ async fn main() {
println!("Wating for TLS reply");
let reply =
timeout(Duration::from_millis(500), request.get_response()).await;
println!("TLS reply: {:?}", reply);
println!("TLS reply: {reply:?}");

drop(request);

Expand All @@ -205,7 +205,7 @@ async fn main() {
let mut request = redun.send_request(req.clone());
let reply = request.get_response().await;
if i == 2 {
println!("redundant connection reply: {:?}", reply);
println!("redundant connection reply: {reply:?}");
}
}

Expand All @@ -224,16 +224,15 @@ async fn main() {
//
// Get the reply
let reply = request.get_response().await;
println!("Dgram reply: {:?}", reply);
println!("Dgram reply: {reply:?}");

// Create a single TCP transport connection. This is usefull for a
// single request or a small burst of requests.
let tcp_conn = match TcpStream::connect(server_addr).await {
Ok(conn) => conn,
Err(err) => {
println!(
"TCP Connection to {} failed: {}, exiting",
server_addr, err
"TCP Connection to {server_addr} failed: {err}, exiting",
);
return;
}
Expand All @@ -250,7 +249,7 @@ async fn main() {

// Get the reply
let reply = request.get_response().await;
println!("TCP reply: {:?}", reply);
println!("TCP reply: {reply:?}");

drop(tcp);
}
4 changes: 2 additions & 2 deletions examples/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ fn main() {
for option in response.opt().unwrap().opt().iter::<AllOptData<_, _>>() {
let opt = option.unwrap();
match opt {
AllOptData::Nsid(nsid) => println!("{}", nsid),
AllOptData::Nsid(nsid) => println!("{nsid}"),
AllOptData::ExtendedError(extendederror) => {
println!("{}", extendederror)
println!("{extendederror}")
}
_ => println!("NO OPT!"),
}
Expand Down
119 changes: 119 additions & 0 deletions examples/common/serve-utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use bytes::Bytes;
use domain::base::{Dname, Message, MessageBuilder, ParsedDname, Rtype};
use domain::rdata::ZoneRecordData;
use domain::zonetree::Answer;

pub fn generate_wire_query(
qname: &Dname<Bytes>,
qtype: Rtype,
) -> Message<Vec<u8>> {
let query = MessageBuilder::new_vec();
let mut query = query.question();
query.push((qname, qtype)).unwrap();
query.into()
}

pub fn generate_wire_response(
wire_query: &Message<Vec<u8>>,
zone_answer: Answer,
) -> Message<Vec<u8>> {
let builder = MessageBuilder::new_vec();
let response = zone_answer.to_message(wire_query, builder);
response.into()
}

pub fn print_dig_style_response(
query: &Message<Vec<u8>>,
response: &Message<Vec<u8>>,
short: bool,
) {
if !short {
let qh = query.header();
let rh = response.header();
println!("; (1 server found)");
println!(";; global options:");
println!(";; Got answer:");
println!(
";; ->>HEADER<<- opcode: {}, status: {}, id: {}",
qh.opcode(),
rh.rcode(),
rh.id()
);
print!(";; flags: ");
if rh.aa() {
print!("aa ");
}
if rh.ad() {
print!("ad ");
}
if rh.cd() {
print!("cd ");
}
if rh.qr() {
print!("qr ");
}
if rh.ra() {
print!("ra ");
}
if rh.rd() {
print!("rd ");
}
if rh.tc() {
print!("tc ");
}
let counts = response.header_counts();
println!(
"; QUERY: {}, ANSWER: {}, AUTHORITY: {}, ADDITIONAL: {}",
counts.qdcount(),
counts.ancount(),
counts.arcount(),
counts.adcount()
);

// TODO: add OPT PSEUDOSECTION

if let Ok(question) = query.sole_question() {
println!(";; QUESTION SECTION:");
println!(
";{} {} {}",
question.qname(),
question.qclass(),
question.qtype()
);
println!();
}
}

let sections = [
("ANSWER", response.answer()),
("AUTHORITY", response.authority()),
("ADDITIONAL", response.additional()),
];
for (name, section) in sections {
if let Ok(section) = section {
if section.count() > 0 {
if !short {
println!(";; {name} SECTION:");
}

for record in section {
let record = record
.unwrap()
.into_record::<ZoneRecordData<_, ParsedDname<_>>>()
.unwrap()
.unwrap();

if short {
println!("{}", record.data());
} else {
println!("{record}");
}
}

if !short {
println!();
}
}
}
}
}
8 changes: 4 additions & 4 deletions examples/download-rust-lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async fn main() {
{
Ok(addr) => addr,
Err(err) => {
eprintln!("DNS query failed: {}", err);
eprintln!("DNS query failed: {err}");
return;
}
};
Expand All @@ -30,7 +30,7 @@ async fn main() {
let mut socket = match TcpStream::connect(&addr).await {
Ok(socket) => socket,
Err(err) => {
eprintln!("Failed to connect to {}: {}", addr, err);
eprintln!("Failed to connect to {addr}: {err}");
return;
}
};
Expand All @@ -45,12 +45,12 @@ async fn main() {
)
.await
{
eprintln!("Failed to send request: {}", err);
eprintln!("Failed to send request: {err}");
return;
};
let mut response = Vec::new();
if let Err(err) = socket.read_to_end(&mut response).await {
eprintln!("Failed to read response: {}", err);
eprintln!("Failed to read response: {err}");
return;
}

Expand Down
12 changes: 6 additions & 6 deletions examples/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ async fn forward(resolver: &StubResolver, name: UncertainDname<Vec<u8>>) {
}
let canon = answer.canonical_name();
if canon != answer.qname() {
println!("{} is an alias for {}", answer.qname(), canon);
println!("{} is an alias for {canon}", answer.qname());
}
for addr in answer.iter() {
println!("{} has address {}", canon, addr);
println!("{canon} has address {addr}");
}
}
Err(err) => {
println!("Query failed: {}", err);
println!("Query failed: {err}");
}
}
}
Expand All @@ -36,10 +36,10 @@ async fn reverse(resolver: &StubResolver, addr: IpAddr) {
match resolver.lookup_addr(addr).await {
Ok(answer) => {
for name in answer.iter() {
println!("Host {} has domain name pointer {}", addr, name);
println!("Host {addr} has domain name pointer {name}");
}
}
Err(err) => println!("Query failed: {}", err),
Err(err) => println!("Query failed: {err}"),
}
}

Expand All @@ -58,7 +58,7 @@ async fn main() {
} else if let Ok(name) = UncertainDname::from_str(&name) {
forward(&resolver, name).await;
} else {
println!("Not a domain name: {}", name);
println!("Not a domain name: {name}");
}
}
}
Loading

0 comments on commit b5b411e

Please sign in to comment.