Merge pull request #11 from japaric/dns-shark

add eavesdrop API & test a RFC requirement with it
This commit is contained in:
Jorge Aparicio 2024-02-20 10:23:32 +01:00 committed by GitHub
commit 3707bdfd3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 882 additions and 6 deletions

385
Cargo.lock generated
View File

@ -2,6 +2,33 @@
# 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"
@ -14,12 +41,40 @@ 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"
@ -27,14 +82,74 @@ dependencies = [
"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 = "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 = "dns-test"
version = "0.1.0"
dependencies = [
"minijinja",
"serde",
"serde_json",
"serde_with",
"tempfile",
]
[[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"
@ -51,6 +166,96 @@ 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 = "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 = "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 = "libc"
version = "0.2.153"
@ -63,6 +268,12 @@ 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"
@ -72,6 +283,33 @@ dependencies = [
"serde",
]
[[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 = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.78"
@ -112,6 +350,12 @@ dependencies = [
"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"
@ -132,6 +376,53 @@ dependencies = [
"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"
@ -156,12 +447,106 @@ dependencies = [
"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 = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[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"

View File

@ -1,3 +1,4 @@
//! DNSSEC functionality
mod rfc4035;
mod scenarios;

View File

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

View File

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

View File

@ -0,0 +1,54 @@
use dns_test::client::{Client, Dnssec, Recurse};
use dns_test::name_server::NameServer;
use dns_test::record::RecordType;
use dns_test::tshark::{Capture, Direction};
use dns_test::zone_file::Root;
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
#[test]
#[ignore]
fn edns_support() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(FQDN::ROOT, network)?.start()?;
let resolver = Resolver::start(
dns_test::subject(),
&[Root::new(ns.fqdn().clone(), ns.ipv4_addr())],
&TrustAnchor::empty(),
network,
)?;
let mut tshark = resolver.eavesdrop()?;
let client = Client::new(network)?;
let ans = client.dig(
Recurse::Yes,
Dnssec::Yes,
resolver.ipv4_addr(),
RecordType::SOA,
&FQDN::ROOT,
)?;
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

@ -7,6 +7,9 @@ version = "0.1.0"
[dependencies]
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"
[lib]

View File

@ -17,6 +17,10 @@ impl Client {
})
}
pub fn ipv4_addr(&self) -> Ipv4Addr {
self.inner.ipv4_addr()
}
pub fn delv(
&self,
server: Ipv4Addr,
@ -233,6 +237,11 @@ impl DigStatus {
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 {

View File

@ -3,17 +3,17 @@ mod network;
use core::str;
use std::fs;
use std::net::Ipv4Addr;
use std::process::{self, ExitStatus};
use std::process::{self, ChildStdout, ExitStatus};
use std::process::{Command, Stdio};
use std::sync::atomic::AtomicUsize;
use std::sync::{atomic, Arc};
use tempfile::{NamedTempFile, TempDir};
pub use crate::container::network::Network;
use crate::{Error, Implementation, Result};
pub use crate::container::network::Network;
#[derive(Clone)]
pub struct Container {
inner: Arc<Inner>,
}
@ -52,9 +52,18 @@ impl Container {
let count = container_count();
let name = format!("{PACKAGE_NAME}-{implementation}-{pid}-{count}");
command
.args(["run", "--rm", "--detach", "--name", &name])
.arg("-it")
.args(["--network", network.name()])
.args([
"run",
"--rm",
"--detach",
"--cap-add=NET_RAW",
"--cap-add=NET_ADMIN",
"--network",
network.name(),
"--name",
&name,
"-it",
])
.arg(image_tag)
.args(["sleep", "infinity"]);
@ -187,6 +196,17 @@ pub struct Child {
}
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()

View File

@ -18,6 +18,7 @@ pub mod name_server;
pub mod record;
mod resolver;
mod trust_anchor;
pub mod tshark;
pub mod zone_file;
#[derive(Clone, Copy)]

View File

@ -2,6 +2,7 @@ use core::sync::atomic::{self, AtomicUsize};
use std::net::Ipv4Addr;
use crate::container::{Child, Container, Network};
use crate::tshark::Tshark;
use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS};
use crate::{Implementation, Result, FQDN};
@ -214,6 +215,11 @@ impl<'a> NameServer<'a, Signed> {
}
impl<'a> NameServer<'a, 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 = "/run/nsd/nsd.pid";

View File

@ -3,6 +3,7 @@ use std::net::Ipv4Addr;
use crate::container::{Child, Container, Network};
use crate::trust_anchor::TrustAnchor;
use crate::tshark::Tshark;
use crate::zone_file::Root;
use crate::{Implementation, Result};
@ -72,6 +73,10 @@ impl Resolver {
Ok(Self { child, container })
}
pub fn eavesdrop(&self) -> Result<Tshark> {
self.container.eavesdrop()
}
pub fn ipv4_addr(&self) -> Ipv4Addr {
self.container.ipv4_addr()
}

View File

@ -0,0 +1,390 @@
//! `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()
}
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, Dnssec, Recurse};
use crate::name_server::NameServer;
use crate::record::RecordType;
use crate::zone_file::Root;
use crate::{Implementation, Network, Resolver, TrustAnchor, FQDN};
use super::*;
#[test]
fn nameserver() -> Result<()> {
let network = &Network::new()?;
let ns = NameServer::new(FQDN::ROOT, network)?.start()?;
let mut tshark = ns.eavesdrop()?;
let client = Client::new(network)?;
let resp = client.dig(
Recurse::No,
Dnssec::No,
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(FQDN::ROOT, network)?;
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.start()?;
com_ns.referral(
nameservers_ns.zone().clone(),
nameservers_ns.fqdn().clone(),
nameservers_ns.ipv4_addr(),
);
let com_ns = com_ns.start()?;
root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr());
let root_ns = root_ns.start()?;
let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
let resolver = Resolver::start(
Implementation::Unbound,
roots,
&TrustAnchor::empty(),
network,
)?;
let mut tshark = resolver.eavesdrop()?;
let resolver_addr = resolver.ipv4_addr();
let client = Client::new(network)?;
let output = client.dig(
Recurse::Yes,
Dnssec::No,
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(())
}
}