├── .gitignore ├── fuzz ├── .gitignore ├── fuzz_targets │ └── from_slice.rs ├── Cargo.toml └── Cargo.lock ├── src ├── cfg.rs ├── zones │ ├── preprocessor.pest │ ├── zones.pest │ ├── preprocessor.rs │ ├── mod.rs │ ├── parser.rs │ └── process.rs ├── clients │ ├── mime.rs │ ├── to_urls.rs │ ├── mod.rs │ ├── stats.rs │ ├── udp.rs │ ├── tcp.rs │ ├── resolver.rs │ ├── doh.rs │ └── json.rs ├── errors.rs ├── util.rs ├── from_str.rs ├── io.rs ├── lib.rs ├── resource.rs ├── dns.rs ├── display.rs └── types.rs ├── generate_tests ├── Cargo.toml └── main.rs ├── .github └── workflows │ └── rust.yml ├── dig ├── Cargo.toml ├── util.rs └── main.rs ├── README.tpl ├── tests ├── dns.rs ├── resolver.rs └── test_data.yaml ├── Cargo.toml ├── README.md ├── LICENSE └── old_src └── punycode.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.pcapng -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/from_slice.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] 3 | extern crate libfuzzer_sys; 4 | extern crate rustdns; 5 | 6 | fuzz_target!(|data: &[u8]| { 7 | #[allow(unused_must_use)] 8 | { 9 | rustdns::Packet::from_slice(data); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/cfg.rs: -------------------------------------------------------------------------------- 1 | // Macro borrowed from https://github.com/hyperium/hyper 2 | // hyper is provided under the MIT license. See LICENSE. 3 | 4 | macro_rules! cfg_feature { 5 | ( 6 | #![$meta:meta] 7 | $($item:item)* 8 | ) => { 9 | $( 10 | #[cfg($meta)] 11 | #[cfg_attr(docsrs, doc(cfg($meta)))] 12 | $item 13 | )* 14 | } 15 | } -------------------------------------------------------------------------------- /generate_tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | # generate_tests/Cargo.toml 2 | 3 | [package] 4 | name = "generate_tests" 5 | version = "0.1.0" 6 | edition = "2018" 7 | 8 | [dependencies] 9 | serde = { version = "1.0.126", features = ["derive"] } 10 | serde_yaml = "0.8.17" 11 | hex = "0.4.3" 12 | rustdns = { path = "../", default-features = false, features = ["udp"] } 13 | 14 | 15 | [[bin]] 16 | name = "generate_tests" 17 | path = "main.rs" 18 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "rustdns-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.rustdns] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "from_slice" 24 | path = "fuzz_targets/from_slice.rs" 25 | test = false 26 | doc = false 27 | -------------------------------------------------------------------------------- /src/zones/preprocessor.pest: -------------------------------------------------------------------------------- 1 | /// DNS Zone file parsing (pre-processor) 2 | /// 3 | /// Due to the rules around braces we preprocess the file, 4 | /// and write out something easier to parse. 5 | 6 | 7 | comment = { ";" ~ (!NEWLINE ~ ANY)* } 8 | open = { "(" } 9 | close = { ")" } 10 | newline = { NEWLINE } 11 | token = { (!(comment | open | close | newline) ~ ANY)+ } 12 | 13 | // TODO We might want to capture " strings 14 | 15 | tokens = { 16 | ( 17 | comment 18 | | open 19 | | close 20 | | newline 21 | | token 22 | )* 23 | } 24 | 25 | file = { 26 | SOI ~ tokens ~ EOI 27 | } 28 | -------------------------------------------------------------------------------- /src/clients/mime.rs: -------------------------------------------------------------------------------- 1 | use http::HeaderValue; 2 | use mime::Mime; 3 | use std::str::FromStr; 4 | 5 | pub(crate) fn content_type_equal(content_type: &HeaderValue, expected: &str) -> bool { 6 | // Parse the content type, into it's "essence" which is just "type/subtype", instead of 7 | // "type/subtype+suffix; param=value..." 8 | let content_type = match content_type.to_str() { 9 | Ok(t) => t, 10 | Err(_err) => return false, 11 | }; 12 | let content_type = match Mime::from_str(content_type) { 13 | Ok(t) => t, 14 | Err(_err) => return false, 15 | }; 16 | 17 | content_type.essence_str() == expected 18 | } 19 | -------------------------------------------------------------------------------- /dig/Cargo.toml: -------------------------------------------------------------------------------- 1 | # dig/Cargo.toml 2 | 3 | [package] 4 | name = "dig" 5 | version = "0.1.0" 6 | edition = "2018" 7 | 8 | [dependencies] 9 | rustdns = { path = "../", default-features = false, features = ["clients"] } 10 | 11 | encoding8 = "0.3.2" # Used for pretty-printing 12 | strum = "0.21" # Simple macros for making Enum better 13 | strum_macros = "0.21" 14 | tokio = { version = "1.6.1", features = ["macros", "rt-multi-thread"] } 15 | clap = "3.0.0-beta.2" # Command line parsing 16 | time = "0.2.26" 17 | url = "2.2.2" 18 | http = "0.2.4" 19 | thiserror = "1.0.30" 20 | 21 | [dev-dependencies] 22 | pretty_assertions = "0.7.2" 23 | 24 | [[bin]] 25 | name = "dig" 26 | path = "main.rs" 27 | -------------------------------------------------------------------------------- /dig/util.rs: -------------------------------------------------------------------------------- 1 | use encoding8::ascii; 2 | 3 | // Dumps out the slice in a pretty way 4 | pub fn hexdump(slice: &[u8]) { 5 | const WIDTH: usize = 16; 6 | let mut offset = 0; 7 | 8 | for row in slice.chunks(WIDTH) { 9 | let row_hex: String = row.iter().map(|x| format!("{0:02X} ", x)).collect(); 10 | 11 | // For each byte on this row, only print out the ascii printable ones. 12 | let row_str: String = row 13 | .iter() 14 | .map(|x| { 15 | if ascii::is_printable(*x) { 16 | *x as char 17 | } else { 18 | '.' 19 | } 20 | }) 21 | .collect(); 22 | 23 | println!("{0:>08x}: {1:<48} {2:}", offset, row_hex, row_str); 24 | 25 | offset += WIDTH 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/clients/to_urls.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::iter; 3 | use std::slice; 4 | use std::vec; 5 | use url::Url; 6 | 7 | /// A trait for objects which can be converted or resolved to one or more 8 | /// [`Url`] values. Heavily inspired by [`ToSocketAddrs`]. 9 | pub trait ToUrls { 10 | type Iter: Iterator; 11 | 12 | fn to_urls(&self) -> io::Result; 13 | } 14 | 15 | impl ToUrls for &str { 16 | type Iter = vec::IntoIter; 17 | 18 | fn to_urls(&self) -> io::Result> { 19 | Ok(vec![self.parse().unwrap()].into_iter()) // TODO FIX THE PANIC! 20 | } 21 | } 22 | 23 | impl<'a> ToUrls for &'a [Url] { 24 | type Iter = iter::Cloned>; 25 | 26 | fn to_urls(&self) -> io::Result { 27 | Ok(self.iter().cloned()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/clients/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::Message; 2 | 3 | #[cfg(feature = "doh")] 4 | pub mod doh; 5 | 6 | #[cfg(feature = "json")] 7 | pub mod json; 8 | 9 | #[cfg(feature = "tcp")] 10 | pub mod tcp; 11 | 12 | cfg_feature! { 13 | #![feature = "udp"] 14 | 15 | pub mod udp; 16 | mod resolver; 17 | pub use self::resolver::Resolver; 18 | } 19 | 20 | cfg_feature! { 21 | #![feature = "http_deps"] 22 | 23 | mod to_urls; 24 | 25 | pub use self::to_urls::ToUrls; 26 | } 27 | 28 | #[cfg(any(feature = "doh", feature = "json"))] 29 | mod mime; 30 | 31 | mod stats; 32 | 33 | /// Exchanger takes a query and returns a response. 34 | pub trait Exchanger { 35 | fn exchange(&self, query: &Message) -> Result; 36 | } 37 | 38 | use async_trait::async_trait; 39 | 40 | #[async_trait] 41 | pub trait AsyncExchanger { 42 | async fn exchange(&self, query: &Message) -> Result; 43 | } 44 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/rustdns.svg)](https://crates.io/crates/rustdns) 2 | [![Documentation](https://docs.rs/rustdns/badge.svg)](https://docs.rs/rustdns) 3 | [![Build Status](https://github.com/bramp/rustdns/actions/workflows/rust.yml/badge.svg)](https://github.com/bramp/rustdns) 4 | 5 | # {{crate}} 6 | 7 | {{readme}} 8 | 9 | ## License: Apache-2.0 10 | 11 | ``` 12 | Copyright 2021 Andrew Brampton (bramp.net) 13 | 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unless required by applicable law or agreed to in writing, software 21 | distributed under the License is distributed on an "AS IS" BASIS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | ``` -------------------------------------------------------------------------------- /src/clients/stats.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | use crate::Stats; 3 | use std::net::SocketAddr; 4 | use std::time::Instant; 5 | 6 | /// Builder class to aid in the construction of Stats objects. 7 | pub(crate) struct StatsBuilder { 8 | start: SystemTime, 9 | timer: Instant, 10 | request_size: usize, 11 | } 12 | 13 | impl StatsBuilder { 14 | /// Call just before the request is sent, with the payload size. 15 | pub fn start(request_size: usize) -> StatsBuilder { 16 | StatsBuilder { 17 | start: SystemTime::now(), 18 | timer: Instant::now(), 19 | 20 | request_size, 21 | } 22 | } 23 | 24 | /// Call just after the response is receivesd. Consumes the MetadataBuilder and returns a Metadata. 25 | pub fn end(self, server: SocketAddr, response_size: usize) -> Stats { 26 | Stats { 27 | start: self.start, 28 | duration: self.timer.elapsed(), 29 | 30 | request_size: self.request_size, 31 | 32 | server, 33 | response_size, 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::Type; 2 | use crate::from_str::FromStrError; 3 | use core::num::ParseIntError; 4 | use std::net::AddrParseError; 5 | use thiserror::Error; 6 | 7 | /// Handy macro for returning a formatted [`std::io::Error`] message. 8 | /// TODO Delete 9 | /// 10 | /// ```rust 11 | /// use rustdns::bail; 12 | /// 13 | /// fn example(field: &str) -> std::io::Result<()> { 14 | /// bail!(InvalidData, "unable to parse field '{}'", field); 15 | /// } 16 | /// ``` 17 | #[macro_export] 18 | macro_rules! bail { 19 | ($kind:ident, $($arg:tt)*) => { 20 | // Construct the I/O error. 21 | return Err( 22 | ::std::io::Error::new(::std::io::ErrorKind::$kind, format!($($arg)*)).into() 23 | ) 24 | }; 25 | } 26 | 27 | #[derive(Error, Debug)] 28 | pub enum Error { 29 | #[error("invalid argument: {0}")] 30 | InvalidArgument(String), 31 | 32 | #[cfg(feature = "http")] 33 | #[error(transparent)] 34 | HttpError(#[from] http::Error), 35 | 36 | #[cfg(feature = "hyper")] 37 | #[error(transparent)] 38 | HyperError(#[from] hyper::Error), 39 | 40 | #[cfg(feature = "http")] 41 | #[error(transparent)] 42 | InvalidUri(#[from] http::uri::InvalidUri), 43 | 44 | #[error(transparent)] 45 | ParseError(#[from] ParseError), 46 | 47 | #[error(transparent)] 48 | IoError(#[from] std::io::Error), 49 | } 50 | 51 | #[derive(Error, Debug)] 52 | pub enum ParseError { 53 | #[error(transparent)] 54 | IntError(#[from] ParseIntError), 55 | 56 | #[error(transparent)] 57 | AddrError(#[from] AddrParseError), 58 | 59 | /// Invalid JSON was parsed. 60 | #[cfg(feature = "serde_json")] 61 | #[error(transparent)] 62 | JsonError(#[from] serde_json::Error), 63 | 64 | #[error("invalid rcode status: '{0}'")] 65 | InvalidStatus(u32), 66 | 67 | #[error("invalid record type: '{0}'")] 68 | InvalidType(u16), 69 | 70 | #[error("invalid {0} resource: '{1}'")] 71 | InvalidResource(Type, FromStrError), 72 | 73 | #[error("invalid rname email address: '{0}'")] 74 | InvalidRname(String), 75 | } 76 | -------------------------------------------------------------------------------- /tests/dns.rs: -------------------------------------------------------------------------------- 1 | // TODO Switch this to use datatest after 0.6.3 (which is broken): 2 | // https://github.com/commure/datatest/pull/30 3 | // and custom_test_frameworks is supported https://github.com/rust-lang/rust/issues/50297 4 | use pretty_assertions::assert_eq; 5 | use regex::Regex; 6 | use rustdns::Message; 7 | use serde::Deserialize; 8 | use std::fs; 9 | 10 | const TEST_DATA_FILENAME: &str = "tests/test_data.yaml"; 11 | 12 | #[derive(Deserialize)] 13 | struct TestCase { 14 | // Name of the test case. 15 | name: String, 16 | 17 | // Hex encoded binary string. 18 | // TODO Change this to a binary type, when serde_yaml supports it: https://github.com/dtolnay/serde-yaml/issues/91 19 | binary: String, 20 | 21 | // Dig-ish formatted output. 22 | // TODO Change this to a multi-line string type, for easier viewing in the generated YAML. 23 | string: String, 24 | } 25 | 26 | #[test] 27 | fn tests() { 28 | let s = fs::read(TEST_DATA_FILENAME).expect("failed read test input"); 29 | let tests: Vec = 30 | serde_yaml::from_slice(&s).expect("failed to deserialise test input"); 31 | 32 | for case in tests { 33 | test_from_slice(case); 34 | } 35 | } 36 | 37 | fn normalise_whitespace(s: &str) -> String { 38 | let re = Regex::new(r"[ ]+").unwrap(); 39 | return re.replace_all(s, " ").to_string(); 40 | } 41 | 42 | fn test_from_slice(case: TestCase) { 43 | let input = match hex::decode(case.binary) { 44 | Err(e) => panic!("{}: Invalid test case input: {}", case.name, e), 45 | Ok(i) => i, 46 | }; 47 | let m = match Message::from_slice(&input) { 48 | Err(e) => panic!("{}: Unable to parse: {}", case.name, e), 49 | Ok(p) => p, 50 | }; 51 | 52 | // TODO Split this into a few tests. from_slice(), fmt(), to_vec() 53 | 54 | // Normalise the formatted output a little (to allow little whitespace changes). 55 | let got = normalise_whitespace(&format!("{}", m)); 56 | let want = normalise_whitespace(&case.string); 57 | 58 | assert_eq!(got, want, "{}: Formatted string doesn't match", case.name); 59 | 60 | // TODO Test writing the result back out. 61 | } 62 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | use std::net::IpAddr; 3 | use std::net::IpAddr::V4; 4 | use std::net::IpAddr::V6; 5 | 6 | #[cfg(test)] 7 | use pretty_assertions::assert_eq; 8 | 9 | /// Returns the reverse DNS name for this IP address. Suitable for use with 10 | /// [`Type::PTR`] records. See [rfc1035] and [rfc3596] for IPv4 and IPv6 respectively. 11 | /// 12 | /// # Example 13 | /// 14 | /// ```rust 15 | /// use rustdns::util::reverse; 16 | /// 17 | /// let ip4 = "127.0.0.1".parse().unwrap(); 18 | /// let ip6 = "2001:db8::567:89ab".parse().unwrap(); 19 | /// 20 | /// assert_eq!(reverse(ip4), "1.0.0.127.in-addr.arpa."); 21 | /// assert_eq!(reverse(ip6), "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa."); 22 | /// ``` 23 | /// 24 | /// [`Type::PTR`]: crate::Type::PTR 25 | /// [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.5 26 | /// [rfc3596]: https://datatracker.ietf.org/doc/html/rfc3596#section-2.5 27 | pub fn reverse(ip: IpAddr) -> String { 28 | match ip { 29 | V4(ipv4) => { 30 | let octets = ipv4.octets(); 31 | format!( 32 | "{}.{}.{}.{}.in-addr.arpa.", 33 | octets[3], octets[2], octets[1], octets[0] 34 | ) 35 | } 36 | V6(ipv6) => { 37 | let mut result = String::new(); 38 | for o in ipv6.octets().iter().rev() { 39 | write!( 40 | result, 41 | "{:x}.{:x}.", 42 | o & 0b0000_1111, 43 | (o & 0b1111_0000) >> 4 44 | ) 45 | .unwrap(); // Impossible for write! to fail when appending to a string. 46 | } 47 | result.push_str("ip6.arpa."); 48 | result 49 | } 50 | } 51 | } 52 | 53 | #[test] 54 | fn test_reverse() { 55 | let tests: Vec<(IpAddr, &str)> = vec![ 56 | ("127.0.0.1".parse().unwrap(), "1.0.0.127.in-addr.arpa."), 57 | ("8.8.4.4".parse().unwrap(), "4.4.8.8.in-addr.arpa."), 58 | ( 59 | "2001:db8::567:89ab".parse().unwrap(), 60 | "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", 61 | ), 62 | ]; 63 | 64 | for test in tests { 65 | assert_eq!(reverse(test.0), test.1); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/resolver.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[cfg(feature = "udp")] 3 | mod tests { 4 | use pretty_assertions::assert_eq; 5 | use rustdns::clients::Exchanger; 6 | use rustdns::clients::Resolver; 7 | use rustdns::types::*; 8 | use rustdns::Message; 9 | use rustdns::Record; 10 | use rustdns::Resource; 11 | use std::net::IpAddr; 12 | use std::time::Duration; 13 | 14 | struct MockClient {} 15 | 16 | impl Exchanger for MockClient { 17 | /// Sends the query [`Message`] to the `server` via UDP and returns the result. 18 | fn exchange(&self, _query: &Message) -> Result { 19 | //let mut records = HashMap::new(); 20 | 21 | // TODO FINISH! 22 | let _a = Record { 23 | name: "a.bramp.net".to_string(), 24 | class: Class::Internet, 25 | ttl: Duration::new(10, 0), 26 | resource: Resource::A("127.0.0.1".parse().unwrap()), 27 | }; 28 | 29 | /* 30 | records.insert("aaaa.bramp.net", Resource::A("127.0.0.1".parse())); 31 | records.insert("aaaaa.bramp.net", Resource::A("127.0.0.1".parse())); 32 | records.insert("aaaaa.bramp.net", Resource::AAAA("::1".parse())); 33 | */ 34 | 35 | panic!() 36 | } 37 | } 38 | 39 | // This test may be flakly, if it is running in an environment that doesn't 40 | // have both IPv4 and IPv6, and has DNS queries that can fail. 41 | // TODO Mock out the client. 42 | #[test] 43 | fn test_resolver() { 44 | struct TestCase<'a> { 45 | name: &'a str, 46 | want: Vec<&'a str>, 47 | } 48 | 49 | let tests = vec![ 50 | TestCase { 51 | name: "a.bramp.net", 52 | want: vec!["127.0.0.1"], 53 | }, 54 | TestCase { 55 | name: "aaaa.bramp.net", 56 | want: vec!["::1"], 57 | }, 58 | TestCase { 59 | name: "aaaaa.bramp.net", 60 | want: vec!["::1", "127.0.0.1"], 61 | }, 62 | TestCase { 63 | name: "cname.bramp.net", 64 | want: vec!["127.0.0.1"], 65 | }, 66 | ]; 67 | 68 | let resolver = Resolver::new(); 69 | 70 | for test in tests { 71 | let mut want: Vec = test 72 | .want 73 | .iter() 74 | .map(|x| x.parse().expect("invalid test input")) 75 | .collect(); 76 | let mut got = resolver.lookup(test.name).expect("failed to lookup"); 77 | 78 | // Sort because ::1 and 127.0.0.1 may switch places. 79 | want.sort(); 80 | got.sort(); 81 | 82 | assert_eq!(got, want, "when resolving {}", test.name); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 2 | 3 | [package] 4 | name = "rustdns" 5 | version = "0.4.0" 6 | authors = ["Andrew Brampton "] 7 | categories = ["encoding", "network-programming"] 8 | description = "A DNS parsing library" 9 | edition = "2018" 10 | keywords = ["dns", "idna", "serialization"] 11 | license = "Apache-2.0" 12 | readme = "README.md" 13 | repository = "https://github.com/bramp/rustdns" 14 | 15 | [workspace] 16 | members = [ 17 | "generate_tests", # Used to generate test data (by querying real servers) 18 | "dig", # Example dig client 19 | # "nslookup", # Example nslookup client 20 | ] 21 | 22 | [features] 23 | default = ["clients", "zones"] 24 | 25 | # Enable the DNS client 26 | clients = ["doh", "json", "tcp", "udp"] 27 | 28 | # DNS over HTTPS (DoH) client (rfc8484). 29 | doh = ["http_deps", "base64"] 30 | 31 | # DNS over HTTPS JSON client 32 | json = ["http_deps", "serde", "serde_json"] 33 | 34 | # DNS over TCP client 35 | tcp = [] 36 | 37 | # DNS over UDP client 38 | udp = [] 39 | 40 | # Enable the Zone Parser 41 | zones = ["pest", "pest_consume", "pest_derive"] 42 | 43 | # A private feature for common http dependencies. 44 | http_deps = ["http", "url", "hyper", "hyper-alpn", "mime"] 45 | 46 | [dependencies] 47 | 48 | # Used for the web clients 49 | http = { version = "0.2.5", optional = true } 50 | url = { version = "2.3.1", optional = true } 51 | hyper = { version = "0.14.16", features = ["client", "runtime", "http1", "http2"], optional = true } 52 | hyper-alpn = { version = "0.3.0", optional = true } 53 | mime = { version = "0.3.16", optional = true } 54 | 55 | # Needed for DNS over HTTP (DoH) 56 | base64 = { version = "0.13.0", optional = true } 57 | 58 | # Needed for DNS over HTTP Json 59 | serde = { version = "1.0.132", features = ["derive"], optional = true } 60 | serde_json = { version = "1.0.74", optional = true } 61 | 62 | # Needed for Zone file parsing 63 | pest = { version = "2.1.3", optional = true } 64 | pest_consume = { version = "1.1.1", optional = true } 65 | pest_derive = { version = "2.1.0", optional = true } 66 | 67 | # Everything else 68 | async-trait = "0.1.52" 69 | chrono = "0.4.19" 70 | byteorder = "1.4.3" 71 | bytes = "1.1.0" 72 | derivative = "2.2.0" 73 | idna = "0.3.0" 74 | lazy_static = "1.4.0" 75 | log = "0.4.14" 76 | num-derive = "0.3.3" 77 | num-traits = "0.2.14" 78 | rand = "0.8.4" 79 | regex = "1.5.4" 80 | strum = "0.23.0" 81 | strum_macros = "0.23.1" 82 | thiserror = "1.0.30" 83 | 84 | ### 85 | 86 | [dev-dependencies] 87 | env_logger = "0.9.0" 88 | hex = "0.4.3" 89 | pretty_assertions = "1.0.0" 90 | regex = "1.5.4" 91 | serde = { version = "1.0.132", features = ["derive"] } 92 | serde_yaml = "0.8.23" 93 | json_comments = "0.2.0" 94 | test-env-log = "0.2.8" 95 | tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } 96 | 97 | [package.metadata.cargo-all-features] 98 | skip_optional_dependencies = true 99 | denylist = ["http_deps"] 100 | -------------------------------------------------------------------------------- /src/zones/zones.pest: -------------------------------------------------------------------------------- 1 | /// DNS Zone file parsing 2 | /// 3 | /// Format is defined in rfc1035 Section 5, extended rfc2308 Section 4. 4 | /// 5 | /// [] 6 | /// [] 7 | /// ``` 8 | /// 9 | /// contents take one of the following forms: 10 | /// ```text 11 | /// [] [] 12 | /// [] [] 13 | /// ``` 14 | 15 | // We use explict WHITESPACE to ensure there is whitespace between matching tokens 16 | // as opposed to optional WHITESPACE. 17 | ws = _{ 18 | ( " " 19 | | "\t" 20 | 21 | // Also match ( and ) because after preprocessing, we can effectively 22 | // ignore them. We only leave them in to make error printing, etc a 23 | // little nicer. 24 | | "(" | ")" 25 | )+ 26 | } 27 | 28 | // Standard comment until end of line 29 | COMMENT = _{";" ~ (!NEWLINE ~ ANY)*} 30 | 31 | // TODO Merge domain and string together 32 | domain = @{ 33 | "@" 34 | | (ASCII_ALPHANUMERIC | "." | "-" )+ 35 | // TODO Handle escape characters 36 | // TODO Handle quoted strings 37 | } 38 | string = @{ (ASCII_ALPHANUMERIC | "." | "-" | "\\")+ } 39 | ip4 = @{ (ASCII_DIGIT | ".")+ } 40 | ip6 = @{ (ASCII_HEX_DIGIT | ":")+ } 41 | number = @{ ASCII_DIGIT+ } 42 | duration = @{ ASCII_DIGIT+ } 43 | class = @{ ^"IN" | ^"CS" | ^"CH" | ^"HS" } 44 | resource = _{ 45 | resource_a 46 | | resource_aaaa 47 | | resource_cname 48 | | resource_ns 49 | | resource_mx 50 | | resource_ptr 51 | | resource_soa 52 | } 53 | 54 | resource_a = {^"A" ~ ws ~ ip4} 55 | resource_aaaa = {^"AAAA" ~ ws ~ ip6} 56 | resource_cname = {^"CNAME" ~ ws ~ domain} 57 | resource_ns = {^"NS" ~ ws ~ domain} 58 | resource_mx = {^"MX" ~ ws ~ number ~ ws ~ domain} 59 | resource_ptr = {^"PTR" ~ ws ~ domain} 60 | resource_soa = {^"SOA" ~ ws ~ domain ~ ws ~ string ~ ws ~ number ~ ws ~ duration ~ ws ~ duration ~ ws ~ duration ~ ws ~ duration} 61 | 62 | // Entry for full file. 63 | file = { 64 | // TODO records can be split across many lines 65 | //NEWLINE* ~ (entry? ~ NEWLINE?)* ~ EOI 66 | entry ~ (NEWLINE ~ entry)* ~ EOI 67 | } 68 | 69 | // Entry for a single resource record. 70 | single_record = { 71 | SOI ~ ws? ~ record ~ ws? ~ EOI 72 | } 73 | 74 | entry = _{ 75 | ws? ~ ( 76 | origin 77 | | ttl 78 | | record 79 | | ws? // blank record 80 | ) ~ ws? 81 | } 82 | 83 | origin = { 84 | ^"$ORIGIN" ~ ws ~ domain 85 | } 86 | 87 | ttl = { 88 | ^"$TTL" ~ ws ~ duration 89 | } 90 | 91 | record = { 92 | // This is perhaps more verbose than needed, but this ensures 93 | // we parse this ambiguous text in a well defined order. 94 | // For example, "IN" could be a domain, or a class. 95 | 96 | // All arguments are provided 97 | (domain ~ ws ~ duration ~ ws ~ class ~ ws ~ resource) 98 | | (domain ~ ws ~ class ~ ws ~ duration ~ ws ~ resource) 99 | 100 | // No domain provided, but match these first, we assume IN means class, not domain 101 | | (duration ~ ws ~ class ~ ws ~ resource) 102 | | (duration ~ ws ~ resource) 103 | | (class ~ ws ~ duration ~ ws ~ resource) 104 | | (class ~ ws ~ resource) 105 | 106 | // Match the ones listing domains 107 | | (domain ~ ws ~ class ~ ws ~ resource) 108 | | (domain ~ ws ~ duration ~ ws ~ resource) 109 | | (domain ~ ws ~ resource) 110 | 111 | // Finally no domain. 112 | | resource 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/clients/udp.rs: -------------------------------------------------------------------------------- 1 | use crate::clients::Exchanger; 2 | use crate::Message; 3 | use crate::clients::stats::StatsBuilder; 4 | use std::net::SocketAddr; 5 | use std::net::ToSocketAddrs; 6 | use std::net::UdpSocket; 7 | use std::time::Duration; 8 | 9 | pub const GOOGLE_IPV4_PRIMARY: &str = "8.8.8.8:53"; 10 | pub const GOOGLE_IPV4_SECONDARY: &str = "8.8.4.4:53"; 11 | pub const GOOGLE_IPV6_PRIMARY: &str = "2001:4860:4860::8888:53"; 12 | pub const GOOGLE_IPV6_SECONDARY: &str = "2001:4860:4860::8844:53"; 13 | 14 | pub const GOOGLE: [&str; 4] = [ 15 | GOOGLE_IPV4_PRIMARY, 16 | GOOGLE_IPV4_SECONDARY, 17 | GOOGLE_IPV6_PRIMARY, 18 | GOOGLE_IPV6_SECONDARY, 19 | ]; 20 | 21 | /// A UDP DNS Client. 22 | /// 23 | /// # Example 24 | /// 25 | /// ```rust 26 | /// use rustdns::clients::Exchanger; 27 | /// use rustdns::clients::udp::Client; 28 | /// use rustdns::types::*; 29 | /// 30 | /// fn main() -> Result<(), rustdns::Error> { 31 | /// let mut query = Message::default(); 32 | /// query.add_question("bramp.net", Type::A, Class::Internet); 33 | /// 34 | /// let response = Client::new("8.8.8.8:53")? 35 | /// .exchange(&query) 36 | /// .expect("could not exchange message"); 37 | /// 38 | /// println!("{}", response); 39 | /// Ok(()) 40 | /// } 41 | /// ``` 42 | /// 43 | /// See 44 | // TODO Document all the options. 45 | pub struct Client { 46 | servers: Vec, 47 | 48 | read_timeout: Option, 49 | } 50 | 51 | impl Default for Client { 52 | fn default() -> Self { 53 | Client { 54 | servers: Vec::default(), 55 | read_timeout: Some(Duration::new(5, 0)), 56 | } 57 | } 58 | } 59 | 60 | impl Client { 61 | /// Creates a new Client bound to the specific servers. 62 | // TODO Document how it fails. 63 | // TODO Document how you can give it a set of addresses. 64 | // TODO Document how they should be IP addresses, not hostnames. 65 | pub fn new(servers: A) -> Result { 66 | let servers = servers.to_socket_addrs()?.collect(); 67 | // TODO Check for zero servers. 68 | Ok(Self { 69 | servers, 70 | 71 | ..Default::default() 72 | }) 73 | } 74 | } 75 | 76 | impl Exchanger for Client { 77 | /// Sends the query [`Message`] to the `server` via UDP and returns the result. 78 | fn exchange(&self, query: &Message) -> Result { 79 | // TODO Implement retries, backoffs, and cycling of servers. 80 | // per https://datatracker.ietf.org/doc/html/rfc1035#section-4.2.1 81 | 82 | let socket = UdpSocket::bind("0.0.0.0:0")?; 83 | socket.set_read_timeout(self.read_timeout)?; 84 | 85 | // Connect us to the server, meaning recv will only receive directly 86 | // from the server. 87 | socket.connect(self.servers.as_slice())?; 88 | 89 | let req = query.to_vec()?; 90 | 91 | let stats = StatsBuilder::start(req.len()); 92 | socket.send(&req)?; 93 | 94 | // TODO Set this to the size in req. 95 | let mut buf = [0; 4096]; 96 | let len = socket.recv(&mut buf)?; 97 | let mut resp = Message::from_slice(&buf[0..len])?; 98 | 99 | resp.stats = Some(stats.end(socket.peer_addr()?, len)); 100 | 101 | Ok(resp) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/zones/preprocessor.rs: -------------------------------------------------------------------------------- 1 | // TODO Use https://github.com/Nadrieril/pest_consume 2 | use pest::error::Error; 3 | use pest::iterators::Pair; 4 | use pest::Parser; 5 | use std::result; 6 | 7 | #[derive(Parser)] 8 | #[grammar = "zones/preprocessor.pest"] 9 | struct ZonePreprocessor; 10 | 11 | type Result = result::Result>; 12 | 13 | fn parse_tokens(pair: Pair) -> Result { 14 | assert_eq!(pair.as_rule(), Rule::tokens); 15 | 16 | let mut result = String::new(); 17 | let mut opens = 0; 18 | 19 | for pair in pair.into_inner() { 20 | match pair.as_rule() { 21 | Rule::open => { 22 | opens += 1; 23 | result.push_str(pair.as_str()); 24 | } 25 | Rule::close => { 26 | opens -= 1; 27 | result.push_str(pair.as_str()); 28 | } 29 | Rule::newline | Rule::comment => { 30 | if opens > 0 { 31 | // Replace newlines or comments with spaces 32 | for _i in 0..pair.as_str().len() { 33 | result.push(' '); 34 | } 35 | } else { 36 | result.push_str(pair.as_str()); 37 | } 38 | } 39 | _ => result.push_str(pair.as_str()), 40 | } 41 | } 42 | 43 | Ok(result) 44 | } 45 | 46 | /// Preprocess the input to handle braces. Specifically 47 | /// ( and ) allow a record to span multiple lines, so this 48 | /// replaces new lines with spaces when they are within braces. 49 | pub(crate) fn preprocess(input: &str) -> Result { 50 | let mut result = String::new(); 51 | let file = ZonePreprocessor::parse(Rule::file, input)?.next().unwrap(); // TODO 52 | for pair in file.into_inner() { 53 | match pair.as_rule() { 54 | Rule::tokens => result.push_str(&parse_tokens(pair)?), 55 | Rule::EOI => (), // Nothing 56 | _ => unreachable!("Unexpected rule: {:?}", pair.as_rule()), 57 | } 58 | } 59 | Ok(result) 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | use pretty_assertions::assert_eq; 66 | 67 | // Test Full files 68 | #[test] 69 | fn test_preprocessor() { 70 | let tests = vec![ 71 | // Examples from https://www.nlnetlabs.nl/documentation/nsd/grammar-for-dns-zone-files/ 72 | ("SOA ( 1 2 3 4 5 6 )", "SOA ( 1 2 3 4 5 6 )"), 73 | ( 74 | "SOA ( 1 2 ) ( 3 4 ) ( 5 ) ( 6 )\nA 127.0.0.1", 75 | "SOA ( 1 2 ) ( 3 4 ) ( 5 ) ( 6 )\nA 127.0.0.1", 76 | ), 77 | ( 78 | "SOA soa soa ( 1\n2\n3\n4\n5\n6)", 79 | "SOA soa soa ( 1 2 3 4 5 6)", 80 | ), 81 | ( 82 | // Comments are handled correctly 83 | "SOA ; blah\nA 127.0.0.1", 84 | "SOA ; blah\nA 127.0.0.1", 85 | ), 86 | ( 87 | // Comments are removed when in a '(' 88 | "SOA (; blah\nA 127.0.0.1)", 89 | "SOA ( A 127.0.0.1)", 90 | ), 91 | ( 92 | // '(' within a comment shouldn't change the parsing 93 | "SOA ; ( blah\nA 127.0.0.1", 94 | "SOA ; ( blah\nA 127.0.0.1", 95 | ), 96 | ]; 97 | 98 | for (input, want) in tests { 99 | match preprocess(input) { 100 | Ok(got) => assert_eq!(got, want, "incorrect result for '{}'", input), 101 | Err(err) => panic!("'{}' failed:\n{}", input, err), 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/clients/tcp.rs: -------------------------------------------------------------------------------- 1 | use crate::clients::Exchanger; 2 | use crate::Message; 3 | use crate::clients::stats::StatsBuilder; 4 | use std::io::Read; 5 | use std::io::Write; 6 | use std::net::SocketAddr; 7 | use std::net::TcpStream; 8 | use std::net::ToSocketAddrs; 9 | use std::time::Duration; 10 | 11 | pub const GOOGLE_IPV4_PRIMARY: &str = "8.8.8.8:53"; 12 | pub const GOOGLE_IPV4_SECONDARY: &str = "8.8.4.4:53"; 13 | pub const GOOGLE_IPV6_PRIMARY: &str = "2001:4860:4860::8888:53"; 14 | pub const GOOGLE_IPV6_SECONDARY: &str = "2001:4860:4860::8844:53"; 15 | 16 | pub const GOOGLE: [&str; 4] = [ 17 | GOOGLE_IPV4_PRIMARY, 18 | GOOGLE_IPV4_SECONDARY, 19 | GOOGLE_IPV6_PRIMARY, 20 | GOOGLE_IPV6_SECONDARY, 21 | ]; 22 | 23 | /// A TCP DNS Client. 24 | /// 25 | /// # Example 26 | /// 27 | /// ```rust 28 | /// use rustdns::clients::Exchanger; 29 | /// use rustdns::clients::tcp::Client; 30 | /// use rustdns::types::*; 31 | /// 32 | /// fn main() -> Result<(), rustdns::Error> { 33 | /// let mut query = Message::default(); 34 | /// query.add_question("bramp.net", Type::A, Class::Internet); 35 | /// 36 | /// let response = Client::new("8.8.8.8:53")? 37 | /// .exchange(&query) 38 | /// .expect("could not exchange message"); 39 | /// 40 | /// println!("{}", response); 41 | /// Ok(()) 42 | /// } 43 | /// ``` 44 | /// 45 | /// See 46 | // TODO Document all the options. 47 | pub struct Client { 48 | servers: Vec, 49 | 50 | connect_timeout: Duration, 51 | read_timeout: Option, 52 | write_timeout: Option, 53 | } 54 | 55 | impl Default for Client { 56 | fn default() -> Self { 57 | Client { 58 | servers: Vec::default(), 59 | connect_timeout: Duration::new(5, 0), 60 | read_timeout: Some(Duration::new(5, 0)), 61 | write_timeout: Some(Duration::new(5, 0)), 62 | } 63 | } 64 | } 65 | 66 | impl Client { 67 | /// Creates a new Client bound to the specific servers. 68 | // TODO Document how it fails. 69 | pub fn new(servers: A) -> Result { 70 | let servers = servers.to_socket_addrs()?.collect(); 71 | // TODO Check for zero servers. 72 | Ok(Self { 73 | servers, 74 | 75 | ..Default::default() 76 | }) 77 | } 78 | } 79 | 80 | impl Exchanger for Client { 81 | /// Sends the [`Message`] to the `server` via TCP and returns the result. 82 | fn exchange(&self, query: &Message) -> Result { 83 | let mut stream = TcpStream::connect_timeout(&self.servers[0], self.connect_timeout)?; 84 | stream.set_nodelay(true)?; // We send discrete packets, so we can send as soon as possible. 85 | stream.set_read_timeout(self.read_timeout)?; 86 | stream.set_write_timeout(self.write_timeout)?; 87 | 88 | let message = query.to_vec()?; 89 | 90 | let stats = StatsBuilder::start(message.len() + 2); 91 | 92 | // Two byte length prefix followed by the message. 93 | // TODO Move this into a single message! 94 | stream.write_all(&(message.len() as u16).to_be_bytes())?; 95 | stream.write_all(&message)?; 96 | 97 | // Now receive a two byte length 98 | let buf = &mut [0; 2]; 99 | stream.read_exact(buf)?; 100 | let len = u16::from_be_bytes(*buf); 101 | 102 | // and finally the message 103 | let mut buf = vec![0; len.into()]; 104 | 105 | stream.read_exact(&mut buf)?; 106 | 107 | let mut resp = Message::from_slice(&buf)?; 108 | resp.stats = Some(stats.end(stream.peer_addr()?, (len + 2).into())); 109 | 110 | Ok(resp) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /generate_tests/main.rs: -------------------------------------------------------------------------------- 1 | /// Simple tool that issues multiple DNS requests, captures their output 2 | /// and writes out test data. 3 | /// 4 | /// # Example 5 | /// 6 | /// ``` 7 | /// cargo run -p generate_tests 8 | /// ``` 9 | /// If the tool fails, that indicates maybe a problem with the library. 10 | use serde::Serialize; 11 | use std::fs; 12 | use std::net::UdpSocket; 13 | use std::str::FromStr; 14 | 15 | use rustdns::types::*; 16 | 17 | const TEST_DATA_FILENAME: &str = "tests/test_data.yaml"; 18 | 19 | // Set of actual queries we do, to get example output. 20 | const TESTS: [&str; 19] = [ 21 | // Some Google domains I found. 22 | "A www.google.com", 23 | "AAAA www.google.com", 24 | "ANY www.google.com", 25 | "CNAME code.google.com", 26 | "MX google.com", 27 | "PTR 4.4.8.8.in-addr.arpa", 28 | "SOA google.com", 29 | "SRV _ldap._tcp.google.com", 30 | "TXT google.com", 31 | "A ☺️.com", 32 | "A a", // Invalid TLD 33 | // Some test domains I setup. 34 | "A a.bramp.net", 35 | "AAAA aaaa.bramp.net", 36 | "CNAME aaaa.bramp.net", 37 | "MX aaaa.bramp.net", 38 | "CNAME cname-loop1.bramp.net", 39 | "CNAME cname-loop2.bramp.net", 40 | "NS ns.bramp.net", 41 | "TXT txt.bramp.net", 42 | ]; 43 | 44 | #[derive(Serialize)] 45 | struct TestCase { 46 | // Name of the test case. 47 | name: String, 48 | 49 | // Hex encoded binary string. 50 | // TODO Change this to a binary type, when serde_yaml supports it: https://github.com/dtolnay/serde-yaml/issues/91 51 | binary: String, 52 | 53 | // Dig-ish formatted output. 54 | // TODO Change this to a multi-line string type, for easier viewing in the generated YAML. 55 | string: String, 56 | } 57 | 58 | fn main() -> std::io::Result<()> { 59 | let socket = UdpSocket::bind("0.0.0.0:0").expect("couldn't bind to address"); 60 | 61 | let mut output = Vec::new(); 62 | 63 | for test in &TESTS { 64 | println!("Running {}", test); 65 | 66 | let args: Vec<&str> = test.split_whitespace().collect(); 67 | 68 | if args.len() != 2 { 69 | panic!("invalid number of arguments"); 70 | } 71 | 72 | let r#type = Type::from_str(&args[0]).expect("invalid Type"); 73 | let domain = &args[1]; 74 | 75 | let mut req = Message { 76 | id: 0xeccb, // randomise 77 | qr: QR::Query, 78 | opcode: Opcode::Query, 79 | rd: true, 80 | ad: true, 81 | 82 | ..Default::default() 83 | }; 84 | 85 | req.add_question(domain, r#type, Class::Internet); 86 | 87 | let req_buf = req.to_vec().expect("failed to encode message"); 88 | 89 | output.push(TestCase { 90 | name: "Request ".to_owned() + test, 91 | string: format!("{}", req), // dig formatted 92 | binary: hex::encode(&req_buf), // binary encoded 93 | }); 94 | 95 | // Send the request, and always expect a response. 96 | socket 97 | .send_to(&req_buf, "8.8.8.8:53") 98 | .expect("could not send data"); 99 | 100 | let mut resp_buf = [0; 1500]; 101 | let (amt, _) = socket 102 | .recv_from(&mut resp_buf) 103 | .expect("received no response"); 104 | 105 | let resp = Message::from_slice(&resp_buf[0..amt]).expect("invalid response from server"); 106 | 107 | output.push(TestCase { 108 | name: "Response ".to_owned() + test, 109 | string: format!("{}", resp), // dig formatted 110 | binary: hex::encode(&resp_buf[0..amt]), // binary encoded 111 | }); 112 | } 113 | 114 | println!("Writing new test data to {}", TEST_DATA_FILENAME); 115 | 116 | match serde_yaml::to_string(&output) { 117 | Err(e) => eprintln!("Failed to serialise test results: {:?}", e), 118 | Ok(s) => fs::write(TEST_DATA_FILENAME, s)?, 119 | } 120 | 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /src/clients/resolver.rs: -------------------------------------------------------------------------------- 1 | use crate::bail; 2 | use crate::clients::udp::Client as UdpClient; 3 | use crate::clients::Exchanger; 4 | use crate::types::*; 5 | use crate::Extension; 6 | use crate::Message; 7 | use std::collections::HashSet; 8 | use std::net::IpAddr; 9 | use std::net::SocketAddr; 10 | use std::net::ToSocketAddrs; 11 | 12 | // TODO https://docs.rs/hyper/0.14.9/src/hyper/client/connect/http.rs.html#32-35 13 | // https://docs.rs/hyper/0.14.9/src/hyper/client/client.rs.html#26-31 14 | // Lots of good example: 15 | // https://docs.rs/tower/0.4.8/src/tower/limit/concurrency/service.rs.html#26-55 16 | pub struct Resolver { 17 | client: E, 18 | } 19 | 20 | // TODO 21 | 22 | // 23 | // Should track the RA bit from remove servers (to know if they support recursion) 24 | // Should track `batting stats`, distribution of delays, etc. 25 | // 1. Host name to host address translation. (name -> ips) 26 | // 2. Host address to host name translation. (ip -> name) 27 | // 3. General lookup function. (name, type -> records) 28 | 29 | impl Default for Resolver { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | 35 | impl Resolver { 36 | /// Creates a new Resolver using the system's default DNS server. 37 | pub fn new() -> Resolver { 38 | let servers = crate::clients::udp::GOOGLE 39 | .iter() 40 | .flat_map(|a| a.to_socket_addrs()) 41 | .flatten() 42 | .collect::>(); 43 | 44 | let client = UdpClient::new(&servers[..]).unwrap(); // TODO Fix this 45 | Resolver::new_with_client(client) 46 | } 47 | } 48 | 49 | impl Resolver 50 | where 51 | E: Exchanger, 52 | { 53 | /// Creates a new Resolver using the system's default DNS server. 54 | pub fn new_with_client(client: E) -> Resolver { 55 | Resolver { client } 56 | } 57 | 58 | //pub fn new_with_client(Exchanger) 59 | 60 | /// Resolves a name into one or more IP address. 61 | // 62 | /// See [rfc1035#section-7] and [rfc1034#section-5]. 63 | /// 64 | /// [rfc1035#section-7]: https://datatracker.ietf.org/doc/html/rfc1035#section-7 65 | /// [rfc1034#section-5]: https://datatracker.ietf.org/doc/html/ 66 | // TODO Should this return a Iterator, or a Vector? Check other APIs. 67 | // https://docs.rs/tokio/1.6.1/tokio/net/fn.lookup_host.html yield a iterator 68 | pub fn lookup(&self, name: &str) -> Result, crate::Error> { 69 | let mut results = HashSet::new(); 70 | 71 | // TODO Change this to make both DNS requests in parallel 72 | // If we returned a iterator, perhaps we could start to return entries 73 | // before they have all complete? 74 | 75 | // Send two queries, a A and a AAAA. 76 | for r#type in &[Type::A, Type::AAAA] { 77 | let mut query = Message::default(); 78 | query.add_question(name, *r#type, Class::Internet); 79 | query.add_extension(Extension { 80 | payload_size: 4096, // Allow for bigger responses. 81 | 82 | ..Default::default() 83 | }); 84 | 85 | let response = self.client.exchange(&query)?; // TODO Better error message 86 | 87 | println!( 88 | "{}: Trying {} and got {}", 89 | name, 90 | r#type, 91 | response.answers.len() 92 | ); 93 | 94 | match response.rcode { 95 | Rcode::NoError => (), // Nothing 96 | _ => bail!(InvalidInput, "query failed with rcode: {}", response.rcode), 97 | }; 98 | 99 | for answer in response.answers { 100 | // TODO Check the answer is for this question. 101 | match answer.resource { 102 | Resource::A(ip4) => results.insert(IpAddr::V4(ip4)), 103 | Resource::AAAA(ip6) => results.insert(IpAddr::V6(ip6)), 104 | //Resource::A(ip4) => results.push(ip4), 105 | _ => false, // Ignore other types 106 | }; 107 | } 108 | } 109 | 110 | Ok(results.into_iter().collect()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/zones/mod.rs: -------------------------------------------------------------------------------- 1 | /// TODO Document 2 | // TODO https://github.com/Badcow/DNS-Parser has a nice custom format extension. Perhaps include? 3 | use crate::zones::preprocessor::preprocess; 4 | use crate::zones::parser::Rule; 5 | use crate::zones::parser::ZoneParser; 6 | use crate::Class; 7 | use crate::Resource; 8 | use pest_consume::Parser; 9 | use std::str::FromStr; 10 | use std::time::Duration; 11 | use strum_macros::Display; 12 | 13 | mod parser; 14 | mod parser_tests; 15 | mod preprocessor; 16 | mod process; 17 | 18 | /// A Zone File. This is the unprocessed version of the zone file 19 | /// where domains such as "@" have not yet been resolved, and fields 20 | /// are optional. To turn this into [`Vec`] call 21 | /// [`process`]. 22 | #[derive(Clone, Debug, PartialEq)] 23 | pub struct File { 24 | /// The origin as defined when creating the Zone File. This is different than 25 | /// a origin set within the zone file. 26 | /// 27 | /// This should always be a absolute domain, but we don't need the dot on the end. 28 | pub origin: Option, 29 | 30 | /// The list of Entries within the Zone File. 31 | pub entries: Vec, 32 | } 33 | 34 | impl File { 35 | pub fn new(mut origin: Option, entries: Vec) -> File { 36 | if let Some(domain) = origin { 37 | if let Some(domain) = domain.strip_suffix('.') { 38 | origin = Some(domain.to_owned()) 39 | } else { 40 | panic!("TODO Origin wasn't a absolute domain"); 41 | } 42 | } 43 | 44 | File { origin, entries } 45 | } 46 | } 47 | 48 | impl FromStr for File { 49 | type Err = pest_consume::Error; 50 | 51 | /// Parse a full zone file. 52 | /// 53 | /// ``` 54 | /// use rustdns::Resource; 55 | /// use rustdns::zones::{File, Entry, Record}; 56 | /// use std::str::FromStr; 57 | /// 58 | /// let file = File::from_str("$ORIGIN example.com.\n www A 192.0.2.1"); 59 | /// assert_eq!(file, Ok(File::new(None, vec![ 60 | /// Entry::Origin("example.com.".to_string()), 61 | /// Entry::Record(Record { 62 | /// name: Some("www".to_string()), 63 | /// ttl: None, 64 | /// class: None, 65 | /// resource: Resource::A("192.0.2.1".parse().unwrap()), 66 | /// }), 67 | /// ]))); 68 | /// ``` 69 | fn from_str(input_str: &str) -> Result { 70 | let input_str = preprocess(input_str).unwrap(); // TODO 71 | 72 | let inputs = ZoneParser::parse(Rule::file, &input_str)?; 73 | let input = inputs.single()?; 74 | 75 | ZoneParser::file(input).map(|x| File::new(None, x)) 76 | } 77 | } 78 | 79 | /// Internal struct for capturing each entry. 80 | #[derive(Clone, Debug, Display, PartialEq)] 81 | pub enum Entry { 82 | Origin(String), 83 | TTL(Duration), 84 | // TODO support $INCLUDE 85 | Record(Record), 86 | } 87 | 88 | /// Very similar to a [`rustdns::Record`] but allows for 89 | /// optional values. When parsing a full zone file 90 | /// those options can be derived from previous entries. 91 | // TODO Implement a Display to turn this back into Zone format. 92 | #[derive(Clone, Debug, PartialEq)] 93 | pub struct Record { 94 | pub name: Option, 95 | pub ttl: Option, 96 | pub class: Option, 97 | pub resource: Resource, 98 | } 99 | 100 | impl Default for Record { 101 | fn default() -> Self { 102 | Self { 103 | name: None, 104 | ttl: None, 105 | class: None, 106 | resource: Resource::ANY, // This is not really a good default, but it's atleast invalid. 107 | } 108 | } 109 | } 110 | 111 | impl FromStr for Record { 112 | type Err = pest_consume::Error; 113 | 114 | /// Parse a single zone file resource record. 115 | /// 116 | /// For example: 117 | /// 118 | /// ``` 119 | /// use rustdns::Resource; 120 | /// use rustdns::zones::Record; 121 | /// use std::str::FromStr; 122 | /// 123 | /// let record = Record::from_str("example.com. A 192.0.2.1"); 124 | /// assert_eq!(record, Ok(Record { 125 | /// name: Some("example.com.".to_string()), 126 | /// ttl: None, 127 | /// class: None, 128 | /// resource: Resource::A("192.0.2.1".parse().unwrap()), 129 | /// })); 130 | /// ``` 131 | /// 132 | /// This function is mostly useful for test code, or quickly parsing a 133 | /// single record. Please prefer to use [`File::from_str`] to parse full files. 134 | fn from_str(input_str: &str) -> Result { 135 | let inputs = ZoneParser::parse(Rule::single_record, input_str)?; 136 | let input = inputs.single()?; 137 | ZoneParser::single_record(input) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/from_str.rs: -------------------------------------------------------------------------------- 1 | //! Implements the FromStr trait for the various types, to be able to parse in `dig` style. 2 | // Refer to https://github.com/tigeli/bind-utils/blob/master/bin/dig/dig.c for reference. 3 | 4 | use crate::TXT; 5 | use crate::Resource; 6 | use crate::Type; 7 | use crate::MX; 8 | use crate::SOA; 9 | use crate::SRV; 10 | use core::num::ParseIntError; 11 | use core::str::FromStr; 12 | use regex::Regex; 13 | use std::net::AddrParseError; 14 | use std::time::Duration; 15 | use thiserror::Error; 16 | 17 | #[derive(Error, Debug)] 18 | pub enum FromStrError { 19 | #[error("that resource type doesn't have a text representation")] 20 | UnsupportedType, 21 | 22 | #[error("string doesn't match expected format")] 23 | InvalidFormat, 24 | 25 | #[error(transparent)] 26 | ParseIntError(#[from] ParseIntError), 27 | 28 | #[error(transparent)] 29 | AddrParseError(#[from] AddrParseError), 30 | } 31 | 32 | impl Resource { 33 | // Similar to the FromStr but needs the record Type since they are ambiguous. 34 | pub fn from_str(r#type: Type, s: &str) -> Result { 35 | Ok(match r#type { 36 | // IP Addresses 37 | Type::A => Resource::A(s.parse()?), 38 | Type::AAAA => Resource::AAAA(s.parse()?), 39 | 40 | // Simple strings (domains) 41 | Type::NS => Resource::NS(s.to_string()), 42 | Type::CNAME => Resource::CNAME(s.to_string()), 43 | Type::PTR => Resource::PTR(s.to_string()), 44 | 45 | // Complex types 46 | Type::MX => Resource::MX(s.parse()?), 47 | Type::SRV => Resource::SRV(s.parse()?), 48 | Type::SOA => Resource::SOA(s.parse()?), 49 | Type::SPF => Resource::SPF(s.parse()?), 50 | Type::TXT => Resource::TXT(s.parse()?), 51 | 52 | // This should never appear in a answer record unless we have invalid data. 53 | Type::Reserved | Type::OPT | Type::ANY => return Err(FromStrError::UnsupportedType), 54 | }) 55 | } 56 | } 57 | 58 | impl FromStr for SOA { 59 | type Err = FromStrError; 60 | 61 | fn from_str(s: &str) -> Result { 62 | lazy_static! { 63 | // "ns1.google.com. dns-admin.google.com. 376337657 900 900 1800 60" 64 | // "{mname} {rname} {serial} {refresh} {retry} {expire} {minimum}", 65 | static ref RE: Regex = Regex::new(r"^(\S+) (\S+) (\d+) (\d+) (\d+) (\d+) (\d+)$").unwrap(); 66 | } 67 | 68 | if let Some(caps) = RE.captures(s) { 69 | let rname = caps[2].to_string(); 70 | let rname = match Self::rname_to_email(&rname) { 71 | Ok(name) => name, 72 | Err(_) => rname, // Ignore the error 73 | }; 74 | 75 | Ok(SOA { 76 | mname: caps[1].to_string(), 77 | rname, 78 | serial: caps[3].parse()?, 79 | refresh: Duration::from_secs(caps[4].parse()?), 80 | retry: Duration::from_secs(caps[5].parse()?), 81 | expire: Duration::from_secs(caps[6].parse()?), 82 | minimum: Duration::from_secs(caps[7].parse()?), 83 | }) 84 | } else { 85 | Err(FromStrError::InvalidFormat) 86 | } 87 | } 88 | } 89 | 90 | impl FromStr for MX { 91 | type Err = FromStrError; 92 | 93 | fn from_str(s: &str) -> Result { 94 | lazy_static! { 95 | // "10 aspmx.l.google.com." 96 | // "{preference} {exchange}", 97 | static ref RE: Regex = Regex::new(r"^(\d+) (.+)$").unwrap(); 98 | } 99 | if let Some(caps) = RE.captures(s) { 100 | Ok(MX { 101 | preference: caps[1].parse()?, 102 | exchange: caps[2].to_string(), 103 | }) 104 | } else { 105 | Err(FromStrError::InvalidFormat) 106 | } 107 | } 108 | } 109 | 110 | impl FromStr for SRV { 111 | type Err = FromStrError; 112 | 113 | fn from_str(s: &str) -> Result { 114 | lazy_static! { 115 | // "5 0 389 ldap.google.com." 116 | // "{priority} {weight} {port} {name}", 117 | static ref RE: Regex = Regex::new(r"^(\d+) (\d+) (\d+) (.+)$").unwrap(); 118 | } 119 | if let Some(caps) = RE.captures(s) { 120 | Ok(SRV { 121 | priority: caps[1].parse()?, 122 | weight: caps[2].parse()?, 123 | port: caps[3].parse()?, 124 | name: caps[4].to_string(), 125 | }) 126 | } else { 127 | Err(FromStrError::InvalidFormat) 128 | } 129 | } 130 | } 131 | 132 | impl FromStr for TXT { 133 | type Err = FromStrError; 134 | 135 | fn from_str(s: &str) -> Result { 136 | lazy_static! { 137 | // TODO Handle escaped quotes 138 | static ref RE: Regex = Regex::new(r#""(.*?)""#).unwrap(); 139 | } 140 | 141 | if !s.starts_with('"') && !s.ends_with('"') { 142 | // Assume a single unquoted string 143 | return Ok(TXT::from(s)) 144 | } 145 | 146 | // Otherparse parse multiple "..." strings 147 | let mut txts = Vec::new(); 148 | for caps in RE.captures_iter(s) { 149 | txts.push(caps[1].as_bytes().to_vec()); 150 | }; 151 | 152 | if txts.is_empty() { 153 | return Err(FromStrError::InvalidFormat); 154 | } 155 | 156 | // TODO Also check we parsed the full record 157 | 158 | Ok(TXT(txts)) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/io.rs: -------------------------------------------------------------------------------- 1 | //! Various traits to help parsing of DNS messages. 2 | 3 | use crate::bail; 4 | use crate::types::{Class, Type}; 5 | use byteorder::{ReadBytesExt, BE}; 6 | use num_traits::FromPrimitive; 7 | use std::convert::TryInto; 8 | use std::io; 9 | use std::io::Cursor; 10 | use std::io::SeekFrom; 11 | 12 | pub trait SeekExt: io::Seek { 13 | /// Returns the number of bytes remaining to be consumed. 14 | /// This is used as a way to check for malformed input. 15 | fn remaining(&mut self) -> io::Result { 16 | let pos = self.stream_position()?; 17 | let len = self.seek(SeekFrom::End(0))?; 18 | 19 | // reset position 20 | self.seek(SeekFrom::Start(pos))?; 21 | 22 | Ok(len - pos) 23 | } 24 | } 25 | 26 | impl<'a> SeekExt for Cursor<&'a [u8]> { 27 | fn remaining(self: &mut std::io::Cursor<&'a [u8]>) -> io::Result { 28 | let pos = self.position() as usize; 29 | let len = self.get_ref().len() as usize; 30 | 31 | Ok((len - pos).try_into().unwrap()) 32 | } 33 | } 34 | 35 | pub trait CursorExt { 36 | /// Return a cursor that is bounded over the original cursor by start-end. 37 | /// 38 | /// The returned cursor contains all values with start <= x < end. It is empty if start >= end. 39 | /// 40 | /// Similar to `Take` but allows the start-end range to be specified, instead of just the next 41 | /// N values. 42 | fn sub_cursor(&mut self, start: usize, end: usize) -> io::Result>; 43 | } 44 | 45 | impl<'a> CursorExt<&'a [u8]> for Cursor<&'a [u8]> { 46 | fn sub_cursor(&mut self, start: usize, end: usize) -> io::Result> { 47 | let buf = self.get_ref(); 48 | 49 | let start = start.clamp(0, buf.len()); 50 | let end = end.clamp(start, buf.len()); 51 | 52 | let record = Cursor::new(&buf[start..end]); 53 | Ok(record) 54 | } 55 | } 56 | 57 | /// All types that implement `Read` and `Seek` get methods defined 58 | /// in `DNSReadExt` for free. 59 | impl DNSReadExt for R {} 60 | 61 | /// Extensions to io::Read to add some DNS specific types. 62 | pub trait DNSReadExt: io::Read + io::Seek { 63 | /// Reads a puny encoded domain name from a byte array. 64 | /// 65 | /// Used for extracting a encoding ASCII domain name from a DNS message. Will 66 | /// returns the Unicode domain name, as well as the length of this name (ignoring 67 | /// any compressed pointers) in bytes. 68 | /// 69 | /// # Errors 70 | /// 71 | /// Will return a io::Error(InvalidData) if the read domain name is invalid, or 72 | /// a more general io::Error on any other read failure. 73 | fn read_qname(&mut self) -> io::Result { 74 | let mut qname = String::new(); 75 | let start = self.stream_position()?; 76 | 77 | // Read each label one at a time, to build up the full domain name. 78 | loop { 79 | // Length of the first label 80 | let len = self.read_u8()?; 81 | if len == 0 { 82 | if qname.is_empty() { 83 | qname.push('.') // Root domain 84 | } 85 | break; 86 | } 87 | 88 | match len & 0xC0 { 89 | // No compression 90 | 0x00 => { 91 | let mut label = vec![0; len.into()]; 92 | self.read_exact(&mut label)?; 93 | 94 | // Really this is meant to be ASCII, but we read as utf8 95 | // (as that what Rust provides). 96 | let label = match std::str::from_utf8(&label) { 97 | Err(e) => bail!(InvalidData, "invalid label: {}", e), 98 | Ok(s) => s, 99 | }; 100 | 101 | if !label.is_ascii() { 102 | bail!(InvalidData, "invalid label '{:}': not valid ascii", label); 103 | } 104 | 105 | // Now puny decode this label returning its original unicode. 106 | let label = match idna::domain_to_unicode(label) { 107 | (label, Err(e)) => bail!(InvalidData, "invalid label '{:}': {}", label, e), 108 | (label, Ok(_)) => label, 109 | }; 110 | 111 | qname.push_str(&label); 112 | qname.push('.'); 113 | } 114 | 115 | // Compression 116 | 0xC0 => { 117 | // Read the 14 bit pointer. 118 | let b2 = self.read_u8()? as u16; 119 | let ptr = ((len as u16 & !0xC0) << 8 | b2) as u64; 120 | 121 | // Make sure we don't get into a loop. 122 | if ptr >= start { 123 | bail!( 124 | InvalidData, 125 | "invalid compressed pointer pointing to future bytes" 126 | ); 127 | } 128 | 129 | // We are going to jump backwards, so record where we 130 | // currently are. So we can reset it later. 131 | let current = self.stream_position()?; 132 | 133 | // Jump and start reading the qname again. 134 | self.seek(SeekFrom::Start(ptr))?; 135 | qname.push_str(&self.read_qname()?); 136 | 137 | // Reset ourselves. 138 | self.seek(SeekFrom::Start(current))?; 139 | 140 | break; 141 | } 142 | 143 | // Unknown 144 | _ => bail!( 145 | InvalidData, 146 | "unsupported compression type {0:b}", 147 | len & 0xC0 148 | ), 149 | } 150 | } 151 | 152 | Ok(qname) 153 | } 154 | 155 | /// Reads a DNS Type. 156 | fn read_type(&mut self) -> io::Result { 157 | let r#type = self.read_u16::()?; 158 | let r#type = match FromPrimitive::from_u16(r#type) { 159 | Some(t) => t, 160 | None => bail!(InvalidData, "invalid Type({})", r#type), 161 | }; 162 | 163 | Ok(r#type) 164 | } 165 | 166 | /// Reads a DNS Class. 167 | fn read_class(&mut self) -> io::Result { 168 | let class = self.read_u16::()?; 169 | let class = match FromPrimitive::from_u16(class) { 170 | Some(t) => t, 171 | None => bail!(InvalidData, "invalid Class({})", class), 172 | }; 173 | 174 | Ok(class) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/clients/doh.rs: -------------------------------------------------------------------------------- 1 | use crate::bail; 2 | use crate::clients::mime::content_type_equal; 3 | use crate::clients::AsyncExchanger; 4 | use crate::clients::ToUrls; 5 | use crate::Message; 6 | use crate::clients::stats::StatsBuilder; 7 | use async_trait::async_trait; 8 | use http::header::*; 9 | use http::{Method, Request}; 10 | use hyper::client::connect::HttpInfo; 11 | use hyper::{Body, Client as HyperClient}; 12 | use hyper_alpn::AlpnConnector; 13 | use std::net::IpAddr; 14 | use std::net::Ipv4Addr; 15 | use std::net::SocketAddr; 16 | use std::time::Duration; 17 | use url::Url; 18 | 19 | pub const GOOGLE: &str = "https://dns.google/dns-query"; 20 | 21 | // For use in Content-type and Accept headers 22 | const CONTENT_TYPE_APPLICATION_DNS_MESSAGE: &str = "application/dns-message"; 23 | 24 | // The param name that contains the DNS request. 25 | const DNS_QUERY_PARAM: &str = "dns"; 26 | 27 | /// A DNS over HTTPS (DoH) Client (rfc8484). 28 | /// 29 | /// # Example 30 | /// 31 | /// ```rust 32 | /// use crate::rustdns::clients::AsyncExchanger; 33 | /// use http::method::Method; 34 | /// use rustdns::clients::doh::Client; 35 | /// use rustdns::types::*; 36 | /// 37 | /// #[tokio::main] 38 | /// async fn main() -> Result<(), rustdns::Error> { 39 | /// let mut query = Message::default(); 40 | /// query.add_question("bramp.net", Type::A, Class::Internet); 41 | /// 42 | /// let response = Client::new("https://dns.google/dns-query", Method::GET)? 43 | /// .exchange(&query) 44 | /// .await 45 | /// .expect("could not exchange message"); 46 | /// 47 | /// println!("{}", response); 48 | /// Ok(()) 49 | /// } 50 | /// ``` 51 | /// 52 | /// See 53 | // TODO Document all the options. 54 | pub struct Client { 55 | servers: Vec, 56 | method: Method, // One of POST or GET 57 | } 58 | 59 | impl Default for Client { 60 | fn default() -> Self { 61 | Client { 62 | servers: Vec::default(), 63 | method: Method::GET, 64 | } 65 | } 66 | } 67 | 68 | impl Client { 69 | /// Creates a new Client bound to the specific servers. 70 | /// 71 | /// Be aware that the servers will typically be in the form of `https://domain_name/`. That 72 | /// `domain_name` will be resolved by the system's standard DNS library. I don't have a good 73 | /// work-around for this yet. 74 | // TODO Document how it fails. 75 | pub fn new(servers: A, method: Method) -> Result { 76 | match method { 77 | Method::GET | Method::POST => (), // Nothing, 78 | _ => bail!(InvalidInput, "only GET and POST allowed"), 79 | } 80 | 81 | Ok(Self { 82 | servers: servers.to_urls()?.collect(), 83 | method, 84 | }) 85 | } 86 | } 87 | 88 | #[async_trait] 89 | impl AsyncExchanger for Client { 90 | /// Sends the [`Message`] to the `server` via HTTP and returns the result. 91 | // TODO Decide if this should be async or not. 92 | // Can return ::std::io::Error 93 | async fn exchange(&self, query: &Message) -> Result { 94 | let mut query = query.clone(); 95 | query.id = 0; 96 | 97 | let p = query.to_vec()?; 98 | 99 | // Create a Alpn client, so our connection will upgrade to HTTP/2. 100 | // TODO Move the client into the struct/new() 101 | // TODO Change the Connector Connect method to allow us to override the DNS 102 | // resolution in the connector! 103 | let alpn = AlpnConnector::new(); 104 | 105 | let client = HyperClient::builder() 106 | .pool_idle_timeout(Duration::from_secs(30)) 107 | .http2_only(true) // TODO POST stop working when this is false. Figure that out. 108 | .build::<_, hyper::Body>(alpn); 109 | 110 | // Base request common to both GET and POST 111 | let req = Request::builder() 112 | .method(&self.method) 113 | .header(ACCEPT, CONTENT_TYPE_APPLICATION_DNS_MESSAGE); 114 | 115 | let req = match self.method { 116 | Method::GET => { 117 | // Encode the message as a base64 string 118 | let mut buf = String::new(); 119 | base64::encode_config_buf(p, base64::URL_SAFE_NO_PAD, &mut buf); 120 | 121 | // and add to the query params. 122 | let mut url = self.servers[0].clone(); // TODO Support more than one server 123 | url.query_pairs_mut().append_pair(DNS_QUERY_PARAM, &buf); 124 | 125 | // We have to do this wierd as_str().parse() thing because the 126 | // http::Uri doesn't provide a way to easily mutate or construct it. 127 | let uri: hyper::Uri = url.as_str().parse()?; 128 | req.uri(uri).body(Body::empty()) 129 | } 130 | Method::POST => { 131 | req.uri(self.servers[0].as_str()) // TODO Support more than one server 132 | .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_DNS_MESSAGE) 133 | .body(Body::from(p)) // content-length header will be added. 134 | } 135 | _ => bail!(InvalidInput, "only GET and POST allowed"), 136 | }; 137 | 138 | let stats = StatsBuilder::start(0); 139 | 140 | let resp = client.request(req.unwrap()).await?; 141 | // TODO This media type restricts the maximum size of the DNS message to 65535 bytes 142 | 143 | if let Some(content_type) = resp.headers().get(CONTENT_TYPE) { 144 | if !content_type_equal(content_type, CONTENT_TYPE_APPLICATION_DNS_MESSAGE) { 145 | bail!( 146 | InvalidData, 147 | "recevied invalid content-type: {:?} expected {}", 148 | content_type, 149 | CONTENT_TYPE_APPLICATION_DNS_MESSAGE, 150 | ); 151 | } 152 | } 153 | 154 | if resp.status().is_success() { 155 | // TODO check Content-Length, but don't allow us to consume a body longer than 65535 bytes! 156 | 157 | // Get connection information (if available) 158 | let remote_addr = match resp.extensions().get::() { 159 | Some(http_info) => http_info.remote_addr(), 160 | 161 | // TODO Maybe remote_addr should be optional? 162 | None => SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0), // Dummy address 163 | }; 164 | 165 | // Read the full body 166 | let body = hyper::body::to_bytes(resp.into_body()).await?; 167 | 168 | let mut m = Message::from_slice(&body)?; 169 | m.stats = Some(stats.end(remote_addr, body.len())); 170 | 171 | return Ok(m); 172 | } 173 | 174 | // TODO Retry on 500s. If this is a 4xx we should not retry. Should we follow 3xx? 175 | bail!( 176 | InvalidInput, 177 | "recevied unexpected HTTP status code: {:}", 178 | resp.status() 179 | ); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/rustdns.svg)](https://crates.io/crates/rustdns) 2 | [![Documentation](https://docs.rs/rustdns/badge.svg)](https://docs.rs/rustdns) 3 | [![Build Status](https://github.com/bramp/rustdns/actions/workflows/rust.yml/badge.svg)](https://github.com/bramp/rustdns) 4 | 5 | # rustdns 6 | 7 | ## rustdns 8 | 9 | rustdns is a simple, fast, and fully fledged DNS library for interacting 10 | with domain name services at a high or low level. 11 | 12 | ## Features 13 | * Parsing and generating the following record types: 14 | * A, 15 | * AAAA, 16 | * CNAME, 17 | * MX, 18 | * NS, 19 | * SOA, 20 | * PTR, 21 | * TXT, and 22 | * SRV 23 | * Extension Mechanisms for DNS ([EDNS(0)]). 24 | * Support [International Domain Names (IDNA)](https://en.wikipedia.org/wiki/Internationalized_domain_name) - Different scripts, alphabets, anhd even emojis! 25 | * Sample `dig` style [command line](#usage-cli). 26 | * Fully [tested](#testing), and [fuzzed](#fuzzing). 27 | 28 | ## Usage (low-level library) 29 | 30 | ```rust 31 | use rustdns::Message; 32 | use rustdns::types::*; 33 | use std::net::UdpSocket; 34 | use std::time::Duration; 35 | 36 | fn udp_example() -> std::io::Result<()> { 37 | // A DNS Message can be easily constructed 38 | let mut m = Message::default(); 39 | m.add_question("bramp.net", Type::A, Class::Internet); 40 | m.add_extension(Extension { // Optionally add a EDNS extension 41 | payload_size: 4096, // which supports a larger payload size. 42 | ..Default::default() 43 | }); 44 | 45 | // Setup a UDP socket for sending to a DNS server. 46 | let socket = UdpSocket::bind("0.0.0.0:0")?; 47 | socket.set_read_timeout(Some(Duration::new(5, 0)))?; 48 | socket.connect("8.8.8.8:53")?; // Google's Public DNS Servers 49 | 50 | // Encode the DNS Message as a Vec. 51 | let question = m.to_vec()?; 52 | 53 | // Send to the server. 54 | socket.send(&question)?; 55 | 56 | // Wait for a response from the DNS server. 57 | let mut resp = [0; 4096]; 58 | let len = socket.recv(&mut resp)?; 59 | 60 | // Take the response bytes and turn it into another DNS Message. 61 | let answer = Message::from_slice(&resp[0..len])?; 62 | 63 | // Now do something with `answer`, in this case print it! 64 | println!("DNS Response:\n{}", answer); 65 | 66 | Ok(()) 67 | } 68 | ``` 69 | 70 | If successful something like the following will be printed: 71 | 72 | ``` 73 | ;; ->>HEADER<<- opcode: Query, status: NoError, id: 44857 74 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 75 | 76 | ;; OPT PSEUDOSECTION: 77 | ; EDNS: version: 0, flags:; udp: 512 78 | ;; QUESTION SECTION: 79 | ; bramp.net. IN A 80 | 81 | ; ANSWER SECTION: 82 | bramp.net. 299 IN A 104.21.62.200 83 | bramp.net. 299 IN A 172.67.138.196 84 | ``` 85 | 86 | ## Features 87 | The following optional features are available: 88 | 89 | - `clients`: Enables the following clients: 90 | - `doh`: DNS over HTTPS (DoH) client (rfc8484). 91 | - `json`: DNS over HTTPS JSON client 92 | - `tcp`: Enables the DNS over TCP client 93 | - `udp`: Enables the DNS over UDP client 94 | - `zones`: Enable a Zone File Parser 95 | 96 | ## Usage (cli) 97 | 98 | To use the [demo CLI](https://github.com/bramp/rustdns/blob/main/src/rustdns/dig/main.rs): 99 | 100 | ```shell 101 | $ cargo run -p dig -- A www.google.com 102 | ... 103 | ;; ->>HEADER<<- opcode: Query, status: NoError, id: 34327 104 | ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 105 | 106 | ;; OPT PSEUDOSECTION: 107 | ; EDNS: version: 0, flags:; udp: 512 108 | ;; QUESTION SECTION: 109 | ; www.google.com. IN A 110 | 111 | ; ANSWER SECTION: 112 | www.google.com. 110 IN A 142.250.72.196 113 | 114 | # More examples 115 | $ cargo run -p dig -- AAAA www.google.com 116 | $ cargo run -p dig -- ANY www.google.com 117 | $ cargo run -p dig -- CNAME code.google.com 118 | $ cargo run -p dig -- MX google.com 119 | $ cargo run -p dig -- PTR 4.4.8.8.in-addr.arpa 120 | $ cargo run -p dig -- SOA google.com 121 | $ cargo run -p dig -- SRV _ldap._tcp.google.com 122 | $ cargo run -p dig -- TXT google.com 123 | ``` 124 | ## Testing 125 | 126 | ```shell 127 | $ cargo test --all 128 | 129 | # or the handy 130 | $ cargo watch -- cargo test --all -- --nocapture 131 | ``` 132 | 133 | The test suite is full of stored real life examples, from querying real DNS records. 134 | This was generated with `cargo run -p generate_tests`. 135 | 136 | ### Fuzzing 137 | 138 | The library has been extensively fuzzed. Try for yourself: 139 | 140 | ```shell 141 | $ cargo fuzz run from_slice 142 | ``` 143 | 144 | ### Test Data 145 | 146 | To aid in testing features, I have a set of pre-configured records setup: 147 | 148 | | Domain | Description | 149 | | --------------------- | ----------- | 150 | | a.bramp.net | Single A record pointing at 127.0.0.1 | 151 | | aaaa.bramp.net | Single AAAA record pointing at ::1 | 152 | | aaaaa.bramp.net | One A record, and one AAAA record resolving to 127.0.0.1 and ::1 | 153 | | cname.bramp.net | Single CNAME record pointing at a.bramp.net | 154 | | cname-loop1.bramp.net | Single CNAME record pointing at cname-loop2.bramp.net | 155 | | cname-loop2.bramp.net | Single CNAME record pointing at cname-loop1.bramp.net | 156 | | mx.bramp.net | Single MX record pointing at a.bramp.net | 157 | | ns.bramp.net | Single NS record pointing at a.bramp.net | 158 | | txt.bramp.net | Single TXT Record "A TXT record!" | 159 | 160 | ## Releasing 161 | 162 | ```shell 163 | # Bump version number 164 | $ cargo test-all-features 165 | $ cargo readme > README.md 166 | $ cargo publish --dry-run 167 | $ cargo publish 168 | ``` 169 | 170 | ## TODO (in order of priority) 171 | * [ ] Document UDP/TCP library 172 | * [ ] Client side examples 173 | * [ ] Server side examples 174 | * [ ] DNSSEC: Signing, validating and key generation for DSA, RSA, ECDSA and Ed25519 175 | * [ ] NSID, Cookies, AXFR/IXFR, TSIG, SIG(0) 176 | * [ ] Runtime-independence 177 | * [ ] Change the API to have getters and setters. 178 | * [ ] Change hyper-alpn to support tokio-native-tls for people that want that. 179 | * [ ] Implement more dig features, such as +trace 180 | * [ ] Maybe convert the binary parsing to Nom format. 181 | * [ ] Can I parse these https://www.iana.org/domains/root/files ? 182 | 183 | ### Reference 184 | 185 | * [rfc1034]: DOMAIN NAMES - CONCEPTS AND FACILITIES 186 | * [rfc1035]: DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION 187 | * [rfc6895]: Domain Name System (DNS) IANA Considerations 188 | * [IANA Domain Name System (DNS) Parameters](https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml) 189 | * [Computer Networks CPS365 FALL 2016](https://courses.cs.duke.edu//fall16/compsci356/DNS/DNS-primer.pdf) 190 | * [miekg's Go DNS Library](https://github.com/miekg/dns) 191 | 192 | [EDNS(0)]: https://en.wikipedia.org/wiki/Extension_Mechanisms_for_DNS 193 | [rfc1034]: https://datatracker.ietf.org/doc/html/rfc1034 194 | [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035 195 | [rfc6895]: https://datatracker.ietf.org/doc/html/rfc6895 196 | 197 | ## License: Apache-2.0 198 | 199 | ``` 200 | Copyright 2021 Andrew Brampton (bramp.net) 201 | 202 | Licensed under the Apache License, Version 2.0 (the "License"); 203 | you may not use this file except in compliance with the License. 204 | You may obtain a copy of the License at 205 | 206 | http://www.apache.org/licenses/LICENSE-2.0 207 | 208 | Unless required by applicable law or agreed to in writing, software 209 | distributed under the License is distributed on an "AS IS" BASIS, 210 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 211 | See the License for the specific language governing permissions and 212 | limitations under the License. 213 | ``` 214 | -------------------------------------------------------------------------------- /fuzz/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "arbitrary" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "237430fd6ed3740afe94eefcc278ae21e050285be882804e0d6e8695f0c94691" 10 | 11 | [[package]] 12 | name = "autocfg" 13 | version = "1.0.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 16 | 17 | [[package]] 18 | name = "byteorder" 19 | version = "1.4.3" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 22 | 23 | [[package]] 24 | name = "cc" 25 | version = "1.0.68" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" 28 | 29 | [[package]] 30 | name = "encoding8" 31 | version = "0.3.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "3144e455c7aeda487c72555cac2ef84ccac173b29a57b07382ba27016e57b246" 34 | 35 | [[package]] 36 | name = "heck" 37 | version = "0.3.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" 40 | dependencies = [ 41 | "unicode-segmentation", 42 | ] 43 | 44 | [[package]] 45 | name = "idna" 46 | version = "0.2.3" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 49 | dependencies = [ 50 | "matches", 51 | "unicode-bidi", 52 | "unicode-normalization", 53 | ] 54 | 55 | [[package]] 56 | name = "libfuzzer-sys" 57 | version = "0.4.2" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "36a9a84a6e8b55dfefb04235e55edb2b9a2a18488fcae777a6bdaa6f06f1deb3" 60 | dependencies = [ 61 | "arbitrary", 62 | "cc", 63 | "once_cell", 64 | ] 65 | 66 | [[package]] 67 | name = "matches" 68 | version = "0.1.8" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 71 | 72 | [[package]] 73 | name = "num" 74 | version = "0.4.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" 77 | dependencies = [ 78 | "num-bigint", 79 | "num-complex", 80 | "num-integer", 81 | "num-iter", 82 | "num-rational", 83 | "num-traits", 84 | ] 85 | 86 | [[package]] 87 | name = "num-bigint" 88 | version = "0.4.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" 91 | dependencies = [ 92 | "autocfg", 93 | "num-integer", 94 | "num-traits", 95 | ] 96 | 97 | [[package]] 98 | name = "num-complex" 99 | version = "0.4.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" 102 | dependencies = [ 103 | "num-traits", 104 | ] 105 | 106 | [[package]] 107 | name = "num-derive" 108 | version = "0.3.3" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 111 | dependencies = [ 112 | "proc-macro2", 113 | "quote", 114 | "syn", 115 | ] 116 | 117 | [[package]] 118 | name = "num-integer" 119 | version = "0.1.44" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 122 | dependencies = [ 123 | "autocfg", 124 | "num-traits", 125 | ] 126 | 127 | [[package]] 128 | name = "num-iter" 129 | version = "0.1.42" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 132 | dependencies = [ 133 | "autocfg", 134 | "num-integer", 135 | "num-traits", 136 | ] 137 | 138 | [[package]] 139 | name = "num-rational" 140 | version = "0.4.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" 143 | dependencies = [ 144 | "autocfg", 145 | "num-bigint", 146 | "num-integer", 147 | "num-traits", 148 | ] 149 | 150 | [[package]] 151 | name = "num-traits" 152 | version = "0.2.14" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 155 | dependencies = [ 156 | "autocfg", 157 | ] 158 | 159 | [[package]] 160 | name = "once_cell" 161 | version = "1.7.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" 164 | 165 | [[package]] 166 | name = "proc-macro2" 167 | version = "1.0.27" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 170 | dependencies = [ 171 | "unicode-xid", 172 | ] 173 | 174 | [[package]] 175 | name = "quote" 176 | version = "1.0.9" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 179 | dependencies = [ 180 | "proc-macro2", 181 | ] 182 | 183 | [[package]] 184 | name = "rustdns" 185 | version = "0.1.0" 186 | dependencies = [ 187 | "byteorder", 188 | "encoding8", 189 | "idna", 190 | "num", 191 | "num-derive", 192 | "num-traits", 193 | "strum", 194 | "strum_macros", 195 | ] 196 | 197 | [[package]] 198 | name = "rustdns-fuzz" 199 | version = "0.0.0" 200 | dependencies = [ 201 | "libfuzzer-sys", 202 | "rustdns", 203 | ] 204 | 205 | [[package]] 206 | name = "strum" 207 | version = "0.21.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" 210 | 211 | [[package]] 212 | name = "strum_macros" 213 | version = "0.21.1" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" 216 | dependencies = [ 217 | "heck", 218 | "proc-macro2", 219 | "quote", 220 | "syn", 221 | ] 222 | 223 | [[package]] 224 | name = "syn" 225 | version = "1.0.72" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" 228 | dependencies = [ 229 | "proc-macro2", 230 | "quote", 231 | "unicode-xid", 232 | ] 233 | 234 | [[package]] 235 | name = "tinyvec" 236 | version = "1.2.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" 239 | dependencies = [ 240 | "tinyvec_macros", 241 | ] 242 | 243 | [[package]] 244 | name = "tinyvec_macros" 245 | version = "0.1.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 248 | 249 | [[package]] 250 | name = "unicode-bidi" 251 | version = "0.3.5" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" 254 | dependencies = [ 255 | "matches", 256 | ] 257 | 258 | [[package]] 259 | name = "unicode-normalization" 260 | version = "0.1.19" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 263 | dependencies = [ 264 | "tinyvec", 265 | ] 266 | 267 | [[package]] 268 | name = "unicode-segmentation" 269 | version = "1.7.1" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 272 | 273 | [[package]] 274 | name = "unicode-xid" 275 | version = "0.2.2" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 278 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // TODO #![deny(missing_docs)] 2 | // TODO #![deny(missing_debug_implementations)] 3 | //! # rustdns 4 | //! 5 | //! rustdns is a simple, fast, and fully fledged DNS library for interacting 6 | //! with domain name services at a high or low level. 7 | //! 8 | //! # Features 9 | //! * Parsing and generating the following record types: 10 | //! * A, 11 | //! * AAAA, 12 | //! * CNAME, 13 | //! * MX, 14 | //! * NS, 15 | //! * SOA, 16 | //! * PTR, 17 | //! * TXT, and 18 | //! * SRV 19 | //! * Extension Mechanisms for DNS ([EDNS(0)]). 20 | //! * Support [International Domain Names (IDNA)](https://en.wikipedia.org/wiki/Internationalized_domain_name) - Different scripts, alphabets, anhd even emojis! 21 | //! * Sample `dig` style [command line](#usage-cli). 22 | //! * Fully [tested](#testing), and [fuzzed](#fuzzing). 23 | //! 24 | //! # Usage (low-level library) 25 | //! 26 | //! ```rust 27 | //! use rustdns::Message; 28 | //! use rustdns::types::*; 29 | //! use std::net::UdpSocket; 30 | //! use std::time::Duration; 31 | //! 32 | //! fn udp_example() -> std::io::Result<()> { 33 | //! // A DNS Message can be easily constructed 34 | //! let mut m = Message::default(); 35 | //! m.add_question("bramp.net", Type::A, Class::Internet); 36 | //! m.add_extension(Extension { // Optionally add a EDNS extension 37 | //! payload_size: 4096, // which supports a larger payload size. 38 | //! ..Default::default() 39 | //! }); 40 | //! 41 | //! // Setup a UDP socket for sending to a DNS server. 42 | //! let socket = UdpSocket::bind("0.0.0.0:0")?; 43 | //! socket.set_read_timeout(Some(Duration::new(5, 0)))?; 44 | //! socket.connect("8.8.8.8:53")?; // Google's Public DNS Servers 45 | //! 46 | //! // Encode the DNS Message as a Vec. 47 | //! let question = m.to_vec()?; 48 | //! 49 | //! // Send to the server. 50 | //! socket.send(&question)?; 51 | //! 52 | //! // Wait for a response from the DNS server. 53 | //! let mut resp = [0; 4096]; 54 | //! let len = socket.recv(&mut resp)?; 55 | //! 56 | //! // Take the response bytes and turn it into another DNS Message. 57 | //! let answer = Message::from_slice(&resp[0..len])?; 58 | //! 59 | //! // Now do something with `answer`, in this case print it! 60 | //! println!("DNS Response:\n{}", answer); 61 | //! 62 | //! Ok(()) 63 | //! } 64 | //! ``` 65 | //! 66 | //! If successful something like the following will be printed: 67 | //! 68 | //! ```text 69 | //! ;; ->>HEADER<<- opcode: Query, status: NoError, id: 44857 70 | //! ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 71 | //! 72 | //! ;; OPT PSEUDOSECTION: 73 | //! ; EDNS: version: 0, flags:; udp: 512 74 | //! ;; QUESTION SECTION: 75 | //! ; bramp.net. IN A 76 | //! 77 | //! ; ANSWER SECTION: 78 | //! bramp.net. 299 IN A 104.21.62.200 79 | //! bramp.net. 299 IN A 172.67.138.196 80 | //! ``` 81 | //! 82 | //! # Features 83 | //! The following optional features are available: 84 | //! 85 | //! - `clients`: Enables the following clients: 86 | //! - `doh`: DNS over HTTPS (DoH) client (rfc8484). 87 | //! - `json`: DNS over HTTPS JSON client 88 | //! - `tcp`: Enables the DNS over TCP client 89 | //! - `udp`: Enables the DNS over UDP client 90 | //! - `zones`: Enable a Zone File Parser 91 | //! 92 | //! # Usage (cli) 93 | //! 94 | //! To use the [demo CLI](https://github.com/bramp/rustdns/blob/main/src/rustdns/dig/main.rs): 95 | //! 96 | //! ```shell 97 | //! $ cargo run -p dig -- A www.google.com 98 | //! ... 99 | //! ;; ->>HEADER<<- opcode: Query, status: NoError, id: 34327 100 | //! ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 101 | //! 102 | //! ;; OPT PSEUDOSECTION: 103 | //! ; EDNS: version: 0, flags:; udp: 512 104 | //! ;; QUESTION SECTION: 105 | //! ; www.google.com. IN A 106 | //! 107 | //! ; ANSWER SECTION: 108 | //! www.google.com. 110 IN A 142.250.72.196 109 | //! 110 | //! # More examples 111 | //! $ cargo run -p dig -- AAAA www.google.com 112 | //! $ cargo run -p dig -- ANY www.google.com 113 | //! $ cargo run -p dig -- CNAME code.google.com 114 | //! $ cargo run -p dig -- MX google.com 115 | //! $ cargo run -p dig -- PTR 4.4.8.8.in-addr.arpa 116 | //! $ cargo run -p dig -- SOA google.com 117 | //! $ cargo run -p dig -- SRV _ldap._tcp.google.com 118 | //! $ cargo run -p dig -- TXT google.com 119 | //! ``` 120 | //! # Testing 121 | //! 122 | //! ```shell 123 | //! $ cargo test --all 124 | //! 125 | //! # or the handy 126 | //! $ cargo watch -- cargo test --all -- --nocapture 127 | //! ``` 128 | //! 129 | //! The test suite is full of stored real life examples, from querying real DNS records. 130 | //! This was generated with `cargo run -p generate_tests`. 131 | //! 132 | //! ## Fuzzing 133 | //! 134 | //! The library has been extensively fuzzed. Try for yourself: 135 | //! 136 | //! ```shell 137 | //! $ cargo fuzz run from_slice 138 | //! ``` 139 | //! 140 | //! ## Test Data 141 | //! 142 | //! To aid in testing features, I have a set of pre-configured records setup: 143 | //! 144 | //! | Domain | Description | 145 | //! | --------------------- | ----------- | 146 | //! | a.bramp.net | Single A record pointing at 127.0.0.1 | 147 | //! | aaaa.bramp.net | Single AAAA record pointing at ::1 | 148 | //! | aaaaa.bramp.net | One A record, and one AAAA record resolving to 127.0.0.1 and ::1 | 149 | //! | cname.bramp.net | Single CNAME record pointing at a.bramp.net | 150 | //! | cname-loop1.bramp.net | Single CNAME record pointing at cname-loop2.bramp.net | 151 | //! | cname-loop2.bramp.net | Single CNAME record pointing at cname-loop1.bramp.net | 152 | //! | mx.bramp.net | Single MX record pointing at a.bramp.net | 153 | //! | ns.bramp.net | Single NS record pointing at a.bramp.net | 154 | //! | txt.bramp.net | Single TXT Record "A TXT record!" | 155 | //! 156 | //! # Releasing 157 | //! 158 | //! ```shell 159 | //! # Bump version number 160 | //! $ cargo test-all-features 161 | //! $ cargo readme > README.md 162 | //! $ cargo publish --dry-run 163 | //! $ cargo publish 164 | //! ``` 165 | //! 166 | //! # TODO (in order of priority) 167 | //! * [ ] Document UDP/TCP library 168 | //! * [ ] Client side examples 169 | //! * [ ] Server side examples 170 | //! * [ ] DNSSEC: Signing, validating and key generation for DSA, RSA, ECDSA and Ed25519 171 | //! * [ ] NSID, Cookies, AXFR/IXFR, TSIG, SIG(0) 172 | //! * [ ] Runtime-independence 173 | //! * [ ] Change the API to have getters and setters. 174 | //! * [ ] Change hyper-alpn to support tokio-native-tls for people that want that. 175 | //! * [ ] Implement more dig features, such as +trace 176 | //! * [ ] Maybe convert the binary parsing to Nom format. 177 | //! * [ ] Can I parse these https://www.iana.org/domains/root/files ? 178 | //! 179 | //! ## Reference 180 | //! 181 | //! * [rfc1034]: DOMAIN NAMES - CONCEPTS AND FACILITIES 182 | //! * [rfc1035]: DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION 183 | //! * [rfc6895]: Domain Name System (DNS) IANA Considerations 184 | //! * [IANA Domain Name System (DNS) Parameters](https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml) 185 | //! * [Computer Networks CPS365 FALL 2016](https://courses.cs.duke.edu//fall16/compsci356/DNS/DNS-primer.pdf) 186 | //! * [miekg's Go DNS Library](https://github.com/miekg/dns) 187 | //! 188 | //! [EDNS(0)]: https://en.wikipedia.org/wiki/Extension_Mechanisms_for_DNS 189 | //! [rfc1034]: https://datatracker.ietf.org/doc/html/rfc1034 190 | //! [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035 191 | //! [rfc6895]: https://datatracker.ietf.org/doc/html/rfc6895 192 | 193 | #[macro_use] 194 | mod cfg; 195 | 196 | #[cfg(any(feature = "doh", feature = "json", feature = "tcp", feature = "udp"))] 197 | pub mod clients; 198 | 199 | mod display; 200 | mod dns; 201 | mod errors; 202 | mod from_str; 203 | mod io; 204 | pub mod resource; 205 | pub mod types; 206 | pub mod util; 207 | 208 | cfg_feature! { 209 | #![feature = "zones"] 210 | 211 | #[macro_use] 212 | extern crate pest_derive; 213 | 214 | pub mod zones; 215 | } 216 | 217 | #[macro_use] 218 | extern crate num_derive; 219 | 220 | #[macro_use] 221 | extern crate derivative; 222 | 223 | #[macro_use] 224 | extern crate lazy_static; 225 | 226 | // Pull up the various types that should be on the front page of the docs. 227 | #[doc(inline)] 228 | pub use crate::types::*; 229 | 230 | #[doc(inline)] 231 | pub use crate::resource::*; 232 | 233 | #[doc(inline)] 234 | #[cfg(feature = "udp")] 235 | // TODO Allow this resolve to use any available client 236 | pub use crate::clients::Resolver; 237 | 238 | pub use crate::errors::Error; 239 | pub use crate::errors::ParseError; 240 | -------------------------------------------------------------------------------- /tests/test_data.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Request A www.google.com 3 | binary: eccb012000010000000000000377777706676f6f676c6503636f6d0000010001 4 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; www.google.com. IN A \n\n\n\n" 5 | - name: Response A www.google.com 6 | binary: eccb818000010001000000000377777706676f6f676c6503636f6d0000010001c00c000100010000003c0004acd9a464 7 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; www.google.com. IN A \n\n\n; ANSWER SECTION:\nwww.google.com. 60 IN A 172.217.164.100\n\n\n" 8 | - name: Request AAAA www.google.com 9 | binary: eccb012000010000000000000377777706676f6f676c6503636f6d00001c0001 10 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; www.google.com. IN AAAA \n\n\n\n" 11 | - name: Response AAAA www.google.com 12 | binary: eccb818000010001000000000377777706676f6f676c6503636f6d00001c0001c00c001c00010000002b00102607f8b0400508050000000000002004 13 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; www.google.com. IN AAAA \n\n\n; ANSWER SECTION:\nwww.google.com. 43 IN AAAA 2607:f8b0:4005:805::2004\n\n\n" 14 | - name: Request ANY www.google.com 15 | binary: eccb012000010000000000000377777706676f6f676c6503636f6d0000ff0001 16 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; www.google.com. IN ANY \n\n\n\n" 17 | - name: Response ANY www.google.com 18 | binary: eccb818000010002000000000377777706676f6f676c6503636f6d0000ff0001c00c000100010000003f0004acd90e64c00c001c00010000003f00102607f8b04007080e0000000000002004 19 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; www.google.com. IN ANY \n\n\n; ANSWER SECTION:\nwww.google.com. 63 IN A 172.217.14.100\nwww.google.com. 63 IN AAAA 2607:f8b0:4007:80e::2004\n\n\n" 20 | - name: Request CNAME code.google.com 21 | binary: eccb0120000100000000000004636f646506676f6f676c6503636f6d0000050001 22 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; code.google.com. IN CNAME \n\n\n\n" 23 | - name: Response CNAME code.google.com 24 | binary: eccb8180000100010000000004636f646506676f6f676c6503636f6d0000050001c00c000500010000545f000904636f6465016cc011 25 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; code.google.com. IN CNAME \n\n\n; ANSWER SECTION:\ncode.google.com. 21599 IN CNAME code.l.google.com.\n\n\n" 26 | - name: Request MX google.com 27 | binary: eccb0120000100000000000006676f6f676c6503636f6d00000f0001 28 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; google.com. IN MX \n\n\n\n" 29 | - name: Response MX google.com 30 | binary: eccb8180000100050000000006676f6f676c6503636f6d00000f0001c00c000f0001000000f6000c000a056173706d78016cc00cc00c000f0001000000f60009001e04616c7432c02ac00c000f0001000000f60009001404616c7431c02ac00c000f0001000000f60009003204616c7434c02ac00c000f0001000000f60009002804616c7433c02a 31 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; google.com. IN MX \n\n\n; ANSWER SECTION:\ngoogle.com. 246 IN MX 10 aspmx.l.google.com.\ngoogle.com. 246 IN MX 30 alt2.aspmx.l.google.com.\ngoogle.com. 246 IN MX 20 alt1.aspmx.l.google.com.\ngoogle.com. 246 IN MX 50 alt4.aspmx.l.google.com.\ngoogle.com. 246 IN MX 40 alt3.aspmx.l.google.com.\n\n\n" 32 | - name: Request PTR 4.4.8.8.in-addr.arpa 33 | binary: eccb01200001000000000000013401340138013807696e2d61646472046172706100000c0001 34 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; 4.4.8.8.in-addr.arpa. IN PTR \n\n\n\n" 35 | - name: Response PTR 4.4.8.8.in-addr.arpa 36 | binary: eccb81800001000100000000013401340138013807696e2d61646472046172706100000c0001c00c000c0001000050aa000c03646e7306676f6f676c6500 37 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; 4.4.8.8.in-addr.arpa. IN PTR \n\n\n; ANSWER SECTION:\n4.4.8.8.in-addr.arpa. 20650 IN PTR dns.google.\n\n\n" 38 | - name: Request SOA google.com 39 | binary: eccb0120000100000000000006676f6f676c6503636f6d0000060001 40 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; google.com. IN SOA \n\n\n\n" 41 | - name: Response SOA google.com 42 | binary: eccb8180000100010000000006676f6f676c6503636f6d0000060001c00c00060001000000380026036e7331c00c09646e732d61646d696ec00c16978f7a0000038400000384000007080000003c 43 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; google.com. IN SOA \n\n\n; ANSWER SECTION:\ngoogle.com. 56 IN SOA ns1.google.com. dns-admin.google.com. 379031418 900 900 1800 60\n\n\n" 44 | - name: Request SRV _ldap._tcp.google.com 45 | binary: eccb01200001000000000000055f6c646170045f74637006676f6f676c6503636f6d0000210001 46 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; _ldap._tcp.google.com. IN SRV \n\n\n\n" 47 | - name: Response SRV _ldap._tcp.google.com 48 | binary: eccb81800001000100000000055f6c646170045f74637006676f6f676c6503636f6d0000210001c00c002100010000545f0017000500000185046c64617006676f6f676c6503636f6d00 49 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; _ldap._tcp.google.com. IN SRV \n\n\n; ANSWER SECTION:\n_ldap._tcp.google.com. 21599 IN SRV 5 0 389 ldap.google.com.\n\n\n" 50 | - name: Request TXT google.com 51 | binary: eccb0120000100000000000006676f6f676c6503636f6d0000100001 52 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; google.com. IN TXT \n\n\n\n" 53 | - name: Response TXT google.com 54 | binary: eccb8380000100000000000006676f6f676c6503636f6d0000100001 55 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr tc rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; google.com. IN TXT \n\n\n\n" 56 | - name: Request A ☺️.com 57 | binary: eccb0120000100000000000007786e2d2d37346803636f6d0000010001 58 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; ☺.com. IN A \n\n\n\n" 59 | - name: Response A ☺️.com 60 | binary: eccb8180000100010000000007786e2d2d37346803636f6d0000010001c00c00010001000002570004504a9a64 61 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; ☺.com. IN A \n\n\n; ANSWER SECTION:\n☺.com. 599 IN A 80.74.154.100\n\n\n" 62 | - name: Request A a 63 | binary: eccb0120000100000000000001610000010001 64 | string: ";; ->>HEADER<<- opcode: Query, status: NoError, id: 60619\n;; flags: rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; a. IN A \n\n\n\n" 65 | - name: Response A a 66 | binary: eccb81a3000100000001000001610000010001000006000100015125004001610c726f6f742d73657276657273036e657400056e73746c640c766572697369676e2d67727303636f6d007876f251000007080000038400093a8000015180 67 | string: ";; ->>HEADER<<- opcode: Query, status: NXDomain, id: 60619\n;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0\n\n;; QUESTION SECTION:\n; a. IN A \n\n\n; AUTHORITY SECTION:\n. 86309 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2021061201 1800 900 604800 86400\n\n\n" 68 | -------------------------------------------------------------------------------- /src/zones/parser.rs: -------------------------------------------------------------------------------- 1 | // Parses a Zone File following RFC 1035 (section 5). 2 | 3 | use crate::zones::Entry; 4 | use crate::zones::Record; 5 | use crate::zones::Resource; 6 | use crate::Class; 7 | use crate::MX; 8 | use crate::SOA; 9 | use pest_consume::match_nodes; 10 | use pest_consume::Error; 11 | use pest_consume::Parser; 12 | use std::net::Ipv4Addr; 13 | use std::net::Ipv6Addr; 14 | use std::str::FromStr; 15 | use std::time::Duration; 16 | 17 | #[derive(Parser)] 18 | #[grammar = "zones/zones.pest"] 19 | pub(crate) struct ZoneParser; 20 | 21 | type Result = std::result::Result>; 22 | type Node<'i> = pest_consume::Node<'i, Rule, ()>; 23 | 24 | #[pest_consume::parser] 25 | impl ZoneParser { 26 | fn EOI(input: Node) -> Result<()> { 27 | assert_eq!(input.as_rule(), Rule::EOI); 28 | Ok(()) 29 | } 30 | 31 | fn ip4(input: Node) -> Result { 32 | assert_eq!(input.as_rule(), Rule::ip4); 33 | 34 | match Ipv4Addr::from_str(input.as_str()) { 35 | Ok(ip4) => Ok(ip4), 36 | Err(e) => Err(input.error(e)), 37 | } 38 | } 39 | 40 | fn ip6(input: Node) -> Result { 41 | assert_eq!(input.as_rule(), Rule::ip6); 42 | 43 | match Ipv6Addr::from_str(input.as_str()) { 44 | Ok(ip6) => Ok(ip6), 45 | Err(e) => Err(input.error(e)), 46 | } 47 | } 48 | 49 | fn duration(input: Node) -> Result { 50 | assert_eq!(input.as_rule(), Rule::duration); 51 | 52 | // TODO Support more complex duration types (e.g "1d") 53 | match input.as_str().parse() { 54 | Ok(i) => Ok(Duration::new(i, 0)), 55 | Err(e) => Err(input.error(e)), 56 | } 57 | } 58 | 59 | fn string(input: Node) -> Result<&str> { 60 | assert_eq!(input.as_rule(), Rule::string); 61 | 62 | Ok(input.as_str()) 63 | } 64 | 65 | fn domain(input: Node) -> Result<&str> { 66 | assert_eq!(input.as_rule(), Rule::domain); 67 | 68 | // TODO Should I do some validation? 69 | Ok(input.as_str()) 70 | } 71 | 72 | fn class(input: Node) -> Result { 73 | assert_eq!(input.as_rule(), Rule::class); 74 | 75 | match input.as_str().parse() { 76 | Ok(class) => Ok(class), 77 | Err(e) => Err(input.error(e)), 78 | } 79 | } 80 | 81 | fn number(input: Node) -> Result 82 | where 83 | T::Err: std::fmt::Display, 84 | { 85 | assert_eq!(input.as_rule(), Rule::number); 86 | 87 | match input.as_str().parse() { 88 | Ok(i) => Ok(i), 89 | Err(e) => Err(input.error(e)), 90 | } 91 | } 92 | 93 | #[alias(resource)] 94 | fn resource_a(input: Node) -> Result { 95 | assert_eq!(input.as_rule(), Rule::resource_a); 96 | 97 | Ok(match_nodes!(input.into_children(); 98 | [ip4(ip)] => Resource::A(ip), 99 | )) 100 | } 101 | 102 | #[alias(resource)] 103 | fn resource_aaaa(input: Node) -> Result { 104 | assert_eq!(input.as_rule(), Rule::resource_aaaa); 105 | 106 | Ok(match_nodes!(input.into_children(); 107 | [ip6(ip)] => Resource::AAAA(ip), 108 | )) 109 | } 110 | 111 | #[alias(resource)] 112 | fn resource_cname(input: Node) -> Result { 113 | assert_eq!(input.as_rule(), Rule::resource_cname); 114 | 115 | Ok(match_nodes!(input.into_children(); 116 | [domain(name)] => Resource::CNAME(name.to_string()), 117 | )) 118 | } 119 | 120 | #[alias(resource)] 121 | fn resource_ns(input: Node) -> Result { 122 | assert_eq!(input.as_rule(), Rule::resource_ns); 123 | 124 | Ok(match_nodes!(input.into_children(); 125 | [domain(name)] => Resource::NS(name.to_string()), 126 | )) 127 | } 128 | 129 | #[alias(resource)] 130 | fn resource_mx(input: Node) -> Result { 131 | assert_eq!(input.as_rule(), Rule::resource_mx); 132 | 133 | Ok(match_nodes!(input.into_children(); 134 | [number(preference), domain(exchange)] => Resource::MX(MX { 135 | preference, 136 | exchange: exchange.to_string() 137 | }), 138 | )) 139 | } 140 | 141 | #[alias(resource)] 142 | fn resource_ptr(input: Node) -> Result { 143 | assert_eq!(input.as_rule(), Rule::resource_ptr); 144 | 145 | Ok(match_nodes!(input.into_children(); 146 | [domain(name)] => Resource::PTR(name.to_string()), 147 | )) 148 | } 149 | 150 | #[alias(resource)] 151 | fn resource_soa(input: Node) -> Result { 152 | assert_eq!(input.as_rule(), Rule::resource_soa); 153 | 154 | Ok(match_nodes!(input.into_children(); 155 | [domain(mname), string(rname), number(serial), duration(refresh), duration(retry), duration(expire), duration(minimum)] => Resource::SOA(SOA { 156 | mname: mname.to_string(), 157 | rname: rname.to_string(), // TODO Should this actually be a domain? 158 | serial, refresh, retry, expire, minimum 159 | }), 160 | )) 161 | } 162 | 163 | #[alias(entry)] 164 | fn origin(input: Node) -> Result { 165 | assert_eq!(input.as_rule(), Rule::origin); 166 | 167 | Ok(match_nodes!(input.into_children(); 168 | [domain(d)] => Entry::Origin(d.to_string()), 169 | )) 170 | } 171 | 172 | #[alias(entry)] 173 | fn ttl(input: Node) -> Result { 174 | assert_eq!(input.as_rule(), Rule::ttl); 175 | 176 | Ok(match_nodes!(input.into_children(); 177 | [duration(ttl)] => Entry::TTL(ttl), 178 | )) 179 | } 180 | 181 | #[alias(entry)] 182 | fn record(input: Node) -> Result { 183 | assert_eq!(input.as_rule(), Rule::record); 184 | 185 | let record = Self::parse_record(input)?; 186 | 187 | // Wrap in a Entry 188 | Ok(Entry::Record(record)) 189 | } 190 | 191 | pub fn single_record(input: Node) -> Result { 192 | assert_eq!(input.as_rule(), Rule::single_record); 193 | 194 | match_nodes!(input.into_children(); 195 | [record, _EOI] => Ok(Self::parse_record(record)?) 196 | ) 197 | } 198 | 199 | pub fn file(input: Node) -> Result> { 200 | assert_eq!(input.as_rule(), Rule::file); 201 | 202 | match_nodes!(input.into_children(); 203 | [entry(entrys).., _EOI] => Ok(entrys.collect()), 204 | ) 205 | } 206 | } 207 | 208 | impl ZoneParser { 209 | // parse_record does the heavy lifting parsing a single record entry. 210 | // This is in a seperate ZoneParser impl, due to limitations with 211 | // `#[pest_consume::parser]` which does not allow aliased methods to be 212 | // called, or used in match_nodes. 213 | fn parse_record(input: Node) -> Result { 214 | assert_eq!(input.as_rule(), Rule::record); 215 | 216 | let mut record = Record { 217 | name: None, 218 | ttl: None, 219 | class: None, 220 | resource: Resource::ANY, 221 | }; 222 | 223 | // All the assert! are due to programming errors, hopefully 224 | // never due to a parsing error. 225 | 226 | // We would prefer to use match_nodes! but we need to match the 227 | // various children in any order. This is due to the near ambigious 228 | // syntax of the resource record entry. 229 | for node in input.into_children() { 230 | let rule = node.as_rule(); 231 | match rule { 232 | Rule::domain => { 233 | assert!(record.name.is_none(), "record domain was set twice"); 234 | 235 | record.name = Some(Self::domain(node)?.to_string()) 236 | } 237 | Rule::duration => { 238 | assert!(record.ttl.is_none(), "record ttl was set twice"); 239 | 240 | record.ttl = Some(Self::duration(node)?) 241 | } 242 | Rule::class => { 243 | assert!(record.class.is_none(), "record class was set twice"); 244 | 245 | record.class = Some(Self::class(node)?) 246 | } 247 | 248 | _ => { 249 | // Rule::resource have many aliases, try one of them. 250 | match Self::rule_alias(rule) { 251 | AliasedRule::resource => { 252 | assert!( 253 | record.resource == Resource::ANY, 254 | "record resource was set twice" 255 | ); 256 | 257 | record.resource = Self::resource(node)? 258 | } 259 | 260 | _ => panic!("Unexpected token: {:?} '{:?}'", rule, node.as_str()), 261 | } 262 | } 263 | } 264 | } 265 | 266 | // By the end atleast this should be set 267 | assert_ne!(record.resource, Resource::ANY); 268 | 269 | Ok(record) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/zones/process.rs: -------------------------------------------------------------------------------- 1 | // Process a Zone File turning it into actual Records. 2 | 3 | use crate::resource::*; 4 | use crate::zones::Entry; 5 | use crate::zones::File; 6 | use crate::Class; 7 | use crate::Record; 8 | use crate::Resource; 9 | use core::time::Duration; 10 | 11 | impl File { 12 | pub fn into_records(self) -> Result, ()> { 13 | let mut results = Vec::::new(); 14 | 15 | // Useful to refer to: 16 | // https://datatracker.ietf.org/doc/html/rfc1035#section-5.1 17 | // https://datatracker.ietf.org/doc/html/rfc2308#section-4 18 | // https://www-uxsup.csx.cam.ac.uk/pub/doc/redhat/redhat7.3/rhl-rg-en-7.3/s1-bind-configuration.html 19 | 20 | // TODO Implement: 21 | // TTL in RSet must match https://datatracker.ietf.org/doc/html/rfc2181#section-5.2 22 | // Duration times https://www-uxsup.csx.cam.ac.uk/pub/doc/redhat/redhat7.3/rhl-rg-en-7.3/s1-bind-configuration.html 23 | 24 | let mut origin: Option<&str> = self.origin.as_deref(); 25 | let mut default_ttl: Option<&Duration> = None; 26 | 27 | let mut last_name: Option = None; 28 | let mut last_class: Option<&Class> = None; 29 | 30 | for entry in self.entries.iter() { 31 | match entry { 32 | Entry::Origin(new_origin) => { 33 | // Always trim the dot from the end. 34 | if let Some(new_origin) = new_origin.strip_suffix('.') { 35 | origin = Some(new_origin) 36 | } else { 37 | panic!("TODO Origin wasn't a absolute domain"); 38 | } 39 | } 40 | Entry::TTL(ttl) => default_ttl = Some(ttl), 41 | Entry::Record(record) => { 42 | let full_name: String = match record.name.as_ref() { 43 | Some(name) => Self::resolve_name(name, origin), 44 | None => { 45 | if last_name.is_none() { 46 | // TODO What's the behaviour if $origin is set? 47 | panic!("TODO Blank domain without a previous domain set"); 48 | } 49 | last_name.unwrap().to_string() 50 | } 51 | }; 52 | last_name = Some(full_name.to_owned()); 53 | 54 | let ttl = record 55 | .ttl 56 | .as_ref() 57 | .or(default_ttl) 58 | .expect("TODO Blank ttl without a default TTL set"); // TODO Turn these into errors 59 | 60 | let class = record 61 | .class 62 | .as_ref() 63 | .or(last_class) 64 | .expect("TODO Blank Class without a previous Class set"); // TODO Turn these into errors 65 | 66 | last_class = Some(class); 67 | 68 | results.push(crate::Record { 69 | name: full_name, 70 | class: *class, 71 | ttl: *ttl, 72 | resource: Self::resolve_resource(&record.resource, origin), 73 | }) 74 | } 75 | } 76 | } 77 | 78 | Ok(results) 79 | } 80 | 81 | fn resolve_name(name: &str, origin: Option<&str>) -> String { 82 | // Absolute domain name 83 | if let Some(name) = name.strip_suffix('.') { 84 | return name.to_string(); 85 | } 86 | 87 | // Everything past here requires a origin 88 | if origin.is_none() { 89 | panic!("TODO Relative domain without a origin set"); 90 | } 91 | 92 | if name == "@" { 93 | return origin.unwrap().to_string(); 94 | } 95 | 96 | // Relative domain name 97 | name.to_owned() + "." + origin.unwrap() 98 | } 99 | 100 | fn resolve_resource(resource: &Resource, origin: Option<&str>) -> Resource { 101 | match resource { 102 | // These types don't include a domain, so clone as is. 103 | Resource::A(_) 104 | | Resource::AAAA(_) 105 | | Resource::TXT(_) 106 | | Resource::SPF(_) 107 | | Resource::OPT 108 | | Resource::ANY => resource.clone(), 109 | 110 | // The rest need some kind of tweaking 111 | Resource::CNAME(domain) => Resource::CNAME(Self::resolve_name(domain, origin)), 112 | Resource::NS(domain) => Resource::NS(Self::resolve_name(domain, origin)), 113 | Resource::PTR(domain) => Resource::PTR(Self::resolve_name(domain, origin)), 114 | Resource::MX(mx) => Resource::MX(MX { 115 | preference: mx.preference, 116 | exchange: Self::resolve_name(&mx.exchange, origin), 117 | }), 118 | Resource::SOA(soa) => Resource::SOA(SOA { 119 | mname: Self::resolve_name(&soa.mname, origin), 120 | rname: SOA::rname_to_email(&Self::resolve_name(&soa.rname, origin)).unwrap(), 121 | serial: soa.serial, 122 | refresh: soa.refresh, 123 | retry: soa.retry, 124 | expire: soa.expire, 125 | minimum: soa.minimum, 126 | }), 127 | Resource::SRV(srv) => Resource::SRV(SRV { 128 | priority: srv.priority, 129 | weight: srv.weight, 130 | port: srv.port, 131 | name: Self::resolve_name(&srv.name, origin), 132 | }), 133 | } 134 | } 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use crate::resource::*; 140 | use crate::zones::File; 141 | use crate::Class; 142 | use crate::Record; 143 | use crate::Resource; 144 | use core::time::Duration; 145 | use pretty_assertions::assert_eq; 146 | use std::str::FromStr; 147 | 148 | #[test] 149 | fn test_into_records() { 150 | let tests = vec![ 151 | (" 152 | $ORIGIN example.com. ; designates the start of this zone file in the namespace 153 | $TTL 3600 ; default expiration time (in seconds) of all RRs without their own TTL value 154 | example.com. IN SOA ns.example.com. username.example.com. ( 2020091025 7200 3600 1209600 3600 ) 155 | example.com. IN NS ns ; ns.example.com is a nameserver for example.com 156 | example.com. IN NS ns.somewhere.example. ; ns.somewhere.example is a backup nameserver for example.com 157 | example.com. IN MX 10 mail.example.com. ; mail.example.com is the mailserver for example.com 158 | @ IN MX 20 mail2.example.com. ; equivalent to above line, '@' represents zone origin 159 | @ IN MX 50 mail3 ; equivalent to above line, but using a relative host name 160 | example.com. IN A 192.0.2.1 ; IPv4 address for example.com 161 | IN AAAA 2001:db8:10::1 ; IPv6 address for example.com 162 | ns IN A 192.0.2.2 ; IPv4 address for ns.example.com 163 | IN AAAA 2001:db8:10::2 ; IPv6 address for ns.example.com 164 | www IN CNAME example.com. ; www.example.com is an alias for example.com 165 | wwwtest IN CNAME www ; wwwtest.example.com is another alias for www.example.com 166 | ", 167 | vec![ 168 | Record::new("example.com", Class::Internet, Duration::new(3600, 0), Resource::SOA(SOA { 169 | mname: "ns.example.com".to_string(), 170 | rname: "username@example.com".to_string(), 171 | serial: 2020091025, 172 | refresh: Duration::new(7200, 0), 173 | retry: Duration::new(3600, 0), 174 | expire: Duration::new(1209600, 0), 175 | minimum: Duration::new(3600, 0), 176 | })), 177 | Record::new("example.com", Class::Internet, Duration::new(3600, 0), Resource::NS("ns.example.com".to_string())), 178 | Record::new("example.com", Class::Internet, Duration::new(3600, 0), Resource::NS("ns.somewhere.example".to_string())), 179 | Record::new("example.com", Class::Internet, Duration::new(3600, 0), Resource::MX(MX{ 180 | preference: 10, 181 | exchange: "mail.example.com".to_string() 182 | })), 183 | Record::new("example.com", Class::Internet, Duration::new(3600, 0), Resource::MX(MX{ 184 | preference: 20, 185 | exchange: "mail2.example.com".to_string() 186 | })), 187 | Record::new("example.com", Class::Internet, Duration::new(3600, 0), Resource::MX(MX{ 188 | preference: 50, 189 | exchange: "mail3.example.com".to_string() 190 | })), 191 | Record::new("example.com", Class::Internet, Duration::new(3600, 0), Resource::A("192.0.2.1".parse().unwrap())), 192 | Record::new("example.com", Class::Internet, Duration::new(3600, 0), Resource::AAAA("2001:db8:10::1".parse().unwrap())), 193 | Record::new("ns.example.com", Class::Internet, Duration::new(3600, 0), Resource::A("192.0.2.2".parse().unwrap())), 194 | Record::new("ns.example.com", Class::Internet, Duration::new(3600, 0), Resource::AAAA("2001:db8:10::2".parse().unwrap())), 195 | Record::new("www.example.com", Class::Internet, Duration::new(3600, 0), Resource::CNAME("example.com".parse().unwrap())), 196 | Record::new("wwwtest.example.com", Class::Internet, Duration::new(3600, 0), Resource::CNAME("www.example.com".to_string())), 197 | ]) 198 | ]; 199 | 200 | for (input, want) in tests { 201 | match File::from_str(input) 202 | .expect("failed to parse") 203 | .into_records() 204 | { 205 | Ok(got) => assert_eq!(got, want), 206 | Err(err) => panic!("{} Failed:\n{:?}", input, err), // TODO Make a error and no need to use "{:?}" 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /dig/main.rs: -------------------------------------------------------------------------------- 1 | // Simple dig style command line. 2 | // rustdns {record} {domain} 3 | mod util; 4 | 5 | use http::method::Method; 6 | use rustdns::clients::doh::Client as DohClient; 7 | use rustdns::clients::json::Client as JsonClient; 8 | use rustdns::clients::tcp::Client as TcpClient; 9 | use rustdns::clients::udp::Client as UdpClient; 10 | use rustdns::clients::AsyncExchanger; 11 | use rustdns::clients::Exchanger; 12 | use rustdns::types::*; 13 | use std::env; 14 | use std::io; 15 | use std::net::SocketAddr; 16 | use std::net::ToSocketAddrs; 17 | use std::process; 18 | use std::str::FromStr; 19 | use std::vec; 20 | use strum_macros::{Display, EnumString}; 21 | use thiserror::Error; 22 | use url::Url; 23 | 24 | #[cfg(test)] 25 | #[macro_use] 26 | extern crate pretty_assertions; 27 | 28 | #[derive(Display, EnumString, PartialEq)] 29 | enum Client { 30 | Udp, 31 | Tcp, 32 | DoH, 33 | Json, 34 | } 35 | 36 | #[derive(Error, Debug)] 37 | enum DigError { 38 | // A command line argument was bad. 39 | // TODO Could I replace ArgParseError with rustdns::Error::IllegalArgument? 40 | #[error("{0}")] 41 | ArgParseError(String), 42 | 43 | #[error(transparent)] 44 | RustDnsError(#[from] rustdns::Error), 45 | } 46 | 47 | struct Args { 48 | client: Client, 49 | servers: Vec, 50 | 51 | /// Query this types 52 | r#type: rustdns::Type, 53 | 54 | /// Across all these domains 55 | domains: Vec, 56 | } 57 | 58 | /// Parses a string into a SocketAddr allowing for the port to be missing. 59 | fn sockaddr_parse_with_port( 60 | addr: &str, 61 | default_port: u16, 62 | ) -> io::Result> { 63 | match addr.to_socket_addrs() { 64 | // Try parsing again, with the default port. 65 | Err(_e) => (addr, default_port).to_socket_addrs(), 66 | Ok(addrs) => Ok(addrs), 67 | } 68 | } 69 | 70 | /// Helper function to take a vector of domain/port numbers, and return (a possibly larger) `Vec[SocketAddr]`. 71 | fn to_sockaddrs( 72 | servers: &[String], 73 | default_port: u16, 74 | ) -> std::result::Result, DigError> { 75 | Ok(servers 76 | .iter() 77 | .map(|addr| { 78 | // Each address could be invalid, or return multiple SocketAddr. 79 | match sockaddr_parse_with_port(addr, default_port) { 80 | Err(e) => Err(DigError::ArgParseError(format!( 81 | "failed to parse '{}': {}", 82 | addr, e 83 | ))), 84 | Ok(addrs) => Ok(addrs), 85 | } 86 | }) 87 | .collect::, _>>()? 88 | // We now have a collection of vec::IntoIter, flatten. 89 | // We would use .flat_map(), but it doesn't handle the Error case :( 90 | .into_iter() 91 | .flatten() 92 | .collect()) 93 | } 94 | 95 | impl Args { 96 | /// Helper function to return the list of servers as a `Vec[Url]`. 97 | fn servers_to_urls(&self) -> std::result::Result, DigError> { 98 | self.servers 99 | .iter() 100 | .map(|url| match url.parse() { 101 | Err(e) => Err(DigError::ArgParseError(format!( 102 | "failed to parse '{}': {}", 103 | url, e 104 | ))), 105 | Ok(url) => Ok(url), 106 | }) 107 | .collect() 108 | } 109 | } 110 | 111 | impl Default for Args { 112 | fn default() -> Self { 113 | Args { 114 | client: Client::Udp, 115 | servers: Vec::new(), 116 | 117 | r#type: Type::A, 118 | domains: Vec::new(), 119 | } 120 | } 121 | } 122 | 123 | // TODO Move into a integration test (due to the use of network) 124 | #[test] 125 | fn test_to_sockaddrs() { 126 | let servers = vec![ 127 | "1.2.3.4".to_string(), // This requires using the default port. 128 | "aaaaa.bramp.net".to_string(), // This resolves to two records. 129 | "5.6.7.8:453".to_string(), // This uses a different port. 130 | ]; 131 | 132 | // This test may be flakly, if it is running in an environment that doesn't 133 | // have both IPv4 and IPv6, and has DNS queries that can fail. 134 | // TODO Figure out a way to make this more robust. 135 | let mut addrs = to_sockaddrs(&servers, 53).expect("resolution failed"); 136 | let mut want = vec![ 137 | "1.2.3.4:53".parse().unwrap(), 138 | "127.0.0.1:53".parse().unwrap(), 139 | "[::1]:53".parse().unwrap(), 140 | "5.6.7.8:453".parse().unwrap(), 141 | ]; 142 | 143 | // Sort because [::1]:53 or 127.0.0.1:53 may switch places. 144 | addrs.sort(); 145 | want.sort(); 146 | 147 | assert_eq!(addrs, want); 148 | } 149 | 150 | fn parse_args(args: impl Iterator) -> Result { 151 | let mut result = Args::default(); 152 | let mut type_or_domain = Vec::::new(); 153 | 154 | for arg in args { 155 | match arg.as_str() { 156 | "+udp" => result.client = Client::Udp, 157 | "+tcp" => result.client = Client::Tcp, 158 | "+doh" => result.client = Client::DoH, 159 | "+json" => result.client = Client::Json, 160 | 161 | _ => { 162 | if arg.starts_with('+') { 163 | return Err(format!("Unknown flag: {}", arg)); 164 | } 165 | 166 | if arg.starts_with('@') { 167 | result 168 | .servers 169 | .push(arg.strip_prefix('@').unwrap().to_string()) // Unwrap should not panic 170 | } else { 171 | type_or_domain.push(arg) 172 | } 173 | } 174 | } 175 | } 176 | 177 | let mut found_type = false; 178 | 179 | // To be useful, we allow users to say `dig A bramp.net` or `dig bramp.net A` 180 | for arg in type_or_domain { 181 | if !found_type { 182 | // Use the first type we found and assume the rest are domains. 183 | if let Ok(r#type) = Type::from_str(&arg) { 184 | result.r#type = r#type; 185 | found_type = true; 186 | continue; 187 | } 188 | } 189 | 190 | result.domains.push(arg) 191 | } 192 | 193 | if result.domains.is_empty() { 194 | // By default query the root domain 195 | result.domains.push(".".to_string()); 196 | if !found_type { 197 | result.r#type = Type::NS; 198 | } 199 | } 200 | 201 | if result.servers.is_empty() { 202 | // TODO If no servers are provided determine the local server (from /etc/nslookup.conf for example) 203 | eprintln!(";; No servers specified, using Google's DNS servers"); 204 | match result.client { 205 | Client::Udp | Client::Tcp => { 206 | result.servers.push("8.8.8.8".to_string()); 207 | result.servers.push("8.8.4.4".to_string()); 208 | result.servers.push("2001:4860:4860::8888".to_string()); 209 | result.servers.push("2001:4860:4860::8844".to_string()); 210 | } 211 | Client::DoH => result 212 | .servers 213 | .push(rustdns::clients::doh::GOOGLE.to_string()), 214 | 215 | Client::Json => result 216 | .servers 217 | .push(rustdns::clients::json::GOOGLE.to_string()), 218 | } 219 | 220 | /* 221 | // TODO Create a function that returns the appropriate ones from this list: 222 | 223 | Cisco OpenDNS: 224 | 208.67.222.222 and 208.67.220.220; TCP/UDP 225 | https://doh.opendns.com/dns-query 226 | 227 | Cloudflare: 228 | 1.1.1.1 and 1.0.0.1; 229 | 2606:4700:4700::1111 230 | 2606:4700:4700::1001 231 | https://cloudflare-dns.com/dns-query 232 | 233 | Google Public DNS: 234 | 8.8.8.8 and 8.8.4.4; and 235 | 2001:4860:4860::8888 236 | 2001:4860:4860::8844 237 | https://dns.google/dns-query 238 | 239 | Quad9: 9.9.9.9 and 149.112.112.112. 240 | 2620:fe::fe 241 | 2620:fe::9 242 | https://dns.quad9.net/dns-query 243 | tls://dns.quad9.net 244 | */ 245 | } 246 | 247 | Ok(result) 248 | } 249 | 250 | #[tokio::main] 251 | async fn main() -> Result<(), DigError> { 252 | // TODO --help doesn't work 253 | 254 | let args = match parse_args(env::args().skip(1)) { 255 | Ok(args) => args, 256 | Err(e) => { 257 | eprintln!("{}", e); 258 | eprintln!("Usage: dig [@server] [+udp|+tcp|+doh|+json] {{domain}} {{type}}"); 259 | process::exit(1); 260 | } 261 | }; 262 | 263 | let mut query = Message::default(); 264 | for domain in &args.domains { 265 | query.add_question(domain, args.r#type, Class::Internet); 266 | } 267 | query.add_extension(Extension { 268 | payload_size: 4096, 269 | 270 | ..Default::default() 271 | }); 272 | 273 | // TODO Add this as a extra verbose flag 274 | // println!("query:"); 275 | // util::hexdump(&query.to_vec().expect("failed to encode the query")); 276 | // println!(); 277 | println!("{}", query); 278 | 279 | // TODO make all DNS client implement a Exchange trait 280 | let resp = match args.client { 281 | Client::Udp => UdpClient::new(to_sockaddrs(&args.servers, 53)?.as_slice())? 282 | .exchange(&query) 283 | .expect("could not exchange message"), 284 | 285 | Client::Tcp => TcpClient::new(to_sockaddrs(&args.servers, 53)?.as_slice())? 286 | .exchange(&query) 287 | .expect("could not exchange message"), 288 | 289 | Client::DoH => DohClient::new(args.servers_to_urls()?.as_slice(), Method::GET)? 290 | .exchange(&query) 291 | .await 292 | .expect("could not exchange message"), 293 | 294 | Client::Json => JsonClient::new(args.servers_to_urls()?.as_slice())? 295 | .exchange(&query) 296 | .await 297 | .expect("could not exchange message"), 298 | }; 299 | 300 | println!("response:"); 301 | println!("{}", resp); 302 | 303 | Ok(()) 304 | } 305 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /src/resource.rs: -------------------------------------------------------------------------------- 1 | use crate::bail; 2 | use crate::io::{CursorExt, DNSReadExt, SeekExt}; 3 | use crate::types::*; 4 | use crate::ParseError; 5 | use byteorder::{ReadBytesExt, BE}; 6 | use std::io; 7 | use std::io::Cursor; 8 | use std::io::Read; 9 | use std::net::{Ipv4Addr, Ipv6Addr}; 10 | use std::time::Duration; 11 | 12 | /// IPv4 Address (A) record. 13 | pub type A = Ipv4Addr; 14 | 15 | /// IPv6 Address (AAAA) record. 16 | #[allow(clippy::upper_case_acronyms)] 17 | pub type AAAA = Ipv6Addr; 18 | 19 | /// Name Server (NS) record for delegating a the given authoritative name 20 | /// servers. 21 | pub type NS = String; 22 | 23 | /// Canonical name (CNAME) record, for aliasing one name to another. 24 | #[allow(clippy::upper_case_acronyms)] 25 | pub type CNAME = String; 26 | 27 | /// Pointer (PTR) record most commonly used for most common use is for 28 | /// implementing reverse DNS lookups. 29 | #[allow(clippy::upper_case_acronyms)] 30 | pub type PTR = String; 31 | 32 | /// Text (TXT) record for arbitrary human-readable text in a DNS record. 33 | #[allow(clippy::upper_case_acronyms)] 34 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 35 | pub struct TXT(pub Vec>); 36 | 37 | impl Record { 38 | pub(crate) fn parse( 39 | cur: &mut Cursor<&[u8]>, 40 | name: String, 41 | r#type: Type, 42 | class: Class, 43 | ) -> io::Result { 44 | let ttl = cur.read_u32::()?; 45 | let len = cur.read_u16::()?; 46 | 47 | // Create a new Cursor that is limited to the len field. 48 | // 49 | // cur [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10...] 50 | // ^ pos & len = 2 51 | // 52 | // record [0, 1, 2, 3, 4, 5, 6] 53 | // ^ pos 54 | // 55 | // The record starts from zero, instead of being [4,6], this is 56 | // so it can jump backwards for a qname (or similar) read. 57 | 58 | let pos = cur.position(); 59 | let end = pos as usize + len as usize; 60 | let mut record = cur.sub_cursor(0, end)?; 61 | record.set_position(pos); 62 | 63 | // If parsing fails for this record, (and the length seems correct), 64 | // we could turn this into a warning instead of a full error. 65 | 66 | // TODO Consider changing these parse methods to some kind of common function 67 | // that accepts Cursor and Class. 68 | let resource = match r#type { 69 | Type::A => Resource::A(parse_a(&mut record, class)?), 70 | Type::AAAA => Resource::AAAA(parse_aaaa(&mut record, class)?), 71 | 72 | Type::NS => Resource::NS(record.read_qname()?), 73 | Type::SOA => Resource::SOA(SOA::parse(&mut record)?), 74 | Type::CNAME => Resource::CNAME(record.read_qname()?), 75 | Type::PTR => Resource::PTR(record.read_qname()?), 76 | Type::MX => Resource::MX(MX::parse(&mut record)?), 77 | Type::TXT => Resource::TXT(parse_txt(&mut record)?), 78 | Type::SPF => Resource::SPF(parse_txt(&mut record)?), 79 | Type::SRV => Resource::SRV(SRV::parse(&mut record)?), 80 | 81 | // This should never appear in a answer record unless we have invalid data. 82 | Type::Reserved | Type::OPT | Type::ANY => { 83 | // TODO This could be a warning, instead of a full error. 84 | bail!(InvalidData, "invalid record type '{}'", r#type); 85 | } 86 | }; 87 | 88 | if record.remaining()? > 0 { 89 | bail!( 90 | Other, 91 | "finished '{}' parsing record with {} bytes left over", 92 | r#type, 93 | record.remaining()? 94 | ); 95 | } 96 | 97 | // Now catch up (this is safe since record.len() < cur.len()) 98 | cur.set_position(record.position()); 99 | 100 | Ok(Record { 101 | name, 102 | class, 103 | ttl: Duration::from_secs(ttl.into()), 104 | resource, 105 | }) 106 | } 107 | } 108 | 109 | /// Mail EXchanger (MX) record specifies the mail server responsible 110 | /// for accepting email messages on behalf of a domain name. 111 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 112 | pub struct MX { 113 | /// The preference given to this RR among others at the same owner. 114 | /// Lower values are preferred. 115 | pub preference: u16, 116 | 117 | /// A host willing to act as a mail exchange for the owner name. 118 | pub exchange: String, 119 | } 120 | 121 | /// Start of Authority (SOA) record containing administrative information 122 | /// about the zone. See [rfc1035]. 123 | /// 124 | /// [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035 125 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 126 | #[allow(clippy::upper_case_acronyms)] 127 | pub struct SOA { 128 | /// The name server that was the original or primary source of data for this zone. 129 | pub mname: String, 130 | 131 | /// The mailbox of the person responsible for this zone. 132 | /// 133 | /// This is stored as a valid email address, e.g "dns.admin@example.com", as opposed 134 | /// to the format it's typically stored in SOA records "dns\.admin.example.com". Use 135 | /// [`rname_to_email`] and [`rname_to_email`] to convert between the formats. 136 | pub rname: String, 137 | 138 | pub serial: u32, 139 | 140 | pub refresh: Duration, 141 | pub retry: Duration, 142 | pub expire: Duration, 143 | pub minimum: Duration, 144 | } 145 | 146 | /// Service (SRV) record, containg hostname and port number information of specified services. See [rfc2782]. 147 | /// 148 | /// [rfc2782]: 149 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 150 | #[allow(clippy::upper_case_acronyms)] 151 | pub struct SRV { 152 | pub priority: u16, 153 | pub weight: u16, 154 | pub port: u16, 155 | pub name: String, 156 | } 157 | 158 | fn parse_a(cur: &mut Cursor<&[u8]>, class: Class) -> io::Result { 159 | let mut buf = [0_u8; 4]; 160 | cur.read_exact(&mut buf)?; 161 | 162 | match class { 163 | Class::Internet => Ok(A::new(buf[0], buf[1], buf[2], buf[3])), 164 | 165 | _ => bail!(InvalidData, "unsupported A record class '{}'", class), 166 | } 167 | } 168 | 169 | fn parse_aaaa(cur: &mut Cursor<&[u8]>, class: Class) -> io::Result { 170 | let mut buf = [0_u8; 16]; 171 | cur.read_exact(&mut buf)?; 172 | 173 | match class { 174 | Class::Internet => Ok(AAAA::from(buf)), 175 | 176 | _ => bail!(InvalidData, "unsupported AAAA record class '{}'", class), 177 | } 178 | } 179 | 180 | fn parse_txt(cur: &mut Cursor<&[u8]>) -> io::Result { 181 | let mut txts = Vec::new(); 182 | 183 | loop { 184 | // Keep reading until EOF is reached. 185 | let len = match cur.read_u8() { 186 | Ok(len) => len, 187 | Err(e) => match e.kind() { 188 | io::ErrorKind::UnexpectedEof => break, 189 | _ => return Err(e), 190 | }, 191 | }; 192 | 193 | let mut txt = vec![0; len.into()]; 194 | cur.read_exact(&mut txt)?; 195 | txts.push(txt) 196 | } 197 | 198 | Ok(TXT(txts)) 199 | } 200 | 201 | impl SOA { 202 | pub(crate) fn parse(cur: &mut Cursor<&[u8]>) -> io::Result { 203 | let mname = cur.read_qname()?; 204 | let rname = Self::rname_to_email(&cur.read_qname()?).unwrap(); // TODO error handling 205 | 206 | let serial = cur.read_u32::()?; 207 | let refresh = cur.read_u32::()?; 208 | let retry = cur.read_u32::()?; 209 | let expire = cur.read_u32::()?; 210 | let minimum = cur.read_u32::()?; 211 | 212 | Ok(SOA { 213 | mname, 214 | rname, 215 | 216 | serial, 217 | refresh: Duration::from_secs(refresh.into()), 218 | retry: Duration::from_secs(retry.into()), 219 | expire: Duration::from_secs(expire.into()), 220 | minimum: Duration::from_secs(minimum.into()), 221 | }) 222 | } 223 | 224 | /// Converts rnames to email address, for example, "admin.example.com" is 225 | /// converted to "admin@example.com", per the rules in 226 | /// https://datatracker.ietf.org/doc/html/rfc1035#section-8 227 | pub fn rname_to_email(domain: &str) -> Result { 228 | // The logic is simple. 229 | // Find first unescaped dot and replace with a @ 230 | 231 | // Handle the escaping. Replace the first . which isn't escapated with a \ 232 | let mut result = String::with_capacity(domain.len()); 233 | let mut last_char = ' '; 234 | let mut done = false; 235 | for c in domain.chars() { 236 | if last_char == '\\' { 237 | // Last character was escape, so always append this one. 238 | result.push(c); 239 | } else if c == '.' && !done { 240 | result.push('@'); 241 | done = true; 242 | } else if c != '\\' { 243 | // Otherwise append if not an escape. 244 | result.push(c); 245 | } 246 | 247 | last_char = c; 248 | } 249 | 250 | if !done { 251 | return Err(ParseError::InvalidRname(domain.to_string())); 252 | } 253 | 254 | Ok(result) 255 | } 256 | 257 | pub fn email_to_rname(email: &str) -> Result { 258 | match email.split_once('@') { 259 | None => Err(ParseError::InvalidRname(email.to_string())), 260 | 261 | // Escape all the dots to the left of the '@', 262 | // replace the '@' with a '.', and leave everything after the '@' alone. 263 | Some((left, right)) => Ok(left.replace('.', "\\.") + "." + right), 264 | } 265 | } 266 | } 267 | 268 | impl MX { 269 | pub(crate) fn parse(cur: &mut Cursor<&[u8]>) -> io::Result { 270 | let preference = cur.read_u16::()?; 271 | let exchange = cur.read_qname()?; 272 | 273 | Ok(MX { 274 | preference, 275 | exchange, 276 | }) 277 | } 278 | } 279 | 280 | impl SRV { 281 | pub(crate) fn parse(cur: &mut Cursor<&[u8]>) -> io::Result { 282 | let priority = cur.read_u16::()?; 283 | let weight = cur.read_u16::()?; 284 | let port = cur.read_u16::()?; 285 | 286 | let name = cur.read_qname()?; 287 | 288 | Ok(SRV { 289 | priority, 290 | weight, 291 | port, 292 | name, 293 | }) 294 | } 295 | } 296 | 297 | impl From<&str> for TXT { 298 | fn from(txt: &str) -> TXT { 299 | TXT(vec![txt.as_bytes().to_vec()]) 300 | } 301 | } 302 | 303 | impl From<&[&str]> for TXT { 304 | fn from(txts: &[&str]) -> TXT { 305 | TXT(txts.iter().map(|row| row.as_bytes().to_vec()).collect()) 306 | } 307 | } 308 | 309 | #[cfg(test)] 310 | mod tests { 311 | use crate::SOA; 312 | use pretty_assertions::assert_eq; 313 | 314 | static RNAME_TESTS: &[(&str, &str)] = &[ 315 | ("username.example.com", "username@example.com"), 316 | ("root.localhost", "root@localhost"), 317 | ("Action\\.domains.ISI.EDU", "Action.domains@ISI.EDU"), 318 | ("a\\.b\\.c.ISI.EDU", "a.b.c@ISI.EDU"), 319 | ]; 320 | 321 | #[test] 322 | fn test_soa_rname_to_email() { 323 | for (domain, email) in RNAME_TESTS { 324 | match SOA::rname_to_email(domain) { 325 | Ok(got) => assert_eq!(got, *email, "incorrect result for '{}'", domain), 326 | Err(err) => panic!("'{}' Failed:\n{:?}", domain, err), 327 | } 328 | } 329 | } 330 | 331 | #[test] 332 | fn test_soa_rname_from_email() { 333 | for (domain, email) in RNAME_TESTS { 334 | match SOA::email_to_rname(email) { 335 | Ok(got) => assert_eq!(got, *domain, "incorrect result for '{}'", email), 336 | Err(err) => panic!("'{}' Failed:\n{:?}", email, err), 337 | } 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /old_src/punycode.rs: -------------------------------------------------------------------------------- 1 | // This is a punycode implementation. Current it's not used, and dead code. 2 | // 3 | // https://www.gnu.org/software/libidn/doxygen/punycode_8c_source.html 4 | // https://r12a.github.io/app-conversion/ 5 | // https://github.com/algo26-matthias/idna-convert 6 | // https://unicode.org/reports/tr46/ 7 | // 8 | #![allow(clippy::absurd_extreme_comparisons)] // To support the const_assert below. 9 | 10 | // Puncode parameters: https://datatracker.ietf.org/doc/html/rfc3492#section-5 11 | const BASE: u32 = 36; 12 | const TMIN: u32 = 1; 13 | const TMAX: u32 = 26; 14 | const SKEW: u32 = 38; 15 | const DAMP: u32 = 700; 16 | const INITIAL_BIAS: u32 = 72; 17 | const INITIAL_N: char = 0x80 as char; // The first non-ASCII code point. // TODO Should this be a char? 18 | const DELIMITER: char = '-'; 19 | 20 | // Bootstring parameters: https://datatracker.ietf.org/doc/html/rfc3492#section-4 21 | const_assert!(TMIN <= TMAX && TMAX < BASE); 22 | const_assert!(SKEW >= 1); 23 | const_assert!(DAMP >= 2); 24 | const_assert!(INITIAL_BIAS % BASE <= BASE - TMIN); 25 | 26 | // from_char maps the encoded code-points to their digit values. 27 | // The returned value is in the range 0 to BASE. 28 | fn from_char(c: char) -> u8 { 29 | match c { 30 | 'A'..='Z' => c as u8 - b'A', 31 | 'a'..='z' => c as u8 - b'a', 32 | '0'..='9' => c as u8 - b'0' + 26, 33 | _ => panic!("invalid char"), // TODO 34 | } 35 | } 36 | 37 | // to_char maps the digit-values to their lower-case unicode. 38 | // The supplied value must be in the range 0 to BASE. 39 | fn to_char(i: u8) -> char { 40 | (match i { 41 | 0..=25 => b'a' + i, 42 | 26..=35 => b'0' + (i - 26), 43 | _ => panic!("invalid digit"), // TODO 44 | }) as char 45 | } 46 | 47 | // Bias adaptation 48 | // 49 | // num_points is the total number of code points encoded/decoded so 50 | // far (including the one corresponding to this delta itself, and 51 | // including the basic code points). 52 | // 53 | // Reference: 54 | // * https://datatracker.ietf.org/doc/html/rfc3492#section-3.4 55 | // * https://datatracker.ietf.org/doc/html/rfc3492#section-6.1 56 | fn adapt(delta: u32, num_points: u32, first_time: bool) -> u32 { 57 | // delta is scaled in order to avoid overflow. 58 | let mut delta = if first_time { 59 | // When this is the very first delta, the divisor is not 2, but 60 | // instead a constant called damp. This compensates for the fact 61 | // that the second delta is usually much smaller than the first. 62 | delta / DAMP 63 | } else { 64 | delta / 2 65 | }; 66 | 67 | // Delta is increased to compensate for the fact that the next delta 68 | // will be inserting into a longer string. 69 | delta = delta + (delta / num_points); 70 | 71 | let mut k = 0; 72 | while delta > ((BASE - TMIN) * TMAX) / 2 { 73 | delta /= BASE - TMIN; 74 | k += BASE; 75 | } 76 | 77 | k + (((BASE - TMIN + 1) * delta) / (delta + SKEW)) 78 | } 79 | 80 | // Encodes a string into punycode. 81 | pub fn encode(input: &str) -> Result { 82 | let mut output = String::new(); 83 | let mut unicode = Vec::::new(); // Non-basic code points (the ones we need to encode). 84 | for c in input.chars() { 85 | if c.is_ascii() { 86 | // All basic code points appearing in the extended string are 87 | // represented literally at the beginning of the basic string, in their 88 | // original order, followed by a delimiter if (and only if) the number 89 | // of basic code points is nonzero. 90 | output.push(c); 91 | } else { 92 | unicode.push(c); // Perhaps store the index to make below faster 93 | } 94 | } 95 | 96 | let basic_len = output.len() as u32; 97 | 98 | if !output.is_empty() { 99 | output.push(DELIMITER); 100 | } 101 | 102 | if unicode.is_empty() { 103 | return Ok(output); 104 | } 105 | 106 | // "Insertion unsort coding" encodes the non-basic code points as deltas, 107 | // and processes the code points in numerical order rather than in order 108 | // of appearance, which typically results in smaller deltas. 109 | unicode.sort_by(|a, b| b.cmp(&a)); 110 | unicode.dedup(); 111 | 112 | if *unicode.last().unwrap() < INITIAL_N { 113 | // If the input contains a non-basic code point < n then fail 114 | panic!("the input contains a non-basic code point < n"); 115 | } 116 | 117 | let mut last_char = INITIAL_N as u32; // The first non-ASCII code point. 118 | let mut bias = INITIAL_BIAS; 119 | let mut delta = 0; 120 | let mut h = basic_len; 121 | 122 | //while h < input.len() { 123 | while let Some(c) = unicode.pop() { 124 | // The minimum {non-basic} code point >= n in the input 125 | let cur_char = c as u32; 126 | 127 | // println!("next code point to insert is {:X}", m as u32); 128 | 129 | // The deltas are represented as "generalized variable-length integers", 130 | // which use basic code points to represent nonnegative integers. 131 | delta += (cur_char - last_char) * (h + 1); // TODO fail on overflow 132 | 133 | // TODO We can remove this `c in input` loop, all it does is move the delta along. 134 | for c in input.chars() { 135 | let c = c as u32; 136 | if c < cur_char { 137 | delta += 1; // TODO Fail on overflow 138 | } 139 | 140 | // We found the current unicode character, so lets encode it. 141 | if c == cur_char { 142 | // print!("needed delta is {}, encodes as ", delta); 143 | 144 | let mut q = delta; 145 | let mut k = BASE; 146 | loop { 147 | assert!(!(bias < k && k < (bias + TMIN))); 148 | let t = if k <= bias { 149 | TMIN 150 | } else if k >= bias + TMAX { 151 | TMAX 152 | } else { 153 | k - bias 154 | }; 155 | 156 | if q < t { 157 | break; 158 | } 159 | 160 | // TODO Check that this value will always fit into a u8. 161 | // output the code point for digit t + ((q - t) mod (base - t)) 162 | let output_c = to_char((t + ((q - t) % (BASE - t))) as u8); 163 | output.push(output_c); 164 | // print!("{}", output_c); 165 | 166 | q = (q - t) / (BASE - t); 167 | k += BASE; 168 | } 169 | 170 | // print!("{}", to_char(q as u8)); 171 | 172 | // Output the last digit for cur_char. 173 | output.push(to_char(q as u8)); 174 | 175 | bias = adapt(delta, h + 1, h == basic_len); 176 | 177 | // println!(); 178 | // println!("bias becomes {}", bias); 179 | 180 | delta = 0; 181 | h += 1; 182 | } 183 | } 184 | 185 | delta += 1; 186 | last_char = cur_char + 1; 187 | } 188 | 189 | // println!("output is \"{}\"", output); 190 | 191 | Ok(output) 192 | } 193 | 194 | // A set of test cases. 195 | // Shout out to https://r12a.github.io/app-conversion/ which helped validate some of the unicode. 196 | #[cfg(test)] 197 | static TESTS: [(&str, &str, &str); 47] = [ 198 | // From rfc3492, various examples of "Why can't they just speak in ?" 199 | ( 200 | "ليهمابتكلموشعربي؟", 201 | "egbpdaj6bu4bxfgehfvwxn", 202 | "Arabic (Egyptian)", 203 | ), 204 | ( 205 | "他们为什么不说中文", 206 | "ihqwcrb4cv8a8dqg056pqjye", 207 | "Chinese (simplified)", 208 | ), 209 | ( 210 | "他們爲什麽不說中文", 211 | "ihqwctvzc91f659drss3x8bo0yb", 212 | "Chinese (traditional)", 213 | ), 214 | ( 215 | "Pročprostěnemluvíčesky", 216 | "Proprostnemluvesky-uyb24dma41a", 217 | "Czech", 218 | ), 219 | ( 220 | "למההםפשוטלאמדבריםעברית", 221 | "4dbcagdahymbxekheh6e0a7fei0b", 222 | "Hebrew", 223 | ), 224 | ( 225 | "यहलोगहिन्दीक्योंनहींबोलसकतेहैं", 226 | "i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd", 227 | "Hindi (Devanagari)", 228 | ), 229 | ( 230 | "なぜみんな日本語を話してくれないのか", 231 | "n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa", 232 | "Japanese (kanji and hiragana)", 233 | ), 234 | ( 235 | "세계의모든사람들이한국어를이해한다면얼마나좋을까", 236 | "989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5jpsd879ccm6fea98c", 237 | "Korean (Hangul syllables)", 238 | ), 239 | ( 240 | "почемужеонинеговорятпорусски", 241 | "b1abfaaepdrnnbgefbadotcwatmq2g4l", 242 | "Russian (Cyrillic)", 243 | ), 244 | ( 245 | "PorquénopuedensimplementehablarenEspañol", 246 | "PorqunopuedensimplementehablarenEspaol-fmd56a", 247 | "Spanish", 248 | ), 249 | ( 250 | "TạisaohọkhôngthểchỉnóitiếngViệt", 251 | "TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g", 252 | "Vietnamese", 253 | ), 254 | // Japanese music artists, song titles, and TV programs, from rfc3492. 255 | ( 256 | "3年B組金八先生", 257 | "3B-ww4c5e180e575a65lsy2b", 258 | "3B", 259 | ), 260 | ( 261 | "安室奈美恵-with-SUPER-MONKEYS", 262 | "-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n", 263 | "-with-SUPER-MONKEYS", 264 | ), 265 | ( 266 | "Hello-Another-Way-それぞれの場所", 267 | "Hello-Another-Way--fc4qua05auwb3674vfr0b", 268 | "Hello-Another-Way-", 269 | ), 270 | ( 271 | "ひとつ屋根の下2", 272 | "2-u9tlzr9756bt3uc0v", 273 | "2", 274 | ), 275 | ( 276 | "MajiでKoiする5秒前", 277 | "MajiKoi5-783gue6qz075azm5e", 278 | "MajiKoi5", 279 | ), 280 | ("パフィーdeルンバ", "de-jg4avhby1noc0d", "de"), 281 | ("そのスピードで", "d9juau41awczczp", ""), 282 | // From https://en.wikipedia.org/wiki/Punycode#Examples 283 | ("bücher", "bcher-kva", "Simple wikipedia example."), 284 | ("", "", "The empty string."), 285 | ("a", "a-", "Only ASCII characters, one, lowercase."), 286 | ("A", "A-", "Only ASCII characters, one, uppercase."), 287 | ("3", "3-", "Only ASCII characters, one, a digit."), 288 | ("-", "--", "Only ASCII characters, one, a hyphen."), 289 | ("--", "---", "Only ASCII characters, two hyphens."), 290 | ( 291 | "London", 292 | "London-", 293 | "Only ASCII characters, more than one, no hyphens.", 294 | ), 295 | ( 296 | "Lloyd-Atkinson", 297 | "Lloyd-Atkinson-", 298 | "Only ASCII characters, one hyphen.", 299 | ), 300 | ( 301 | "This has spaces", 302 | "This has spaces-", 303 | "Only ASCII characters, with spaces.", 304 | ), 305 | ( 306 | "-> $1.00 <-", 307 | "-> $1.00 <--", 308 | "Only ASCII characters, mixed symbols.", 309 | ), 310 | ( 311 | "ü", 312 | "tda", 313 | "No ASCII characters, one Latin-1 Supplement character.", 314 | ), 315 | ("α", "mxa", "No ASCII characters, one Greek character."), 316 | ("例", "fsq", "No ASCII characters, one CJK character."), 317 | ("😉", "n28h", "No ASCII characters, one emoji character."), 318 | ( 319 | "αβγ", 320 | "mxacd", 321 | "No ASCII characters, more than one character.", 322 | ), 323 | ( 324 | "München", 325 | "Mnchen-3ya", 326 | "Mixed string, with one character that is not an ASCII character.", 327 | ), 328 | ( 329 | "Mnchen-3ya", 330 | "Mnchen-3ya-", 331 | "Double-encoded Punycode of \"München\".", 332 | ), 333 | ( 334 | "München-Ost", 335 | "Mnchen-Ost-9db", 336 | "Mixed string, with one character that is not ASCII, and a hyphen.", 337 | ), 338 | ( 339 | "Bahnhof München-Ost", 340 | "Bahnhof Mnchen-Ost-u6b", 341 | "Mixed string, with one space, one hyphen, and one character that is not ASCII.", 342 | ), 343 | ( 344 | "abæcdöef", 345 | "abcdef-qua4k", 346 | "Mixed string, two non-ASCII characters.", 347 | ), 348 | ("правда", "80aafi6cg", "Russian, without ASCII."), 349 | ("ยจฆฟคฏข", "22cdfh1b8fsa", "Thai, without ASCII."), 350 | ("도메인", "hq1bm8jm9l", "Korean, without ASCII."), 351 | ( 352 | "ドメイン名例", 353 | "eckwd4c7cu47r2wf", 354 | "Japanese, without ASCII.", 355 | ), 356 | ( 357 | "MajiでKoiする5秒前", 358 | "MajiKoi5-783gue6qz075azm5e", 359 | "Japanese with ASCII.", 360 | ), 361 | ( 362 | "「bücher」", 363 | "bcher-kva8445foa", 364 | "Mixed non-ASCII scripts (Latin-1 Supplement and CJK).", 365 | ), 366 | // Others edge cases 367 | ("☺", "74h", "Smiling Face."), 368 | ("i❤", "i-7iq", "i❤️.ws"), 369 | // ("☺️", "74h", "Smiling Face followed by a variation Selector-16 (U+FE0F)."), 370 | // ("i❤️", "i-7iq", "i❤️.ws with a variation Selector-16 (U+FE0F)"), 371 | ]; 372 | 373 | #[test] 374 | pub fn test_encode() { 375 | for test in TESTS { 376 | assert_eq!(encode(test.0).unwrap(), test.1); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/dns.rs: -------------------------------------------------------------------------------- 1 | use crate::bail; 2 | use crate::io::{DNSReadExt, SeekExt}; 3 | use crate::types::Record; 4 | use crate::types::*; 5 | use byteorder::{ReadBytesExt, BE}; 6 | use num_traits::FromPrimitive; 7 | use rand::Rng; 8 | use std::io; 9 | use std::io::BufRead; 10 | use std::io::Cursor; 11 | 12 | #[derive(Copy, Clone, PartialEq)] 13 | enum RecordSection { 14 | Answers, 15 | Authorities, 16 | Additionals, 17 | } 18 | 19 | /// A helper class to hold state while the parsing is happening. 20 | // TODO add list of parse errors 21 | pub(crate) struct MessageParser<'a> { 22 | // TODO Once https://github.com/tokio-rs/bytes/issues/330 is resolved, consider 23 | // switching to bytes:Buf 24 | cur: Cursor<&'a [u8]>, 25 | m: Message, 26 | } 27 | 28 | impl<'a> MessageParser<'a> { 29 | fn new(buf: &[u8]) -> MessageParser { 30 | MessageParser { 31 | cur: Cursor::new(buf), 32 | m: Message::default(), 33 | } 34 | } 35 | 36 | /// Consume the [`MessageParser`] and returned the resulting Message. 37 | fn parse(mut self) -> io::Result { 38 | self.m.id = self.cur.read_u16::()?; 39 | 40 | let b = self.cur.read_u8()?; 41 | self.m.qr = QR::from_bool(0b1000_0000 & b != 0); 42 | let opcode = (0b0111_1000 & b) >> 3; 43 | self.m.aa = (0b0000_0100 & b) != 0; 44 | self.m.tc = (0b0000_0010 & b) != 0; 45 | self.m.rd = (0b0000_0001 & b) != 0; 46 | 47 | self.m.opcode = match FromPrimitive::from_u8(opcode) { 48 | Some(t) => t, 49 | None => bail!(InvalidData, "invalid Opcode({})", opcode), 50 | }; 51 | 52 | let b = self.cur.read_u8()?; 53 | self.m.ra = (0b1000_0000 & b) != 0; 54 | self.m.z = (0b0100_0000 & b) != 0; // Unused 55 | self.m.ad = (0b0010_0000 & b) != 0; 56 | self.m.cd = (0b0001_0000 & b) != 0; 57 | let rcode = 0b0000_1111 & b; 58 | 59 | self.m.rcode = match FromPrimitive::from_u8(rcode) { 60 | Some(t) => t, 61 | None => bail!(InvalidData, "invalid RCode({})", opcode), 62 | }; 63 | 64 | let qd_count = self.cur.read_u16::()?; 65 | let an_count = self.cur.read_u16::()?; 66 | let ns_count = self.cur.read_u16::()?; 67 | let ar_count = self.cur.read_u16::()?; 68 | 69 | self.read_questions(qd_count)?; 70 | self.read_records(an_count, RecordSection::Answers)?; 71 | self.read_records(ns_count, RecordSection::Authorities)?; 72 | self.read_records(ar_count, RecordSection::Additionals)?; 73 | 74 | if self.cur.remaining()? > 0 { 75 | bail!( 76 | Other, 77 | "finished parsing with {} bytes left over", 78 | self.cur.remaining()? 79 | ); 80 | } 81 | 82 | Ok(self.m) 83 | } 84 | 85 | fn read_questions(&mut self, count: u16) -> io::Result<()> { 86 | self.m.questions.reserve_exact(count.into()); 87 | 88 | for _ in 0..count { 89 | let name = self.cur.read_qname()?; 90 | let r#type = self.cur.read_type()?; 91 | let class = self.cur.read_class()?; 92 | 93 | self.m.questions.push(Question { 94 | name, 95 | r#type, 96 | class, 97 | }); 98 | } 99 | 100 | Ok(()) 101 | } 102 | 103 | fn read_records(&mut self, count: u16, section: RecordSection) -> io::Result<()> { 104 | let records = match section { 105 | RecordSection::Answers => &mut self.m.answers, 106 | RecordSection::Authorities => &mut self.m.authoritys, 107 | RecordSection::Additionals => &mut self.m.additionals, 108 | }; 109 | records.reserve_exact(count.into()); 110 | 111 | for _ in 0..count { 112 | let name = self.cur.read_qname()?; 113 | let r#type = self.cur.read_type()?; 114 | 115 | if section == RecordSection::Additionals && r#type == Type::OPT { 116 | if self.m.extension.is_some() { 117 | bail!( 118 | InvalidData, 119 | "multiple EDNS(0) extensions. Expected only one." 120 | ); 121 | } 122 | 123 | let ext = Extension::parse(&mut self.cur, name, r#type)?; 124 | 125 | self.m.extension = Some(ext); 126 | } else { 127 | let class = self.cur.read_class()?; 128 | let record = Record::parse(&mut self.cur, name, r#type, class)?; 129 | 130 | records.push(record); 131 | } 132 | } 133 | 134 | Ok(()) 135 | } 136 | } 137 | 138 | /// Defaults to a [`Message`] with sensibles values for querying. 139 | impl Default for Message { 140 | fn default() -> Self { 141 | Message { 142 | id: Message::random_id(), 143 | rd: true, 144 | tc: false, 145 | aa: false, 146 | opcode: Opcode::Query, 147 | qr: QR::Query, 148 | rcode: Rcode::NoError, 149 | cd: false, 150 | ad: true, 151 | z: false, 152 | ra: false, 153 | 154 | questions: Vec::default(), 155 | answers: Vec::default(), 156 | authoritys: Vec::default(), 157 | additionals: Vec::default(), 158 | extension: None, 159 | 160 | stats: None, 161 | } 162 | } 163 | } 164 | 165 | impl Message { 166 | /// Returns a random u16 suitable for the [`Message`] id field. 167 | /// 168 | /// This is generated with the [`rand::rngs::StdRng`] which is a suitable 169 | /// cryptographically secure pseudorandom number generator. 170 | pub fn random_id() -> u16 { 171 | rand::thread_rng().gen() 172 | } 173 | 174 | /// Decodes the supplied buffer and returns a [`Message`]. 175 | pub fn from_slice(buf: &[u8]) -> io::Result { 176 | MessageParser::new(buf).parse() 177 | } 178 | 179 | /// Takes a unicode domain, converts to ascii, and back to unicode. 180 | /// This has the effective of normalising it, so its easier to compare 181 | /// what was queried, and what was returned. 182 | fn normalise_domain(&mut self, domain: &str) -> Result { 183 | let ascii = idna::domain_to_ascii(domain)?; 184 | let (mut unicode, result) = idna::domain_to_unicode(&ascii); 185 | match result { 186 | Ok(_) => { 187 | if !unicode.ends_with('.') { 188 | unicode.push('.') 189 | } 190 | Ok(unicode) 191 | } 192 | Err(errors) => Err(errors), 193 | } 194 | } 195 | 196 | /// Adds a question to the message. 197 | /// 198 | /// Note: DNS servers typically do not support more than one question. There is ambiguity in how to handle 199 | /// rcode, etc. See [§4.1.2 of rfc1035] or 200 | /// 201 | /// [§4.1.2 of rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.2. 202 | pub fn add_question(&mut self, domain: &str, r#type: Type, class: Class) { 203 | let domain = self.normalise_domain(domain).expect("invalid domain"); // TODO fix 204 | 205 | // TODO Don't allow more than 255 questions. 206 | let q = Question { 207 | name: domain, 208 | r#type, 209 | class, 210 | }; 211 | 212 | self.questions.push(q); 213 | } 214 | 215 | /// Adds a EDNS(0) extension record, as defined by [rfc6891](https://datatracker.ietf.org/doc/html/rfc6891). 216 | pub fn add_extension(&mut self, ext: Extension) { 217 | // Don't allow if self.additionals.len() + 1 > 255 218 | self.extension = Some(ext); 219 | } 220 | 221 | /// Encodes this DNS [`Message`] as a [`Vec`] ready to be sent, as defined by [rfc1035]. 222 | /// 223 | /// [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035 224 | pub fn to_vec(&self) -> io::Result> { 225 | let mut req = Vec::::with_capacity(512); 226 | 227 | req.extend_from_slice(&(self.id as u16).to_be_bytes()); 228 | 229 | let mut b = 0_u8; 230 | b |= if self.qr.to_bool() { 0b1000_0000 } else { 0 }; 231 | b |= ((self.opcode as u8) << 3) & 0b0111_1000; 232 | b |= if self.aa { 0b0000_0100 } else { 0 }; 233 | b |= if self.tc { 0b0000_0010 } else { 0 }; 234 | b |= if self.rd { 0b0000_0001 } else { 0 }; 235 | req.push(b); 236 | 237 | let mut b = 0_u8; 238 | b |= if self.ra { 0b1000_0000 } else { 0 }; 239 | b |= if self.z { 0b0100_0000 } else { 0 }; 240 | b |= if self.ad { 0b0010_0000 } else { 0 }; 241 | b |= if self.cd { 0b0001_0000 } else { 0 }; 242 | b |= (self.rcode as u8) & 0b0000_1111; 243 | 244 | req.push(b); 245 | 246 | let ar_count = self.additionals.len() as u16 + self.extension.is_some() as u16; 247 | 248 | req.extend_from_slice(&(self.questions.len() as u16).to_be_bytes()); 249 | req.extend_from_slice(&(self.answers.len() as u16).to_be_bytes()); 250 | req.extend_from_slice(&(self.authoritys.len() as u16).to_be_bytes()); 251 | req.extend_from_slice(&ar_count.to_be_bytes()); 252 | 253 | for question in &self.questions { 254 | // TODO use Question::as_vec() 255 | Message::write_qname(&mut req, &question.name)?; 256 | 257 | req.extend_from_slice(&(question.r#type as u16).to_be_bytes()); 258 | req.extend_from_slice(&(question.class as u16).to_be_bytes()); 259 | } 260 | 261 | // TODO Implement answers, etc types. 262 | assert!(self.answers.is_empty()); 263 | assert!(self.authoritys.is_empty()); 264 | assert!(self.additionals.is_empty()); 265 | 266 | if let Some(e) = &self.extension { 267 | e.write(&mut req)? 268 | } 269 | 270 | // TODO if the Vec is too long, truncate the request. 271 | 272 | Ok(req) 273 | } 274 | 275 | /// Writes a Unicode domain name into the supplied [`Vec`]. 276 | /// 277 | /// Used for writing out a encoded ASCII domain name into a DNS message. Will 278 | /// returns the Unicode domain name, as well as the length of this qname (ignoring 279 | /// any compressed pointers) in bytes. 280 | /// 281 | // TODO Support compression. 282 | fn write_qname(buf: &mut Vec, domain: &str) -> io::Result<()> { 283 | // Decode this label into the original unicode. 284 | // TODO Switch to using our own idna::Config. (but we can't use disallowed_by_std3_ascii_rules). 285 | let domain = match idna::domain_to_ascii(domain) { 286 | Err(e) => { 287 | bail!(InvalidData, "invalid dns name '{0}': {1}", domain, e); 288 | } 289 | Ok(domain) => domain, 290 | }; 291 | 292 | if !domain.is_empty() && domain != "." { 293 | for label in domain.split_terminator('.') { 294 | if label.is_empty() { 295 | bail!(InvalidData, "empty label in domain name '{}'", domain); 296 | } 297 | 298 | if label.len() > 63 { 299 | bail!(InvalidData, "label '{0}' longer than 63 characters", label); 300 | } 301 | 302 | // Write the length. 303 | buf.push(label.len() as u8); 304 | 305 | // Then the actual label. 306 | buf.extend_from_slice(label.as_bytes()); 307 | } 308 | } 309 | 310 | buf.push(0); 311 | 312 | Ok(()) 313 | } 314 | } 315 | 316 | impl Extension { 317 | pub fn parse(cur: &mut Cursor<&[u8]>, domain: String, r#type: Type) -> io::Result { 318 | assert!(r#type == Type::OPT); 319 | 320 | if domain != "." { 321 | bail!( 322 | InvalidData, 323 | "expected root domain for EDNS(0) extension, got '{}'", 324 | domain 325 | ); 326 | } 327 | 328 | let payload_size = cur.read_u16::()?; 329 | let extend_rcode = cur.read_u8()?; 330 | 331 | let version = cur.read_u8()?; 332 | let b = cur.read_u8()?; 333 | let dnssec_ok = b & 0b1000_0000 == 0b1000_0000; 334 | 335 | let _z = cur.read_u8()?; 336 | 337 | // TODO implement this 338 | let rd_len = cur.read_u16::()?; 339 | cur.consume(rd_len.into()); 340 | 341 | Ok(Extension { 342 | payload_size, 343 | extend_rcode, 344 | version, 345 | dnssec_ok, 346 | }) 347 | } 348 | 349 | pub fn write(&self, buf: &mut Vec) -> io::Result<()> { 350 | buf.push(0); // A single "." domain name // 0-1 351 | buf.extend_from_slice(&(Type::OPT as u16).to_be_bytes()); // 1-3 352 | buf.extend_from_slice(&(self.payload_size as u16).to_be_bytes()); // 3-5 353 | 354 | buf.push(self.extend_rcode); // 5-6 355 | buf.push(self.version); // 6-7 356 | 357 | let mut b = 0_u8; 358 | b |= if self.dnssec_ok { 0b1000_0000 } else { 0 }; 359 | 360 | // 16 bits 361 | buf.push(b); 362 | buf.push(0); 363 | 364 | // 16 bit RDLEN - TODO 365 | buf.push(0); 366 | buf.push(0); 367 | 368 | Ok(()) 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | //! Implements the Display trait for the various types, so they output 2 | //! in `dig` style. 3 | // Refer to https://github.com/tigeli/bind-utils/blob/master/bin/dig/dig.c for reference. 4 | 5 | use crate::resource::TXT; 6 | use crate::resource::MX; 7 | use crate::resource::SOA; 8 | use crate::resource::SRV; 9 | use crate::Message; 10 | use crate::Question; 11 | use crate::Record; 12 | use crate::Resource; 13 | use crate::Stats; 14 | use chrono::prelude::*; 15 | use std::fmt; 16 | 17 | /// Displays this message in a format resembling `dig` output. 18 | impl fmt::Display for Message { 19 | // TODO There seems to be whitespace/newlines in this output. Fix. 20 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 21 | self.fmt_header(f)?; 22 | 23 | // ;; OPT PSEUDOSECTION: 24 | // ; EDNS: version: 0, flags:; udp: 512 25 | if let Some(e) = &self.extension { 26 | writeln!(f, ";; OPT PSEUDOSECTION:")?; 27 | // TODO Support the flags 28 | writeln!( 29 | f, 30 | "; EDNS: version: {version}, flags:; udp: {payload_size}", 31 | version = e.version, 32 | payload_size = e.payload_size, 33 | )?; 34 | } 35 | 36 | // Always display the question section, but optionally 37 | // display the other sections. 38 | writeln!(f, ";; QUESTION SECTION:")?; 39 | for question in &self.questions { 40 | question.fmt(f)?; 41 | } 42 | writeln!(f)?; 43 | 44 | if !self.answers.is_empty() { 45 | writeln!(f, "; ANSWER SECTION:")?; 46 | for answer in &self.answers { 47 | answer.fmt(f)?; 48 | } 49 | writeln!(f)?; 50 | } 51 | 52 | if !self.authoritys.is_empty() { 53 | writeln!(f, "; AUTHORITY SECTION:")?; 54 | for answer in &self.authoritys { 55 | answer.fmt(f)?; 56 | } 57 | writeln!(f)?; 58 | } 59 | 60 | if !self.additionals.is_empty() { 61 | writeln!(f, "; ADDITIONAL SECTION:")?; 62 | for answer in &self.additionals { 63 | answer.fmt(f)?; 64 | } 65 | writeln!(f)?; 66 | } 67 | 68 | if let Some(stats) = &self.stats { 69 | stats.fmt(f)?; 70 | } 71 | 72 | writeln!(f) 73 | } 74 | } 75 | 76 | impl Message { 77 | fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result { 78 | writeln!( 79 | f, 80 | ";; ->>HEADER<<- opcode: {opcode}, status: {rcode}, id: {id}", 81 | opcode = self.opcode, 82 | rcode = self.rcode, 83 | id = self.id, 84 | )?; 85 | 86 | let mut flags = String::new(); 87 | 88 | if self.qr.to_bool() { 89 | flags.push_str(" qr") 90 | } 91 | if self.aa { 92 | flags.push_str(" aa") 93 | } 94 | if self.tc { 95 | flags.push_str(" tc") 96 | } 97 | if self.rd { 98 | flags.push_str(" rd") 99 | } 100 | if self.ra { 101 | flags.push_str(" ra") 102 | } 103 | if self.ad { 104 | flags.push_str(" ad") 105 | } 106 | if self.cd { 107 | flags.push_str(" cd") 108 | } 109 | 110 | let ar_count = self.additionals.len() as u16 + self.extension.is_some() as u16; 111 | 112 | writeln!(f, ";; flags:{flags}; QUERY: {qd_count}, ANSWER: {an_count}, AUTHORITY: {ns_count}, ADDITIONAL: {ar_count}", 113 | flags = flags, 114 | qd_count = self.questions.len(), 115 | an_count = self.answers.len(), 116 | ns_count = self.authoritys.len(), 117 | ar_count = ar_count, 118 | )?; 119 | 120 | writeln!(f) 121 | } 122 | } 123 | 124 | impl fmt::Display for Stats { 125 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 126 | writeln!(f, ";; Query time: {} msec", self.duration.as_millis())?; // TODO Support usec as well 127 | writeln!(f, ";; SERVER: {}", self.server)?; 128 | 129 | let start: chrono::DateTime = self.start.into(); 130 | // ;; WHEN: Sat Jun 12 12:14:21 PDT 2021 131 | writeln!(f, ";; WHEN: {}", start.format("%a %b %-d %H:%M:%S %z %-Y"))?; 132 | writeln!( 133 | f, 134 | ";; MSG SIZE sent: {} rcvd: {}", 135 | self.request_size, self.response_size 136 | ) 137 | } 138 | } 139 | 140 | impl fmt::Display for Question { 141 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 142 | writeln!( 143 | f, 144 | "; {name:<18} {class:4} {type:6}\n", 145 | name = self.name, 146 | class = self.class, 147 | r#type = self.r#type, 148 | ) 149 | } 150 | } 151 | 152 | impl fmt::Display for Record { 153 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 154 | writeln!( 155 | f, 156 | "{name:<20} {ttl:>4} {class:4} {type:6} {resource}", 157 | name = self.name, 158 | ttl = self.ttl.as_secs(), 159 | class = self.class, 160 | r#type = self.r#type(), 161 | resource = self.resource, 162 | ) 163 | } 164 | } 165 | 166 | impl fmt::Display for Resource { 167 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 168 | match self { 169 | Resource::A(ip) => ip.fmt(f), 170 | Resource::AAAA(ip) => ip.fmt(f), 171 | 172 | Resource::NS(name) => name.fmt(f), 173 | Resource::CNAME(name) => name.fmt(f), 174 | Resource::PTR(name) => name.fmt(f), 175 | 176 | Resource::SOA(soa) => soa.fmt(f), 177 | Resource::TXT(txts) | Resource::SPF(txts) => txts.fmt(f), 178 | Resource::MX(mx) => mx.fmt(f), 179 | Resource::SRV(srv) => srv.fmt(f), 180 | 181 | Resource::OPT => write!(f, "OPT (TODO)"), 182 | Resource::ANY => write!(f, "*"), 183 | } 184 | } 185 | } 186 | 187 | impl fmt::Display for MX { 188 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 189 | // "10 aspmx.l.google.com." 190 | write!( 191 | f, 192 | "{preference} {exchange}", 193 | preference = self.preference, 194 | exchange = self.exchange, 195 | ) 196 | } 197 | } 198 | 199 | impl fmt::Display for SOA { 200 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 201 | // "ns1.google.com. dns-admin.google.com. 376337657 900 900 1800 60" 202 | 203 | // It's arguable that dns-admin@google.com looks better, but 204 | // for now we'll keep the format dns-admin.google.com. 205 | let rname = match Self::email_to_rname(&self.rname) { 206 | Ok(name) => name, 207 | Err(_) => self.rname.to_owned(), // Ignore the error 208 | }; 209 | 210 | write!( 211 | f, 212 | "{mname} {rname} {serial} {refresh} {retry} {expire} {minimum}", 213 | mname = self.mname, 214 | rname = rname, 215 | serial = self.serial, 216 | refresh = self.refresh.as_secs(), 217 | retry = self.retry.as_secs(), 218 | expire = self.expire.as_secs(), 219 | minimum = self.minimum.as_secs(), 220 | ) 221 | } 222 | } 223 | 224 | impl fmt::Display for SRV { 225 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 226 | // "5 0 389 ldap.google.com." 227 | write!( 228 | f, 229 | "{priority} {weight} {port} {name}", 230 | priority = self.priority, 231 | weight = self.weight, 232 | port = self.port, 233 | name = self.name, 234 | ) 235 | } 236 | } 237 | 238 | impl fmt::Display for TXT { 239 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 240 | let output = self.0 241 | .iter() 242 | .map(|txt| { 243 | match std::str::from_utf8(txt) { 244 | // TODO Escape the " character (and maybe others) 245 | Ok(txt) => "\"".to_owned() + txt + "\"", 246 | 247 | // TODO Try our best to convert this to valid UTF, and use 248 | // https://doc.rust-lang.org/std/str/struct.Utf8Error.html to show what we can. 249 | Err(_e) => "invalid".to_string(), 250 | } 251 | }) 252 | .collect::>() 253 | .join(" "); 254 | 255 | write!(f, "{}", output) 256 | } 257 | } 258 | 259 | #[cfg(test)] 260 | mod tests { 261 | use crate::TXT; 262 | use crate::Resource; 263 | use crate::MX; 264 | use crate::SOA; 265 | use crate::SRV; 266 | use core::time::Duration; 267 | use pretty_assertions::assert_eq; 268 | 269 | lazy_static! { 270 | static ref DISPLAY_TESTS : Vec<(Resource, &'static str)> = { 271 | vec![ 272 | ( 273 | Resource::A("172.217.164.100".parse().unwrap()), 274 | "172.217.164.100", 275 | ), 276 | ( 277 | Resource::AAAA("2607:f8b0:4005:805::2004".parse().unwrap()), 278 | "2607:f8b0:4005:805::2004", 279 | ), 280 | ( 281 | Resource::CNAME("code.l.google.com.".to_string()), 282 | "code.l.google.com.", 283 | ), 284 | ( 285 | Resource::NS("ns4.google.com.".to_string()), 286 | "ns4.google.com.", 287 | ), 288 | (Resource::PTR("dns.google.".to_string()), "dns.google."), 289 | ( 290 | Resource::SOA(SOA { 291 | mname: "ns1.google.com.".to_string(), 292 | rname: "dns-admin@google.com.".to_string(), 293 | 294 | serial: 379031418, 295 | 296 | refresh: Duration::from_secs(900), 297 | retry: Duration::from_secs(900), 298 | expire: Duration::from_secs(1800), 299 | minimum: Duration::from_secs(60), 300 | }), 301 | "ns1.google.com. dns-admin.google.com. 379031418 900 900 1800 60", 302 | ), 303 | ( 304 | Resource::MX(MX { 305 | preference: 10, 306 | exchange: "aspmx.l.google.com.".to_string(), 307 | }), 308 | "10 aspmx.l.google.com.", 309 | ), 310 | ( 311 | Resource::SRV(SRV { 312 | priority: 5, 313 | weight: 0, 314 | port: 389, 315 | name: "ldap.google.com.".to_string(), 316 | }), 317 | "5 0 389 ldap.google.com.", 318 | ), 319 | ( 320 | Resource::TXT(TXT::from("v=spf1 include:_spf.google.com ~all")), 321 | "\"v=spf1 include:_spf.google.com ~all\"", 322 | ), 323 | ( 324 | // Example from TXT s1024._domainkey.yahoo.com. 325 | Resource::TXT(TXT::from(&[ 326 | "k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfm", 327 | "JiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB; n=A 1024 bit key;" 328 | ][..])), 329 | "\"k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfm\" \"JiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB; n=A 1024 bit key;\"", 330 | ), 331 | ] 332 | }; 333 | } 334 | 335 | #[test] 336 | fn test_display() { 337 | for (resource, display) in (*DISPLAY_TESTS).iter() { 338 | assert_eq!(format!("{}", resource), *display); 339 | } 340 | } 341 | 342 | #[test] 343 | fn test_from_str() { 344 | for (resource, display) in (*DISPLAY_TESTS).iter() { 345 | match Resource::from_str(resource.r#type(), display) { 346 | Ok(got) => assert_eq!(&got, resource), 347 | Err(err) => panic!( 348 | "from_str({}, '{}') failed: {}", 349 | resource.r#type(), 350 | display, 351 | err 352 | ), 353 | } 354 | } 355 | } 356 | 357 | /// Test resource->display->from_string to make sure we can round trip between types. 358 | #[test] 359 | fn test_identity() { 360 | for (resource, _) in (*DISPLAY_TESTS).iter() { 361 | let display = format!("{}", resource); 362 | match Resource::from_str(resource.r#type(), &display) { 363 | Ok(got) => assert_eq!(&got, resource), 364 | Err(err) => panic!( 365 | "from_str({}, '{}') failed: {}", 366 | resource.r#type(), 367 | display, 368 | err 369 | ), 370 | } 371 | } 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/clients/json.rs: -------------------------------------------------------------------------------- 1 | use crate::bail; 2 | use crate::clients::mime::content_type_equal; 3 | use crate::clients::AsyncExchanger; 4 | use crate::clients::ToUrls; 5 | use crate::errors::ParseError; 6 | use crate::Class; 7 | use crate::Error; 8 | use crate::Message; 9 | use crate::Question; 10 | use crate::Record; 11 | use crate::Resource; 12 | use crate::clients::stats::StatsBuilder; 13 | use async_trait::async_trait; 14 | use core::convert::TryInto; 15 | use http::header::*; 16 | use http::Method; 17 | use http::Request; 18 | use hyper::client::connect::HttpInfo; 19 | use hyper::{Body, Client as HyperClient}; 20 | use hyper_alpn::AlpnConnector; 21 | use num_traits::FromPrimitive; 22 | use serde::{Deserialize, Serialize}; 23 | use serde_json; 24 | use std::net::IpAddr; 25 | use std::net::Ipv4Addr; 26 | use std::net::SocketAddr; 27 | use std::time::Duration; 28 | use url::Url; 29 | 30 | pub const GOOGLE: &str = "https://dns.google/resolve"; 31 | pub const CLOUDFLARE: &str = "https://cloudflare-dns.com/dns-query"; 32 | 33 | // For use in Content-type and Accept headers 34 | // Google actually uses "application/json", but Cloud Flare requires "application/dns-json". 35 | // Since Google's API seems to accept either, we default to dns-json. 36 | const CONTENT_TYPE_APPLICATION_DNS_JSON: &str = "application/dns-json"; 37 | const CONTENT_TYPE_APPLICATION_JSON: &str = "application/json"; 38 | 39 | #[derive(Serialize, Deserialize)] 40 | #[serde(rename_all = "PascalCase")] 41 | struct MessageJson { 42 | pub status: u32, // NOERROR - Standard DNS response code (32 bit integer). 43 | 44 | #[serde(rename = "TC")] 45 | pub tc: bool, // Whether the response is truncated 46 | 47 | #[serde(rename = "RD")] 48 | pub rd: bool, // Always true for Google Public DNS 49 | 50 | #[serde(rename = "RA")] 51 | pub ra: bool, // Always true for Google Public DNS 52 | 53 | #[serde(rename = "AD")] 54 | pub ad: bool, // Whether all response data was validated with DNSSEC 55 | 56 | #[serde(rename = "CD")] 57 | pub cd: bool, // Whether the client asked to disable DNSSEC 58 | 59 | pub question: Vec, 60 | 61 | #[serde(default)] // Prefer empty Vec, over Optional 62 | pub answer: Vec, 63 | 64 | pub comment: Option, 65 | 66 | #[serde(rename = "edns_client_subnet")] 67 | pub edns_client_subnet: Option, // IP address / scope prefix-length 68 | } 69 | 70 | impl TryInto for MessageJson { 71 | type Error = ParseError; 72 | 73 | fn try_into(self) -> Result { 74 | let rcode = 75 | FromPrimitive::from_u32(self.status).ok_or(ParseError::InvalidStatus(self.r#status))?; 76 | 77 | let mut m = Message { 78 | rcode, 79 | tc: self.tc, 80 | rd: self.rd, 81 | ra: self.ra, 82 | ad: self.ad, 83 | cd: self.cd, 84 | 85 | ..Default::default() 86 | }; 87 | 88 | // TODO Do something with edns_client_subnet 89 | // TODO Do something with comment 90 | 91 | for question in self.question { 92 | m.questions.push(question.try_into()?) 93 | } 94 | 95 | for answer in self.answer { 96 | m.answers.push(answer.try_into()?) 97 | } 98 | 99 | Ok(m) 100 | } 101 | } 102 | 103 | // Basically a Question 104 | #[derive(Serialize, Deserialize)] 105 | #[serde(rename_all = "lowercase")] 106 | struct QuestionJson { 107 | pub name: String, // FQDN with trailing dot 108 | pub r#type: u16, // A - Standard DNS RR type 109 | } 110 | 111 | impl TryInto for QuestionJson { 112 | type Error = ParseError; 113 | 114 | fn try_into(self) -> Result { 115 | let r#type = 116 | FromPrimitive::from_u16(self.r#type).ok_or(ParseError::InvalidType(self.r#type))?; 117 | 118 | Ok(Question { 119 | name: self.name, // TODO Do I need to remove the trailing dot? 120 | r#type, 121 | class: Class::Internet, 122 | }) 123 | } 124 | } 125 | 126 | // Basically a Record + Resource 127 | #[derive(Serialize, Deserialize)] 128 | #[serde(rename_all = "lowercase")] 129 | struct RecordJson { 130 | pub name: String, 131 | pub r#type: u16, // A - Standard DNS RR type 132 | 133 | #[serde(rename = "TTL")] 134 | pub ttl: u32, 135 | pub data: String, 136 | } 137 | 138 | impl TryInto for RecordJson { 139 | type Error = ParseError; 140 | 141 | fn try_into(self) -> Result { 142 | let r#type = 143 | FromPrimitive::from_u16(self.r#type).ok_or(ParseError::InvalidType(self.r#type))?; 144 | 145 | let resource = 146 | Resource::from_str(r#type, &self.data).map_err(|x| ParseError::InvalidResource(r#type, x))?; 147 | 148 | Ok(Record { 149 | name: self.name, // TODO Do I need to remove the trailing dot? 150 | class: Class::Internet, 151 | ttl: Duration::from_secs(self.ttl.into()), 152 | resource, 153 | }) 154 | } 155 | } 156 | 157 | /// A DNS over HTTPS client using the Google JSON API. 158 | /// 159 | /// # Example 160 | /// 161 | /// ```rust 162 | /// use rustdns::clients::AsyncExchanger; 163 | /// use rustdns::clients::json; 164 | /// use rustdns::types::*; 165 | /// 166 | /// #[tokio::main] 167 | /// async fn main() -> Result<(), rustdns::Error> { 168 | /// let mut query = Message::default(); 169 | /// query.add_question("bramp.net", Type::A, Class::Internet); 170 | /// 171 | /// let response = json::Client::new("https://dns.google/resolve")? 172 | /// .exchange(&query) 173 | /// .await 174 | /// .expect("could not exchange message"); 175 | /// 176 | /// println!("{}", response); 177 | /// Ok(()) 178 | /// } 179 | /// ``` 180 | /// 181 | /// See and 182 | /// 183 | // TODO Document all the options. 184 | pub struct Client { 185 | servers: Vec, 186 | } 187 | 188 | impl Default for Client { 189 | fn default() -> Self { 190 | Client { 191 | servers: Vec::default(), 192 | } 193 | } 194 | } 195 | 196 | impl Client { 197 | /// Creates a new Client bound to the specific servers. 198 | /// 199 | /// Be aware that the servers will typically be in the form of `https://domain_name/`. That 200 | /// `domain_name` will be resolved by the system's standard DNS library. I don't have a good 201 | /// work-around for this yet. 202 | // TODO Document how it fails. 203 | pub fn new(servers: A) -> Result { 204 | Ok(Self { 205 | servers: servers.to_urls()?.collect(), 206 | }) 207 | } 208 | } 209 | 210 | #[async_trait] 211 | impl AsyncExchanger for Client { 212 | /// Sends the [`Message`] to the `server` via HTTP and returns the result. 213 | // TODO Decide if this should be async or not. 214 | // Can return ::std::io::Error 215 | async fn exchange(&self, query: &Message) -> Result { 216 | if query.questions.len() != 1 { 217 | return Err(Error::InvalidArgument( 218 | "expected exactly one question must be provided".to_string(), 219 | )); 220 | } 221 | 222 | // Create a Alpn client, so our connection will upgrade to HTTP/2. 223 | // TODO Move the client into the struct/new() 224 | // TODO Change the Connector Connect method to allow us to override the DNS 225 | // resolution in the connector! 226 | let alpn = AlpnConnector::new(); 227 | 228 | let client = HyperClient::builder() 229 | .pool_idle_timeout(Duration::from_secs(30)) 230 | .http2_only(true) 231 | .build::<_, hyper::Body>(alpn); 232 | 233 | let question = &query.questions[0]; 234 | 235 | let mut url = self.servers[0].clone(); // TODO Support more than one server 236 | url.query_pairs_mut().append_pair("name", &question.name); 237 | url.query_pairs_mut() 238 | .append_pair("type", &question.r#type.to_string()); 239 | 240 | url.query_pairs_mut() 241 | .append_pair("cd", &query.cd.to_string()); 242 | url.query_pairs_mut() 243 | .append_pair("ct", CONTENT_TYPE_APPLICATION_DNS_JSON); 244 | 245 | if let Some(extension) = &query.extension { 246 | url.query_pairs_mut() 247 | .append_pair("do", &extension.dnssec_ok.to_string()); 248 | } 249 | 250 | // TODO Support the following 251 | // url.query_pairs_mut().append_pair("edns_client_subnet", ); 252 | // url.query_pairs_mut().append_pair("random_padding", ); 253 | 254 | // We have to do this wierd as_str().parse() thing because the 255 | // http::Uri doesn't provide a way to easily mutate or construct it. 256 | let uri: hyper::Uri = url.as_str().parse()?; 257 | 258 | let req = Request::builder() 259 | .method(Method::GET) 260 | .uri(uri) 261 | .header(ACCEPT, CONTENT_TYPE_APPLICATION_DNS_JSON) 262 | .body(Body::empty())?; 263 | 264 | let stats = StatsBuilder::start(0); 265 | let resp = client.request(req).await?; 266 | 267 | if let Some(content_type) = resp.headers().get(CONTENT_TYPE) { 268 | if !content_type_equal(content_type, CONTENT_TYPE_APPLICATION_DNS_JSON) 269 | && !content_type_equal(content_type, CONTENT_TYPE_APPLICATION_JSON) 270 | { 271 | bail!( 272 | InvalidData, 273 | "recevied invalid content-type: {:?} expected {} or {}", 274 | content_type, 275 | CONTENT_TYPE_APPLICATION_DNS_JSON, 276 | CONTENT_TYPE_APPLICATION_JSON, 277 | ); 278 | } 279 | } 280 | 281 | if resp.status().is_success() { 282 | // Get connection information (if available) 283 | let remote_addr = match resp.extensions().get::() { 284 | Some(http_info) => http_info.remote_addr(), 285 | 286 | // TODO Maybe remote_addr should be optional? 287 | None => SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0), // Dummy address 288 | }; 289 | 290 | // Read the full body 291 | let body = hyper::body::to_bytes(resp.into_body()).await?; 292 | 293 | println!("{:?}", body); 294 | 295 | let m: MessageJson = serde_json::from_slice(&body).map_err(ParseError::JsonError)?; 296 | let mut m: Message = m.try_into()?; 297 | m.stats = Some(stats.end(remote_addr, body.len())); 298 | 299 | return Ok(m); 300 | } 301 | 302 | // TODO Retry on 500s. If this is a 4xx we should not retry. Should we follow 3xx? 303 | bail!( 304 | InvalidInput, 305 | "recevied unexpected HTTP status code: {:}", 306 | resp.status() 307 | ); 308 | } 309 | } 310 | 311 | 312 | #[cfg(test)] 313 | mod tests { 314 | use std::io::Read; 315 | use std::convert::TryInto; 316 | use crate::clients::json::MessageJson; 317 | use json_comments::StripComments; 318 | use crate::Message; 319 | 320 | #[test] 321 | fn test_parse_response() { 322 | // From https://developers.google.com/speed/public-dns/docs/doh/json 323 | let tests = [r#"{ 324 | "Status": 0, // NOERROR - Standard DNS response code (32 bit integer). 325 | "TC": false, // Whether the response is truncated 326 | "RD": true, // Always true for Google Public DNS 327 | "RA": true, // Always true for Google Public DNS 328 | "AD": false, // Whether all response data was validated with DNSSEC 329 | "CD": false, // Whether the client asked to disable DNSSEC 330 | "Question": 331 | [ 332 | { 333 | "name": "apple.com.", // FQDN with trailing dot 334 | "type": 1 // A - Standard DNS RR type 335 | } 336 | ], 337 | "Answer": 338 | [ 339 | { 340 | "name": "apple.com.", // Always matches name in the Question section 341 | "type": 1, // A - Standard DNS RR type 342 | "TTL": 3599, // Record's time-to-live in seconds 343 | "data": "17.178.96.59" // Data for A - IP address as text 344 | }, 345 | { 346 | "name": "apple.com.", 347 | "type": 1, 348 | "TTL": 3599, 349 | "data": "17.172.224.47" 350 | }, 351 | { 352 | "name": "apple.com.", 353 | "type": 1, 354 | "TTL": 3599, 355 | "data": "17.142.160.59" 356 | } 357 | ], 358 | "edns_client_subnet": "12.34.56.78/0" // IP address / scope prefix-length 359 | }"# 360 | , 361 | r#" 362 | { 363 | "Status": 2, // SERVFAIL - Standard DNS response code (32 bit integer). 364 | "TC": false, // Whether the response is truncated 365 | "RD": true, // Always true for Google Public DNS 366 | "RA": true, // Always true for Google Public DNS 367 | "AD": false, // Whether all response data was validated with DNSSEC 368 | "CD": false, // Whether the client asked to disable DNSSEC 369 | "Question": 370 | [ 371 | { 372 | "name": "dnssec-failed.org.", // FQDN with trailing dot 373 | "type": 1 // A - Standard DNS RR type 374 | } 375 | ], 376 | "Comment": "DNSSEC validation failure. Please check http://dnsviz.net/d/dnssec-failed.org/dnssec/." 377 | } 378 | "# 379 | , 380 | r#" 381 | { 382 | "Status": 0, // NOERROR - Standard DNS response code (32 bit integer). 383 | "TC": false, // Whether the response is truncated 384 | "RD": true, // Always true for Google Public DNS 385 | "RA": true, // Always true for Google Public DNS 386 | "AD": false, // Whether all response data was validated with DNSSEC 387 | "CD": false, // Whether the client asked to disable DNSSEC 388 | "Question": [ 389 | { 390 | "name": "*.dns-example.info.", // FQDN with trailing dot 391 | "type": 99 // SPF - Standard DNS RR type 392 | } 393 | ], 394 | "Answer": [ 395 | { 396 | "name": "*.dns-example.info.", // Always matches name in Question 397 | "type": 99, // SPF - Standard DNS RR type 398 | "TTL": 21599, // Record's time-to-live in seconds 399 | "data": "\"v=spf1 -all\"" // Data for SPF - quoted string 400 | } 401 | ], 402 | "Comment": "Response from 216.239.38.110" 403 | // Uncached responses are attributed to the authoritative name server 404 | }"# 405 | , 406 | r#"{ 407 | "Status": 0, // NOERROR - Standard DNS response code (32 bit integer). 408 | "TC": false, // Whether the response is truncated 409 | "RD": true, // Always true for Google Public DNS 410 | "RA": true, // Always true for Google Public DNS 411 | "AD": false, // Whether all response data was validated with DNSSEC 412 | "CD": false, // Whether the client asked to disable DNSSEC 413 | "Question": [ 414 | { 415 | "name": "s1024._domainkey.yahoo.com.", // FQDN with trailing dot 416 | "type": 16 // TXT - Standard DNS RR type 417 | } 418 | ], 419 | "Answer": [ 420 | { 421 | "name": "s1024._domainkey.yahoo.com.", // Always matches Question name 422 | "type": 16, // TXT - Standard DNS RR type 423 | "TTL": 21599, // Record's time-to-live in seconds 424 | "data": "\"k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfm\"\"JiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB; n=A 1024 bit key;\"" 425 | // Data for TXT - multiple quoted strings 426 | } 427 | ] 428 | }"#, 429 | 430 | // From https://developers.cloudflare.com/1.1.1.1/encrypted-dns/dns-over-https/make-api-requests/dns-json 431 | r#"{ 432 | "Status": 0, 433 | "TC": false, 434 | "RD": true, 435 | "RA": true, 436 | "AD": true, 437 | "CD": false, 438 | "Question": [ 439 | { 440 | "name": "example.com.", 441 | "type": 28 442 | } 443 | ], 444 | "Answer": [ 445 | { 446 | "name": "example.com.", 447 | "type": 28, 448 | "TTL": 1726, 449 | "data": "2606:2800:220:1:248:1893:25c8:1946" 450 | } 451 | ] 452 | }"#]; 453 | 454 | for test in tests { 455 | // Strip comments in the test, as a easy way to keep this test data annotated. 456 | let mut stripped = String::new(); 457 | StripComments::new(test.as_bytes()) 458 | .read_to_string(&mut stripped) 459 | .unwrap(); 460 | 461 | let m: MessageJson = match serde_json::from_str(&stripped) { 462 | Ok(m) => m, 463 | Err(err) => panic!("failed to parse JSON: {}\n{}", err, stripped), 464 | }; 465 | let _m: Message = m.try_into().expect("failed to turn MessageJson into a Message"); 466 | // TODO Check this is what we expect 467 | } 468 | } 469 | } -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use crate::resource::*; 2 | use std::net::SocketAddr; 3 | use std::time::Duration; 4 | use std::time::SystemTime; 5 | use strum_macros::{Display, EnumString}; 6 | 7 | /// DNS Message that serves as the root of all DNS requests and responses. 8 | /// 9 | /// # Examples 10 | /// 11 | /// For constructing a message and encoding: 12 | /// 13 | /// ```rust 14 | /// use rustdns::Message; 15 | /// use rustdns::types::*; 16 | /// use std::net::UdpSocket; 17 | /// use std::time::Duration; 18 | /// 19 | /// // Setup some UDP socket for sending to a DNS server. 20 | /// let socket = UdpSocket::bind("0.0.0.0:0").expect("couldn't bind to address"); 21 | /// socket.set_read_timeout(Some(Duration::new(5, 0))).expect("set_read_timeout call failed"); 22 | /// socket.connect("8.8.8.8:53").expect("connect call failed"); 23 | /// 24 | /// // Construct a simple query. 25 | /// let mut m = Message::default(); 26 | /// m.add_question("bramp.net", Type::A, Class::Internet); 27 | /// 28 | /// // Encode the query as a Vec. 29 | /// let req = m.to_vec().expect("failed to encode DNS request"); 30 | /// 31 | /// // Send to the server 32 | /// socket.send(&req).expect("failed to send request"); 33 | /// 34 | /// // Some time passes 35 | /// 36 | /// // Receive a response from the DNS server 37 | /// let mut resp = [0; 4096]; 38 | /// let len = socket.recv(&mut resp).expect("failed to receive response"); 39 | /// 40 | /// // Take a Vec and turn it into a message. 41 | /// let m = Message::from_slice(&resp[0..len]).expect("invalid response"); 42 | /// 43 | /// // Now do something with `m`, in this case print it! 44 | /// println!("DNS Response:\n{}", m); 45 | /// ``` 46 | #[derive(Clone, Debug, Derivative)] 47 | #[derivative(Eq, Hash, PartialEq)] 48 | #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] 49 | pub struct Message { 50 | /// 16-bit identifier assigned by the program that generates any kind of 51 | /// query. This identifier is copied into the corresponding reply and can be 52 | /// used by the requester to match up replies to outstanding queries. 53 | pub id: u16, 54 | 55 | /// Recursion Desired - this bit directs the name server to pursue the query 56 | /// recursively. 57 | pub rd: bool, 58 | 59 | /// Truncation - specifies that this message was truncated. 60 | pub tc: bool, 61 | 62 | /// Authoritative Answer - Specifies that the responding name server is an 63 | /// authority for the domain name in question section. 64 | pub aa: bool, 65 | 66 | /// Specifies kind of query in this message. 0 represents a standard query. 67 | /// See 68 | pub opcode: Opcode, 69 | 70 | /// Specifies whether this message is a query (0), or a response (1). 71 | pub qr: QR, 72 | 73 | /// Response code. 74 | pub rcode: Rcode, 75 | 76 | /// Checking Disabled. See [RFC4035] and [RFC6840]. 77 | /// 78 | /// [rfc4035]: https://datatracker.ietf.org/doc/html/rfc4035 79 | /// [rfc6840]: https://datatracker.ietf.org/doc/html/rfc6840 80 | pub cd: bool, 81 | 82 | /// Authentic Data. See [RFC4035] and [RFC6840]. 83 | /// 84 | /// [rfc4035]: https://datatracker.ietf.org/doc/html/rfc4035 85 | /// [rfc6840]: https://datatracker.ietf.org/doc/html/rfc6840 86 | pub ad: bool, 87 | 88 | /// Z Reserved for future use. You must set this field to 0. 89 | pub z: bool, 90 | 91 | /// Recursion Available - this be is set or cleared in a response, and 92 | /// denotes whether recursive query support is available in the name server. 93 | pub ra: bool, 94 | 95 | /// The questions. 96 | pub questions: Vec, 97 | 98 | /// The answer records. 99 | pub answers: Vec, 100 | 101 | /// The authoritive records. 102 | pub authoritys: Vec, 103 | 104 | /// The additional records. 105 | pub additionals: Vec, 106 | 107 | /// Optional EDNS(0) record. 108 | pub extension: Option, 109 | 110 | /// Optional stats about this request, populated by the DNS client. 111 | /// TODO Maybe this field should be elsewhere, as it's metadata about a request 112 | #[derivative(PartialEq = "ignore")] 113 | #[derivative(Hash = "ignore")] 114 | pub stats: Option, 115 | } 116 | 117 | /// Question struct containing a domain name, question [`Type`] and question [`Class`]. 118 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 119 | #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] 120 | pub struct Question { 121 | /// The domain name in question. Must be a valid UTF-8 encoded domain name. 122 | pub name: String, 123 | 124 | /// The question's type. 125 | /// 126 | /// All Type's are valid, including the pseudo types (e.g [`Type::ANY`]). 127 | pub r#type: Type, 128 | 129 | /// The question's class. 130 | pub class: Class, 131 | } 132 | 133 | /// Resource Record (RR) returned by DNS servers containing a answer to the question. 134 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 135 | #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] 136 | pub struct Record { 137 | /// A valid UTF-8 encoded domain name. 138 | pub name: String, 139 | 140 | /// The resource's class. 141 | pub class: Class, 142 | 143 | /// The number of seconds that the resource record may be cached 144 | /// before the source of the information should again be consulted. 145 | /// Zero is interpreted to mean that the RR can only be used for the 146 | /// transaction in progress. 147 | pub ttl: Duration, 148 | 149 | /// The actual resource. 150 | pub resource: Resource, 151 | } 152 | 153 | impl Record { 154 | pub fn new(name: &str, class: Class, ttl: Duration, resource: Resource) -> Self { 155 | Self { 156 | name: name.to_owned(), 157 | class, 158 | ttl, 159 | resource, 160 | } 161 | } 162 | 163 | pub fn r#type(&self) -> Type { 164 | self.resource.r#type() 165 | } 166 | } 167 | 168 | /// EDNS(0) extension record as defined in [rfc2671] and [rfc6891]. 169 | /// 170 | /// [rfc2671]: https://datatracker.ietf.org/doc/html/rfc2671 171 | /// [rfc6891]: https://datatracker.ietf.org/doc/html/rfc6891 172 | // 173 | // TODO Support EDNS0_NSID (RFC 5001) and EDNS0_SUBNET (RFC 7871) records within the extension. 174 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 175 | #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] 176 | pub struct Extension { 177 | /// Requestor's UDP payload size. 178 | pub payload_size: u16, 179 | 180 | /// Extended RCode. 181 | pub extend_rcode: u8, 182 | 183 | /// Version of the extension. 184 | pub version: u8, 185 | 186 | /// DNSSEC OK bit as defined by [rfc3225]. 187 | /// 188 | /// [rfc3225]: https://datatracker.ietf.org/doc/html/rfc3225 189 | pub dnssec_ok: bool, 190 | } 191 | 192 | impl Default for Extension { 193 | fn default() -> Self { 194 | Extension { 195 | payload_size: 4096, 196 | extend_rcode: 0, 197 | version: 0, 198 | dnssec_ok: false, 199 | } 200 | } 201 | } 202 | 203 | /// Stats related to the specific query, optionally filed in by the client 204 | /// and does not change the query behaviour. 205 | #[derive(Clone, Debug, PartialEq)] 206 | #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] 207 | pub struct Stats { 208 | /// The time the query was sent to the server. 209 | pub start: SystemTime, 210 | 211 | /// The duration of the request. 212 | pub duration: Duration, 213 | 214 | /// The server used to service this query. 215 | pub server: SocketAddr, 216 | 217 | // TODO Add another field for the requested server, vs the SocketAddr we actually used. 218 | /// The size of the request sent to the server. 219 | // TODO Should this include other overheads? 220 | pub request_size: usize, 221 | 222 | /// The size of the response from the server. 223 | pub response_size: usize, 224 | } 225 | 226 | /// Query or Response bit. 227 | #[derive(Copy, Clone, Debug, EnumString, Eq, Hash, PartialEq)] 228 | pub enum QR { 229 | Query = 0, 230 | Response = 1, 231 | } 232 | 233 | /// Defaults to [`QR::Query`]. 234 | impl Default for QR { 235 | fn default() -> Self { 236 | QR::Query 237 | } 238 | } 239 | 240 | impl QR { 241 | pub fn from_bool(b: bool) -> QR { 242 | match b { 243 | false => QR::Query, 244 | true => QR::Response, 245 | } 246 | } 247 | 248 | pub fn to_bool(self) -> bool { 249 | match self { 250 | QR::Query => false, 251 | QR::Response => true, 252 | } 253 | } 254 | } 255 | 256 | /// Specifies kind of query in this message. See [rfc1035], [rfc6895] and [DNS Parameters]. 257 | /// 258 | /// [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035 259 | /// [rfc6895]: https://datatracker.ietf.org/doc/html/rfc6895 260 | /// [DNS Parameters]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-5 261 | #[derive(Copy, Clone, Debug, Display, EnumString, Eq, Hash, FromPrimitive, PartialEq)] 262 | #[allow(clippy::upper_case_acronyms)] 263 | #[repr(u8)] // Really only 4 bits 264 | pub enum Opcode { 265 | /// Query. 266 | Query = 0, 267 | 268 | /// Inverse Query (OBSOLETE). See [rfc3425]. 269 | /// 270 | /// [rfc3425]: https://datatracker.ietf.org/doc/html/rfc3425 271 | IQuery = 1, 272 | Status = 2, 273 | 274 | /// See [rfc1996] 275 | /// 276 | /// [rfc1996]: https://datatracker.ietf.org/doc/html/rfc1996 277 | Notify = 4, 278 | 279 | /// See [rfc2136] 280 | /// 281 | /// [rfc2136]: https://datatracker.ietf.org/doc/html/rfc2136 282 | Update = 5, 283 | 284 | /// DNS Stateful Operations (DSO). See [rfc8490] 285 | /// 286 | /// [rfc8490]: https://datatracker.ietf.org/doc/html/rfc8490 287 | DSO = 6, 288 | // 3 and 7-15 Remain unassigned. 289 | } 290 | 291 | /// Defaults to [`Opcode::Query`]. 292 | impl Default for Opcode { 293 | fn default() -> Self { 294 | Opcode::Query 295 | } 296 | } 297 | 298 | /// Response Codes. 299 | /// See [rfc1035] and [DNS Parameters]. 300 | /// 301 | /// [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035 302 | /// [DNS Parameters]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 303 | #[derive(Copy, Clone, Debug, Display, EnumString, Eq, Hash, FromPrimitive, PartialEq)] 304 | #[allow(clippy::upper_case_acronyms)] 305 | #[repr(u16)] // In headers it is 4 bits, in extended OPTS it is 16. 306 | pub enum Rcode { 307 | /// No Error 308 | NoError = 0, 309 | 310 | /// Format Error 311 | FormErr = 1, 312 | 313 | /// Server Failure 314 | ServFail = 2, 315 | 316 | /// Non-Existent Domain 317 | NXDomain = 3, 318 | 319 | /// Not Implemented 320 | NotImp = 4, 321 | 322 | /// Query Refused 323 | Refused = 5, 324 | 325 | /// Name Exists when it should not. See [rfc2136] and [rfc6672]. 326 | /// 327 | /// [rfc2136]: https://datatracker.ietf.org/doc/html/rfc2136 328 | /// [rfc6672]: https://datatracker.ietf.org/doc/html/rfc6672 329 | YXDomain = 6, 330 | 331 | /// RR Set Exists when it should not. See [rfc2136]. 332 | /// 333 | /// [rfc2136]: https://datatracker.ietf.org/doc/html/rfc2136 334 | YXRRSet = 7, 335 | 336 | /// RR Set that should exist does not. See [rfc2136]. 337 | /// 338 | /// [rfc2136]: https://datatracker.ietf.org/doc/html/rfc2136 339 | NXRRSet = 8, 340 | 341 | /// Note on error number 9 (NotAuth): This error number means either 342 | /// "Not Authoritative" [rfc2136] or "Not Authorized" [rfc2845]. 343 | /// If 9 appears as the RCODE in the header of a DNS response without a 344 | /// TSIG RR or with a TSIG RR having a zero error field, then it means 345 | /// "Not Authoritative". If 9 appears as the RCODE in the header of a 346 | /// DNS response that includes a TSIG RR with a non-zero error field, 347 | /// then it means "Not Authorized". 348 | /// 349 | /// [rfc2136]: https://datatracker.ietf.org/doc/html/rfc2136 350 | /// [rfc2845]: https://datatracker.ietf.org/doc/html/rfc2845 351 | NotAuth = 9, 352 | 353 | /// Name not contained in zone. See [rfc2136]. 354 | /// 355 | /// [rfc2136]: https://datatracker.ietf.org/doc/html/rfc2136 356 | NotZone = 10, 357 | 358 | /// DSO-TYPE Not Implemented. See [rfc8490]. 359 | /// 360 | /// [rfc8490]: https://datatracker.ietf.org/doc/html/rfc8490 361 | DSOTYPENI = 11, 362 | // 12-15 Unassigned 363 | } 364 | 365 | /// Defaults to [`Rcode::NoError`]. 366 | impl Default for Rcode { 367 | fn default() -> Self { 368 | Rcode::NoError 369 | } 370 | } 371 | /* 372 | pub enum ExtendedRcode { 373 | Rcode, 374 | BADVERS_or_BADSIG = 16 // Bad OPT Version [RFC6891] or TSIG Signature Failure [RFC8945] 375 | BADKEY = 17 // Key not recognized [RFC8945] 376 | BADTIME = 18 // Signature out of time window [RFC8945] 377 | BADMODE = 19 // Bad TKEY Mode [RFC2930] 378 | BADNAME = 20 // Duplicate key name [RFC2930] 379 | BADALG = 21 // Algorithm not supported [RFC2930] 380 | BADTRUNC = 22 // Bad Truncation [RFC8945] 381 | BADCOOKIE = 23 // Bad/missing Server Cookie [RFC7873] 382 | // 24-3840 Unassigned 383 | // 3841-4095 Reserved for Private Use [RFC6895] 384 | // 4096-65534 Unassigned 385 | // 65535 = Reserved Can be allocated by Standards Action [RFC6895] 386 | } 387 | */ 388 | 389 | /// Resource Record Type, for example, A, CNAME or SOA. 390 | /// 391 | #[derive(Copy, Clone, Debug, Display, EnumString, Eq, FromPrimitive, Hash, PartialEq)] 392 | #[allow(clippy::upper_case_acronyms)] 393 | #[repr(u16)] 394 | pub enum Type { 395 | Reserved = 0, 396 | 397 | /// (Default) IPv4 Address. 398 | A = 1, 399 | NS = 2, 400 | CNAME = 5, 401 | SOA = 6, 402 | 403 | /// Domain name pointer. See [`util::reverse()`] to create a valid domain name from a IP address. 404 | /// 405 | /// [`util::reverse()`]: crate::util::reverse() 406 | PTR = 12, 407 | 408 | /// Mail exchange. 409 | MX = 15, 410 | 411 | /// Text strings. 412 | TXT = 16, 413 | 414 | /// IPv6 Address. 415 | AAAA = 28, 416 | 417 | /// Server Selection 418 | SRV = 33, 419 | 420 | /// EDNS(0) Opt type. See [rfc3225] and [rfc6891]. 421 | /// 422 | /// [rfc3225]: https://datatracker.ietf.org/doc/html/rfc3225 423 | /// [rfc6891]: https://datatracker.ietf.org/doc/html/rfc6891 424 | OPT = 41, 425 | 426 | /// Sender Policy Framework. See [rfc4408] 427 | /// Discontinued in [rfc7208] due to widespread lack of support. 428 | /// 429 | /// [rfc4408]: https://datatracker.ietf.org/doc/html/rfc4408 430 | /// [rfc7208]: https://datatracker.ietf.org/doc/html/rfc7208 431 | SPF = 99, 432 | 433 | /// Any record type. 434 | /// Only valid as a Question Type. 435 | ANY = 255, 436 | } 437 | 438 | /// Defaults to [`Type::ANY`]. 439 | impl Default for Type { 440 | fn default() -> Self { 441 | Type::ANY 442 | } 443 | } 444 | 445 | /// Resource Record Class, for example Internet. 446 | #[derive(Copy, Clone, Debug, Display, EnumString, Eq, FromPrimitive, Hash, PartialEq)] 447 | #[repr(u16)] 448 | pub enum Class { 449 | /// Reserved per [RFC6895] 450 | /// 451 | /// [rfc6895]: https://datatracker.ietf.org/doc/html/rfc6895 452 | Reserved = 0, 453 | 454 | /// (Default) The Internet (IN), see [rfc1035]. 455 | /// 456 | /// [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035 457 | #[strum(serialize = "IN")] 458 | Internet = 1, 459 | 460 | /// CSNET (CS), obsolete (used only for examples in some obsolete RFCs). 461 | #[strum(serialize = "CS")] 462 | CsNet = 2, 463 | 464 | /// Chaosnet (CH), obsolete LAN protocol created at MIT in the mid-1970s. See [D. Moon, "Chaosnet", A.I. Memo 628, Massachusetts Institute of Technology Artificial Intelligence Laboratory, June 1981.] 465 | #[strum(serialize = "CH")] 466 | Chaos = 3, 467 | 468 | /// Hesiod (HS), an information service developed by MIT’s Project Athena. See [Dyer, S., and F. Hsu, "Hesiod", Project Athena Technical Plan - Name Service, April 1987.] 469 | #[strum(serialize = "HS")] 470 | Hesiod = 4, 471 | 472 | /// No class specified, see [rfc2136] 473 | /// 474 | /// [rfc2136]: https://datatracker.ietf.org/doc/html/rfc2136 475 | None = 254, 476 | 477 | /// * (ANY) See [rfc1035] 478 | /// 479 | /// [rfc1035]: https://datatracker.ietf.org/doc/html/rfc1035 480 | #[strum(serialize = "*")] 481 | Any = 255, 482 | // 5-253 Unassigned 483 | // 256-65279 Unassigned 484 | // 65280-65534 Reserved for Private Use [RFC6895] 485 | // 65535 Reserved [RFC6895] 486 | } 487 | 488 | /// Defaults to [`Class::Internet`]. 489 | impl Default for Class { 490 | fn default() -> Self { 491 | Class::Internet 492 | } 493 | } 494 | 495 | /// Recource Record Definitions. 496 | #[allow(clippy::upper_case_acronyms)] 497 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 498 | pub enum Resource { 499 | A(A), // Support non-Internet classes? 500 | AAAA(AAAA), 501 | 502 | CNAME(CNAME), 503 | NS(NS), 504 | PTR(PTR), 505 | 506 | // TODO Implement RFC 1464 for further parsing of the text 507 | // TODO per RFC 4408 a TXT record is allowed to contain multiple strings 508 | TXT(TXT), 509 | SPF(TXT), 510 | 511 | MX(MX), 512 | SOA(SOA), 513 | SRV(SRV), 514 | 515 | OPT, 516 | 517 | ANY, // Not a valid Record Type, but is a Type 518 | } 519 | 520 | impl Resource { 521 | pub fn r#type(&self) -> Type { 522 | // This should be kept in sync with Type. 523 | // TODO Determine if I can generate this with a macro. 524 | match self { 525 | Resource::A(_) => Type::A, 526 | Resource::AAAA(_) => Type::AAAA, 527 | Resource::CNAME(_) => Type::CNAME, 528 | Resource::NS(_) => Type::NS, 529 | Resource::PTR(_) => Type::PTR, 530 | Resource::TXT(_) => Type::TXT, 531 | Resource::MX(_) => Type::MX, 532 | Resource::SOA(_) => Type::SOA, 533 | Resource::SRV(_) => Type::SRV, 534 | Resource::SPF(_) => Type::SPF, 535 | Resource::OPT => Type::OPT, 536 | Resource::ANY => Type::ANY, 537 | } 538 | } 539 | } 540 | --------------------------------------------------------------------------------