From abbd3a3172c722e6b4ef29032392d05ee07bfd51 Mon Sep 17 00:00:00 2001 From: Jeffsky Date: Fri, 6 Dec 2024 19:15:02 +0800 Subject: [PATCH] feat: implement client-side command --- Cargo.toml | 11 +- README.md | 27 ++++- justfile | 5 + src/client/doh.rs | 8 +- src/client/dot.rs | 4 +- src/config.rs | 4 +- src/lib.rs | 5 + src/main.rs | 268 +++++++++++++++++++++++++++++++++-------- src/misc/mod.rs | 1 + src/misc/resolvconf.rs | 8 ++ src/protocol/dns.rs | 18 ++- src/protocol/frame.rs | 170 +++++++++++++++++++++++--- 12 files changed, 443 insertions(+), 86 deletions(-) mode change 100644 => 100755 justfile create mode 100644 src/misc/resolvconf.rs diff --git a/Cargo.toml b/Cargo.toml index de10bcc..43916ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0-alpha.5" edition = "2021" license = "MIT" readme = "README.md" +authors = ["Jeffsky Tsai "] repository = "https://github.com/jjeffcaii/zerodns" homepage = "https://github.com/jjeffcaii/zerodns" description = "A DNS server in Rust, which is inspired from chinadns/dnsmasq." @@ -33,9 +34,9 @@ bytes = "1.5.0" tokio = { version = "1.36.0", features = ["full"] } tokio-util = { version = "0.7.10", features = ["full"] } anyhow = "1.0.75" -thiserror = "1.0.64" +thiserror = "2.0.4" cfg-if = "1.0.0" -clap = { version = "4.4.4", features = ["derive"] } +clap = { version = "4.5.22", features = ["derive", "cargo"] } once_cell = "1.18.0" futures = "0.3.28" parking_lot = "0.12.1" @@ -57,14 +58,16 @@ strum_macros = "0.26.1" deadpool = "0.10.0" socket2 = "0.5.5" mlua = { version = "0.9.9", features = ["luajit", "vendored", "serialize", "async", "macros", "send", "parking_lot"] } -garde = { version = "0.20.0", features = ["serde", "derive", "pattern", "regex"] } +garde = { version = "0.20.0", features = ["serde", "derive", "regex"] } rustls = "0.23.16" webpki-roots = "0.26.6" tokio-rustls = "0.26.0" httparse = "1.9.5" http = "1.1.0" -urlencoding = "2.1.3" +#urlencoding = "2.1.3" string_cache = "0.8" +rand = "0.8.5" +resolv-conf = "0.7.0" [dev-dependencies] hex = "0.4.3" diff --git a/README.md b/README.md index b18cd64..ec45710 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,27 @@ a DNS server in Rust, which is inspired from chinadns/dnsmasq. ## Quick Start +### Client-Side + +ZeroDNS provides similar functionality to dig, but supports more DNS protocols. Here are some examples: + +```shell +$ # Simple resolve, will read dns server from /etc/resolv.conf +$ zerodns resolve www.youtube.com +$ # Use short output, similar with 'dig +short ...' +$ zerodns resolve --short www.youtube.com +$ # Resolve over google TCP +$ zerodns resolve -s tcp://8.8.8.8 www.youtube.com +$ # Resolve over google DoT +$ zerodns resolve -s dot://dns.google www.youtube.com +$ # Resolve over cloudflare DoH +$ zerodns resolve -s doh://1.1.1.1 www.youtube.com +$ # Resolve MX records +$ zerodns resolve -t mx gmail.com +``` + +### Server-Side + > Notice: ensure you have [just](https://github.com/casey/just) installed on your machine! run an example: @@ -29,7 +50,7 @@ $ just r $ dig @127.0.0.1 -p5454 www.youtube.com ``` -## Configuration +#### Configuration Here's an example configuration file: @@ -118,3 +139,7 @@ filters = ["chinadns"] ##### RULES END ##### ``` + +### Client API + +// TODO diff --git a/justfile b/justfile old mode 100644 new mode 100755 index 60370b6..26b978e --- a/justfile +++ b/justfile @@ -1,10 +1,15 @@ #!/usr/bin/env just --justfile alias r := run +alias i := install +alias l := lint release: @cargo build --release +install: + @cargo install --path . + lint: @cargo clippy diff --git a/src/client/doh.rs b/src/client/doh.rs index a9b6c97..6a59df3 100644 --- a/src/client/doh.rs +++ b/src/client/doh.rs @@ -1,6 +1,6 @@ use super::Client; use crate::misc::http::{SimpleHttp1Codec, CRLF}; -use crate::protocol::Message; +use crate::protocol::{Message, DEFAULT_HTTP_PORT, DEFAULT_TLS_PORT}; use futures::StreamExt; use once_cell::sync::Lazy; use smallvec::{smallvec, SmallVec}; @@ -94,7 +94,7 @@ impl DoHClient { pub const DEFAULT_PATH: &'static str = "/dns-query"; pub fn builder<'a>(addr: SocketAddr) -> DoHClientBuilder<'a> { - let https = addr.port() == 443; + let https = addr.port() == DEFAULT_TLS_PORT; DoHClientBuilder { https, addr, @@ -186,12 +186,12 @@ impl DoHClient { impl Display for DoHClient { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.https { - if self.addr.port() == 443 { + if self.addr.port() == DEFAULT_TLS_PORT { write!(f, "doh+https://{}", self.addr.ip())?; } else { write!(f, "doh+https://{}", self.addr)?; } - } else if self.addr.port() == 80 { + } else if self.addr.port() == DEFAULT_HTTP_PORT { write!(f, "doh+http://{}", self.addr.ip())?; } else { write!(f, "doh+http://{}", self.addr)?; diff --git a/src/client/dot.rs b/src/client/dot.rs index 777b294..5132311 100644 --- a/src/client/dot.rs +++ b/src/client/dot.rs @@ -1,6 +1,6 @@ use super::Client; use crate::misc::tls; -use crate::protocol::{Codec, Message}; +use crate::protocol::{Codec, Message, DEFAULT_DOT_PORT}; use crate::Result; use futures::{SinkExt, StreamExt}; @@ -13,8 +13,6 @@ use tokio::net::TcpStream; use tokio_rustls::client::TlsStream; use tokio_util::codec::{FramedRead, FramedWrite}; -const DEFAULT_DOT_PORT: u16 = 853; - static GOOGLE: Lazy = Lazy::new(|| { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), DEFAULT_DOT_PORT); DoTClient::builder(addr) diff --git a/src/config.rs b/src/config.rs index 51f7c3e..e4004b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -32,7 +32,7 @@ pub struct Rule { pub filters: Vec, } -pub fn read_from_toml(pt: PathBuf) -> anyhow::Result { +pub fn read_from_toml(pt: &PathBuf) -> anyhow::Result { let b = std::fs::read(pt)?; let s = String::from_utf8(b)?; let c: Config = toml::from_str(&s)?; @@ -46,7 +46,7 @@ mod tests { #[test] fn test_read_from_toml() { let pt = PathBuf::from("config.toml"); - let c = read_from_toml(pt); + let c = read_from_toml(&pt); assert!(c.is_ok_and(|c| { !c.server.listen.is_empty() && !c.rules.is_empty() && !c.filters.is_empty() diff --git a/src/lib.rs b/src/lib.rs index 80671ca..bf25cce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,3 +40,8 @@ pub(crate) use error::Error; pub type Result = anyhow::Result; pub use builtin::{setup, setup_logger}; + +pub use misc::resolvconf::read as read_resolvconf; + +pub const DEFAULT_RESOLV_CONF_PATH: &str = "/etc/resolv.conf"; +pub const DEFAULT_UDP_PORT: u16 = 53; diff --git a/src/main.rs b/src/main.rs index 9cd8fa8..fa32c06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,71 +1,245 @@ #[macro_use] +extern crate anyhow; +#[macro_use] extern crate log; +use chrono::{DateTime, Local}; +use clap::{arg, command, value_parser, ArgAction, ArgMatches, Command}; +use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; - -use clap::{Parser, Subcommand}; +use std::time::Duration; use tokio::sync::Notify; +use zerodns::client::request as resolve; +use zerodns::protocol::DNS; +use zerodns::protocol::{Class, Flags, Kind, Message}; -#[derive(Parser)] -#[command(name = "ZeroDNS")] -#[command(author = "Jeffsky ")] -#[command(version = "0.1.0")] -#[command(about = "A modern, simple and fast DNS server.", long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, -} +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let cmds = command!() // requires `cargo` feature + .propagate_version(true) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("run") + .about("Run a ZeroDNS server") + .arg(arg!(-c --config "a config file path").required(true)), + ) + .subcommand( + Command::new("resolve") + .about("Resolve an address") + .arg(arg!(-s --server "the dns server address")) + .arg(arg!(-c --class "class of resolve").value_parser(value_parser!(Class))) + .arg( + arg!(-t --type "type of resolve") + .action(ArgAction::Append) + .value_parser(value_parser!(Kind)), + ) + .arg(arg!(--timeout "timeout seconds for the DNS request")) + .arg(arg!(--short "display nothing except short form of answer")) + .arg(arg!([DOMAIN] "the domain to be resolved")), + ) + .get_matches(); + + match cmds.subcommand() { + Some(("run", sm)) => { + subcommand_run(sm).await?; + } + Some(("resolve", sm)) => { + subcommand_resolve(sm).await?; + } + _ => unreachable!("no sub-command"), + } -#[derive(Subcommand)] -enum Commands { - /// Run a ZeroDNS server from a configuration TOML file - Run { - #[arg(short, long, value_name = "FILE")] - config: PathBuf, - }, + // RUN: + // dig @127.0.0.1 -p5454 www.youtube.com + + Ok(()) } -#[tokio::main(flavor = "current_thread")] -async fn main() -> anyhow::Result<()> { - match Cli::parse().command { - Commands::Run { config: path } => { - let c = zerodns::config::read_from_toml(path)?; - - match &c.logger { - Some(lc) => zerodns::setup_logger(lc)?, - None => { - let lc = zerodns::logger::Config::default(); - zerodns::setup_logger(&lc)?; - } +async fn subcommand_resolve(sm: &ArgMatches) -> anyhow::Result<()> { + // --timeout 5 + let mut timeout = Duration::from_secs(5); + + let domain = sm.get_one::("DOMAIN").cloned().unwrap_or_default(); + let short = sm.get_one::("short").cloned().unwrap_or(false); + let mut class = sm.get_one::("class").cloned().unwrap_or(Class::IN); + let mut types = vec![]; + if domain.is_empty() { + types.push(Kind::NS); + class = Class::IN; + } else { + if let Some(vals) = sm.get_many::("type") { + for next in vals { + types.push(*next); } + } + if types.is_empty() { + types.push(Kind::A); + } + } + + // arg: --server + let dns = { + match sm.get_one::("server") { + None => { + use resolv_conf::ScopedIp; - zerodns::setup(); + let c = zerodns::read_resolvconf(zerodns::DEFAULT_RESOLV_CONF_PATH).await?; + let first = c.nameservers.first().ok_or_else(|| { + anyhow!( + "no nameserver found in {}!", + zerodns::DEFAULT_RESOLV_CONF_PATH + ) + })?; - let closer = Arc::new(Notify::new()); - let stopped = Arc::new(Notify::new()); + if c.timeout > 0 { + timeout = Duration::from_secs(c.timeout as u64); + } - { - let closer = Clone::clone(&closer); - let stopped = Clone::clone(&stopped); - tokio::spawn(async move { - if let Err(e) = zerodns::bootstrap::run(c, closer).await { - error!("zerodns server is stopped: {:?}", e); - } - stopped.notify_one(); - }); + let ipaddr = match first { + ScopedIp::V4(v4) => IpAddr::V4(*v4), + ScopedIp::V6(v6, _) => IpAddr::V6(*v6), + }; + DNS::UDP(SocketAddr::new(ipaddr, zerodns::DEFAULT_UDP_PORT)) } + Some(s) => s.parse::()?, + } + }; + + if let Some(n) = sm.get_one::("timeout") { + timeout = Duration::from_secs(n.parse::()?); + } + + let flags = Flags::builder().request().recursive_query(true).build(); + let req = { + let mut bu = Message::builder() + .id({ + use rand::Rng; + let mut rng = rand::thread_rng(); + rng.gen_range(1024..u16::MAX) + }) + .flags(flags); + + for next in types { + bu = bu.question(&domain, next, class); + } - tokio::signal::ctrl_c().await?; + bu.build()? + }; - closer.notify_waiters(); + let begin = Local::now(); + let res = resolve(&dns, &req, timeout).await?; - stopped.notified().await + if short { + for next in res.answers() { + println!("{}", next.rdata()?); } + } else { + print_resolve_result(&domain, &dns, &req, &res, begin)?; } - // RUN: - // dig @127.0.0.1 -p5454 www.youtube.com + println!(); + + Ok(()) +} + +#[inline] +fn print_resolve_result( + domain: &str, + dns: &DNS, + req: &Message, + res: &Message, + begin: DateTime, +) -> anyhow::Result<()> { + let cost = Local::now() - begin; + + println!(); + println!( + "; <<>> ZeroDNS {} <<>> @{} {}", + env!("CARGO_PKG_VERSION"), + &dns, + domain + ); + println!("; (1 server found)"); + println!(";; global options: +cmd"); + println!(";; Got answer:"); + println!( + ";; ->>HEADER<<- opcode: {:?}, status: {:?}, id: {}", + res.flags().opcode(), + res.flags().response_code(), + res.id() + ); + + println!(); + println!(";; OPT PSEUDOSECTION:"); + println!("; EDNS: version: 0, flags:; udp: 512"); + println!(";; QUESTION SECTION:"); + for question in req.questions() { + println!( + ";{}.\t\t{}\t{}", + question.name(), + question.class(), + question.kind() + ); + } + + println!(); + println!(";; ANSWER SECTION:"); + + for answer in res.answers() { + println!( + "{}.\t{}\t{}\t{}\t{}", + answer.name(), + answer.time_to_live(), + answer.class(), + answer.kind(), + answer.rdata()? + ); + } + + println!(); + println!(";; Query time: {} msec", cost.num_milliseconds()); + println!(";; SERVER: {}", &dns); + println!(";; WHEN: {}", &begin); + println!(";; MSG SIZE\trcvd: {}", res.len()); + + Ok(()) +} + +async fn subcommand_run(sm: &ArgMatches) -> anyhow::Result<()> { + let path = sm.get_one::("config").unwrap(); + let c = zerodns::config::read_from_toml(path)?; + + match &c.logger { + Some(lc) => zerodns::setup_logger(lc)?, + None => { + let lc = zerodns::logger::Config::default(); + zerodns::setup_logger(&lc)?; + } + } + + zerodns::setup(); + + let closer = Arc::new(Notify::new()); + let stopped = Arc::new(Notify::new()); + + { + let closer = Clone::clone(&closer); + let stopped = Clone::clone(&stopped); + tokio::spawn(async move { + if let Err(e) = zerodns::bootstrap::run(c, closer).await { + error!("zerodns server is stopped: {:?}", e); + } + stopped.notify_one(); + }); + } + + tokio::signal::ctrl_c().await?; + + closer.notify_waiters(); + + stopped.notified().await; Ok(()) } diff --git a/src/misc/mod.rs b/src/misc/mod.rs index 26e2735..46b54a8 100644 --- a/src/misc/mod.rs +++ b/src/misc/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod http; +pub(crate) mod resolvconf; pub(crate) mod tcp; pub(crate) mod tls; diff --git a/src/misc/resolvconf.rs b/src/misc/resolvconf.rs new file mode 100644 index 0000000..d37e775 --- /dev/null +++ b/src/misc/resolvconf.rs @@ -0,0 +1,8 @@ +use resolv_conf::Config; +use std::path::Path; + +pub async fn read(path: impl AsRef) -> anyhow::Result { + let b = tokio::fs::read(path).await?; + let parsed_config = Config::parse(&b[..])?; + Ok(parsed_config) +} diff --git a/src/protocol/dns.rs b/src/protocol/dns.rs index f2fe933..2388a9c 100644 --- a/src/protocol/dns.rs +++ b/src/protocol/dns.rs @@ -4,6 +4,12 @@ use std::net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6}; use std::str::FromStr; use url::Url; +pub const DEFAULT_UDP_PORT: u16 = 53; +pub const DEFAULT_TCP_PORT: u16 = 53; +pub const DEFAULT_DOT_PORT: u16 = 853; +pub const DEFAULT_HTTP_PORT: u16 = 80; +pub const DEFAULT_TLS_PORT: u16 = 443; + #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum DNS { @@ -111,7 +117,7 @@ impl DNS { "udp" => { if let Some(host) = url.host_str() { if let Ok(ip) = host.parse::() { - let addr = Self::from_host_port(ip, url.port().unwrap_or(53)); + let addr = Self::from_host_port(ip, url.port().unwrap_or(DEFAULT_UDP_PORT)); return Some(DNS::UDP(addr)); } } @@ -119,18 +125,18 @@ impl DNS { "tcp" => { if let Some(host) = url.host_str() { if let Ok(ip) = host.parse::() { - let addr = Self::from_host_port(ip, url.port().unwrap_or(53)); + let addr = Self::from_host_port(ip, url.port().unwrap_or(DEFAULT_TCP_PORT)); return Some(DNS::TCP(addr)); } } } "dot" => { - if let Some(addr) = extract_addr(853) { + if let Some(addr) = extract_addr(DEFAULT_DOT_PORT) { return Some(DNS::DoT(addr)); } } "doh" | "doh+https" | "https" => { - if let Some(addr) = extract_addr(443) { + if let Some(addr) = extract_addr(DEFAULT_TLS_PORT) { let path = match url.path() { "" | "/" => None, other => Some(Cachestr::from(other)), @@ -143,7 +149,7 @@ impl DNS { } } "doh+http" | "http" => { - if let Some(addr) = extract_addr(80) { + if let Some(addr) = extract_addr(DEFAULT_HTTP_PORT) { let path = match url.path() { "" | "/" => None, other => Some(Cachestr::from(other)), @@ -177,7 +183,7 @@ impl FromStr for DNS { return Ok(DNS::UDP(addr)); } else { let ip = IpAddr::from_str(s)?; - return Ok(DNS::UDP(SocketAddr::new(ip, 53))); + return Ok(DNS::UDP(SocketAddr::new(ip, DEFAULT_UDP_PORT))); } bail!(crate::Error::InvalidDNSUrl(s.into())) diff --git a/src/protocol/frame.rs b/src/protocol/frame.rs index 6310e6d..7c45eef 100644 --- a/src/protocol/frame.rs +++ b/src/protocol/frame.rs @@ -1,29 +1,32 @@ use std::fmt::{Debug, Display, Formatter}; use std::net::{Ipv4Addr, Ipv6Addr}; -use ahash::HashMap; use byteorder::{BigEndian, ByteOrder}; use bytes::{BufMut, Bytes, BytesMut}; +use clap::builder::PossibleValue; +use clap::ValueEnum; +use hashbrown::HashMap; use once_cell::sync::Lazy; -use regex::Regex; use strum::IntoEnumIterator; use strum_macros::EnumIter; macro_rules! parse_u16 { ($name:ident) => { - impl $name { - pub fn parse_u16(code: u16) -> Option<$name> { + impl TryFrom for $name { + type Error = (); + + fn try_from(value: u16) -> Result { static IDX: Lazy> = Lazy::new(|| { - let mut m = HashMap::with_capacity_and_hasher( - $name::iter().len(), - ahash::RandomState::default(), - ); + let mut m = HashMap::with_capacity($name::iter().len()); $name::iter().for_each(|next| { m.insert(next as u16, next); }); m }); - IDX.get(&code).cloned() + if let Some(c) = IDX.get(&value).cloned() { + return Ok(c); + } + Err(()) } } }; @@ -157,6 +160,120 @@ pub enum Kind { OPT = 41, } +impl ValueEnum for Kind { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self::A, + Self::AAAA, + Self::AFSDB, + Self::APL, + Self::CAA, + Self::CDNSKEY, + Self::CDS, + Self::CERT, + Self::CNAME, + Self::CSYNC, + Self::DHCID, + Self::DLV, + Self::DNAME, + Self::DNSKEY, + Self::DS, + Self::EUI48, + Self::EUI64, + Self::HINFO, + Self::HIP, + Self::HTTPS, + Self::IPSECKEY, + Self::KEY, + Self::KX, + Self::LOC, + Self::MX, + Self::NAPTR, + Self::NS, + Self::NSEC, + Self::NSEC3, + Self::NSEC3PARAM, + Self::OPENPGPKEY, + Self::PTR, + Self::RRSIG, + Self::RP, + Self::SIG, + Self::SMIMEA, + Self::SOA, + Self::SRV, + Self::SSHFP, + Self::SVCB, + Self::TA, + Self::TKEY, + Self::TLSA, + Self::TSIG, + Self::TXT, + Self::URI, + Self::ZONEMD, + Self::ANY, + Self::AXFR, + Self::IXFR, + Self::OPT, + ] + } + + fn to_possible_value(&self) -> Option { + Some(match self { + Self::A => PossibleValue::new("a").help("Type A"), + Self::AAAA => PossibleValue::new("aaaa").help("Type AAAA"), + Self::AFSDB => PossibleValue::new("afsdb").help("Type AFSDB"), + Self::APL => PossibleValue::new("apl").help("Type APL"), + Self::CAA => PossibleValue::new("caa").help("Type CAA"), + Self::CDNSKEY => PossibleValue::new("cdnskey").help("Type CDNSKEY"), + Self::CDS => PossibleValue::new("cds").help("Type CDS"), + Self::CERT => PossibleValue::new("cert").help("Type CERT"), + Self::CNAME => PossibleValue::new("cname").help("Type CNAME"), + Self::CSYNC => PossibleValue::new("csync").help("Type CSYNC"), + Self::DHCID => PossibleValue::new("dhcid").help("Type DHCID"), + Self::DLV => PossibleValue::new("dlv").help("Type DLV"), + Self::DNAME => PossibleValue::new("dname").help("Type DNAME"), + Self::DNSKEY => PossibleValue::new("dnskey").help("Type DNSKEY"), + Self::DS => PossibleValue::new("ds").help("Type DS"), + Self::EUI48 => PossibleValue::new("eui48").help("Type EUI48"), + Self::EUI64 => PossibleValue::new("eui64").help("Type EUI64"), + Self::HINFO => PossibleValue::new("hinfo").help("Type HINFO"), + Self::HIP => PossibleValue::new("hip").help("Type HIP"), + Self::HTTPS => PossibleValue::new("https").help("Type HTTPS"), + Self::IPSECKEY => PossibleValue::new("ipseckey").help("Type IPSECKEY"), + Self::KEY => PossibleValue::new("key").help("Type KEY"), + Self::KX => PossibleValue::new("kx").help("Type KX"), + Self::LOC => PossibleValue::new("loc").help("Type LOC"), + Self::MX => PossibleValue::new("mx").help("Type MX"), + Self::NAPTR => PossibleValue::new("naptr").help("Type NAPTR"), + Self::NS => PossibleValue::new("ns").help("Type NS"), + Self::NSEC => PossibleValue::new("nsec").help("Type NSEC"), + Self::NSEC3 => PossibleValue::new("nsec3").help("Type NSEC3"), + Self::NSEC3PARAM => PossibleValue::new("nsec3param").help("Type NSEC3PARAM"), + Self::OPENPGPKEY => PossibleValue::new("openpgpkey").help("Type OPENPGPKEY"), + Self::PTR => PossibleValue::new("ptr").help("Type PTR"), + Self::RRSIG => PossibleValue::new("rrsig").help("Type RRSIG"), + Self::RP => PossibleValue::new("rp").help("Type RP"), + Self::SIG => PossibleValue::new("sig").help("Type SIG"), + Self::SMIMEA => PossibleValue::new("smimea").help("Type SMIMEA"), + Self::SOA => PossibleValue::new("soa").help("Type SOA"), + Self::SRV => PossibleValue::new("srv").help("Type SRV"), + Self::SSHFP => PossibleValue::new("sshfp").help("Type SSHFP"), + Self::SVCB => PossibleValue::new("svcb").help("Type SVCB"), + Self::TA => PossibleValue::new("ta").help("Type TA"), + Self::TKEY => PossibleValue::new("tkey").help("Type TKEY"), + Self::TLSA => PossibleValue::new("tlsa").help("Type TLSA"), + Self::TSIG => PossibleValue::new("tsig").help("Type TSIG"), + Self::TXT => PossibleValue::new("txt").help("Type TXT"), + Self::URI => PossibleValue::new("uri").help("Type URI"), + Self::ZONEMD => PossibleValue::new("zonemd").help("Type ZONEMD"), + Self::ANY => PossibleValue::new("any").help("Type ANY"), + Self::AXFR => PossibleValue::new("axfr").help("Type AXFR"), + Self::IXFR => PossibleValue::new("ixfr").help("Type IXFR"), + Self::OPT => PossibleValue::new("opt").help("Type OPT"), + }) + } +} + impl Display for Kind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -192,7 +309,7 @@ impl Display for Kind { Kind::NSEC3PARAM => f.write_str("NSEC3PARAM"), Kind::OPENPGPKEY => f.write_str("OPENPGPKEY"), Kind::PTR => f.write_str("PTR"), - Kind::RRSIG => f.write_str("RSIG"), + Kind::RRSIG => f.write_str("RRSIG"), Kind::RP => f.write_str("RP"), Kind::SIG => f.write_str("SIG"), Kind::SMIMEA => f.write_str("SMIMEA"), @@ -227,6 +344,21 @@ pub enum Class { HS = 4, } +impl ValueEnum for Class { + fn value_variants<'a>() -> &'a [Self] { + &[Self::IN, Self::CS, Self::CH, Self::HS] + } + + fn to_possible_value(&self) -> Option { + Some(match self { + Class::IN => PossibleValue::new("in").help("Class IN"), + Class::CS => PossibleValue::new("cs").help("Class CS"), + Class::CH => PossibleValue::new("ch").help("Class CH"), + Class::HS => PossibleValue::new("hs").help("Class HS"), + }) + } +} + impl Display for Class { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -333,7 +465,7 @@ impl Flags { } pub fn opcode(&self) -> OpCode { - OpCode::parse_u16((self.0 >> 11) & 0x000f).expect("Invalid Opcode!") + OpCode::try_from((self.0 >> 11) & 0x000f).expect("Invalid Opcode!") } pub fn is_authoritative(&self) -> bool { @@ -474,7 +606,7 @@ impl<'a> MessageBuilder<'a> { b.put_u16(additionals.len() as u16); for next in queries { - if !is_valid_domain(next.name) { + if next.kind != Kind::NS && !is_valid_domain(next.name) { bail!("invalid question name '{}'", next.name); } for label in next @@ -493,7 +625,7 @@ impl<'a> MessageBuilder<'a> { // http://www.tcpipguide.com/free/t_DNSMessageResourceRecordFieldFormats-2.htm for next in answers { - if !is_valid_domain(next.name) { + if next.kind != Kind::NS && !is_valid_domain(next.name) { bail!("invalid answer name '{}'", next.name); } // name @@ -700,12 +832,12 @@ impl Question<'_> { pub fn kind(&self) -> Kind { let n = self.offset + self.name().len(); - Kind::parse_u16(BigEndian::read_u16(&self.raw[n..])).expect("Invalid question type!") + Kind::try_from(BigEndian::read_u16(&self.raw[n..])).expect("Invalid question type!") } pub fn class(&self) -> Class { let n = self.offset + self.name().len() + 2; - Class::parse_u16(BigEndian::read_u16(&self.raw[n..])).expect("Invalid question class!") + Class::try_from(BigEndian::read_u16(&self.raw[n..])).expect("Invalid question class!") } } @@ -762,13 +894,13 @@ impl RR<'_> { pub fn kind(&self) -> Kind { let offset = self.offset + self.name().len(); let code = BigEndian::read_u16(&self.raw[offset..]); - Kind::parse_u16(code).expect("Invalid RR type!") + Kind::try_from(code).expect("Invalid RR type!") } pub fn class(&self) -> Class { let offset = self.offset + self.name().len() + 2; let n = BigEndian::read_u16(&self.raw[offset..]); - Class::parse_u16(n).expect("Invalid RR class!") + Class::try_from(n).expect("Invalid RR class!") } #[inline(always)] @@ -1402,8 +1534,8 @@ fn is_valid_domain(domain: &str) -> bool { return true; } - static RE: Lazy = - Lazy::new(|| Regex::new("^([a-z0-9_-]{1,63})(\\.[a-z0-9_-]{1,63})+\\.?$").unwrap()); + static RE: Lazy = + Lazy::new(|| regex::Regex::new("^([a-z0-9_-]{1,63})(\\.[a-z0-9_-]{1,63})+\\.?$").unwrap()); RE.is_match(domain) }