diff --git a/Cargo.lock b/Cargo.lock index e0b9564..a82667b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -164,6 +170,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.5", ] @@ -273,6 +280,8 @@ dependencies = [ "chrono", "clap", "domain", + "serde", + "serde_json", "tempfile", "tokio", "tokio-rustls", @@ -281,21 +290,29 @@ dependencies = [ [[package]] name = "domain" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eefe29e8dd614abbee51a1616654cab123c4c56850ab83f5b7f1e1f9977bf7c" +version = "0.10.3" +source = "git+https://github.com/NLnetLabs/domain.git#f5deabdf9f762940294993570a40373f4970afaf" dependencies = [ "bytes", "futures-util", + "hashbrown", + "log", "moka", "octseq", "rand", + "serde", "smallvec", "time", "tokio", "tracing", ] +[[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.9" @@ -387,6 +404,15 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "allocator-api2", +] + [[package]] name = "heck" version = "0.5.0" @@ -422,12 +448,28 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "js-sys" version = "0.3.69" @@ -461,9 +503,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "memchr" @@ -551,11 +593,12 @@ dependencies = [ [[package]] name = "octseq" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ed2eaec452d98ccc1c615dd843fd039d9445f2fb4da114ee7e6af5fcb68be98" +checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" dependencies = [ "bytes", + "serde", "smallvec", ] @@ -620,9 +663,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -744,9 +787,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "log", "once_cell", @@ -759,21 +802,27 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -788,24 +837,37 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "slab" version = "0.4.9" @@ -851,9 +913,9 @@ checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" [[package]] name = "syn" -version = "2.0.66" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -957,12 +1019,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] @@ -972,6 +1033,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index b296663..330f5e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,16 +10,26 @@ categories = ["command-line-utilities"] readme = "README.md" keywords = ["DNS", "domain"] license = "BSD-3-Clause" -exclude = [ ".github", ".gitignore" ] +exclude = [".github", ".gitignore"] [dependencies] -bytes = "1" -clap = { version = "4", features = ["derive", "unstable-doc"] } -chrono = { version = "0.4.38", features = [ "alloc", "clock" ] } -domain = { version = "0.10", features = ["resolv", "unstable-client-transport"]} +bytes = "1" +chrono = { version = "0.4.38", features = ["alloc", "clock", "serde"] } +clap = { version = "4", features = ["derive", "unstable-doc"] } +domain = { version = "0.10", git = "https://github.com/NLnetLabs/domain.git", features = [ + "resolv", + "unstable-client-transport", + "serde", +] } +serde = { version = "1.0.217", features = ["derive"] } +serde_json = { version = "1.0.135", features = ["preserve_order"] } tempfile = "3.1.0" -tokio = { version = "1.33", features = ["rt-multi-thread"] } -tokio-rustls = { version = "0.26.0", default-features = false, features = [ "ring", "logging", "tls12" ] } +tokio = { version = "1.33", features = ["rt-multi-thread"] } +tokio-rustls = { version = "0.26.1", default-features = false, features = [ + "ring", + "logging", + "tls12", +] } webpki-roots = "0.26.3" [package.metadata.deb] @@ -30,5 +40,5 @@ aspects of the DNS.""" [package.metadata.generate-rpm] license = "BSD" assets = [ - { source = "target/release/dnsi", dest = "/usr/bin/dnsi", mode = "755" }, + { source = "target/release/dnsi", dest = "/usr/bin/dnsi", mode = "755" }, ] diff --git a/src/client.rs b/src/client.rs index f2d1093..1b8f685 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,9 +8,12 @@ use domain::base::message_builder::MessageBuilder; use domain::base::name::ToName; use domain::base::question::Question; use domain::net::client::protocol::UdpConnect; -use domain::net::client::request::{RequestMessage, SendRequest}; +use domain::net::client::request::{ + RequestMessage, RequestMessageMulti, SendRequest, +}; use domain::net::client::{dgram, stream}; use domain::resolv::stub::conf; +use serde::{Serialize, Serializer}; use std::fmt; use std::net::SocketAddr; use std::sync::Arc; @@ -62,7 +65,7 @@ impl Client { let mut res = res.question(); res.push(question.into()).unwrap(); - self.request(RequestMessage::new(res)).await + self.request(RequestMessage::new(res)?).await } pub async fn request( @@ -132,9 +135,11 @@ impl Client { ) -> Result { let mut stats = Stats::new(server.addr, Protocol::Tcp); let socket = TcpStream::connect(server.addr).await?; - let (conn, tran) = stream::Connection::with_config( - socket, - Self::stream_config(server), + let (conn, tran) = stream::Connection::< + _, + RequestMessageMulti>, + >::with_config( + socket, Self::stream_config(server) ); tokio::spawn(tran.run()); let message = conn.send_request(request).get_response().await?; @@ -170,9 +175,11 @@ impl Client { })?; let tls_socket = tls_connector.connect(server_name, tcp_socket).await?; - let (conn, tran) = stream::Connection::with_config( - tls_socket, - Self::stream_config(server), + let (conn, tran) = stream::Connection::< + _, + RequestMessageMulti>, + >::with_config( + tls_socket, Self::stream_config(server) ); tokio::spawn(tran.run()); let message = conn.send_request(request).get_response().await?; @@ -256,14 +263,26 @@ impl AsRef> for Answer { //------------ Stats --------------------------------------------------------- -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Serialize)] pub struct Stats { pub start: DateTime, + #[serde(serialize_with = "serialize_time_delta")] pub duration: TimeDelta, pub server_addr: SocketAddr, pub server_proto: Protocol, } +fn serialize_time_delta( + t: &TimeDelta, + serializer: S, +) -> Result +where + S: Serializer, +{ + let msecs = t.num_milliseconds(); + serializer.serialize_i64(msecs) +} + impl Stats { fn new(server_addr: SocketAddr, server_proto: Protocol) -> Self { Stats { @@ -281,7 +300,8 @@ impl Stats { //------------ Protocol ------------------------------------------------------ -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "UPPERCASE")] pub enum Protocol { Udp, Tcp, diff --git a/src/commands/query.rs b/src/commands/query.rs index 9c09d6a..18e54ec 100644 --- a/src/commands/query.rs +++ b/src/commands/query.rs @@ -320,7 +320,7 @@ impl Query { let mut res = res.question(); res.push((&self.qname.to_name(), self.qtype())).unwrap(); - let mut req = RequestMessage::new(res); + let mut req = RequestMessage::new(res).unwrap(); if self.dnssec_ok { // Avoid touching the EDNS Opt record unless we need to set DO. req.set_dnssec_ok(true); diff --git a/src/output/friendly.rs b/src/output/friendly.rs index 6513034..913d276 100644 --- a/src/output/friendly.rs +++ b/src/output/friendly.rs @@ -130,7 +130,7 @@ fn write_opt( Expire(expire) => ("EXPIRE", expire.to_string()), TcpKeepalive(opt) => ("TCPKEEPALIVE", opt.to_string()), Padding(padding) => { - let padding = padding.as_slice(); + let padding = padding.as_slice(); let len = padding.len(); let all_zero = if padding.iter().all(|b| *b == 0) { "all zero" diff --git a/src/output/json.rs b/src/output/json.rs new file mode 100644 index 0000000..0e8192e --- /dev/null +++ b/src/output/json.rs @@ -0,0 +1,135 @@ +use crate::client::{Answer, Stats}; +use bytes::Bytes; +use domain::base::iana::{Class, Opcode}; +use domain::base::{ParsedName, Rtype, Ttl}; +use domain::rdata::AllRecordData; +use serde::Serialize; +use std::io; + +use super::error::OutputError; + +#[derive(Serialize)] +struct AnswerOuput { + message: MessageOutput, + stats: Stats, +} + +#[derive(Serialize)] +struct MessageOutput { + id: u16, + qr: bool, + opcode: Opcode, + qdcount: u16, + ancount: u16, + nscount: u16, + arcount: u16, + question: QuestionOutput, + answer: Vec, + authority: Vec, + additional: Vec, +} + +#[derive(Serialize)] +struct QuestionOutput { + name: String, + r#type: Rtype, + class: Class, +} + +#[derive(Serialize)] +struct RecordOutput { + owner: String, + class: Class, + r#type: Rtype, + ttl: Ttl, + data: AllRecordData>, +} + +pub fn write( + answer: &Answer, + target: &mut impl io::Write, +) -> Result<(), OutputError> { + let msg = answer.message(); + let stats = answer.stats(); + let header = msg.header(); + let counts = msg.header_counts(); + + let q = msg.question().next().unwrap().unwrap(); + + // We declare them all up front so that we have sensible defaults if the + // message turns out to be invalid. + let mut answer = Vec::new(); + let mut authority = Vec::new(); + let mut additional = Vec::new(); + + 'outer: { + let Ok(section) = msg.answer() else { + break 'outer; + }; + + for rec in section.limit_to::>() { + let Ok(rec) = rec else { + break; + }; + + answer.push(RecordOutput { + owner: rec.owner().to_string(), + class: rec.class(), + r#type: rec.rtype(), + ttl: rec.ttl(), + data: rec.data().clone(), + }); + } + + let Ok(mut section) = msg.answer() else { + break 'outer; + }; + + for v in [&mut answer, &mut authority, &mut additional] { + let iter = section.limit_to::>(); + + for rec in iter { + let Ok(rec) = rec else { + break 'outer; + }; + + v.push(RecordOutput { + owner: format!("{}.", rec.owner()), + class: rec.class(), + r#type: rec.rtype(), + ttl: rec.ttl(), + data: rec.data().clone(), + }); + } + + let Ok(Some(s)) = section.next_section() else { + break; + }; + section = s; + } + } + + let output = AnswerOuput { + message: MessageOutput { + id: header.id(), + qr: header.qr(), + opcode: header.opcode(), + qdcount: counts.qdcount(), + ancount: counts.ancount(), + nscount: counts.nscount(), + arcount: counts.arcount(), + question: QuestionOutput { + name: format!("{}.", q.qname()), + r#type: q.qtype(), + class: q.qclass(), + }, + answer, + authority, + additional, + }, + stats, + }; + + serde_json::to_writer_pretty(target, &output).unwrap(); + Ok(()) +} diff --git a/src/output/mod.rs b/src/output/mod.rs index a589bbd..c1d6f2a 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -4,6 +4,8 @@ mod ansi; mod dig; mod error; mod friendly; +mod json; +mod rfc8427; mod table; mod table_writer; mod ttl; @@ -25,6 +27,10 @@ pub enum OutputFormat { /// Short readable format Table, + /// Simple JSON format + Json, + /// JSON based on RFC 8427 + RFC8427, } #[derive(Clone, Debug, Parser)] @@ -43,6 +49,8 @@ impl OutputFormat { Self::Dig => self::dig::write(msg, target), Self::Friendly => self::friendly::write(msg, target), Self::Table => self::table::write(msg, target), + Self::Json => self::json::write(msg, target), + Self::RFC8427 => self::rfc8427::write(msg, target), }; match res { Ok(()) => Ok(()), diff --git a/src/output/rfc8427.rs b/src/output/rfc8427.rs new file mode 100644 index 0000000..2bbca10 --- /dev/null +++ b/src/output/rfc8427.rs @@ -0,0 +1,336 @@ +//! Format based on [RFC 8427] +//! +//! [RFC 8427]: https://tools.ietf.org/html/rfc8427 + +use domain::{ + base::{ + iana::Class, + name::FlattenInto, + opt::{AllOptData, OptRecord}, + Header, Name, ParsedName, ParsedRecord, Rtype, UnknownRecordData, + }, + rdata::AllRecordData, + utils::base16, +}; +use serde_json::{json, Map, Value}; + +use crate::client::Answer; +use std::io; + +use super::error::OutputError; + +pub fn write( + answer: &Answer, + mut target: &mut impl io::Write, +) -> Result<(), OutputError> { + let mut map = serde_json::Map::new(); + + fill_map(&mut map, answer); + + serde_json::to_writer_pretty(&mut target, &map).unwrap(); + writeln!(target)?; + Ok(()) +} + +fn fill_map(map: &mut Map, answer: &Answer) { + let stats = answer.stats(); + let msg = answer.msg_slice(); + + insert( + map, + "dateString", + stats + .start + .to_rfc3339_opts(chrono::SecondsFormat::Secs, false), + ); + insert(map, "dateSeconds", stats.start.timestamp()); + insert(map, "msgLength", msg.as_slice().len()); + + let header = msg.header(); + insert(map, "ID", header.id()); + insert(map, "QR", header.qr() as u8); + insert(map, "Opcode", header.opcode().to_int()); + + insert_many( + map, + [ + ("AA", header.aa() as u8), + ("TC", header.tc() as u8), + ("RD", header.rd() as u8), + ("RA", header.ra() as u8), + ("AD", header.ad() as u8), + ("CD", header.cd() as u8), + ], + ); + + insert(map, "RCODE", header.rcode().to_int()); + + let counts = msg.header_counts(); + insert_many( + map, + [ + ("QDCOUNT", counts.qdcount()), + ("ANCOUNT", counts.ancount()), + ("NSCOUNT", counts.nscount()), + ("ARCOUNT", counts.arcount()), + ], + ); + + // The RFC says + // + // > QNAME - String of the name of the first Question section of the + // > message; see Section 2.6 for a description of the contents + // + // and the same for the QTYPE and QCLASS so we take the first question + // (if it's there). + let mut questions = msg.question(); + if let Some(Ok(q)) = questions.next() { + insert(map, "QNAME", q.qname().to_string()); + + let qtype = q.qtype(); + insert(map, "QTYPE", qtype.to_int()); + if let Some(s) = rtype_mnemomic(qtype) { + insert(map, "QTYPEname", s); + } + + let qclass = q.qclass(); + insert(map, "QCLASS", qclass.to_int()); + if let Some(s) = class_mnemomic(qclass) { + insert(map, "QCLASSname", s); + } + } + + // Restart the iterator because we need all records here. + let questions = msg.question(); + + let mut rrs: Vec = Vec::new(); + for q in questions.flatten() { + let mut rr = serde_json::Map::new(); + + insert(&mut rr, "TYPE", q.qtype().to_int()); + if let Some(s) = rtype_mnemomic(q.qtype()) { + insert(&mut rr, "TYPEname", s); + } + + insert(&mut rr, "CLASS", q.qclass().to_int()); + if let Some(s) = class_mnemomic(q.qclass()) { + insert(&mut rr, "CLASSname", s); + } + + rrs.push(rr.into()); + } + + insert(map, "questionRRs", rrs); + + let section = questions.next_section().ok(); + + let mut rrs = Vec::new(); + if let Some(section) = section { + for a in section.into_iter().flatten() { + let mut rr = Map::new(); + record_map(&mut rr, a); + rrs.push(rr) + } + } + + insert(map, "answerRRs", rrs); + + let section = section.and_then(|s| s.next_section().ok()).flatten(); + + let mut rrs = Vec::new(); + if let Some(section) = section { + for a in section.flatten() { + let mut rr = Map::new(); + record_map(&mut rr, a); + rrs.push(rr) + } + } + + insert(map, "authorityRRs", rrs); + + let section = section.and_then(|s| s.next_section().ok()).flatten(); + + let mut rrs = Vec::new(); + if let Some(section) = section { + for a in section.flatten() { + if a.rtype() == Rtype::OPT { + continue; + } + let mut rr = Map::new(); + record_map(&mut rr, a); + rrs.push(rr) + } + } + + insert(map, "additionalRRs", rrs); + + if let Some(opt) = msg.opt() { + let mut edns = Map::new(); + edns_map(&mut edns, &opt, header); + insert(map, "EDNS", edns); + } +} + +fn insert(m: &mut Map, k: impl ToString, v: impl Into) { + m.insert(k.to_string(), v.into()); +} + +fn insert_many>( + m: &mut Map, + i: impl IntoIterator, +) { + for (k, v) in i { + m.insert(k.to_string(), v.into()); + } +} + +fn record_map(rr: &mut Map, r: ParsedRecord<&[u8]>) { + // Necessary for Rust 1.81 or lower + #[allow(irrefutable_let_patterns)] + let Ok(name): Result>, _> = r.owner().try_flatten_into() else { + unreachable!() + }; + insert(rr, "NAME", name.fmt_with_dot().to_string()); + + insert(rr, "TYPE", r.rtype().to_int()); + if let Some(s) = rtype_mnemomic(r.rtype()) { + insert(rr, "TYPEname", s); + } + + insert(rr, "CLASS", r.class().to_int()); + if let Some(s) = class_mnemomic(r.class()) { + insert(rr, "CLASSname", s); + } + + insert(rr, "TTL", r.ttl().as_secs()); + + if let Ok(rec) = + r.to_any_record::>>() + { + let ty = rtype_mnemomic(rec.rtype()).unwrap(); + let data = rec.data().to_string(); + insert(rr, format!("rdata{ty}"), data); + } + + insert(rr, "RDLENGTH", r.rdlen()); + + if let Ok(Some(data)) = r.to_record::>() { + insert(rr, "RDATAHEX", hex(data.data().data())); + } +} + +/// Based on [draft-peltan-edns-presentation-format-03] +/// +/// [draft-peltan-edns-presentation-format-03]: https://www.ietf.org/archive/id/draft-peltan-edns-presentation-format-03.html +fn edns_map( + map: &mut Map, + opt: &OptRecord<&[u8]>, + header: Header, +) { + insert(map, "version", opt.version()); + insert( + map, + "flags", + if opt.dnssec_ok() { &["DO"][..] } else { &[] }, + ); + insert(map, "rcode", opt.rcode(header).to_string()); + insert(map, "udpsize", opt.udp_payload_size()); + for option in opt.opt().iter::>() { + use AllOptData::*; + let Ok(option) = option else { + continue; + }; + match option { + Dau(dau) => insert( + map, + "DAU", + dau.iter().map(|x| x.to_int()).collect::>(), + ), + Dhu(dhu) => insert( + map, + "DHU", + dhu.iter().map(|x| x.to_int()).collect::>(), + ), + N3u(n3u) => insert( + map, + "N3U", + n3u.iter().map(|x| x.to_int()).collect::>(), + ), + Chain(chain) => insert(map, "CHAIN", chain.to_string()), + Cookie(cookie) => { + let cc = cookie.client().to_string(); + match cookie.server() { + Some(sc) => { + insert(map, "COOKIE", &[cc, sc.to_string()][..]) + } + None => insert(map, "COOKIE", &[cc][..]), + }; + } + Expire(expire) => { + match expire.expire() { + Some(x) => insert(map, "EXPIRE", x), + None => insert(map, "EXPIRE", "NONE"), + }; + } + ExtendedError(error) => insert( + map, + "EDE", + json!({ + "CODE": error.code().to_int(), + "Purpose": error.code().to_mnemonic().unwrap_or(b""), + "TEXT": if let Some(Ok(s)) = error.text() { + s + } else { + "" + }, + }), + ), + TcpKeepalive(tcpkeepalive) => { + insert( + map, + "KEEPALIVE", + // According to the EDNS RFC draft, this is not optional, + // but it is optional in a DNS messsage. + tcpkeepalive.timeout().map(Into::::into), + ) + } + KeyTag(keytag) => { + insert(map, "KEYTAG", keytag.iter().collect::>()) + } + Nsid(nsid) => insert( + map, + "NSID", + json!({ + "HEX": hex(nsid.as_slice()), + // The draft is inconsistent about TEXT vs TXT + "TEXT": std::str::from_utf8(nsid.as_slice()).unwrap_or(""), + }), + ), + Padding(padding) => insert( + map, + "PADDING", + json!({ + "LENGTH": padding.as_slice().len(), + "HEX": hex(padding.as_slice()), + }), + ), + ClientSubnet(subnet) => insert(map, "ECS", subnet.to_string()), + Other(opt) => { + insert(map, format!("OPT{}", opt.code()), hex(opt.as_slice())) + } + _ => {} + } + } +} + +fn hex(x: &[u8]) -> String { + base16::encode_string(x) +} + +fn rtype_mnemomic(x: Rtype) -> Option<&'static str> { + x.to_mnemonic().and_then(|m| std::str::from_utf8(m).ok()) +} + +fn class_mnemomic(x: Class) -> Option<&'static str> { + x.to_mnemonic().and_then(|m| std::str::from_utf8(m).ok()) +}