diff --git a/proxy-lib/src/constants.rs b/proxy-lib/src/constants.rs index f79500f..725f89e 100644 --- a/proxy-lib/src/constants.rs +++ b/proxy-lib/src/constants.rs @@ -96,6 +96,10 @@ pub const HEALTHCHECK_TARGET_ADDR: &str = "8.8.8.8"; pub const BLOCK_MESSAGE_HINFO_CPU: &str = "BLOCKED"; /// Block message for query manipulation (HINFO OS field) pub const BLOCK_MESSAGE_HINFO_OS: &str = "POWERED-BY-DOH-AUTH-PROXY"; +/// Not-forwarded message for query manipulation (HINFO CPU field) +pub const NOT_FORWARDED_MESSAGE_HINFO_CPU: &str = "NOT-FORWARDED-BY-DEFAULT"; +/// Not-forwarded message for query manipulation (HINFO OS field) +pub const NOT_FORWARDED_MESSAGE_HINFO_OS: &str = "POWERED-BY-DOH-AUTH-PROXY"; // Logging /// Query log channel size diff --git a/proxy-lib/src/doh_client/dns_message.rs b/proxy-lib/src/doh_client/dns_message.rs index d3acbbe..1b0c3cc 100644 --- a/proxy-lib/src/doh_client/dns_message.rs +++ b/proxy-lib/src/doh_client/dns_message.rs @@ -120,6 +120,14 @@ pub fn build_response_nx(msg: &Message) -> Message { res } +/// Build a DNS response message with REFUSED +pub fn build_response_refused(msg: &Message) -> Message { + let mut res = msg.clone(); + res.set_message_type(hickory_proto::op::MessageType::Response); + res.set_response_code(hickory_proto::op::ResponseCode::Refused); + res +} + /// Build a DNS response message for given QueryKey and IP address pub fn build_response_given_ipaddr(msg: &Message, q_key: &QueryKey, ipaddr: &IpAddr, min_ttl: u32) -> anyhow::Result { let mut res = msg.clone(); diff --git a/proxy-lib/src/doh_client/doh_client_main.rs b/proxy-lib/src/doh_client/doh_client_main.rs index 16226b2..1ef1af6 100644 --- a/proxy-lib/src/doh_client/doh_client_main.rs +++ b/proxy-lib/src/doh_client/doh_client_main.rs @@ -45,7 +45,7 @@ pub struct DoHClient { /// health check interval pub(super) healthcheck_period_sec: tokio::time::Duration, /// Query manipulation pulugins - query_manipulators: Option, + query_manipulators: QueryManipulators, /// Query logging sender query_log_tx: crossbeam_channel::Sender, } @@ -119,10 +119,10 @@ impl DoHClient { let healthcheck_period_sec = globals.proxy_config.healthcheck_period_sec; // query manipulators - let query_manipulators: Option = if let Some(q) = &globals.proxy_config.query_manipulation_config { - q.as_ref().try_into().ok() + let query_manipulators: QueryManipulators = if let Some(q) = &globals.proxy_config.query_manipulation_config { + q.as_ref().try_into().unwrap_or_default() } else { - None + QueryManipulators::default() }; Ok(Self { @@ -186,20 +186,29 @@ impl DoHClient { })?; // Process query plugins from the beginning of vec, e.g., domain filtering, cloaking, etc. - if let Some(manipulators) = &self.query_manipulators { - let execution_result = manipulators.apply(&query_msg, &req.0[0]).await?; - match execution_result { - QueryManipulationResult::PassThrough => (), - QueryManipulationResult::SyntheticResponseBlocked(response_msg) => { - let res = dns_message::encode(&response_msg)?; - self.log_dns_message(&res, proto, src, DoHResponseType::Blocked, None, start); - return Ok(res); - } - QueryManipulationResult::SyntheticResponseOverridden(response_msg) => { - let res = dns_message::encode(&response_msg)?; - self.log_dns_message(&res, proto, src, DoHResponseType::Overridden, None, start); - return Ok(res); - } + + let execution_result = self.query_manipulators.apply(&query_msg, &req.0[0]).await?; + match execution_result { + QueryManipulationResult::PassThrough => (), + QueryManipulationResult::SyntheticResponseBlocked(response_msg) => { + let res = dns_message::encode(&response_msg)?; + self.log_dns_message(&res, proto, src, DoHResponseType::Blocked, None, start); + return Ok(res); + } + QueryManipulationResult::SyntheticResponseOverridden(response_msg) => { + let res = dns_message::encode(&response_msg)?; + self.log_dns_message(&res, proto, src, DoHResponseType::Overridden, None, start); + return Ok(res); + } + QueryManipulationResult::SyntheticResponseNotForwarded(response_msg) => { + let res = dns_message::encode(&response_msg)?; + self.log_dns_message(&res, proto, src, DoHResponseType::NotForwarded, None, start); + return Ok(res); + } + QueryManipulationResult::SyntheticResponseDefaultHost(response_msg) => { + let res = dns_message::encode(&response_msg)?; + self.log_dns_message(&res, proto, src, DoHResponseType::DefaultHost, None, start); + return Ok(res); } } diff --git a/proxy-lib/src/doh_client/manipulation/default_rule.rs b/proxy-lib/src/doh_client/manipulation/default_rule.rs new file mode 100644 index 0000000..5fed6b0 --- /dev/null +++ b/proxy-lib/src/doh_client/manipulation/default_rule.rs @@ -0,0 +1,159 @@ +use super::{ + super::{ + dns_message::{build_response_given_ipaddr, build_response_refused, QueryKey}, + error::DohClientError, + }, + inspect_query_name, QueryManipulation, QueryManipulationResult, +}; +use crate::{ + constants::{NOT_FORWARDED_MESSAGE_HINFO_CPU, NOT_FORWARDED_MESSAGE_HINFO_OS}, + log::*, +}; +use async_trait::async_trait; +use hickory_proto::{op::Message, rr}; +use match_domain::DomainMatchingRule; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +/* -------------------------------------------------------- */ +/// Default not-forwarded domains +const DEFAULT_NOT_FORWARDED_DOMAINS: &[&str] = &[ + // https://www.rfc-editor.org/rfc/rfc9462.html#name-caching-forwarders + "resolver.arpa", +]; +/// Default localhost +const DEFAULT_LOCAL_DOMAINS: &[&str] = &["localhost", "localhost.localdomain"]; +/// Default broadcast +const DEFAULT_BROADCAST_DOMAINS: &[&str] = &["broadcasthost"]; + +#[inline] +fn build_local_v4_response(query_message: &Message, query_key: &QueryKey) -> anyhow::Result { + let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + build_response_given_ipaddr(query_message, query_key, &addr, 0) +} +#[inline] +fn build_local_v6_response(query_message: &Message, query_key: &QueryKey) -> anyhow::Result { + let addr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); + build_response_given_ipaddr(query_message, query_key, &addr, 0) +} +#[inline] +// only v4 +fn build_broadcast_response(query_message: &Message, query_key: &QueryKey) -> anyhow::Result { + let addr = IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255)); + build_response_given_ipaddr(query_message, query_key, &addr, 0) +} +/* -------------------------------------------------------- */ + +#[async_trait] +impl QueryManipulation for DefaultRule { + type Error = DohClientError; + + /// Apply query plugin + async fn apply(&self, query_message: &Message, query_key: &QueryKey) -> Result { + let q_name = inspect_query_name(&query_key.query_name)?; + + // Check if the query is for not-forwarded domains + if self.is_not_forwarded(q_name.as_str())? { + debug!( + "[Not-Forwarded] {} {:?} {:?}", + query_key.query_name, query_key.query_type, query_key.query_class + ); + let response_msg = build_response_not_forwarded(query_message); + return Ok(QueryManipulationResult::SyntheticResponseNotForwarded(response_msg)); + } + + // Check if the query is for localhost + if self.is_localhost(q_name.as_str()) { + debug!( + "[LocalHost] {} {:?} {:?}", + query_key.query_name, query_key.query_type, query_key.query_class + ); + + let response_msg = match query_key.query_type { + rr::RecordType::A => build_local_v4_response(query_message, query_key)?, + rr::RecordType::AAAA => build_local_v6_response(query_message, query_key)?, + _ => build_response_refused(query_message), + }; + return Ok(QueryManipulationResult::SyntheticResponseDefaultHost(response_msg)); + } + + // Check if the query is for broadcast + if self.is_broadcast(q_name.as_str()) { + debug!( + "[Broadcast] {} {:?} {:?}", + query_key.query_name, query_key.query_type, query_key.query_class + ); + + let response_msg = match query_key.query_type { + rr::RecordType::A => build_broadcast_response(query_message, query_key)?, + _ => build_response_refused(query_message), + }; + return Ok(QueryManipulationResult::SyntheticResponseDefaultHost(response_msg)); + } + + return Ok(QueryManipulationResult::PassThrough); + } +} + +/// Build a synthetic response message for default not-forwarded domains +fn build_response_not_forwarded(query_message: &Message) -> Message { + let mut msg = build_response_refused(query_message); + let hinfo = rr::rdata::HINFO::new( + NOT_FORWARDED_MESSAGE_HINFO_CPU.to_string(), + NOT_FORWARDED_MESSAGE_HINFO_OS.to_string(), + ); + msg.add_answer(rr::Record::from_rdata( + query_message.queries()[0].name().clone(), + 0, + rr::RData::HINFO(hinfo), + )); + msg +} + +#[derive(Debug, Clone)] +/// NotForwardedRule is a query manipulation rule that refuses queries based on domain matching +/// This is a default rule, handling the regulations of IETF RFC +pub struct DefaultRule { + /// inner domain matching rule + not_forwarded: DomainMatchingRule, +} + +impl DefaultRule { + /// Create a new NotForwardedRule + pub fn new() -> Self { + let not_forwarded = DomainMatchingRule::try_from(DEFAULT_NOT_FORWARDED_DOMAINS).unwrap(); + DefaultRule { not_forwarded } + } + + /// Check if the query key is in blocklist + fn is_not_forwarded(&self, q_name: &str) -> anyhow::Result { + Ok(self.not_forwarded.is_matched(q_name)) + } + + /// Check if the query key is for localhost + fn is_localhost(&self, q_name: &str) -> bool { + DEFAULT_LOCAL_DOMAINS.iter().any(|&d| d.eq(q_name)) + } + /// Check if the query key is for broadcast + fn is_broadcast(&self, q_name: &str) -> bool { + DEFAULT_BROADCAST_DOMAINS.iter().any(|&d| d.eq(q_name)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_works() { + let default_rule = DefaultRule::new(); + + assert!(default_rule.is_not_forwarded("resolver.arpa").unwrap()); + assert!(default_rule.is_not_forwarded("_dns.resolver.arpa").unwrap()); + assert!(default_rule.is_localhost("localhost")); + assert!(default_rule.is_localhost("localhost.localdomain")); + assert!(!default_rule.is_localhost("localhost.localdomain.com")); + assert!(!default_rule.is_localhost("x.localhost.localdomain")); + assert!(default_rule.is_broadcast("broadcasthost")); + assert!(!default_rule.is_broadcast("broadcasthost.com")); + } +} diff --git a/proxy-lib/src/doh_client/manipulation/domain_block.rs b/proxy-lib/src/doh_client/manipulation/domain_block.rs index 1250316..a9f64f3 100644 --- a/proxy-lib/src/doh_client/manipulation/domain_block.rs +++ b/proxy-lib/src/doh_client/manipulation/domain_block.rs @@ -3,14 +3,13 @@ use super::{ dns_message::{build_response_nx, QueryKey}, error::DohClientError, }, - QueryManipulation, QueryManipulationResult, + inspect_query_name, QueryManipulation, QueryManipulationResult, }; use crate::{ constants::{BLOCK_MESSAGE_HINFO_CPU, BLOCK_MESSAGE_HINFO_OS}, log::*, QueryManipulationConfig, }; -use anyhow::bail; use async_trait::async_trait; use hickory_proto::{op::Message, rr}; use match_domain::DomainMatchingRule; @@ -67,19 +66,8 @@ impl TryFrom<&QueryManipulationConfig> for Option { impl DomainBlockRule { /// Check if the query key is in blocklist pub fn in_blocklist(&self, q_key: &QueryKey) -> anyhow::Result { - // remove final dot - let mut nn = q_key.clone().query_name.to_ascii_lowercase(); - match nn.pop() { - Some(dot) => { - if dot != '.' { - bail!("Invalid query name as fqdn (missing final dot): {}", nn); - } - } - None => { - bail!("Missing query name"); - } - } - + // remove final dot and convert to lowercase + let nn = inspect_query_name(q_key.query_name.as_str())?; Ok(self.inner.is_matched(&nn)) } } diff --git a/proxy-lib/src/doh_client/manipulation/domain_override.rs b/proxy-lib/src/doh_client/manipulation/domain_override.rs index 2cafad2..a8dcc71 100644 --- a/proxy-lib/src/doh_client/manipulation/domain_override.rs +++ b/proxy-lib/src/doh_client/manipulation/domain_override.rs @@ -3,6 +3,7 @@ use super::{ dns_message::{build_response_given_ipaddr, QueryKey}, error::DohClientError, }, + inspect_query_name, regexp_vals::*, QueryManipulation, QueryManipulationResult, }; @@ -95,19 +96,10 @@ impl TryFrom<&QueryManipulationConfig> for Option { impl DomainOverrideRule { pub fn find_mapping(&self, q_key: &QueryKey) -> Option<&MapsTo> { let q_type = q_key.query_type; - // remove final dot - let mut nn = q_key.clone().query_name.to_ascii_lowercase(); - match nn.pop() { - Some(dot) => { - if dot != '.' { - return None; - } - } - None => { - warn!("Null request!"); - return None; - } - } + + // remove final dot and convert to lowercase + let nn = inspect_query_name(q_key.query_name.as_str()).ok()?; + // find matches if let Some(targets) = self.inner.get(&nn) { targets.iter().find(|x| match x { diff --git a/proxy-lib/src/doh_client/manipulation/mod.rs b/proxy-lib/src/doh_client/manipulation/mod.rs index bdc70d7..72e3bd0 100644 --- a/proxy-lib/src/doh_client/manipulation/mod.rs +++ b/proxy-lib/src/doh_client/manipulation/mod.rs @@ -1,14 +1,31 @@ +mod default_rule; mod domain_block; mod domain_override; mod regexp_vals; +use self::{default_rule::DefaultRule, domain_block::DomainBlockRule, domain_override::DomainOverrideRule}; use super::{dns_message::QueryKey, error::DohClientError}; use crate::QueryManipulationConfig; use async_trait::async_trait; -use domain_block::DomainBlockRule; -use domain_override::DomainOverrideRule; use hickory_proto::op::Message; +/* -------------------------------------------------------- */ +fn inspect_query_name(query_name: &str) -> anyhow::Result { + let mut nn = query_name.to_ascii_lowercase(); + match nn.pop() { + Some(dot) => { + if dot != '.' { + anyhow::bail!("Invalid query name as fqdn (missing final dot): {}", nn); + } + } + None => { + anyhow::bail!("Missing query name"); + } + } + Ok(nn) +} +/* -------------------------------------------------------- */ + /// Result of application of query manipulation for a given query pub enum QueryManipulationResult { /// Pass the query with no manipulator application @@ -19,6 +36,12 @@ pub enum QueryManipulationResult { /// By the query manipulation, synthetic response is generated /// Overridden response message SyntheticResponseOverridden(Message), + /// By the query manipulation, synthetic response is generated + /// Not-Forwarded response message, this is a default rule due to the nature of DNS forwarder + SyntheticResponseNotForwarded(Message), + /// By the query manipulation, synthetic response is generated + /// Response message for localhost.localdomain, this is a default rule due to the nature of DNS forwarder + SyntheticResponseDefaultHost(Message), } #[async_trait] @@ -50,6 +73,15 @@ impl QueryManipulators { } } +impl Default for QueryManipulators { + fn default() -> Self { + let default_rule = DefaultRule::new(); + QueryManipulators { + manipulators: vec![Box::new(default_rule) as Box + Send + Sync>], + } + } +} + impl TryFrom<&QueryManipulationConfig> for QueryManipulators { type Error = anyhow::Error; fn try_from(config: &QueryManipulationConfig) -> std::result::Result { @@ -65,6 +97,10 @@ impl TryFrom<&QueryManipulationConfig> for QueryManipulators { manipulators.push(Box::new(domain_block) as Box + Send + Sync>); } + // Default rule is the last one + let default_rule = DefaultRule::new(); + manipulators.push(Box::new(default_rule) as Box + Send + Sync>); + Ok(QueryManipulators { manipulators }) } } @@ -96,6 +132,6 @@ mod tests { assert!(manipulators.is_ok()); let manipulators = manipulators.unwrap(); - assert_eq!(manipulators.len(), 2); + assert_eq!(manipulators.len(), 3); } } diff --git a/proxy-lib/src/doh_client/mod.rs b/proxy-lib/src/doh_client/mod.rs index 180ad02..73074ae 100644 --- a/proxy-lib/src/doh_client/mod.rs +++ b/proxy-lib/src/doh_client/mod.rs @@ -18,6 +18,10 @@ pub enum DoHResponseType { Blocked, /// Overridden response Overridden, + /// Not forwarded due to the nature of dns forwarding (like resolver.arpa) + NotForwarded, + /// Overridden response for the localhost and broadcast addresses + DefaultHost, /// Cached response Cached, /// Standard response fetched from upstream @@ -29,6 +33,8 @@ impl std::fmt::Display for DoHResponseType { match self { DoHResponseType::Blocked => write!(f, "Blocked"), DoHResponseType::Overridden => write!(f, "Overridden"), + DoHResponseType::NotForwarded => write!(f, "NotForwarded"), + DoHResponseType::DefaultHost => write!(f, "DefaultHost"), DoHResponseType::Cached => write!(f, "Cached"), DoHResponseType::Normal => write!(f, "Normal"), } diff --git a/proxy-lib/src/log.rs b/proxy-lib/src/log.rs index 2b18d98..581fcf6 100644 --- a/proxy-lib/src/log.rs +++ b/proxy-lib/src/log.rs @@ -70,6 +70,8 @@ impl QueryLoggingBase { let dst = match self.res_type { DoHResponseType::Blocked => "blocked".to_owned(), DoHResponseType::Overridden => "overridden".to_owned(), + DoHResponseType::NotForwarded => "not_forwarded".to_owned(), + DoHResponseType::DefaultHost => "default_host".to_owned(), DoHResponseType::Cached => "cached".to_owned(), DoHResponseType::Normal => { if let Some(dst_url) = &self.dst_url {