diff --git a/Cargo.lock b/Cargo.lock index 25228a4a..f62be013 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,8 +1082,8 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-proto" -version = "0.24.1" -source = "git+https://github.com/mokeyish/hickory-dns.git?rev=bbf1262#bbf12623ec13185e1421077b389798d74d9dcfda" +version = "0.25.0-alpha.1" +source = "git+https://github.com/mokeyish/hickory-dns.git?rev=0.25.0-smartdns.2#12574058a1967f144a50f589a1ae3147f880c370" dependencies = [ "async-recursion", "async-trait", @@ -1118,8 +1118,8 @@ dependencies = [ [[package]] name = "hickory-recursor" -version = "0.24.1" -source = "git+https://github.com/mokeyish/hickory-dns.git?rev=bbf1262#bbf12623ec13185e1421077b389798d74d9dcfda" +version = "0.25.0-alpha.1" +source = "git+https://github.com/mokeyish/hickory-dns.git?rev=0.25.0-smartdns.2#12574058a1967f144a50f589a1ae3147f880c370" dependencies = [ "async-recursion", "async-trait", @@ -1139,8 +1139,8 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.24.1" -source = "git+https://github.com/mokeyish/hickory-dns.git?rev=bbf1262#bbf12623ec13185e1421077b389798d74d9dcfda" +version = "0.25.0-alpha.1" +source = "git+https://github.com/mokeyish/hickory-dns.git?rev=0.25.0-smartdns.2#12574058a1967f144a50f589a1ae3147f880c370" dependencies = [ "cfg-if", "futures-util", @@ -1162,8 +1162,8 @@ dependencies = [ [[package]] name = "hickory-server" -version = "0.24.1" -source = "git+https://github.com/mokeyish/hickory-dns.git?rev=bbf1262#bbf12623ec13185e1421077b389798d74d9dcfda" +version = "0.25.0-alpha.1" +source = "git+https://github.com/mokeyish/hickory-dns.git?rev=0.25.0-smartdns.2#12574058a1967f144a50f589a1ae3147f880c370" dependencies = [ "async-trait", "bytes", @@ -2593,6 +2593,7 @@ dependencies = [ "futures", "futures-intrusive", "futures-util", + "glob", "hickory-proto", "hickory-resolver", "hickory-server", diff --git a/Cargo.toml b/Cargo.toml index c55a2d10..aab8d957 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,12 +131,12 @@ tracing-subscriber = { version = "0.3", features = [ # tracing-appender = "0.2" # hickory dns -hickory-proto = { git = "https://github.com/mokeyish/hickory-dns.git", rev = "bbf1262", version = "0.24", features = ["serde-config"]} -hickory-resolver = { git = "https://github.com/mokeyish/hickory-dns.git", rev = "bbf1262", version = "0.24", features = [ +hickory-proto = { git = "https://github.com/mokeyish/hickory-dns.git", rev = "0.25.0-smartdns.2", version = "0.25.0-alpha.1", features = ["serde-config"]} +hickory-resolver = { git = "https://github.com/mokeyish/hickory-dns.git", rev = "0.25.0-smartdns.2", version = "0.25.0-alpha.1", features = [ "serde-config", "system-config", ] } -hickory-server = { git = "https://github.com/mokeyish/hickory-dns.git", rev = "bbf1262", version = "0.24", features = ["resolver"], optional = true } +hickory-server = { git = "https://github.com/mokeyish/hickory-dns.git", rev = "0.25.0-smartdns.2", version = "0.25.0-alpha.1", features = ["resolver"], optional = true } # ssl webpki-roots = "0.25.2" @@ -155,6 +155,7 @@ hostname = "0.3" byte-unit = { version = "5.0.3", features = ["serde"]} ipnet = "2.7" which = { version = "6.0.1", optional = true } +glob = "0.3.1" # process sysinfo = "0.29" diff --git a/build.rs b/build.rs index de18a7c9..9f64e092 100644 --- a/build.rs +++ b/build.rs @@ -126,7 +126,7 @@ fn main() -> anyhow::Result<()> { println!( "cargo:rustc-env=CARGO_BUILD_TARGET={}", - std::env::var("TARGET").unwrap() + env::var("TARGET").unwrap() ); Ok(()) } diff --git a/src/config/mod.rs b/src/config/mod.rs index 510536fb..888b0888 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -70,7 +70,9 @@ pub struct Config { /// whether resolv local hostname to ip address pub resolv_hostname: Option, - pub hosts_file: Option, + pub hosts_file: Option, + + pub expand_ptr_from_address: Option, /// dns server run user /// diff --git a/src/config/parser/glob_pattern.rs b/src/config/parser/glob_pattern.rs new file mode 100644 index 00000000..edaf17be --- /dev/null +++ b/src/config/parser/glob_pattern.rs @@ -0,0 +1,58 @@ +use glob::Pattern; + +use super::*; + +impl NomParser for Pattern { + fn parse(input: &str) -> IResult<&str, Self> { + let delimited_path = delimited(char('"'), is_not("\""), char('"')); + let unix_path = recognize(tuple(( + opt(char('/')), + separated_list1(char('/'), escaped(is_not("\n \t\\"), '\\', one_of(r#" \"#))), + opt(char('/')), + ))); + let windows_path = recognize(tuple(( + opt(pair(alpha1, tag(":\\"))), + separated_list1(char('\\'), is_not("\\")), + opt(char('\\')), + ))); + map_res( + alt((delimited_path, unix_path, windows_path)), + FromStr::from_str, + )(input) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse() { + assert_eq!(Pattern::parse("a*"), Ok(("", "a*".parse().unwrap()))); + assert_eq!(Pattern::parse("/"), Ok(("", "/".parse().unwrap()))); + assert_eq!( + Pattern::parse("a/b😁/c"), + Ok(("", "a/b😁/c".parse().unwrap())) + ); + assert_eq!( + Pattern::parse("a/ b/c"), + Ok((" b/c", "a/".parse().unwrap())) + ); + assert_eq!( + Pattern::parse("/a/b/c"), + Ok(("", "/a/b/c".parse().unwrap())) + ); + assert_eq!( + Pattern::parse("/a/b/c/"), + Ok(("", "/a/b/c/".parse().unwrap())) + ); + assert_eq!( + Pattern::parse("a/b/c*/"), + Ok(("", "a/b/c*/".parse().unwrap())) + ); + assert_eq!( + Pattern::parse("**/*.rs"), + Ok(("", "**/*.rs".parse().unwrap())) + ); + } +} diff --git a/src/config/parser/mod.rs b/src/config/parser/mod.rs index 5db51026..d61f806d 100644 --- a/src/config/parser/mod.rs +++ b/src/config/parser/mod.rs @@ -14,6 +14,7 @@ mod domain_rule; mod domain_set; mod file_mode; mod forward_rule; +mod glob_pattern; mod ipnet; mod listener; mod log_level; @@ -96,10 +97,11 @@ pub enum OneConfig { DualstackIpSelection(bool), DualstackIpSelectionThreshold(u16), EdnsClientSubnet(IpNet), + ExpandPtrFromAddress(bool), ForceAAAASOA(bool), ForceQtypeSoa(RecordType), ForwardRule(ForwardRule), - HostsFile(PathBuf), + HostsFile(glob::Pattern), IgnoreIp(IpNet), Listener(ListenerConfig), LocalTtl(u64), @@ -182,6 +184,10 @@ pub fn parse_config(input: &str) -> IResult<&str, OneConfig> { parse_item("edns-client-subnet"), OneConfig::EdnsClientSubnet, ), + map( + parse_item("expand-ptr-from-address"), + OneConfig::ExpandPtrFromAddress, + ), map(parse_item("force-AAAA-SOA"), OneConfig::ForceAAAASOA), map(parse_item("force-qtype-soa"), OneConfig::ForceQtypeSoa), map(parse_item("response"), OneConfig::ResponseMode), @@ -199,17 +205,18 @@ pub fn parse_config(input: &str) -> IResult<&str, OneConfig> { map(parse_item("log-num"), OneConfig::LogNum), map(parse_item("log-size"), OneConfig::LogSize), map(parse_item("max-reply-ip-num"), OneConfig::MaxReplyIpNum), - map(parse_item("mdns-lookup"), OneConfig::MdnsLookup), - map(parse_item("nameserver"), OneConfig::ForwardRule), )); let group3 = alt(( + map(parse_item("mdns-lookup"), OneConfig::MdnsLookup), + map(parse_item("nameserver"), OneConfig::ForwardRule), map(parse_item("proxy-server"), OneConfig::ProxyConfig), map(parse_item("rr-ttl-reply-max"), OneConfig::RrTtlReplyMax), map(parse_item("rr-ttl-min"), OneConfig::RrTtlMin), map(parse_item("rr-ttl-max"), OneConfig::RrTtlMax), map(parse_item("rr-ttl"), OneConfig::RrTtl), map(parse_item("resolv-file"), OneConfig::ResolvFile), + map(parse_item("resolv-hostanme"), OneConfig::ResolvHostname), map(parse_item("response-mode"), OneConfig::ResponseMode), map(parse_item("server-name"), OneConfig::ServerName), map(parse_item("speed-check-mode"), OneConfig::SpeedMode), diff --git a/src/dns_conf.rs b/src/dns_conf.rs index 9b173a80..3e56bda5 100644 --- a/src/dns_conf.rs +++ b/src/dns_conf.rs @@ -213,8 +213,14 @@ impl RuntimeConfig { /// hosts file path #[inline] - pub fn hosts_file(&self) -> Option<&Path> { - self.hosts_file.as_deref() + pub fn hosts_file(&self) -> Option<&glob::Pattern> { + self.hosts_file.as_ref() + } + + /// Whether to expand the address record corresponding to PTR record + #[inline] + pub fn expand_ptr_from_address(&self) -> bool { + self.expand_ptr_from_address.unwrap_or_default() } /// whether resolv mdns @@ -775,6 +781,7 @@ impl RuntimeConfigBuilder { CacheFile(v) => self.cache.file = Some(v), CachePersist(v) => self.cache.persist = Some(v), CName(v) => self.cnames.push(v), + ExpandPtrFromAddress(v) => self.expand_ptr_from_address = Some(v), NftSet(config) => self.nftsets.push(config), Server(server) => self.nameservers.push(server), ResponseMode(mode) => self.response_mode = Some(mode), @@ -1116,7 +1123,7 @@ mod tests { #[test] fn test_config_domain_rules_without_args() { let mut cfg = RuntimeConfig::builder(); - cfg.config("domain-set -name domain-forwarding-list -file tests/test_confs/block-list.txt"); + cfg.config("domain-set -name domain-forwarding-list -file tests/test_data/block-list.txt"); cfg.config("domain-rules /domain-set:domain-forwarding-list/"); assert!(cfg.address_rules.last().is_none()); } @@ -1435,7 +1442,7 @@ mod tests { #[test] fn test_parse_load_config_file_b() { - let cfg = RuntimeConfig::load_from_file("tests/test_confs/b_main.conf"); + let cfg = RuntimeConfig::load_from_file("tests/test_data/b_main.conf"); assert_eq!(cfg.server_name, "SmartDNS123".parse().ok()); assert_eq!( @@ -1459,7 +1466,7 @@ mod tests { #[test] #[cfg(failed_tests)] fn test_domain_set() { - let cfg = RuntimeConfig::load_from_file("tests/test_confs/b_main.conf"); + let cfg = RuntimeConfig::load_from_file("tests/test_data/b_main.conf"); assert!(!cfg.domain_sets.is_empty()); diff --git a/src/dns_mw_hosts.rs b/src/dns_mw_hosts.rs index 3c8788c3..553dea01 100644 --- a/src/dns_mw_hosts.rs +++ b/src/dns_mw_hosts.rs @@ -4,9 +4,9 @@ use std::time::{Duration, Instant}; use crate::libdns::proto::op::Query; use tokio::sync::RwLock; -use crate::dns::*; use crate::libdns::resolver::Hosts; use crate::middleware::*; +use crate::{dns::*, log}; pub struct DnsHostsMiddleware(RwLock)>>); @@ -27,8 +27,8 @@ impl Middleware for DnsHostsMiddl next: Next<'_, DnsContext, DnsRequest, DnsResponse, DnsError>, ) -> Result { let query = req.query().original(); - - if query.query_type().is_ip_addr() { + let is_ptr = query.query_type() == RecordType::PTR && ctx.cfg().expand_ptr_from_address(); + if query.query_type().is_ip_addr() || is_ptr { let hosts = self.0.read().await.as_ref().and_then(|(read_time, hosts)| { if Instant::now() - *read_time < EXPIRES { Some(hosts.clone()) @@ -41,19 +41,8 @@ impl Middleware for DnsHostsMiddl Some(v) => v, None => { let hosts = match ctx.cfg().hosts_file() { - Some(file) => { - if file.exists() { - std::fs::OpenOptions::new() - .read(true) - .open(file) - .map(|f| Hosts::default().read_hosts_conf(f)) - .unwrap_or_else(Err) - .unwrap_or_default() - } else { - Hosts::default() - } - } - None => Hosts::new(), + Some(pattern) => read_hosts(pattern.as_str()), + None => Hosts::new(), // read from system hosts file }; let hosts = Arc::new(hosts); *self.0.write().await = Some((Instant::now(), hosts.clone())); @@ -77,3 +66,111 @@ impl Middleware for DnsHostsMiddl next.run(ctx, req).await } } + +fn read_hosts(pattern: &str) -> Hosts { + let mut hosts = Hosts::default(); + match glob::glob(pattern) { + Ok(paths) => { + for entry in paths { + let path = match entry { + Ok(path) => { + if !path.is_file() { + continue; + } + path + } + Err(err) => { + log::error!("{}", err); + continue; + } + }; + + let file = match std::fs::OpenOptions::new().read(true).open(path) { + Ok(file) => file, + Err(err) => { + log::error!("{}", err); + continue; + } + }; + + if let Err(err) = hosts.read_hosts_conf(file) { + log::error!("{}", err); + } + } + } + Err(err) => { + log::error!("{}", err); + } + } + hosts +} + +#[cfg(test)] +mod tests { + use std::{net::IpAddr, str::FromStr}; + + use crate::libdns::proto::rr::rdata::PTR; + + use super::*; + + use crate::{dns_conf::RuntimeConfig, dns_mw::*}; + + #[tokio::test()] + async fn test_query_ip() -> anyhow::Result<()> { + let cfg = RuntimeConfig::builder() + .with("hosts-file ./tests/test_data/hosts/a*.hosts") + .build(); + + let mock = DnsMockMiddleware::mock(DnsHostsMiddleware::new()).build(cfg); + + let lookup = mock.lookup("hi.a1", RecordType::A).await?; + let ip_addrs = lookup + .records() + .iter() + .flat_map(|r| r.data().ip_addr()) + .collect::>(); + assert_eq!(ip_addrs, vec![IpAddr::from_str("1.1.1.1").unwrap()]); + + let lookup = mock.lookup("hi.a2", RecordType::A).await?; + let ip_addrs = lookup + .records() + .iter() + .flat_map(|r| r.data().ip_addr()) + .collect::>(); + assert_eq!(ip_addrs, vec![IpAddr::from_str("2.2.2.2").unwrap()]); + + Ok(()) + } + + #[tokio::test()] + async fn test_query_ptr() -> anyhow::Result<()> { + let cfg = RuntimeConfig::builder() + .with("hosts-file ./tests/test_data/hosts/a*.hosts") + .with("expand-ptr-from-address yes") + .build(); + + let mock = DnsMockMiddleware::mock(DnsHostsMiddleware::new()).build(cfg); + + let lookup = mock + .lookup("1.1.1.1.in-addr.arpa.", RecordType::PTR) + .await?; + let hostnames = lookup + .records() + .iter() + .flat_map(|r| r.data().as_ptr()) + .collect::>(); + assert_eq!(hostnames, vec![&PTR("hi.a1.".parse().unwrap())]); + + let lookup = mock + .lookup("2.2.2.2.in-addr.arpa.", RecordType::PTR) + .await?; + let hostnames = lookup + .records() + .iter() + .flat_map(|r| r.data().as_ptr()) + .collect::>(); + assert_eq!(hostnames, vec![&PTR("hi.a2.".parse().unwrap())]); + + Ok(()) + } +} diff --git a/src/dnsmasq.rs b/src/dnsmasq.rs index 40d4949b..ea4c9b84 100644 --- a/src/dnsmasq.rs +++ b/src/dnsmasq.rs @@ -197,7 +197,7 @@ mod tests { #[test] fn test_read_dnsmasq_lease_file() { - let host_ips = read_lease_file("tests/test_confs/dhcp.leases", None).unwrap(); + let host_ips = read_lease_file("tests/test_data/dhcp.leases", None).unwrap(); assert_eq!( host_ips .find(&Name::from_str("Andy-PC").unwrap()) @@ -225,7 +225,7 @@ mod tests { #[test] fn test_lan_client_store_lookup() { - let store = LanClientStore::new("tests/test_confs/dhcp.leases", Default::default()); + let store = LanClientStore::new("tests/test_data/dhcp.leases", Default::default()); assert_eq!( store.lookup(&"iphone-abc".parse().unwrap(), RecordType::AAAA), @@ -240,7 +240,7 @@ mod tests { #[test] fn test_lan_client_store_lookup_fqdn() { - let store = LanClientStore::new("tests/test_confs/dhcp.leases", Default::default()); + let store = LanClientStore::new("tests/test_data/dhcp.leases", Default::default()); assert_eq!( store.lookup(&"iphone-abc.".parse().unwrap(), RecordType::AAAA), @@ -255,7 +255,7 @@ mod tests { #[test] fn test_lan_client_store_lookup_zone() { - let store = LanClientStore::new("tests/test_confs/dhcp.leases", Name::from_str("xyz").ok()); + let store = LanClientStore::new("tests/test_data/dhcp.leases", Name::from_str("xyz").ok()); assert_eq!( store.lookup(&"iphone-abc.xyz.".parse().unwrap(), RecordType::AAAA), diff --git a/tests/example/smartdns.conf b/tests/example/smartdns.conf index 20ab5889..2af1ee0f 100644 --- a/tests/example/smartdns.conf +++ b/tests/example/smartdns.conf @@ -9,6 +9,9 @@ bind-cert-key-file localhost.key bind-https 0.0.0.0:8453 bind-tls 0.0.0.0:8853 +resolv-hostanme yes +expand-ptr-from-address yes + cache-size 0 server-https https://doh.sb/dns-query diff --git a/tests/test_confs/a_addr.conf b/tests/test_data/a_addr.conf similarity index 100% rename from tests/test_confs/a_addr.conf rename to tests/test_data/a_addr.conf diff --git a/tests/test_confs/a_main.conf b/tests/test_data/a_main.conf similarity index 100% rename from tests/test_confs/a_main.conf rename to tests/test_data/a_main.conf diff --git a/tests/test_confs/a_server.conf b/tests/test_data/a_server.conf similarity index 100% rename from tests/test_confs/a_server.conf rename to tests/test_data/a_server.conf diff --git a/tests/test_confs/b_addr.conf b/tests/test_data/b_addr.conf similarity index 100% rename from tests/test_confs/b_addr.conf rename to tests/test_data/b_addr.conf diff --git a/tests/test_confs/b_main.conf b/tests/test_data/b_main.conf similarity index 100% rename from tests/test_confs/b_main.conf rename to tests/test_data/b_main.conf diff --git a/tests/test_confs/b_server.conf b/tests/test_data/b_server.conf similarity index 100% rename from tests/test_confs/b_server.conf rename to tests/test_data/b_server.conf diff --git a/tests/test_confs/block-list.txt b/tests/test_data/block-list.txt similarity index 100% rename from tests/test_confs/block-list.txt rename to tests/test_data/block-list.txt diff --git a/tests/test_confs/dhcp.leases b/tests/test_data/dhcp.leases similarity index 100% rename from tests/test_confs/dhcp.leases rename to tests/test_data/dhcp.leases diff --git a/tests/test_data/hosts/a1.hosts b/tests/test_data/hosts/a1.hosts new file mode 100644 index 00000000..5f1d5f0c --- /dev/null +++ b/tests/test_data/hosts/a1.hosts @@ -0,0 +1 @@ +1.1.1.1 hi.a1 \ No newline at end of file diff --git a/tests/test_data/hosts/a2.hosts b/tests/test_data/hosts/a2.hosts new file mode 100644 index 00000000..c9a8d079 --- /dev/null +++ b/tests/test_data/hosts/a2.hosts @@ -0,0 +1 @@ +2.2.2.2 hi.a2 \ No newline at end of file diff --git a/tests/test_data/hosts/b.hosts b/tests/test_data/hosts/b.hosts new file mode 100644 index 00000000..f4fd3126 --- /dev/null +++ b/tests/test_data/hosts/b.hosts @@ -0,0 +1 @@ +3.3.3.3 hi.b \ No newline at end of file