From f9976b545642a1bdee82d72c47e1486fd11805b8 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 23 Jan 2024 17:49:18 +0900 Subject: [PATCH 1/4] feat: wip, supporting tcp and arbitrary port for bootstrap dns --- doh-auth-proxy.toml | 5 +- proxy-bin/Cargo.toml | 6 +- proxy-bin/src/config/mod.rs | 1 + proxy-bin/src/config/target_config.rs | 13 ++-- proxy-bin/src/config/utils_dns_proto.rs | 95 +++++++++++++++++++++++++ proxy-bin/src/config/utils_verifier.rs | 1 + proxy-lib/Cargo.toml | 7 +- proxy-lib/src/bootstrap.rs | 54 ++++++++++++-- proxy-lib/src/constants.rs | 8 +-- proxy-lib/src/globals.rs | 61 +++++++++++++--- proxy-lib/src/lib.rs | 4 +- 11 files changed, 224 insertions(+), 31 deletions(-) create mode 100644 proxy-bin/src/config/utils_dns_proto.rs diff --git a/doh-auth-proxy.toml b/doh-auth-proxy.toml index dd8669c..4612e0e 100644 --- a/doh-auth-proxy.toml +++ b/doh-auth-proxy.toml @@ -11,8 +11,9 @@ ## Address to listen to. listen_addresses = ['127.0.0.1:50053', '[::1]:50053'] -## DNS (Do53) resolver addresses for bootstrap -bootstrap_dns = ["8.8.8.8", "1.1.1.1"] +## DNS (Do53) resolver addresses for bootstrap. +## You can omit protocol name and port number, default is udp over port 53. +bootstrap_dns = ["udp://8.8.8.8:53", "1.1.1.1:53", "8.8.4.4", "tcp://1.0.0.1"] ## Minutes to re-resolve the IP addr of the nexthop and authentication endpoint url ## Ip addresses are first resolved by bootstrap DNS, after that, they will be resolved by (MO)DoH resolver itself. diff --git a/proxy-bin/Cargo.toml b/proxy-bin/Cargo.toml index 843034f..18ba031 100644 --- a/proxy-bin/Cargo.toml +++ b/proxy-bin/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "doh-auth-proxy" description = "DNS Proxy for DoH, ODoH and Mutualized ODoH with Authorization" -version = "0.3.1" +version = "0.3.2" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/doh-auth-proxy" repository = "https://github.com/junkurihara/doh-auth-proxy" @@ -37,7 +37,7 @@ doh-auth-proxy-lib = { path = "../proxy-lib/" } anyhow = "1.0.79" mimalloc = { version = "*", default-features = false } serde = { version = "1.0.195", default-features = false, features = ["derive"] } -derive_builder = "0.12.0" +derive_builder = "0.13.0" tokio = { version = "1.35.1", default-features = false, features = [ "net", "rt-multi-thread", @@ -48,7 +48,7 @@ tokio = { version = "1.35.1", default-features = false, features = [ async-trait = "0.1.77" # config -clap = { version = "4.4.14", features = ["std", "cargo", "wrap_help"] } +clap = { version = "4.4.18", features = ["std", "cargo", "wrap_help"] } toml = { version = "0.8.8", default-features = false, features = ["parse"] } hot_reload = "0.1.4" diff --git a/proxy-bin/src/config/mod.rs b/proxy-bin/src/config/mod.rs index c5646ff..a688486 100644 --- a/proxy-bin/src/config/mod.rs +++ b/proxy-bin/src/config/mod.rs @@ -2,6 +2,7 @@ mod parse; mod plugins; mod target_config; mod toml; +mod utils_dns_proto; mod utils_verifier; pub use { diff --git a/proxy-bin/src/config/target_config.rs b/proxy-bin/src/config/target_config.rs index ac884b1..c821eea 100644 --- a/proxy-bin/src/config/target_config.rs +++ b/proxy-bin/src/config/target_config.rs @@ -1,4 +1,4 @@ -use super::{toml::ConfigToml, utils_verifier::*}; +use super::{toml::ConfigToml, utils_dns_proto::parse_proto_sockaddr_str, utils_verifier::*}; use crate::{constants::*, error::*, log::*}; use async_trait::async_trait; use doh_auth_proxy_lib::{ @@ -76,12 +76,17 @@ impl TryInto for &TargetConfig { ///////////////////////////// // bootstrap dns if let Some(val) = &self.config_toml.bootstrap_dns { - if !val.iter().all(|v| verify_ip_addr(v).is_ok()) { + let vec_proto_sockaddr = val.iter().map(parse_proto_sockaddr_str).collect::>(); + if vec_proto_sockaddr.iter().any(|x| x.is_err()) { bail!("Invalid bootstrap DNS address"); } - proxy_config.bootstrap_dns.ips = val.iter().map(|x| x.parse().unwrap()).collect() + proxy_config.bootstrap_dns = vec_proto_sockaddr + .iter() + .map(|x| x.as_ref().unwrap().clone()) + .collect::>() + .try_into()?; }; - info!("Bootstrap DNS: {:?}", proxy_config.bootstrap_dns.ips); + info!("Bootstrap DNS: {}", proxy_config.bootstrap_dns); ///////////////////////////// // endpoint re-resolution period diff --git a/proxy-bin/src/config/utils_dns_proto.rs b/proxy-bin/src/config/utils_dns_proto.rs new file mode 100644 index 0000000..a2cce09 --- /dev/null +++ b/proxy-bin/src/config/utils_dns_proto.rs @@ -0,0 +1,95 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + +const PREFIX_UDP: &str = "udp://"; +const PREFIX_TCP: &str = "tcp://"; +const DEFAULT_DNS_PORT: u16 = 53; + +/// Parse as string in the form of "://:", where "://" can be omitted and then it will be treated as "udp://". +/// ":" can also be omitted and then it will be treated as ":53". +/// - : "udp" or "tcp" +/// - : IPv4 or IPv6 address, where IPv6 address must be enclosed in square brackets like "[::1]" +/// - : port number, which must be explicitly specified. +pub(crate) fn parse_proto_sockaddr_str>(val: T) -> anyhow::Result<(String, SocketAddr)> { + let val = val.as_ref(); + + // parse proto + let (proto, val_rest) = if val.starts_with(PREFIX_UDP) { + ("udp", val.strip_prefix(PREFIX_UDP).unwrap()) + } else if val.starts_with(PREFIX_TCP) { + ("tcp", val.strip_prefix(PREFIX_TCP).unwrap()) + } else { + ("udp", val) + }; + + // parse socket address + let socket_addr = if val.contains('[') && val.contains(']') { + // ipv6 + let mut split = val_rest.strip_prefix('[').unwrap().split(']').filter(|s| !s.is_empty()); + let ip_part = split + .next() + .ok_or(anyhow::anyhow!("Invalid IPv6 address specified"))? + .parse::()?; + let port_part = if let Some(port_part) = split.next() { + anyhow::ensure!(port_part.starts_with(':'), "Invalid port number specified"); + port_part.strip_prefix(':').unwrap().parse::()? + } else { + DEFAULT_DNS_PORT + }; + SocketAddr::new(IpAddr::V6(ip_part), port_part) + } else { + // ipv4 + let mut split = val_rest.split(':').filter(|s| !s.is_empty()); + let ip_part = split + .next() + .ok_or(anyhow::anyhow!("Invalid IPv4 address specified"))? + .parse::()?; + let port_part = if let Some(port_part) = split.next() { + port_part.parse::()? + } else { + DEFAULT_DNS_PORT + }; + SocketAddr::new(IpAddr::V4(ip_part), port_part) + }; + + Ok((proto.to_owned(), socket_addr)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_proto_sockaddr_str() { + let (proto, socket_addr) = parse_proto_sockaddr_str("tcp://[::1]:50053").unwrap(); + assert_eq!(proto, "tcp"); + assert_eq!(socket_addr, SocketAddr::from((Ipv6Addr::LOCALHOST, 50053))); + + let (proto, socket_addr) = parse_proto_sockaddr_str("tcp://[::1]").unwrap(); + assert_eq!(proto, "tcp"); + assert_eq!(socket_addr, SocketAddr::from((Ipv6Addr::LOCALHOST, 53))); + + let (proto, socket_addr) = parse_proto_sockaddr_str("[::1]:50053").unwrap(); + assert_eq!(proto, "udp"); + assert_eq!(socket_addr, SocketAddr::from((Ipv6Addr::LOCALHOST, 50053))); + + let (proto, socket_addr) = parse_proto_sockaddr_str("[::1]").unwrap(); + assert_eq!(proto, "udp"); + assert_eq!(socket_addr, SocketAddr::from((Ipv6Addr::LOCALHOST, 53))); + + let (proto, socket_addr) = parse_proto_sockaddr_str("udp://8.8.8.8:50053").unwrap(); + assert_eq!(proto, "udp"); + assert_eq!(socket_addr, SocketAddr::from(([8, 8, 8, 8], 50053))); + + let (proto, socket_addr) = parse_proto_sockaddr_str("udp://8.8.8.8").unwrap(); + assert_eq!(proto, "udp"); + assert_eq!(socket_addr, SocketAddr::from(([8, 8, 8, 8], 53))); + + let (proto, socket_addr) = parse_proto_sockaddr_str("8.8.8.8:50053").unwrap(); + assert_eq!(proto, "udp"); + assert_eq!(socket_addr, SocketAddr::from(([8, 8, 8, 8], 50053))); + + let (proto, socket_addr) = parse_proto_sockaddr_str("8.8.8.8").unwrap(); + assert_eq!(proto, "udp"); + assert_eq!(socket_addr, SocketAddr::from(([8, 8, 8, 8], 53))); + } +} diff --git a/proxy-bin/src/config/utils_verifier.rs b/proxy-bin/src/config/utils_verifier.rs index b27c246..55d1b25 100644 --- a/proxy-bin/src/config/utils_verifier.rs +++ b/proxy-bin/src/config/utils_verifier.rs @@ -12,6 +12,7 @@ pub(crate) fn verify_sock_addr(arg_val: &str) -> Result<(), String> { } } +#[allow(dead_code)] pub(crate) fn verify_ip_addr(arg_val: &str) -> Result<(), String> { match arg_val.parse::() { Ok(_addr) => Ok(()), diff --git a/proxy-lib/Cargo.toml b/proxy-lib/Cargo.toml index 627f9ea..e63bde6 100644 --- a/proxy-lib/Cargo.toml +++ b/proxy-lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "doh-auth-proxy-lib" description = "DNS Proxy Library for DoH, ODoH and Mutualized ODoH with Authorization" -version = "0.3.1" +version = "0.3.2" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/doh-auth-proxy" repository = "https://github.com/junkurihara/doh-auth-proxy" @@ -59,7 +59,7 @@ hickory-proto = { version = "0.24.0", default-features = false } data-encoding = "2.5.0" hashlink = "0.9.0" cedarwood = "0.4.6" -regex = "1.10.2" +regex = "1.10.3" # network socket2 = "0.5.5" @@ -77,6 +77,9 @@ url = "2.5.0" hickory-resolver = { version = "0.24.0", default-features = false, features = [ "tokio-runtime", ] } +hickory-client = { version = "0.24.0", default-features = false, features = [ + "dnssec", +] } # authentication auth-client = { git = "https://github.com/junkurihara/rust-token-server", package = "rust-token-server-client", branch = "develop" } diff --git a/proxy-lib/src/bootstrap.rs b/proxy-lib/src/bootstrap.rs index 1951c0e..a4d434c 100644 --- a/proxy-lib/src/bootstrap.rs +++ b/proxy-lib/src/bootstrap.rs @@ -13,6 +13,44 @@ use hickory_resolver::{ use reqwest::Url; use std::{net::SocketAddr, sync::Arc}; +/* ---------------------------------------- */ +#[derive(PartialEq, Eq, Debug, Clone)] +/// Bootstrap DNS Protocol +pub enum BootstrapDnsProto { + /// UDP + Udp, + /// TCP + Tcp, +} +impl std::str::FromStr for BootstrapDnsProto { + type Err = DapError; + + fn from_str(s: &str) -> std::result::Result { + match s { + "udp" => Ok(Self::Udp), + "tcp" => Ok(Self::Tcp), + _ => Err(DapError::Other(anyhow!("Invalid bootstrap dns protocol"))), + } + } +} +impl std::fmt::Display for BootstrapDnsProto { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Udp => write!(f, "udp"), + Self::Tcp => write!(f, "tcp"), + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +/// Bootstrap DNS Address with port and protocol +pub struct BootstrapDnsInner { + /// protocol + pub proto: BootstrapDnsProto, + /// socket address + pub addr: SocketAddr, +} + #[derive(Clone)] /// stub resolver using bootstrap DNS resolver pub struct BootstrapDnsResolver { @@ -23,8 +61,8 @@ pub struct BootstrapDnsResolver { impl BootstrapDnsResolver { /// Build stub resolver using bootstrap dns resolver pub async fn try_new(bootstrap_dns: &BootstrapDns, runtime_handle: tokio::runtime::Handle) -> Result { - let ips = &bootstrap_dns.ips; - let port = &bootstrap_dns.port; + let ips = &bootstrap_dns.inner.iter().map(|x| x.addr.ip()).collect::>(); + let port = &bootstrap_dns.inner.iter().map(|x| x.addr.port()).collect::>()[0]; let name_servers = NameServerConfigGroup::from_ips_clear(ips, *port, true); let resolver_config = ResolverConfig::from_parts(None, vec![], name_servers); @@ -87,8 +125,16 @@ mod tests { #[tokio::test] async fn test_bootstrap_dns_resolver() { let bootstrap_dns = BootstrapDns { - ips: vec![IpAddr::from([8, 8, 8, 8])], - port: 53, + inner: vec![ + BootstrapDnsInner { + proto: BootstrapDnsProto::Udp, + addr: SocketAddr::new(IpAddr::from([8, 8, 8, 8]), 53), + }, + BootstrapDnsInner { + proto: BootstrapDnsProto::Tcp, + addr: SocketAddr::new(IpAddr::from([8, 8, 4, 4]), 53), + }, + ], }; let resolver = BootstrapDnsResolver::try_new(&bootstrap_dns, tokio::runtime::Handle::current()) .await diff --git a/proxy-lib/src/constants.rs b/proxy-lib/src/constants.rs index 246ce65..7eabfff 100644 --- a/proxy-lib/src/constants.rs +++ b/proxy-lib/src/constants.rs @@ -27,10 +27,10 @@ pub const MIN_TTL: u32 = 10; /// Default listen address pub const LISTEN_ADDRESSES: &[&str] = &["127.0.0.1:50053", "[::1]:50053"]; -/// Bootstrap DNS address -pub const BOOTSTRAP_DNS_IPS: &[&str] = &["1.1.1.1"]; -/// Bootstrap DNS port -pub const BOOTSTRAP_DNS_PORT: u16 = 53; +/// Bootstrap DNS socket address +pub const BOOTSTRAP_DNS_ADDRS: &[&str] = &["1.1.1.1:53"]; +/// Bootstrap DNS proto +pub const BOOTSTRAP_DNS_PROTO: &str = "udp"; /// Endpoint resolution period in minutes pub const ENDPOINT_RESOLUTION_PERIOD_MIN: u64 = 60; diff --git a/proxy-lib/src/globals.rs b/proxy-lib/src/globals.rs index 7415dc8..170598b 100644 --- a/proxy-lib/src/globals.rs +++ b/proxy-lib/src/globals.rs @@ -1,9 +1,9 @@ -use crate::constants::*; -use auth_client::AuthenticationConfig; -use std::{ - net::{IpAddr, SocketAddr}, - sync::Arc, +use crate::{ + bootstrap::{BootstrapDnsInner, BootstrapDnsProto}, + constants::*, }; +use auth_client::AuthenticationConfig; +use std::{net::SocketAddr, sync::Arc}; use tokio::{sync::Notify, time::Duration}; use url::Url; @@ -71,8 +71,7 @@ pub struct ProxyConfig { #[derive(PartialEq, Eq, Debug, Clone)] /// Bootstrap DNS Addresses pub struct BootstrapDns { - pub ips: Vec, - pub port: u16, + pub inner: Vec, } #[derive(PartialEq, Eq, Debug, Clone)] @@ -129,6 +128,49 @@ impl Default for QueryManipulationConfig { } } +impl Default for BootstrapDns { + fn default() -> Self { + Self { + inner: BOOTSTRAP_DNS_ADDRS + .iter() + .map(|v| BootstrapDnsInner { + proto: ::from_str(BOOTSTRAP_DNS_PROTO).unwrap(), + addr: v.parse().unwrap(), + }) + .collect(), + } + } +} + +impl std::fmt::Display for BootstrapDns { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for v in &self.inner { + if !first { + write!(f, ", ")?; + } + write!(f, "{}://{}", v.proto, v.addr)?; + first = false; + } + Ok(()) + } +} + +impl TryFrom> for BootstrapDns { + type Error = anyhow::Error; + + fn try_from(value: Vec<(String, SocketAddr)>) -> Result { + let inner = value + .into_iter() + .map(|(proto, addr)| BootstrapDnsInner { + proto: ::from_str(&proto).unwrap(), + addr, + }) + .collect(); + Ok(Self { inner }) + } +} + impl Default for ProxyConfig { fn default() -> Self { Self { @@ -136,10 +178,7 @@ impl Default for ProxyConfig { max_connections: MAX_CONNECTIONS, max_cache_size: MAX_CACHE_SIZE, - bootstrap_dns: BootstrapDns { - ips: BOOTSTRAP_DNS_IPS.iter().map(|v| v.parse().unwrap()).collect(), - port: BOOTSTRAP_DNS_PORT, - }, + bootstrap_dns: BootstrapDns::default(), endpoint_resolution_period_sec: Duration::from_secs(ENDPOINT_RESOLUTION_PERIOD_MIN * 60), healthcheck_period_sec: Duration::from_secs(HEALTHCHECK_PERIOD_MIN * 60), diff --git a/proxy-lib/src/lib.rs b/proxy-lib/src/lib.rs index 3a8e34c..ce00cb0 100644 --- a/proxy-lib/src/lib.rs +++ b/proxy-lib/src/lib.rs @@ -17,7 +17,9 @@ use futures::{ use std::sync::Arc; pub use auth_client::AuthenticationConfig; -pub use globals::{NextHopRelayConfig, ProxyConfig, QueryManipulationConfig, SubseqRelayConfig, TargetConfig}; +pub use globals::{ + BootstrapDns, NextHopRelayConfig, ProxyConfig, QueryManipulationConfig, SubseqRelayConfig, TargetConfig, +}; /// entrypoint of DoH w/ Auth Proxy /// This spawns UDP and TCP listeners and spawns the following services From 65e8e1857cf4e9d2cf8595f3740f30ed3240c885 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 23 Jan 2024 19:44:54 +0900 Subject: [PATCH 2/4] feat: wip, sync --- proxy-lib/src/bootstrap.rs | 134 ++++++++++++++++++++++++++++++++++++- proxy-lib/src/constants.rs | 2 + proxy-lib/src/error.rs | 6 ++ proxy-lib/src/globals.rs | 54 +-------------- proxy-lib/src/lib.rs | 5 +- 5 files changed, 143 insertions(+), 58 deletions(-) diff --git a/proxy-lib/src/bootstrap.rs b/proxy-lib/src/bootstrap.rs index a4d434c..84d9129 100644 --- a/proxy-lib/src/bootstrap.rs +++ b/proxy-lib/src/bootstrap.rs @@ -1,17 +1,79 @@ use crate::{ + constants::{BOOTSTRAP_DNS_ADDRS, BOOTSTRAP_DNS_PROTO, BOOTSTRAP_DNS_TIMEOUT_MSEC}, error::*, - globals::BootstrapDns, log::*, trait_resolve_ips::{ResolveIpResponse, ResolveIps}, }; use async_trait::async_trait; +use hickory_client::{ + client::{Client, ClientConnection, SyncClient}, + rr::{DNSClass, Name, RecordType}, + tcp::{TcpClientConnection, TcpClientStream}, + udp::{UdpClientConnection, UdpClientStream}, +}; +use tokio::net::{TcpStream as TokioTcpStream, UdpSocket as TokioUdpSocket}; + use hickory_resolver::{ config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, name_server::{GenericConnector, TokioRuntimeProvider}, AsyncResolver, TokioAsyncResolver, }; use reqwest::Url; -use std::{net::SocketAddr, sync::Arc}; +use std::{ + net::{IpAddr, SocketAddr}, + str::FromStr, + sync::Arc, +}; + +/* ---------------------------------------- */ +#[derive(PartialEq, Eq, Debug, Clone)] +/// Bootstrap DNS Addresses +pub struct BootstrapDns { + pub inner: Vec, +} + +impl Default for BootstrapDns { + fn default() -> Self { + Self { + inner: BOOTSTRAP_DNS_ADDRS + .iter() + .map(|v| BootstrapDnsInner { + proto: ::from_str(BOOTSTRAP_DNS_PROTO).unwrap(), + addr: v.parse().unwrap(), + }) + .collect(), + } + } +} + +impl std::fmt::Display for BootstrapDns { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for v in &self.inner { + if !first { + write!(f, ", ")?; + } + write!(f, "{}://{}", v.proto, v.addr)?; + first = false; + } + Ok(()) + } +} + +impl TryFrom> for BootstrapDns { + type Error = anyhow::Error; + + fn try_from(value: Vec<(String, SocketAddr)>) -> anyhow::Result { + let inner = value + .into_iter() + .map(|(proto, addr)| BootstrapDnsInner { + proto: ::from_str(&proto).unwrap(), + addr, + }) + .collect(); + Ok(Self { inner }) + } +} /* ---------------------------------------- */ #[derive(PartialEq, Eq, Debug, Clone)] @@ -42,6 +104,7 @@ impl std::fmt::Display for BootstrapDnsProto { } } +/* ---------------------------------------- */ #[derive(PartialEq, Eq, Debug, Clone)] /// Bootstrap DNS Address with port and protocol pub struct BootstrapDnsInner { @@ -51,6 +114,60 @@ pub struct BootstrapDnsInner { pub addr: SocketAddr, } +impl BootstrapDnsInner { + /// Lookup the IP addresses associated with a name using the bootstrap resolver connection + pub(crate) fn lookup_ips(&self, target_url: &Url) -> Result> { + // The final dot forces this to be an FQDN, otherwise the search rules as specified + // in `ResolverOpts` will take effect. FQDN's are generally cheaper queries. + let host = target_url + .host_str() + .ok_or_else(|| DapError::Other(anyhow!("Unable to parse target host name")))?; + let host = format!("{host}."); + let result_ips = match self.proto { + BootstrapDnsProto::Udp => self.lookup_ips_inner(&host, UdpClientConnection::new(self.addr)?), + BootstrapDnsProto::Tcp => self.lookup_ips_inner(&host, TcpClientConnection::new(self.addr)?), + }?; + + let port = target_url + .port() + .unwrap_or_else(|| if target_url.scheme() == "https" { 443 } else { 80 }); + + Ok( + result_ips + .iter() + .filter_map(|addr| format!("{}:{}", addr, port).parse::().ok()) + .collect::>(), + ) + } + + /// Inner: Lookup the IP addresses associated with a name using the bootstrap resolver connection + fn lookup_ips_inner(&self, fqdn: &str, conn: C) -> Result> { + let client = SyncClient::new(conn); + let name = Name::from_str(fqdn).map_err(|e| DapError::InvalidFqdn(e.to_string()))?; + // First try to lookup an A record, if failed, try AAAA. + let response = client.query(&name, DNSClass::IN, RecordType::A)?; + let ips = response + .answers() + .iter() + .filter_map(|a| a.data().and_then(|v| v.as_a()).map(|v| IpAddr::V4(v.0))) + .collect::>(); + if !ips.is_empty() { + return Ok(ips); + } + let response = client.query(&name, DNSClass::IN, RecordType::AAAA)?; + let ipv6s = response + .answers() + .iter() + .filter_map(|aaaa| aaaa.data().and_then(|v| v.as_aaaa()).map(|v| IpAddr::V6(v.0))) + .collect::>(); + if ipv6s.is_empty() { + return Err(DapError::InvalidBootstrapDnsResponse); + } + Ok(ipv6s) + } +} + +/* ---------------------------------------- */ #[derive(Clone)] /// stub resolver using bootstrap DNS resolver pub struct BootstrapDnsResolver { @@ -147,4 +264,17 @@ mod tests { assert!(response.addresses.contains(&SocketAddr::from(([8, 8, 8, 8], 443)))); assert!(response.addresses.contains(&SocketAddr::from(([8, 8, 4, 4], 443)))); } + + #[test] + fn test_bootstrap_dns_client_inner() { + let inner = BootstrapDnsInner { + proto: BootstrapDnsProto::Udp, + addr: SocketAddr::new(IpAddr::from([8, 8, 8, 8]), 53), + }; + let target_url = Url::parse("https://dns.google").unwrap(); + let ips = inner.lookup_ips(&target_url).unwrap(); + + assert!(ips.contains(&SocketAddr::from(([8, 8, 8, 8], 443)))); + assert!(ips.contains(&SocketAddr::from(([8, 8, 4, 4], 443)))); + } } diff --git a/proxy-lib/src/constants.rs b/proxy-lib/src/constants.rs index 7eabfff..41f9d25 100644 --- a/proxy-lib/src/constants.rs +++ b/proxy-lib/src/constants.rs @@ -31,6 +31,8 @@ pub const LISTEN_ADDRESSES: &[&str] = &["127.0.0.1:50053", "[::1]:50053"]; pub const BOOTSTRAP_DNS_ADDRS: &[&str] = &["1.1.1.1:53"]; /// Bootstrap DNS proto pub const BOOTSTRAP_DNS_PROTO: &str = "udp"; +/// Bootstrap DNS timeout +pub const BOOTSTRAP_DNS_TIMEOUT_MSEC: usize = 500; /// Endpoint resolution period in minutes pub const ENDPOINT_RESOLUTION_PERIOD_MIN: u64 = 60; diff --git a/proxy-lib/src/error.rs b/proxy-lib/src/error.rs index aabd2cb..f3ce829 100644 --- a/proxy-lib/src/error.rs +++ b/proxy-lib/src/error.rs @@ -10,6 +10,12 @@ pub type Result = std::result::Result; pub enum DapError { #[error("Bootstrap resolver error: {0}")] BootstrapResolverError(#[from] hickory_resolver::error::ResolveError), + #[error("Bootstrap dns client error: {0}")] + BootstrapDnsClientError(#[from] hickory_client::error::ClientError), + #[error("Invalid Fqdn is given to bootstrap dns: {0}")] + InvalidFqdn(String), + #[error("Invalid bootstrap dns response")] + InvalidBootstrapDnsResponse, #[error("Url error: {0}")] UrlError(#[from] url::ParseError), diff --git a/proxy-lib/src/globals.rs b/proxy-lib/src/globals.rs index 170598b..8d32fd0 100644 --- a/proxy-lib/src/globals.rs +++ b/proxy-lib/src/globals.rs @@ -1,7 +1,4 @@ -use crate::{ - bootstrap::{BootstrapDnsInner, BootstrapDnsProto}, - constants::*, -}; +use crate::{bootstrap::BootstrapDns, constants::*}; use auth_client::AuthenticationConfig; use std::{net::SocketAddr, sync::Arc}; use tokio::{sync::Notify, time::Duration}; @@ -68,12 +65,6 @@ pub struct ProxyConfig { pub query_manipulation_config: Option>, } -#[derive(PartialEq, Eq, Debug, Clone)] -/// Bootstrap DNS Addresses -pub struct BootstrapDns { - pub inner: Vec, -} - #[derive(PartialEq, Eq, Debug, Clone)] /// doh, odoh, modoh target settings pub struct TargetConfig { @@ -128,49 +119,6 @@ impl Default for QueryManipulationConfig { } } -impl Default for BootstrapDns { - fn default() -> Self { - Self { - inner: BOOTSTRAP_DNS_ADDRS - .iter() - .map(|v| BootstrapDnsInner { - proto: ::from_str(BOOTSTRAP_DNS_PROTO).unwrap(), - addr: v.parse().unwrap(), - }) - .collect(), - } - } -} - -impl std::fmt::Display for BootstrapDns { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut first = true; - for v in &self.inner { - if !first { - write!(f, ", ")?; - } - write!(f, "{}://{}", v.proto, v.addr)?; - first = false; - } - Ok(()) - } -} - -impl TryFrom> for BootstrapDns { - type Error = anyhow::Error; - - fn try_from(value: Vec<(String, SocketAddr)>) -> Result { - let inner = value - .into_iter() - .map(|(proto, addr)| BootstrapDnsInner { - proto: ::from_str(&proto).unwrap(), - addr, - }) - .collect(); - Ok(Self { inner }) - } -} - impl Default for ProxyConfig { fn default() -> Self { Self { diff --git a/proxy-lib/src/lib.rs b/proxy-lib/src/lib.rs index ce00cb0..8df43a5 100644 --- a/proxy-lib/src/lib.rs +++ b/proxy-lib/src/lib.rs @@ -17,9 +17,8 @@ use futures::{ use std::sync::Arc; pub use auth_client::AuthenticationConfig; -pub use globals::{ - BootstrapDns, NextHopRelayConfig, ProxyConfig, QueryManipulationConfig, SubseqRelayConfig, TargetConfig, -}; +pub use bootstrap::BootstrapDns; +pub use globals::{NextHopRelayConfig, ProxyConfig, QueryManipulationConfig, SubseqRelayConfig, TargetConfig}; /// entrypoint of DoH w/ Auth Proxy /// This spawns UDP and TCP listeners and spawns the following services From c7e83e40fe9284cf37a89bc1680a6d1cdb2b82c9 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 23 Jan 2024 20:39:20 +0900 Subject: [PATCH 3/4] feat: wip, async client --- proxy-lib/src/bootstrap.rs | 84 ++++++++++++++++++++++++++++++-------- proxy-lib/src/constants.rs | 2 +- proxy-lib/src/error.rs | 2 + 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/proxy-lib/src/bootstrap.rs b/proxy-lib/src/bootstrap.rs index 84d9129..0e66d39 100644 --- a/proxy-lib/src/bootstrap.rs +++ b/proxy-lib/src/bootstrap.rs @@ -6,12 +6,20 @@ use crate::{ }; use async_trait::async_trait; use hickory_client::{ - client::{Client, ClientConnection, SyncClient}, + client::{AsyncClient, ClientHandle}, + proto::iocompat::AsyncIoTokioAsStd, rr::{DNSClass, Name, RecordType}, - tcp::{TcpClientConnection, TcpClientStream}, - udp::{UdpClientConnection, UdpClientStream}, + tcp::TcpClientStream, + udp::UdpClientStream, +}; +use hickory_proto::{ + xfer::{DnsExchangeBackground, DnsRequestSender}, + Time, +}; +use tokio::{ + net::{TcpStream as TokioTcpStream, UdpSocket as TokioUdpSocket}, + sync::Notify, }; -use tokio::net::{TcpStream as TokioTcpStream, UdpSocket as TokioUdpSocket}; use hickory_resolver::{ config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, @@ -23,6 +31,7 @@ use std::{ net::{IpAddr, SocketAddr}, str::FromStr, sync::Arc, + time::Duration, }; /* ---------------------------------------- */ @@ -116,17 +125,34 @@ pub struct BootstrapDnsInner { impl BootstrapDnsInner { /// Lookup the IP addresses associated with a name using the bootstrap resolver connection - pub(crate) fn lookup_ips(&self, target_url: &Url) -> Result> { + pub(crate) async fn lookup_ips(&self, target_url: &Url) -> Result> { // The final dot forces this to be an FQDN, otherwise the search rules as specified // in `ResolverOpts` will take effect. FQDN's are generally cheaper queries. let host = target_url .host_str() .ok_or_else(|| DapError::Other(anyhow!("Unable to parse target host name")))?; - let host = format!("{host}."); + let fqdn = format!("{host}."); + let timeout = Duration::from_millis(BOOTSTRAP_DNS_TIMEOUT_MSEC); + let bg_close_notify = Arc::new(Notify::new()); + let result_ips = match self.proto { - BootstrapDnsProto::Udp => self.lookup_ips_inner(&host, UdpClientConnection::new(self.addr)?), - BootstrapDnsProto::Tcp => self.lookup_ips_inner(&host, TcpClientConnection::new(self.addr)?), - }?; + BootstrapDnsProto::Udp => { + let stream = UdpClientStream::::with_timeout(self.addr, timeout); + let (mut client, bg) = AsyncClient::connect(stream).await?; + self + .lookup_ips_inner(&fqdn, &mut client, bg, bg_close_notify.clone()) + .await + } + BootstrapDnsProto::Tcp => { + let (stream, sender) = TcpClientStream::>::with_timeout(self.addr, timeout); + let (mut client, bg) = AsyncClient::with_timeout(stream, sender, timeout, None).await?; + self + .lookup_ips_inner(&fqdn, &mut client, bg, bg_close_notify.clone()) + .await + } + }; + bg_close_notify.notify_one(); + let result_ips = result_ips?; let port = target_url .port() @@ -141,11 +167,27 @@ impl BootstrapDnsInner { } /// Inner: Lookup the IP addresses associated with a name using the bootstrap resolver connection - fn lookup_ips_inner(&self, fqdn: &str, conn: C) -> Result> { - let client = SyncClient::new(conn); + async fn lookup_ips_inner( + &self, + fqdn: &str, + client: &mut AsyncClient, + bg: DnsExchangeBackground, + bg_close_notify: Arc, + ) -> Result> + where + S: DnsRequestSender + 'static + Send + Unpin, + TE: Time + Unpin + 'static + Send, + { + tokio::spawn(async move { + tokio::select! { + _ = bg_close_notify.notified() => debug!("Close bootstrap dns client background task"), + _ = bg => debug!("Bootstrap dns client background task finished") + } + }); let name = Name::from_str(fqdn).map_err(|e| DapError::InvalidFqdn(e.to_string()))?; + // First try to lookup an A record, if failed, try AAAA. - let response = client.query(&name, DNSClass::IN, RecordType::A)?; + let response = client.query(name.clone(), DNSClass::IN, RecordType::A).await?; let ips = response .answers() .iter() @@ -154,7 +196,7 @@ impl BootstrapDnsInner { if !ips.is_empty() { return Ok(ips); } - let response = client.query(&name, DNSClass::IN, RecordType::AAAA)?; + let response = client.query(name, DNSClass::IN, RecordType::AAAA).await?; let ipv6s = response .answers() .iter() @@ -265,14 +307,24 @@ mod tests { assert!(response.addresses.contains(&SocketAddr::from(([8, 8, 4, 4], 443)))); } - #[test] - fn test_bootstrap_dns_client_inner() { + #[tokio::test] + async fn test_bootstrap_dns_client_inner() { let inner = BootstrapDnsInner { proto: BootstrapDnsProto::Udp, addr: SocketAddr::new(IpAddr::from([8, 8, 8, 8]), 53), }; let target_url = Url::parse("https://dns.google").unwrap(); - let ips = inner.lookup_ips(&target_url).unwrap(); + let ips = inner.lookup_ips(&target_url).await.unwrap(); + + assert!(ips.contains(&SocketAddr::from(([8, 8, 8, 8], 443)))); + assert!(ips.contains(&SocketAddr::from(([8, 8, 4, 4], 443)))); + + let inner = BootstrapDnsInner { + proto: BootstrapDnsProto::Tcp, + addr: SocketAddr::new(IpAddr::from([8, 8, 8, 8]), 53), + }; + let target_url = Url::parse("https://dns.google").unwrap(); + let ips = inner.lookup_ips(&target_url).await.unwrap(); assert!(ips.contains(&SocketAddr::from(([8, 8, 8, 8], 443)))); assert!(ips.contains(&SocketAddr::from(([8, 8, 4, 4], 443)))); diff --git a/proxy-lib/src/constants.rs b/proxy-lib/src/constants.rs index 41f9d25..fa0b258 100644 --- a/proxy-lib/src/constants.rs +++ b/proxy-lib/src/constants.rs @@ -32,7 +32,7 @@ pub const BOOTSTRAP_DNS_ADDRS: &[&str] = &["1.1.1.1:53"]; /// Bootstrap DNS proto pub const BOOTSTRAP_DNS_PROTO: &str = "udp"; /// Bootstrap DNS timeout -pub const BOOTSTRAP_DNS_TIMEOUT_MSEC: usize = 500; +pub const BOOTSTRAP_DNS_TIMEOUT_MSEC: u64 = 1000; /// Endpoint resolution period in minutes pub const ENDPOINT_RESOLUTION_PERIOD_MIN: u64 = 60; diff --git a/proxy-lib/src/error.rs b/proxy-lib/src/error.rs index f3ce829..e3a9bcc 100644 --- a/proxy-lib/src/error.rs +++ b/proxy-lib/src/error.rs @@ -12,6 +12,8 @@ pub enum DapError { BootstrapResolverError(#[from] hickory_resolver::error::ResolveError), #[error("Bootstrap dns client error: {0}")] BootstrapDnsClientError(#[from] hickory_client::error::ClientError), + #[error("Bootstrap dns proto error: {0}")] + BootstrapDnsProtoError(#[from] hickory_client::proto::error::ProtoError), #[error("Invalid Fqdn is given to bootstrap dns: {0}")] InvalidFqdn(String), #[error("Invalid bootstrap dns response")] From 5a89d130bcd6ea15e86b5d4873bfa4434528837a Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 23 Jan 2024 21:50:18 +0900 Subject: [PATCH 4/4] feat: implement tcp/udp bootstrap resolever with arbitrary port --- CHANGELOG.md | 13 +++ LICENSE | 2 +- README.md | 2 +- doh-auth-proxy.toml | 1 + proxy-lib/Cargo.toml | 3 - proxy-lib/src/bootstrap.rs | 218 ++++++++++++++----------------------- proxy-lib/src/error.rs | 2 - proxy-lib/src/globals.rs | 53 ++++++++- proxy-lib/src/lib.rs | 5 +- 9 files changed, 151 insertions(+), 148 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be7f575..1f270dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,19 @@ You should also include the user name that made the change. --> +## 0.3.2 + +### Improvements + +- Feat: Support TCP and UDP bootstrap DNS protocols. In addition to the existing format like "1.1.1.1" that means "udp://1.1.1.1:53", formats like "tcp://1.1.1.1:53" (TCP support) "udp://1.2.3.4:50053" (Non standard port) works. See "README.md" and "doh-auth-proxy.toml" for configuration for the detail. +- Refactor: Lots of minor improvements + +## 0.3.1 + +### Bugfix + +- Fix several error handlers + ## 0.3.0 ### Improvements diff --git a/LICENSE b/LICENSE index 967c341..096f5e1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Jun Kurihara +Copyright (c) 2024 Jun Kurihara Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6d81773..947a6e8 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ github.com. 60 IN A 52.69.186.44 ~~~~~~~ ``` -The parameter `bootstrap-dns` is used to resolve the IP address of the host of `target-url` (i.e., target DoH server). +The parameter `bootstrap_dns` is used to resolve the IP address of the host of `target_urls` (i.e., target DoH server). The `bootstrap_dns` allows non-standard DNS ports other than `53` and TCP queries, which can be specified as an url-like format, e.g., `tcp://1.1.1.1`, `tcp://127.0.0.1:12345`, `127.0.0.1:50053`, where UDP and port `53` are used if omitted. If you run without `--config` option, i.e., simply hit `$ ./doh-auth-proxy`, the followings are applied as default parameters: diff --git a/doh-auth-proxy.toml b/doh-auth-proxy.toml index 4612e0e..746fb25 100644 --- a/doh-auth-proxy.toml +++ b/doh-auth-proxy.toml @@ -13,6 +13,7 @@ listen_addresses = ['127.0.0.1:50053', '[::1]:50053'] ## DNS (Do53) resolver addresses for bootstrap. ## You can omit protocol name and port number, default is udp over port 53. +## The first one is used for bootstrap, and the rest are used for fallback as ordered. bootstrap_dns = ["udp://8.8.8.8:53", "1.1.1.1:53", "8.8.4.4", "tcp://1.0.0.1"] ## Minutes to re-resolve the IP addr of the nexthop and authentication endpoint url diff --git a/proxy-lib/Cargo.toml b/proxy-lib/Cargo.toml index e63bde6..212cfe3 100644 --- a/proxy-lib/Cargo.toml +++ b/proxy-lib/Cargo.toml @@ -74,9 +74,6 @@ reqwest = { version = "0.11.23", default-features = false, features = [ url = "2.5.0" # for bootstrap dns resolver -hickory-resolver = { version = "0.24.0", default-features = false, features = [ - "tokio-runtime", -] } hickory-client = { version = "0.24.0", default-features = false, features = [ "dnssec", ] } diff --git a/proxy-lib/src/bootstrap.rs b/proxy-lib/src/bootstrap.rs index 0e66d39..d4dfb89 100644 --- a/proxy-lib/src/bootstrap.rs +++ b/proxy-lib/src/bootstrap.rs @@ -1,6 +1,7 @@ use crate::{ - constants::{BOOTSTRAP_DNS_ADDRS, BOOTSTRAP_DNS_PROTO, BOOTSTRAP_DNS_TIMEOUT_MSEC}, + constants::BOOTSTRAP_DNS_TIMEOUT_MSEC, error::*, + globals::BootstrapDns, log::*, trait_resolve_ips::{ResolveIpResponse, ResolveIps}, }; @@ -21,11 +22,6 @@ use tokio::{ sync::Notify, }; -use hickory_resolver::{ - config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, - name_server::{GenericConnector, TokioRuntimeProvider}, - AsyncResolver, TokioAsyncResolver, -}; use reqwest::Url; use std::{ net::{IpAddr, SocketAddr}, @@ -34,60 +30,10 @@ use std::{ time::Duration, }; -/* ---------------------------------------- */ -#[derive(PartialEq, Eq, Debug, Clone)] -/// Bootstrap DNS Addresses -pub struct BootstrapDns { - pub inner: Vec, -} - -impl Default for BootstrapDns { - fn default() -> Self { - Self { - inner: BOOTSTRAP_DNS_ADDRS - .iter() - .map(|v| BootstrapDnsInner { - proto: ::from_str(BOOTSTRAP_DNS_PROTO).unwrap(), - addr: v.parse().unwrap(), - }) - .collect(), - } - } -} - -impl std::fmt::Display for BootstrapDns { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut first = true; - for v in &self.inner { - if !first { - write!(f, ", ")?; - } - write!(f, "{}://{}", v.proto, v.addr)?; - first = false; - } - Ok(()) - } -} - -impl TryFrom> for BootstrapDns { - type Error = anyhow::Error; - - fn try_from(value: Vec<(String, SocketAddr)>) -> anyhow::Result { - let inner = value - .into_iter() - .map(|(proto, addr)| BootstrapDnsInner { - proto: ::from_str(&proto).unwrap(), - addr, - }) - .collect(); - Ok(Self { inner }) - } -} - /* ---------------------------------------- */ #[derive(PartialEq, Eq, Debug, Clone)] /// Bootstrap DNS Protocol -pub enum BootstrapDnsProto { +pub(crate) enum BootstrapDnsProto { /// UDP Udp, /// TCP @@ -116,22 +62,31 @@ impl std::fmt::Display for BootstrapDnsProto { /* ---------------------------------------- */ #[derive(PartialEq, Eq, Debug, Clone)] /// Bootstrap DNS Address with port and protocol -pub struct BootstrapDnsInner { +pub(crate) struct BootstrapDnsInner { /// protocol - pub proto: BootstrapDnsProto, + proto: BootstrapDnsProto, /// socket address - pub addr: SocketAddr, + addr: SocketAddr, +} + +impl std::fmt::Display for BootstrapDnsInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}://{}", self.proto, self.addr) + } } impl BootstrapDnsInner { + /// + pub(crate) fn try_new(proto: &str, addr: &str) -> Result { + Ok(Self { + proto: ::from_str(proto)?, + addr: addr + .parse() + .map_err(|e| DapError::Other(anyhow!("Invalid bootstrap dns address: {}", e)))?, + }) + } /// Lookup the IP addresses associated with a name using the bootstrap resolver connection - pub(crate) async fn lookup_ips(&self, target_url: &Url) -> Result> { - // The final dot forces this to be an FQDN, otherwise the search rules as specified - // in `ResolverOpts` will take effect. FQDN's are generally cheaper queries. - let host = target_url - .host_str() - .ok_or_else(|| DapError::Other(anyhow!("Unable to parse target host name")))?; - let fqdn = format!("{host}."); + pub(crate) async fn lookup_ips(&self, fqdn: &str, runtime_handle: tokio::runtime::Handle) -> Result> { let timeout = Duration::from_millis(BOOTSTRAP_DNS_TIMEOUT_MSEC); let bg_close_notify = Arc::new(Notify::new()); @@ -140,30 +95,21 @@ impl BootstrapDnsInner { let stream = UdpClientStream::::with_timeout(self.addr, timeout); let (mut client, bg) = AsyncClient::connect(stream).await?; self - .lookup_ips_inner(&fqdn, &mut client, bg, bg_close_notify.clone()) + .lookup_ips_inner(fqdn, &mut client, bg, bg_close_notify.clone(), runtime_handle) .await } BootstrapDnsProto::Tcp => { let (stream, sender) = TcpClientStream::>::with_timeout(self.addr, timeout); let (mut client, bg) = AsyncClient::with_timeout(stream, sender, timeout, None).await?; self - .lookup_ips_inner(&fqdn, &mut client, bg, bg_close_notify.clone()) + .lookup_ips_inner(fqdn, &mut client, bg, bg_close_notify.clone(), runtime_handle) .await } }; bg_close_notify.notify_one(); let result_ips = result_ips?; - let port = target_url - .port() - .unwrap_or_else(|| if target_url.scheme() == "https" { 443 } else { 80 }); - - Ok( - result_ips - .iter() - .filter_map(|addr| format!("{}:{}", addr, port).parse::().ok()) - .collect::>(), - ) + Ok(result_ips) } /// Inner: Lookup the IP addresses associated with a name using the bootstrap resolver connection @@ -173,12 +119,13 @@ impl BootstrapDnsInner { client: &mut AsyncClient, bg: DnsExchangeBackground, bg_close_notify: Arc, + runtime_handle: tokio::runtime::Handle, ) -> Result> where S: DnsRequestSender + 'static + Send + Unpin, TE: Time + Unpin + 'static + Send, { - tokio::spawn(async move { + runtime_handle.spawn(async move { tokio::select! { _ = bg_close_notify.notified() => debug!("Close bootstrap dns client background task"), _ = bg => debug!("Bootstrap dns client background task finished") @@ -212,25 +159,23 @@ impl BootstrapDnsInner { /* ---------------------------------------- */ #[derive(Clone)] /// stub resolver using bootstrap DNS resolver -pub struct BootstrapDnsResolver { - /// wrapper of trust-dns-resolver - pub inner: AsyncResolver>, +pub(crate) struct BootstrapDnsResolver { + /// booststrap dns resolvers + pub(crate) inner: BootstrapDns, + /// tokio runtime handle + pub(crate) runtime_handle: tokio::runtime::Handle, } impl BootstrapDnsResolver { - /// Build stub resolver using bootstrap dns resolver - pub async fn try_new(bootstrap_dns: &BootstrapDns, runtime_handle: tokio::runtime::Handle) -> Result { - let ips = &bootstrap_dns.inner.iter().map(|x| x.addr.ip()).collect::>(); - let port = &bootstrap_dns.inner.iter().map(|x| x.addr.port()).collect::>()[0]; - let name_servers = NameServerConfigGroup::from_ips_clear(ips, *port, true); - let resolver_config = ResolverConfig::from_parts(None, vec![], name_servers); - - let resolver = runtime_handle - .spawn(async { TokioAsyncResolver::tokio(resolver_config, ResolverOpts::default()) }) - .await - .map_err(|e| DapError::Other(anyhow!(e)))?; - - Ok(Self { inner: resolver }) + /// Build DNS client using bootstrap dns resolver + pub(crate) async fn try_new(bootstrap_dns: &BootstrapDns, runtime_handle: tokio::runtime::Handle) -> Result { + Ok(Self { + inner: bootstrap_dns.clone(), + runtime_handle, + }) + } + pub(crate) fn inner(&self) -> &[BootstrapDnsInner] { + self.inner.inner() } } @@ -240,39 +185,41 @@ impl ResolveIps for Arc { async fn resolve_ips(&self, target_url: &Url) -> Result { // The final dot forces this to be an FQDN, otherwise the search rules as specified // in `ResolverOpts` will take effect. FQDN's are generally cheaper queries. - let host_str = target_url + let host = target_url .host_str() .ok_or_else(|| DapError::Other(anyhow!("Unable to parse target host name")))?; + let fqdn = format!("{host}."); + let port = target_url .port() .unwrap_or_else(|| if target_url.scheme() == "https" { 443 } else { 80 }); - let response = self - .inner - .lookup_ip(format!("{}.", host_str)) - .await - .map_err(DapError::BootstrapResolverError)?; // There can be many addresses associated with the name, // this can return IPv4 and/or IPv6 addresses - let target_addrs = response - .iter() - .filter_map(|addr| format!("{}:{}", addr, port).parse::().ok()) - .collect::>(); + for v in self.inner().iter() { + let Ok(ips) = v + .lookup_ips(&fqdn, self.runtime_handle.clone()) + .await + .map(|p| p.iter().map(|ip| SocketAddr::new(*ip, port)).collect::>()) + else { + continue; + }; - if target_addrs.is_empty() { - return Err(DapError::Other(anyhow!( - "Invalid target url: {target_url}, cannot resolve ip address" - ))); + if !ips.is_empty() { + debug!( + "Updated socket addresses for `{}://{host}`: {ips:?} (@{v})", + target_url.scheme(), + ); + return Ok(ResolveIpResponse { + hostname: target_url.host_str().unwrap().to_string(), + addresses: ips, + }); + } } - debug!( - "Updated target url {} ip addresses by using bootstrap dns: {:?}", - host_str, target_addrs - ); - Ok(ResolveIpResponse { - hostname: host_str.to_string(), - addresses: target_addrs, - }) + Err(DapError::Other(anyhow!( + "Invalid target url: {target_url}, cannot resolve ip address" + ))) } } @@ -283,18 +230,12 @@ mod tests { #[tokio::test] async fn test_bootstrap_dns_resolver() { - let bootstrap_dns = BootstrapDns { - inner: vec![ - BootstrapDnsInner { - proto: BootstrapDnsProto::Udp, - addr: SocketAddr::new(IpAddr::from([8, 8, 8, 8]), 53), - }, - BootstrapDnsInner { - proto: BootstrapDnsProto::Tcp, - addr: SocketAddr::new(IpAddr::from([8, 8, 4, 4]), 53), - }, - ], - }; + let inner = vec![ + ("udp".to_owned(), SocketAddr::new(IpAddr::from([8, 8, 8, 8]), 53)), + ("tcp".to_owned(), SocketAddr::new(IpAddr::from([8, 8, 4, 4]), 53)), + ]; + let bootstrap_dns = BootstrapDns::try_from(inner).unwrap(); + let resolver = BootstrapDnsResolver::try_new(&bootstrap_dns, tokio::runtime::Handle::current()) .await .unwrap(); @@ -313,20 +254,21 @@ mod tests { proto: BootstrapDnsProto::Udp, addr: SocketAddr::new(IpAddr::from([8, 8, 8, 8]), 53), }; - let target_url = Url::parse("https://dns.google").unwrap(); - let ips = inner.lookup_ips(&target_url).await.unwrap(); - assert!(ips.contains(&SocketAddr::from(([8, 8, 8, 8], 443)))); - assert!(ips.contains(&SocketAddr::from(([8, 8, 4, 4], 443)))); + let runtime_handle = tokio::runtime::Handle::current(); + let ips = inner.lookup_ips("dns.google.", runtime_handle.clone()).await.unwrap(); + + assert!(ips.contains(&IpAddr::from([8, 8, 8, 8]))); + assert!(ips.contains(&IpAddr::from([8, 8, 4, 4]))); let inner = BootstrapDnsInner { proto: BootstrapDnsProto::Tcp, addr: SocketAddr::new(IpAddr::from([8, 8, 8, 8]), 53), }; - let target_url = Url::parse("https://dns.google").unwrap(); - let ips = inner.lookup_ips(&target_url).await.unwrap(); - assert!(ips.contains(&SocketAddr::from(([8, 8, 8, 8], 443)))); - assert!(ips.contains(&SocketAddr::from(([8, 8, 4, 4], 443)))); + let ips = inner.lookup_ips("dns.google.", runtime_handle).await.unwrap(); + + assert!(ips.contains(&IpAddr::from([8, 8, 8, 8]))); + assert!(ips.contains(&IpAddr::from([8, 8, 4, 4]))); } } diff --git a/proxy-lib/src/error.rs b/proxy-lib/src/error.rs index e3a9bcc..b827ffb 100644 --- a/proxy-lib/src/error.rs +++ b/proxy-lib/src/error.rs @@ -8,8 +8,6 @@ pub type Result = std::result::Result; /// Describes things that can go wrong in the Rpxy #[derive(Debug, Error)] pub enum DapError { - #[error("Bootstrap resolver error: {0}")] - BootstrapResolverError(#[from] hickory_resolver::error::ResolveError), #[error("Bootstrap dns client error: {0}")] BootstrapDnsClientError(#[from] hickory_client::error::ClientError), #[error("Bootstrap dns proto error: {0}")] diff --git a/proxy-lib/src/globals.rs b/proxy-lib/src/globals.rs index 8d32fd0..3b8b177 100644 --- a/proxy-lib/src/globals.rs +++ b/proxy-lib/src/globals.rs @@ -1,4 +1,4 @@ -use crate::{bootstrap::BootstrapDns, constants::*}; +use crate::{bootstrap::BootstrapDnsInner, constants::*}; use auth_client::AuthenticationConfig; use std::{net::SocketAddr, sync::Arc}; use tokio::{sync::Notify, time::Duration}; @@ -119,6 +119,57 @@ impl Default for QueryManipulationConfig { } } +/* ---------------------------------------- */ +#[derive(PartialEq, Eq, Debug, Clone)] +/// Bootstrap DNS Addresses +pub struct BootstrapDns { + inner: Vec, +} + +impl BootstrapDns { + /// Get bootstrap DNS addresses + pub(crate) fn inner(&self) -> &[BootstrapDnsInner] { + &self.inner + } +} + +impl Default for BootstrapDns { + fn default() -> Self { + Self { + inner: BOOTSTRAP_DNS_ADDRS + .iter() + .map(|v| BootstrapDnsInner::try_new(BOOTSTRAP_DNS_PROTO, v).unwrap()) + .collect(), + } + } +} + +impl std::fmt::Display for BootstrapDns { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for v in &self.inner { + if !first { + write!(f, ", ")?; + } + write!(f, "{v}")?; + first = false; + } + Ok(()) + } +} + +impl TryFrom> for BootstrapDns { + type Error = anyhow::Error; + + fn try_from(value: Vec<(String, SocketAddr)>) -> anyhow::Result { + let inner = value + .into_iter() + .map(|(proto, addr)| BootstrapDnsInner::try_new(&proto, &addr.to_string()).unwrap()) + .collect(); + Ok(Self { inner }) + } +} + impl Default for ProxyConfig { fn default() -> Self { Self { diff --git a/proxy-lib/src/lib.rs b/proxy-lib/src/lib.rs index 8df43a5..ce00cb0 100644 --- a/proxy-lib/src/lib.rs +++ b/proxy-lib/src/lib.rs @@ -17,8 +17,9 @@ use futures::{ use std::sync::Arc; pub use auth_client::AuthenticationConfig; -pub use bootstrap::BootstrapDns; -pub use globals::{NextHopRelayConfig, ProxyConfig, QueryManipulationConfig, SubseqRelayConfig, TargetConfig}; +pub use globals::{ + BootstrapDns, NextHopRelayConfig, ProxyConfig, QueryManipulationConfig, SubseqRelayConfig, TargetConfig, +}; /// entrypoint of DoH w/ Auth Proxy /// This spawns UDP and TCP listeners and spawns the following services