diff --git a/Cargo.lock b/Cargo.lock index 46f2b24b..20877964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/packages/conformance-tests/src/resolver/dnssec.rs b/packages/conformance-tests/src/resolver/dnssec.rs index 63400356..f408ef38 100644 --- a/packages/conformance-tests/src/resolver/dnssec.rs +++ b/packages/conformance-tests/src/resolver/dnssec.rs @@ -1,3 +1,4 @@ //! DNSSEC functionality +mod rfc4035; mod scenarios; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs new file mode 100644 index 00000000..289eace0 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs @@ -0,0 +1 @@ +mod section_4; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs new file mode 100644 index 00000000..5779d78f --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs @@ -0,0 +1 @@ +mod section_4_1; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs new file mode 100644 index 00000000..533e31b3 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -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(()) +} diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml index a9c5e601..0851b3e4 100644 --- a/packages/dns-test/Cargo.toml +++ b/packages/dns-test/Cargo.toml @@ -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] diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index bfc57b7b..2119f24b 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -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 { diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index b15f0e14..149f6e89 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -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, } @@ -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 { + 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 { let output = self.inner.take().expect("unreachable").wait_with_output()?; output.try_into() diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 18a625d7..6c9d32e8 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -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)] diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 1189b9cf..66bbb1bf 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -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 { + self.container.eavesdrop() + } + /// gracefully terminates the name server collecting all logs pub fn terminate(self) -> Result { let pidfile = "/run/nsd/nsd.pid"; diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 3a56d11a..02e40721 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -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 { + self.container.eavesdrop() + } + pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs new file mode 100644 index 00000000..d07fe7eb --- /dev/null +++ b/packages/dns-test/src/tshark.rs @@ -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 { + 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>, +} + +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 { + // 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> { + 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 = 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 { + 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 { + 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 { + if let Self::Incoming { source } = self { + Ok(source) + } else { + Err(self) + } + } + + pub fn try_into_outgoing(self) -> CoreResult { + 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(()) + } +}