diff --git a/Cargo.lock b/Cargo.lock index a67da8415c..6bc62e692c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -181,6 +190,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base16" version = "0.2.1" @@ -789,6 +813,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_arbitrary" version = "1.3.2" @@ -859,10 +892,27 @@ checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc" dependencies = [ "cfg-if", "libc", - "socket2", + "socket2 0.5.3", "windows-sys 0.48.0", ] +[[package]] +name = "domain" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e853e3f6d4c6e52a4d73a94c1810c66ad71958fbe24934a7119b447f425aed76" +dependencies = [ + "bytes", + "futures-util", + "libc", + "octseq", + "rand", + "serde", + "smallvec", + "time", + "tokio", +] + [[package]] name = "dyn-clone" version = "1.0.17" @@ -980,6 +1030,43 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1004,6 +1091,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "glob" version = "0.3.1" @@ -1422,6 +1515,17 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mlua" version = "0.9.7" @@ -1503,6 +1607,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.45" @@ -1563,6 +1673,26 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "octseq" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92b38a4aabbacf619b8083841713216e7668178422decfe06bbc70643024c5d" +dependencies = [ + "bytes", + "serde", + "smallvec", +] + [[package]] name = "ofb" version = "0.6.1" @@ -1794,6 +1924,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.27" @@ -1839,6 +1975,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2286,6 +2428,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2524,6 +2672,15 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -2557,6 +2714,16 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.3" @@ -2734,6 +2901,25 @@ dependencies = [ "tikv-jemalloc-sys", ] +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -2768,6 +2954,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.4.10", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + [[package]] name = "toml" version = "0.8.12" @@ -3045,6 +3259,7 @@ dependencies = [ "data-encoding", "digest", "dns-lookup", + "domain", "dyn-clone", "exitcode", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 09e74e2576..32f977799c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ stdlib = [ "dep:ctr", "dep:data-encoding", "dep:digest", + "dep:domain", "dep:dns-lookup", "dep:flate2", "dep:grok", @@ -198,6 +199,7 @@ prost-reflect = { version = "0.13", default-features = false, optional = true} # Dependencies used for non-WASM [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dns-lookup = { version = "2", optional = true } +domain = { version = "0.9.3", optional = true, features = ["resolv-sync", "serde"] } hostname = { version = "0.4", optional = true } grok = { version = "2", optional = true } onig = { version = "6", default-features = false, optional = true } diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index c422c77aac..e93a3a87cc 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -1,4 +1,5 @@ Component,Origin,License,Copyright +addr2line,https://github.com/gimli-rs/addr2line,Apache-2.0 OR MIT,The addr2line Authors adler,https://github.com/jonas-schievink/adler,0BSD OR MIT OR Apache-2.0,Jonas Schievink aead,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers aes,https://github.com/RustCrypto/block-ciphers,MIT OR Apache-2.0,RustCrypto Developers @@ -15,6 +16,7 @@ anstyle-wincon,https://github.com/rust-cli/anstyle,MIT OR Apache-2.0,The anstyle anyhow,https://github.com/dtolnay/anyhow,MIT OR Apache-2.0,David Tolnay arrayvec,https://github.com/bluss/arrayvec,MIT OR Apache-2.0,bluss atomic,https://github.com/Amanieu/atomic-rs,Apache-2.0 OR MIT,Amanieu d'Antras +backtrace,https://github.com/rust-lang/backtrace-rs,MIT OR Apache-2.0,The Rust Project Developers base16,https://github.com/thomcc/rust-base16,CC0-1.0,Thom Chiovoloni base62,https://github.com/fbernier/base62,MIT,"François Bernier , Chai T. Rex , Kevin Darlington , Christopher Tarquini " base64,https://github.com/marshallpierce/rust-base64,MIT OR Apache-2.0,"Alice Maz , Marshall Pierce " @@ -57,11 +59,13 @@ crypto_secretbox,https://github.com/RustCrypto/nacl-compat/tree/master/crypto_se csv,https://github.com/BurntSushi/rust-csv,Unlicense OR MIT,Andrew Gallant ctr,https://github.com/RustCrypto/block-modes,MIT OR Apache-2.0,RustCrypto Developers data-encoding,https://github.com/ia0/data-encoding,MIT,Julien Cretin +deranged,https://github.com/jhpratt/deranged,MIT OR Apache-2.0,Jacob Pratt derive_more,https://github.com/JelteF/derive_more,MIT,Jelte Fennema digest,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers dirs-next,https://github.com/xdg-rs/dirs,MIT OR Apache-2.0,The @xdg-rs members dirs-sys-next,https://github.com/xdg-rs/dirs/tree/master/dirs-sys,MIT OR Apache-2.0,The @xdg-rs members dns-lookup,https://github.com/keeperofdakeys/dns-lookup,MIT OR Apache-2.0,Josh Driver +domain,https://github.com/nlnetlabs/domain,BSD-3-Clause,NLnet Labs dyn-clone,https://github.com/dtolnay/dyn-clone,MIT OR Apache-2.0,David Tolnay either,https://github.com/bluss/either,MIT OR Apache-2.0,bluss encode_unicode,https://github.com/tormol/encode_unicode,Apache-2.0 OR MIT,Torbjørn Birch Moltu @@ -72,8 +76,13 @@ error-code,https://github.com/DoumanAsh/error-code,BSL-1.0,Douman flate2,https://github.com/rust-lang/flate2-rs,MIT OR Apache-2.0,"Alex Crichton , Josh Triplett " funty,https://github.com/myrrlyn/funty,MIT,myrrlyn +futures-core,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-core Authors +futures-macro,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-macro Authors +futures-task,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-task Authors +futures-util,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-util Authors generic-array,https://github.com/fizyk20/generic-array,MIT,"Bartłomiej Kamiński , Aaron Trent " getrandom,https://github.com/rust-random/getrandom,MIT OR Apache-2.0,The Rand Project Developers +gimli,https://github.com/gimli-rs/gimli,MIT OR Apache-2.0,The gimli Authors grok,https://github.com/daschl/grok,Apache-2.0,Michael Nitschinger hashbrown,https://github.com/rust-lang/hashbrown,MIT OR Apache-2.0,Amanieu d'Antras heck,https://github.com/withoutboats/heck,MIT OR Apache-2.0,The heck Authors @@ -107,14 +116,18 @@ md-5,https://github.com/RustCrypto/hashes,MIT OR Apache-2.0,RustCrypto Developer memchr,https://github.com/BurntSushi/memchr,Unlicense OR MIT,"Andrew Gallant , bluss" minimal-lexical,https://github.com/Alexhuszagh/minimal-lexical,MIT OR Apache-2.0,Alex Huszagh miniz_oxide,https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide,MIT OR Zlib OR Apache-2.0,"Frommi , oyvindln " +mio,https://github.com/tokio-rs/mio,MIT,"Carl Lerche , Thomas de Zeeuw , Tokio Contributors " ndk-context,https://github.com/rust-windowing/android-ndk-rs,MIT OR Apache-2.0,The Rust Windowing contributors nix,https://github.com/nix-rust/nix,MIT,The nix-rust Project Developers nom,https://github.com/Geal/nom,MIT,contact@geoffroycouprie.com num-bigint,https://github.com/rust-num/num-bigint,MIT OR Apache-2.0,The Rust Project Developers +num-conv,https://github.com/jhpratt/num-conv,MIT OR Apache-2.0,Jacob Pratt num-integer,https://github.com/rust-num/num-integer,MIT OR Apache-2.0,The Rust Project Developers num-traits,https://github.com/rust-num/num-traits,MIT OR Apache-2.0,The Rust Project Developers num_enum,https://github.com/illicitonion/num_enum,BSD-3-Clause OR MIT OR Apache-2.0,"Daniel Wagner-Hall , Daniel Henry-Mantilla , Vincent Esche " objc,http://github.com/SSheldon/rust-objc,MIT,Steven Sheldon +object,https://github.com/gimli-rs/object,Apache-2.0 OR MIT,The object Authors +octseq,https://github.com/NLnetLabs/octets/,BSD-3-Clause,NLnet Labs ofb,https://github.com/RustCrypto/block-modes,MIT OR Apache-2.0,RustCrypto Developers once_cell,https://github.com/matklad/once_cell,MIT OR Apache-2.0,Aleksey Kladov onig,http://github.com/iwillspeak/rust-onig,MIT,"Will Speak , Ivan Ivashchenko " @@ -126,7 +139,9 @@ peeking_take_while,https://github.com/fitzgen/peeking_take_while,MIT OR Apache-2 pest,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice phf,https://github.com/rust-phf/rust-phf,MIT,Steven Fackler pin-project-lite,https://github.com/taiki-e/pin-project-lite,Apache-2.0 OR MIT,The pin-project-lite Authors +pin-utils,https://github.com/rust-lang-nursery/pin-utils,MIT OR Apache-2.0,Josef Brandl poly1305,https://github.com/RustCrypto/universal-hashes,Apache-2.0 OR MIT,RustCrypto Developers +powerfmt,https://github.com/jhpratt/powerfmt,MIT OR Apache-2.0,Jacob Pratt ppv-lite86,https://github.com/cryptocorrosion/cryptocorrosion,MIT OR Apache-2.0,The CryptoCorrosion Contributors prettydiff,https://github.com/romankoblov/prettydiff,MIT,Roman Koblov prettytable-rs,https://github.com/phsym/prettytable-rs,BSD-3-Clause,Pierre-Henri Symoneaux @@ -154,6 +169,7 @@ rend,https://github.com/djkoloski/rend,MIT,David Koloski rkyv,https://github.com/rkyv/rkyv,MIT,David Koloski roxmltree,https://github.com/RazrFalcon/roxmltree,MIT OR Apache-2.0,Yevhenii Reizner rust_decimal,https://github.com/paupino/rust-decimal,MIT,Paul Mason +rustc-demangle,https://github.com/alexcrichton/rustc-demangle,MIT OR Apache-2.0,Alex Crichton rustix,https://github.com/bytecodealliance/rustix,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,"Dan Gohman , Jakub Konka " rustversion,https://github.com/dtolnay/rustversion,MIT OR Apache-2.0,David Tolnay rustyline,https://github.com/kkawakam/rustyline,MIT,Katsu Kawakami @@ -169,6 +185,8 @@ sha2,https://github.com/RustCrypto/hashes,MIT OR Apache-2.0,RustCrypto Developer sha3,https://github.com/RustCrypto/hashes,MIT OR Apache-2.0,RustCrypto Developers simdutf8,https://github.com/rusticstuff/simdutf8,MIT OR Apache-2.0,Hans Kratz siphasher,https://github.com/jedisct1/rust-siphash,MIT OR Apache-2.0,Frank Denis +slab,https://github.com/tokio-rs/slab,MIT,Carl Lerche +smallvec,https://github.com/servo/rust-smallvec,MIT OR Apache-2.0,The Servo Project Developers snafu,https://github.com/shepmaster/snafu,MIT OR Apache-2.0,Jake Goulding snap,https://github.com/BurntSushi/rust-snappy,BSD-3-Clause,Andrew Gallant socket2,https://github.com/rust-lang/socket2,MIT OR Apache-2.0,"Alex Crichton , Thomas de Zeeuw " @@ -182,8 +200,10 @@ tap,https://github.com/myrrlyn/tap,MIT,"Elliott Linder thiserror,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay +time,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" tinyvec,https://github.com/Lokathor/tinyvec,Zlib OR Apache-2.0 OR MIT,Lokathor tinyvec_macros,https://github.com/Soveu/tinyvec_macros,MIT OR Apache-2.0 OR Zlib,Soveu +tokio,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors toml_datetime,https://github.com/toml-rs/toml,MIT OR Apache-2.0,Alex Crichton toml_edit,https://github.com/toml-rs/toml,MIT OR Apache-2.0,"Andronik Ordian , Ed Page " tracing,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , Tokio Contributors " diff --git a/benches/stdlib.rs b/benches/stdlib.rs index 40a0da3f6c..d0ce8137c3 100644 --- a/benches/stdlib.rs +++ b/benches/stdlib.rs @@ -27,6 +27,7 @@ criterion_group!( decode_percent, decode_punycode, decrypt, + dns_lookup, // TODO: Cannot pass a Path to bench_function //del, downcase, @@ -2297,6 +2298,15 @@ bench_function! { } } +bench_function! { + dns_lookup => vrl::stdlib::DnsLookup; + + localhost { + args: func_args![value: value!("8.8.8.8")], + want: Ok(value!("dns.google")), + } +} + bench_function! { reverse_dns => vrl::stdlib::ReverseDns; diff --git a/changelog.d/764.feature.md b/changelog.d/764.feature.md new file mode 100644 index 0000000000..44ad7ef0ae --- /dev/null +++ b/changelog.d/764.feature.md @@ -0,0 +1,4 @@ +Added experimental `dns_lookup` function. It should be used with caution, since it involves network +calls and is therefore very slow. + +authors: esensar diff --git a/src/stdlib/dns_lookup.rs b/src/stdlib/dns_lookup.rs new file mode 100644 index 0000000000..fb3eda8240 --- /dev/null +++ b/src/stdlib/dns_lookup.rs @@ -0,0 +1,769 @@ +use crate::compiler::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +mod non_wasm { + use std::collections::BTreeMap; + use std::net::ToSocketAddrs; + use std::time::Duration; + + use domain::base::iana::Class; + use domain::base::{Dname, RecordSection, Rtype}; + use domain::rdata::AllRecordData; + use domain::resolv::stub::conf::{ResolvConf, ResolvOptions, ServerConf, Transport}; + use domain::resolv::stub::Answer; + use domain::resolv::StubResolver; + + use crate::compiler::prelude::*; + use crate::value::Value; + + fn dns_lookup(value: Value, qtype: Value, qclass: Value, options: Value) -> Resolved { + let host: Dname> = value + .try_bytes_utf8_lossy()? + .to_string() + .parse() + .map_err(|err| format!("parsing host name failed: {err}"))?; + let qtype: Rtype = qtype + .try_bytes_utf8_lossy()? + .to_string() + .parse() + .map_err(|err| format!("parsing query type failed: {err}"))?; + let qclass: Class = qclass + .try_bytes_utf8_lossy()? + .to_string() + .parse() + .map_err(|err| format!("parsing query class failed: {err}"))?; + + let conf = build_options(options.try_object()?)?; + let answer = StubResolver::run_with_conf(conf, move |stub| async move { + stub.query((host, qtype, qclass)).await + }) + .map_err(|err| format!("query failed: {err}"))?; + + Ok(parse_answer(answer)?.into()) + } + + #[derive(Debug, Clone)] + pub(super) struct DnsLookupFn { + pub(super) value: Box, + pub(super) qtype: Box, + pub(super) class: Box, + pub(super) options: Box, + } + + impl Default for DnsLookupFn { + fn default() -> Self { + Self { + value: expr!(""), + qtype: expr!("A"), + class: expr!("IN"), + options: expr!({}), + } + } + } + + fn build_options(options: ObjectMap) -> Result { + let mut resolv_options = ResolvOptions::default(); + + macro_rules! read_bool_opt { + ($name:ident, $resolv_name:ident) => { + if let Some($name) = options + .get(stringify!($name)) + .map(|v| v.clone().try_boolean()) + .transpose()? + { + resolv_options.$resolv_name = $name; + } + }; + ($name:ident) => { + read_bool_opt!($name, $name); + }; + } + + macro_rules! read_int_opt { + ($name:ident, $resolv_name:ident) => { + if let Some($name) = options + .get(stringify!($name)) + .map(|v| v.clone().try_integer()) + .transpose()? + { + resolv_options.$resolv_name = $name.try_into().map_err(|err| { + format!( + "{} has to be a positive integer, got: {}. ({})", + stringify!($resolv_name), + $name, + err + ) + })?; + } + }; + ($name:ident) => { + read_int_opt!($name, $name); + }; + } + + read_int_opt!(ndots); + read_int_opt!(attempts); + read_bool_opt!(aa_only); + read_bool_opt!(tcp, use_vc); + read_bool_opt!(ignore, ign_tc); + read_bool_opt!(recurse); + read_bool_opt!(rotate); + + if let Some(timeout) = options + .get("timeout") + .map(|v| v.clone().try_integer()) + .transpose()? + { + resolv_options.timeout = Duration::from_secs(timeout.try_into().map_err(|err| { + format!("timeout has to be a positive integer, got: {timeout}. ({err})") + })?); + } + + let mut conf = ResolvConf { + options: resolv_options, + ..Default::default() + }; + + if let Some(servers) = options + .get("servers") + .map(|s| s.clone().try_array()) + .transpose()? + { + for server in servers { + let mut server = server.try_bytes_utf8_lossy()?; + if !server.contains(':') { + server += ":53"; + } + for addr in server + .to_socket_addrs() + .map_err(|err| format!("can't resolve nameserver ({server}): {err}"))? + { + conf.servers.push(ServerConf::new(addr, Transport::Udp)); + conf.servers.push(ServerConf::new(addr, Transport::Tcp)); + } + } + } + + conf.finalize(); + Ok(conf) + } + + fn parse_answer(answer: Answer) -> Result { + let mut result = ObjectMap::new(); + let header_section = answer.header(); + let rcode = header_section.rcode(); + result.insert("fullRcode".into(), rcode.to_int().into()); + result.insert("rcodeName".into(), rcode.to_string().into()); + let header = { + let mut header_obj = ObjectMap::new(); + let counts = answer.header_counts(); + header_obj.insert("aa".into(), header_section.aa().into()); + header_obj.insert("ad".into(), header_section.ad().into()); + header_obj.insert("cd".into(), header_section.cd().into()); + header_obj.insert("ra".into(), header_section.ra().into()); + header_obj.insert("rd".into(), header_section.rd().into()); + header_obj.insert("tc".into(), header_section.tc().into()); + header_obj.insert("qr".into(), header_section.qr().into()); + header_obj.insert("id".into(), header_section.id().into()); + header_obj.insert("opcode".into(), header_section.opcode().to_int().into()); + header_obj.insert("rcode".into(), header_section.rcode().to_int().into()); + header_obj.insert("anCount".into(), counts.ancount().into()); + header_obj.insert("arCount".into(), counts.arcount().into()); + header_obj.insert("nsCount".into(), counts.nscount().into()); + header_obj.insert("qdCount".into(), counts.qdcount().into()); + header_obj + }; + result.insert("header".into(), header.into()); + + let (question, answer_section, authority, additional) = answer + .sections() + .map_err(|err| format!("parsing response sections failed: {err}"))?; + + let question = { + let mut questions = Vec::::new(); + for q in question { + let q = q.map_err(|err| format!("parsing question section failed: {err}"))?; + let mut question_obj = ObjectMap::new(); + question_obj.insert("class".into(), q.qclass().to_string().into()); + question_obj.insert("domainName".into(), q.qname().to_string().into()); + let qtype = q.qtype(); + question_obj.insert("questionType".into(), qtype.to_string().into()); + question_obj.insert("questionTypeId".into(), qtype.to_int().into()); + questions.push(question_obj); + } + questions + }; + result.insert("question".into(), question.into()); + result.insert( + "answers".into(), + parse_record_section(answer_section)?.into(), + ); + result.insert("authority".into(), parse_record_section(authority)?.into()); + result.insert( + "additional".into(), + parse_record_section(additional)?.into(), + ); + + Ok(result) + } + + fn parse_record_section( + section: RecordSection<'_, Bytes>, + ) -> Result, ExpressionError> { + let mut records = Vec::::new(); + for r in section { + let r = r.map_err(|err| format!("parsing record section failed: {err}"))?; + let mut record_obj = ObjectMap::new(); + record_obj.insert("class".into(), r.class().to_string().into()); + record_obj.insert("domainName".into(), r.owner().to_string().into()); + let rtype = r.rtype(); + let record_data = r + .to_record::>() + .map_err(|err| format!("parsing rData failed: {err}"))? + .map(|r| r.data().to_string()); + record_obj.insert("rData".into(), record_data.into()); + record_obj.insert("recordType".into(), rtype.to_string().into()); + record_obj.insert("recordTypeId".into(), rtype.to_int().into()); + record_obj.insert("ttl".into(), r.ttl().as_secs().into()); + records.push(record_obj); + } + Ok(records) + } + + impl FunctionExpression for DnsLookupFn { + fn resolve(&self, ctx: &mut Context) -> Resolved { + let value = self.value.resolve(ctx)?; + let qtype = self.qtype.resolve(ctx)?; + let class = self.class.resolve(ctx)?; + let options = self.options.resolve(ctx)?; + dns_lookup(value, qtype, class, options) + } + + fn type_def(&self, _: &state::TypeState) -> TypeDef { + TypeDef::object(inner_kind()).fallible() + } + } + + fn header_kind() -> BTreeMap { + BTreeMap::from([ + (Field::from("aa"), Kind::boolean()), + (Field::from("ad"), Kind::boolean()), + (Field::from("anCount"), Kind::integer()), + (Field::from("arCount"), Kind::integer()), + (Field::from("cd"), Kind::boolean()), + (Field::from("id"), Kind::integer()), + (Field::from("nsCount"), Kind::integer()), + (Field::from("opcode"), Kind::integer()), + (Field::from("qdCount"), Kind::integer()), + (Field::from("qr"), Kind::integer()), + (Field::from("ra"), Kind::boolean()), + (Field::from("rcode"), Kind::integer()), + (Field::from("rd"), Kind::boolean()), + (Field::from("tc"), Kind::boolean()), + ]) + } + + fn rdata_kind() -> BTreeMap { + BTreeMap::from([ + (Field::from("class"), Kind::bytes()), + (Field::from("domainName"), Kind::bytes()), + (Field::from("rData"), Kind::bytes()), + (Field::from("recordType"), Kind::bytes()), + (Field::from("recordTypeId"), Kind::integer()), + (Field::from("ttl"), Kind::integer()), + ]) + } + + fn question_kind() -> BTreeMap { + BTreeMap::from([ + (Field::from("class"), Kind::bytes()), + (Field::from("domainName"), Kind::bytes()), + (Field::from("questionType"), Kind::bytes()), + (Field::from("questionTypeId"), Kind::integer()), + ]) + } + + pub(super) fn inner_kind() -> BTreeMap { + BTreeMap::from([ + (Field::from("fullRcode"), Kind::integer()), + (Field::from("rcodeName"), Kind::bytes() | Kind::null()), + (Field::from("time"), Kind::bytes() | Kind::null()), + (Field::from("timePrecision"), Kind::bytes() | Kind::null()), + ( + Field::from("answers"), + Kind::array(Collection::from_unknown(Kind::object(rdata_kind()))), + ), + ( + Field::from("authority"), + Kind::array(Collection::from_unknown(Kind::object(rdata_kind()))), + ), + ( + Field::from("additional"), + Kind::array(Collection::from_unknown(Kind::object(rdata_kind()))), + ), + (Field::from("header"), Kind::object(header_kind())), + ( + Field::from("question"), + Kind::array(Collection::from_unknown(Kind::object(question_kind()))), + ), + ]) + } +} + +#[allow(clippy::wildcard_imports)] +#[cfg(not(target_arch = "wasm32"))] +use non_wasm::*; + +#[derive(Clone, Copy, Debug)] +pub struct DnsLookup; + +impl Function for DnsLookup { + fn identifier(&self) -> &'static str { + "dns_lookup" + } + + fn parameters(&self) -> &'static [Parameter] { + &[ + Parameter { + keyword: "value", + kind: kind::BYTES, + required: true, + }, + Parameter { + keyword: "qtype", + kind: kind::BYTES, + required: false, + }, + Parameter { + keyword: "class", + kind: kind::BYTES, + required: false, + }, + Parameter { + keyword: "options", + kind: kind::OBJECT, + required: false, + }, + ] + } + + fn examples(&self) -> &'static [Example] { + &[ + Example { + title: "Basic lookup", + source: r#"dns_lookup!("localhost")"#, + result: Ok(indoc!( + r#"{ + "additional": [ + { + "class": "CLASS65494", + "domainName": "", + "rData": "OPT ...", + "recordType": "OPT", + "recordTypeId": 41, + "ttl": 0 + } + ], + "answers": [ + { + "class": "IN", + "domainName": "localhost", + "rData": "127.0.0.1", + "recordType": "A", + "recordTypeId": 1, + "ttl": 0 + } + ], + "authority": [], + "fullRcode": 0, + "header": { + "aa": true, + "ad": false, + "anCount": 1, + "arCount": 1, + "cd": false, + "id": 0, + "nsCount": 0, + "opcode": 0, + "qdCount": 1, + "qr": true, + "ra": true, + "rcode": 0, + "rd": true, + "tc": false + }, + "question": [ + { + "class": "IN", + "domainName": "localhost", + "questionType": "A", + "questionTypeId": 1 + } + ], + "rcodeName": "NOERROR" + }"# + )), + }, + Example { + title: "Custom class and qtype", + source: r#"dns_lookup!("localhost", class: "IN", qtype: "A")"#, + result: Ok(indoc!( + r#"{ + "additional": [ + { + "class": "CLASS65494", + "domainName": "", + "rData": "OPT ...", + "recordType": "OPT", + "recordTypeId": 41, + "ttl": 0 + } + ], + "answers": [ + { + "class": "IN", + "domainName": "localhost", + "rData": "127.0.0.1", + "recordType": "A", + "recordTypeId": 1, + "ttl": 0 + } + ], + "authority": [], + "fullRcode": 0, + "header": { + "aa": true, + "ad": false, + "anCount": 1, + "arCount": 1, + "cd": false, + "id": 0, + "nsCount": 0, + "opcode": 0, + "qdCount": 1, + "qr": true, + "ra": true, + "rcode": 0, + "rd": true, + "tc": false + }, + "question": [ + { + "class": "IN", + "domainName": "localhost", + "questionType": "A", + "questionTypeId": 1 + } + ], + "rcodeName": "NOERROR" + }"# + )), + }, + Example { + title: "Custom options", + source: r#"dns_lookup!("localhost", options: {"timeout": 30, "attempts": 5})"#, + result: Ok(indoc!( + r#"{ + "additional": [ + { + "class": "CLASS65494", + "domainName": "", + "rData": "OPT ...", + "recordType": "OPT", + "recordTypeId": 41, + "ttl": 0 + } + ], + "answers": [ + { + "class": "IN", + "domainName": "localhost", + "rData": "127.0.0.1", + "recordType": "A", + "recordTypeId": 1, + "ttl": 0 + } + ], + "authority": [], + "fullRcode": 0, + "header": { + "aa": true, + "ad": false, + "anCount": 1, + "arCount": 1, + "cd": false, + "id": 0, + "nsCount": 0, + "opcode": 0, + "qdCount": 1, + "qr": true, + "ra": true, + "rcode": 0, + "rd": true, + "tc": false + }, + "question": [ + { + "class": "IN", + "domainName": "localhost", + "questionType": "A", + "questionTypeId": 1 + } + ], + "rcodeName": "NOERROR" + }"# + )), + }, + Example { + title: "Custom server", + source: r#"dns_lookup!("localhost", options: {"servers": ["dns.google"]})"#, + result: Ok(indoc!( + r#"{ + "additional": [ + { + "class": "CLASS65494", + "domainName": "", + "rData": "OPT ...", + "recordType": "OPT", + "recordTypeId": 41, + "ttl": 0 + } + ], + "answers": [ + { + "class": "IN", + "domainName": "localhost", + "rData": "127.0.0.1", + "recordType": "A", + "recordTypeId": 1, + "ttl": 0 + } + ], + "authority": [], + "fullRcode": 0, + "header": { + "aa": true, + "ad": false, + "anCount": 1, + "arCount": 1, + "cd": false, + "id": 0, + "nsCount": 0, + "opcode": 0, + "qdCount": 1, + "qr": true, + "ra": true, + "rcode": 0, + "rd": true, + "tc": false + }, + "question": [ + { + "class": "IN", + "domainName": "localhost", + "questionType": "A", + "questionTypeId": 1 + } + ], + "rcodeName": "NOERROR" + }"# + )), + }, + ] + } + + #[cfg(not(target_arch = "wasm32"))] + fn compile( + &self, + _state: &state::TypeState, + _ctx: &mut FunctionCompileContext, + arguments: ArgumentList, + ) -> Compiled { + let value = arguments.required("value"); + let qtype = arguments.optional("qtype").unwrap_or_else(|| expr!("A")); + let class = arguments.optional("class").unwrap_or_else(|| expr!("IN")); + let options = arguments.optional("options").unwrap_or_else(|| expr!({})); + + Ok(DnsLookupFn { + value, + qtype, + class, + options, + } + .as_expr()) + } + + #[cfg(target_arch = "wasm32")] + fn compile( + &self, + _state: &state::TypeState, + ctx: &mut FunctionCompileContext, + _arguments: ArgumentList, + ) -> Compiled { + Ok(super::WasmUnsupportedFunction::new(ctx.span(), TypeDef::bytes().fallible()).as_expr()) + } +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use std::collections::{BTreeMap, HashSet}; + + use super::*; + use crate::value; + + #[test] + fn test_invalid_name() { + let result = execute_dns_lookup(DnsLookupFn { + value: expr!("wrong.local"), + ..Default::default() + }); + + assert_ne!(result["fullRcode"], value!(0)); + assert_ne!(result["rcodeName"], value!("NOERROR")); + assert_eq!( + result["question"].as_array_unwrap()[0], + value!({ + "questionTypeId": 1, + "questionType": "A", + "class": "IN", + "domainName": "wrong.local" + }) + ); + } + + #[test] + fn test_localhost() { + let result = execute_dns_lookup(DnsLookupFn { + value: expr!("localhost"), + ..Default::default() + }); + + assert_eq!(result["fullRcode"], value!(0)); + assert_eq!(result["rcodeName"], value!("NOERROR")); + assert_eq!( + result["question"].as_array_unwrap()[0], + value!({ + "questionTypeId": 1, + "questionType": "A", + "class": "IN", + "domainName": "localhost" + }) + ); + let answer = result["answers"].as_array_unwrap()[0].as_object().unwrap(); + assert_eq!(answer["rData"], value!("127.0.0.1")); + } + + #[test] + fn test_custom_class_and_type() { + let result = execute_dns_lookup(DnsLookupFn { + value: expr!("localhost"), + class: expr!("*"), + qtype: expr!("HTTPS"), + ..Default::default() + }); + + assert_eq!(result["fullRcode"], value!(0)); + assert_eq!(result["rcodeName"], value!("NOERROR")); + assert_eq!( + result["question"].as_array_unwrap()[0], + value!({ + "questionTypeId": 65, + "questionType": "HTTPS", + "class": "*", + "domainName": "localhost" + }) + ); + } + + #[test] + fn test_google() { + let result = execute_dns_lookup(DnsLookupFn { + value: expr!("dns.google"), + ..Default::default() + }); + + assert_eq!(result["fullRcode"], value!(0)); + assert_eq!(result["rcodeName"], value!("NOERROR")); + assert_eq!( + result["question"].as_array_unwrap()[0], + value!({ + "questionTypeId": 1, + "questionType": "A", + "class": "IN", + "domainName": "dns.google" + }) + ); + let answers: HashSet = result["answers"] + .as_array_unwrap() + .iter() + .map(|answer| { + answer.as_object().unwrap()["rData"] + .as_str() + .unwrap() + .to_string() + }) + .collect(); + let expected: HashSet = vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()] + .into_iter() + .collect(); + assert_eq!(answers, expected); + } + + #[test] + fn unknown_options_ignored() { + let result = execute_dns_lookup(DnsLookupFn { + value: expr!("localhost"), + options: expr!({"test": "test"}), + ..Default::default() + }); + + assert_eq!(result["rcodeName"], value!("NOERROR")); + } + + #[test] + fn invalid_option_type() { + let result = execute_dns_lookup_with_expected_error(DnsLookupFn { + value: expr!("localhost"), + options: expr!({"tcp": "yes"}), + ..Default::default() + }); + + assert_eq!(result.message(), "expected boolean, got string"); + } + + #[test] + fn negative_int_type() { + let attempts_val = -5; + let result = execute_dns_lookup_with_expected_error(DnsLookupFn { + value: expr!("localhost"), + options: expr!({"attempts": attempts_val}), + ..Default::default() + }); + + assert_eq!( + result.message(), + "attempts has to be a positive integer, got: -5. (out of range integral type conversion attempted)" + ); + } + + fn prepare_dns_lookup(dns_lookup_fn: DnsLookupFn) -> Resolved { + let tz = TimeZone::default(); + let mut object: Value = Value::Object(BTreeMap::new()); + let mut runtime_state = state::RuntimeState::default(); + let mut ctx = Context::new(&mut object, &mut runtime_state, &tz); + dns_lookup_fn.resolve(&mut ctx) + } + + fn execute_dns_lookup(dns_lookup_fn: DnsLookupFn) -> ObjectMap { + prepare_dns_lookup(dns_lookup_fn) + .map_err(|e| format!("{:#}", anyhow::anyhow!(e))) + .unwrap() + .try_object() + .unwrap() + } + + fn execute_dns_lookup_with_expected_error(dns_lookup_fn: DnsLookupFn) -> ExpressionError { + prepare_dns_lookup(dns_lookup_fn).unwrap_err() + } +} diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs index f6923acc1a..b378c950a7 100644 --- a/src/stdlib/mod.rs +++ b/src/stdlib/mod.rs @@ -59,6 +59,7 @@ cfg_if::cfg_if! { mod decode_zstd; mod decrypt; mod del; + mod dns_lookup; mod downcase; mod encode_base16; mod encode_base64; @@ -233,6 +234,7 @@ cfg_if::cfg_if! { pub use decode_zstd::DecodeZstd; pub use decrypt::Decrypt; pub use del::Del; + pub use dns_lookup::DnsLookup; pub use downcase::Downcase; pub use encode_base16::EncodeBase16; pub use encode_base64::EncodeBase64; @@ -411,6 +413,7 @@ pub fn all() -> Vec> { Box::new(DecodeZstd), Box::new(Decrypt), Box::new(Del), + Box::new(DnsLookup), Box::new(Downcase), Box::new(EncodeBase16), Box::new(EncodeBase64),