From cb62effd94e382ecc23b2b9b6bb326766c92b86a Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 10 May 2020 04:18:07 +0800 Subject: [PATCH] Refine handling of ANY queries and CNAME records in answer --- src/acl/mod.rs | 84 +++++++------------- src/context.rs | 52 +++---------- src/relay/dnsrelay/mod.rs | 156 +++++++++++++++++++++++++++++--------- 3 files changed, 160 insertions(+), 132 deletions(-) diff --git a/src/acl/mod.rs b/src/acl/mod.rs index 1b9dedce..c24d0fbc 100644 --- a/src/acl/mod.rs +++ b/src/acl/mod.rs @@ -6,17 +6,14 @@ use std::{ fmt, fs::File, io::{self, BufRead, BufReader, Error, ErrorKind}, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + net::{IpAddr, SocketAddr}, path::Path, }; use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use iprange::IpRange; use regex::{RegexSet, RegexSetBuilder}; -use trust_dns_proto::{ - op::Query, - rr::{DNSClass, Name, RecordType}, -}; +use trust_dns_proto::rr::Name; use crate::{context::Context, relay::socks5::Address}; @@ -89,14 +86,14 @@ impl Rules { self.rule.is_match(host) } - /// Check if there are no rules for the corresponding DNS query type - fn is_rule_empty_for_qtype(&self, qtype: RecordType) -> bool { - match qtype { - RecordType::A => self.ipv4.iter().next().is_none(), - RecordType::AAAA => self.ipv6.iter().next().is_none(), - RecordType::PTR => panic!("PTR records should not reach here"), - _ => true - } + /// Check if there are no rules for IPv4 addresses + fn is_ipv4_empty(&self) -> bool { + self.ipv4.iter().next().is_none() + } + + /// Check if there are no rules for IPv6 addresses + fn is_ipv6_empty(&self) -> bool { + self.ipv6.iter().next().is_none() } } @@ -283,16 +280,13 @@ impl AccessControl { /// Check if domain name is in proxy_list. /// If so, it should be resolved from remote (for Android's DNS relay) - pub fn check_query_in_proxy_list(&self, query: &Query) -> Option { - if query.query_class() != DNSClass::IN || !query.name().is_fqdn() { - // unconditionally use default for all non-IN queries and PQDNs + pub fn check_name_in_proxy_list(&self, name: &Name) -> Option { + if !name.is_fqdn() { + // unconditionally use default for PQDNs return Some(self.is_default_in_proxy_list()); } - if query.query_type() == RecordType::PTR { - return Some(self.check_ptr_qname_in_proxy_list(query.name())); - } // remove the last dot from fqdn name - let mut name = query.name().to_ascii(); + let mut name = name.to_ascii(); name.pop(); let addr = Address::DomainNameAddress(name, 0); // Addresses in proxy_list will be proxied @@ -302,48 +296,22 @@ impl AccessControl { if self.black_list.check_address_matched(&addr) { return Some(false); } - match self.mode { - Mode::BlackList => if self.black_list.is_rule_empty_for_qtype(query.query_type()) { - return Some(true); - }, - Mode::WhiteList => if self.white_list.is_rule_empty_for_qtype(query.query_type()) { - return Some(false); - }, - } None } - fn check_ptr_qname_in_proxy_list(&self, name: &Name) -> bool { - let mut iter = name.iter().rev(); - let mut next = || std::str::from_utf8(iter.next().unwrap_or(&[48])).unwrap_or("*"); - if !"arpa".eq_ignore_ascii_case(next()) { - return self.is_default_in_proxy_list(); + /// If there are no IPv4 rules + pub fn is_ipv4_empty(&self) -> bool { + match self.mode { + Mode::BlackList => self.black_list.is_ipv4_empty(), + Mode::WhiteList => self.white_list.is_ipv4_empty(), } - match &next().to_ascii_lowercase()[..] { - "in-addr" => { - let mut octets: [u8; 4] = [0; 4]; - for octet in octets.iter_mut() { - match next().parse() { - Ok(result) => *octet = result, - Err(_) => return self.is_default_in_proxy_list(), - } - } - self.check_ip_in_proxy_list(&IpAddr::V4(Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]))) - } - "ip6" => { - let mut segments: [u16; 8] = [0; 8]; - for segment in segments.iter_mut() { - match u16::from_str_radix(&[next(), next(), next(), next()].concat(), 16) { - Ok(result) => *segment = result, - Err(_) => return self.is_default_in_proxy_list(), - } - } - self.check_ip_in_proxy_list(&IpAddr::V6(Ipv6Addr::new( - segments[0], segments[1], segments[2], segments[3], - segments[4], segments[5], segments[6], segments[7] - ))) - } - _ => self.is_default_in_proxy_list(), + } + + /// If there are no IPv4 rules + pub fn is_ipv6_empty(&self) -> bool { + match self.mode { + Mode::BlackList => self.black_list.is_ipv6_empty(), + Mode::WhiteList => self.white_list.is_ipv6_empty(), } } diff --git a/src/context.rs b/src/context.rs index 47194495..8c1fb02d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -17,7 +17,6 @@ use bloomfilter::Bloom; use lru_time_cache::LruCache; use spin::Mutex; use tokio::runtime::Handle; -use trust_dns_proto::op::Query; #[cfg(feature = "trust-dns")] use trust_dns_resolver::TokioAsyncResolver; @@ -26,6 +25,7 @@ use crate::relay::dns_resolver::create_resolver; #[cfg(feature = "local-flow-stat")] use crate::relay::flow::ServerFlowStatistic; use crate::{ + acl::AccessControl, config::{Config, ConfigType, ServerConfig}, relay::{dns_resolver::resolve, socks5::Address}, }; @@ -269,7 +269,7 @@ impl Context { /// Check client ACL (for server) pub fn check_client_blocked(&self, addr: &SocketAddr) -> bool { - match self.config.acl { + match self.acl() { None => false, Some(ref a) => a.check_client_blocked(addr), } @@ -277,7 +277,7 @@ impl Context { /// Check outbound address ACL (for server) pub fn check_outbound_blocked(&self, addr: &Address) -> bool { - match self.config.acl { + match self.acl() { None => false, Some(ref a) => a.check_outbound_blocked(addr), } @@ -285,7 +285,7 @@ impl Context { /// Check resolved outbound address ACL (for server) pub fn check_resolved_outbound_blocked(&self, addr: &SocketAddr) -> bool { - match self.config.acl { + match self.acl() { None => false, Some(ref a) => a.check_resolved_outbound_blocked(addr), } @@ -294,7 +294,11 @@ impl Context { /// Add a record to the reverse lookup cache #[cfg(feature = "local-dns-relay")] pub fn add_to_reverse_lookup_cache(&self, addr: &IpAddr, forward: bool) { - let is_exception = self.check_ip_in_proxy_list(addr) != forward; + let is_exception = forward != match self.acl() { + // Proxy everything by default + None => true, + Some(ref a) => a.check_ip_in_proxy_list(addr) + }; let mut reverse_lookup_cache = self.reverse_lookup_cache.lock(); match reverse_lookup_cache.get_mut(addr) { Some(value) => { @@ -313,45 +317,13 @@ impl Context { } } - /// Check if domain name is in proxy_list. - /// If so, it should be resolved from remote (for Android's DNS relay) - pub fn check_query_in_proxy_list(&self, query: &Query) -> Option { - match self.config.acl { - // Proxy everything by default - None => Some(true), - Some(ref a) => a.check_query_in_proxy_list(query), - } - } - - #[cfg(feature = "local-dns-relay")] - pub fn check_ip_in_proxy_list(&self, ip: &IpAddr) -> bool { - match self.config.acl { - // Proxy everything by default - None => true, - Some(ref a) => { - // do the reverse lookup in our local cache - let mut reverse_lookup_cache = self.reverse_lookup_cache.lock(); - // if a qname is found - if let Some(forward) = reverse_lookup_cache.get(ip) { - *forward - } else { - a.check_ip_in_proxy_list(ip) - } - } - } - } - - pub fn is_default_in_proxy_list(&self) -> bool { - match self.config.acl { - // Proxy everything by default - None => true, - Some(ref a) => a.is_default_in_proxy_list(), - } + pub fn acl(&self) -> &Option { + &self.config.acl } /// Check target address ACL (for client) pub async fn check_target_bypassed(&self, target: &Address) -> bool { - match self.config.acl { + match self.acl() { // Proxy everything by default None => false, Some(ref a) => { diff --git a/src/relay/dnsrelay/mod.rs b/src/relay/dnsrelay/mod.rs index 531cd23f..69dc3281 100644 --- a/src/relay/dnsrelay/mod.rs +++ b/src/relay/dnsrelay/mod.rs @@ -1,24 +1,20 @@ use std::{ io, - net::{IpAddr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, sync::Arc, time::Duration, }; use tokio::sync::mpsc; -#[cfg(not(target_os = "android"))] -use std::net::{Ipv4Addr, SocketAddrV4}; -#[cfg(not(target_os = "android"))] -use tokio::net::UdpSocket; - use log::{debug, error, info, warn}; use trust_dns_proto::{ op::{header::MessageType, response_code::ResponseCode, Message, Query}, - rr::RData, + rr::{DNSClass, Name, RData, RecordType}, }; use crate::{ + acl::AccessControl, config::ConfigType, context::SharedContext, relay::{ @@ -30,8 +26,111 @@ use crate::{ mod upstream; +fn should_forward_by_ptr_name(acl: &AccessControl, name: &Name) -> bool { + let mut iter = name.iter().rev(); + let mut next = || std::str::from_utf8(iter.next().unwrap_or(&[48])).unwrap_or("*"); + if !"arpa".eq_ignore_ascii_case(next()) { + return acl.is_default_in_proxy_list(); + } + match &next().to_ascii_lowercase()[..] { + "in-addr" => { + let mut octets: [u8; 4] = [0; 4]; + for octet in octets.iter_mut() { + match next().parse() { + Ok(result) => *octet = result, + Err(_) => return acl.is_default_in_proxy_list(), + } + } + acl.check_ip_in_proxy_list(&IpAddr::V4(Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]))) + } + "ip6" => { + let mut segments: [u16; 8] = [0; 8]; + for segment in segments.iter_mut() { + match u16::from_str_radix(&[next(), next(), next(), next()].concat(), 16) { + Ok(result) => *segment = result, + Err(_) => return acl.is_default_in_proxy_list(), + } + } + acl.check_ip_in_proxy_list(&IpAddr::V6(Ipv6Addr::new( + segments[0], segments[1], segments[2], segments[3], segments[4], segments[5], segments[6], segments[7] + ))) + } + _ => acl.is_default_in_proxy_list(), + } +} + +/// given the query, determine whether remote/local query should be used, or inconclusive +fn should_forward_by_query(acl: &Option, query: &Query) -> Option { + if let Some(acl) = acl { + if query.query_class() != DNSClass::IN { + // unconditionally use default for all non-IN queries + Some(acl.is_default_in_proxy_list()) + } else if query.query_type() == RecordType::PTR { + Some(should_forward_by_ptr_name(acl, query.name())) + } else { + let result = acl.check_name_in_proxy_list(query.name()); + if result == None && match query.query_type() { + RecordType::A => acl.is_ipv4_empty(), + RecordType::AAAA => acl.is_ipv6_empty(), + RecordType::ANY => acl.is_ipv4_empty() && acl.is_ipv6_empty(), + RecordType::PTR => panic!("PTR records should not reach here"), + _ => true, + } { + Some(acl.is_default_in_proxy_list()) + } else { + result + } + } + } else { + Some(true) + } +} + +/// given the local response, determine whether remote response should be used instead +fn should_forward_by_response( + acl: &Option, + local_response: &io::Result, + query: &Query, +) -> bool { + if let Some(acl) = acl { + macro_rules! examine_record { + ($rec:ident, $is_answer:expr) => { + if let RData::CNAME(ref name) = $rec.rdata() { + match acl.check_name_in_proxy_list(name) { + Some(value) => return value, + None => continue, + } + } else if $is_answer && !query.query_type().is_any() && $rec.record_type() != query.query_type() { + warn!("local DNS response has inconsistent answer type {} for query {}", $rec.record_type(), query); + return true; + } + let forward = match $rec.rdata() { + RData::A(ref ip) => acl.check_ip_in_proxy_list(&IpAddr::from(*ip)), + RData::AAAA(ref ip) => acl.check_ip_in_proxy_list(&IpAddr::from(*ip)), + RData::PTR(_) => panic!("PTR records should not reach here"), + _ => acl.is_default_in_proxy_list(), + }; + if !forward { + return false; + } + }; + } + if let Ok(ref local_response) = local_response { + for rec in local_response.answers() { + examine_record!(rec, true); + } + for rec in local_response.additionals() { + examine_record!(rec, false); + } + } + true + } else { + panic!("should not reach here") + } +} + async fn acl_lookup( - context: SharedContext, + acl: &Option, local: Arc, remote: Arc, query: &Query @@ -49,42 +148,31 @@ async fn acl_lookup( let remote_response_fut = try_timeout(remote.lookup(query), Some(Duration::new(3, 0))); let local_response_fut = try_timeout(local.lookup(query), Some(Duration::new(3, 0))); - match context.check_query_in_proxy_list(query) { + match should_forward_by_query(acl, query) { Some(true) => { let remote_response = remote_response_fut.await; - debug!("pick remote response (qname): {:?}", remote_response); + debug!("pick remote response (query): {:?}", remote_response); return (remote_response, true); } Some(false) => { let local_response = local_response_fut.await; - debug!("pick local response (qname): {:?}", local_response); + debug!("pick local response (query): {:?}", local_response); return (local_response, false); } None => (), } // FIXME: spawn(remote_response_fut) - let local_response = local_response_fut.await.unwrap_or_else(|_| Message::new()); - for rec in local_response.answers() { - if rec.record_type() != query.query_type() { - warn!("local DNS response has inconsistent answer type {} for query {}", rec.record_type(), query); - break - } - let forward = match rec.rdata() { - RData::A(ref ip) => context.check_ip_in_proxy_list(&IpAddr::from(*ip)), - RData::AAAA(ref ip) => context.check_ip_in_proxy_list(&IpAddr::from(*ip)), - RData::PTR(_) => panic!("PTR records should not reach here"), - _ => context.is_default_in_proxy_list(), - }; - if !forward { - debug!("pick local response (response): {:?}", local_response); - return (Ok(local_response), false); - } - } + let local_response = local_response_fut.await; - let remote_response = remote_response_fut.await; - debug!("pick remote response (response): {:?}", remote_response); - (remote_response, true) + if should_forward_by_response(acl, &local_response, query) { + let remote_response = remote_response_fut.await; + debug!("pick remote response (response): {:?}", remote_response); + (remote_response, true) + } else { + debug!("pick local response (response): {:?}", local_response); + (local_response, false) + } } /// Start a DNS relay local server @@ -123,11 +211,11 @@ pub async fn run(context: SharedContext) -> io::Result<()> { let config = context.config(); #[cfg(target_os = "android")] - let local_upstream = Arc::new(upstream::UnixSocketUpstream { + let local_upstream = Arc::new(upstream::UnixSocketUpstream { path: config.local_dns_path.clone().expect("local query DNS path"), }); #[cfg(not(target_os = "android"))] - let local_upstream = Arc::new(upstream::UdpUpstream { + let local_upstream = Arc::new(upstream::UdpUpstream { server: config.local_dns_addr.clone().expect("local query DNS address"), }); // FIXME: We use TCP to send remote queries by default, which should be configuable. @@ -174,7 +262,7 @@ pub async fn run(context: SharedContext) -> io::Result<()> { message.set_response_code(ResponseCode::FormErr); } else { let question = &request.queries()[0]; - let (r, forward) = acl_lookup(context.clone(), local_upstream, remote_upstream, question).await; + let (r, forward) = acl_lookup(context.acl(), local_upstream, remote_upstream, question).await; if let Ok(result) = r { for rec in result.answers() {