From e30a188386c8e6287bb0acb22597caf0a48e792c Mon Sep 17 00:00:00 2001 From: dev0 Date: Fri, 27 Sep 2024 00:47:14 +1000 Subject: [PATCH] try to match hostnames when clash dns used --- clash/tests/data/config/rules.yaml | 4 +- .../src/app/dispatcher/dispatcher_impl.rs | 97 ++++---- clash_lib/src/app/dns/mod.rs | 3 + clash_lib/src/app/dns/resolver/enhanced.rs | 234 +++++++++++------- .../src/app/dns/resolver/system_linux.rs | 4 + .../src/app/dns/resolver/system_non_linux.rs | 4 + clash_lib/src/app/router/mod.rs | 5 - clash_lib/src/proxy/shadowsocks/datagram.rs | 42 +--- clash_lib/src/proxy/shadowsocks/mod.rs | 17 +- clash_lib/src/proxy/utils/mod.rs | 4 +- 10 files changed, 242 insertions(+), 172 deletions(-) diff --git a/clash/tests/data/config/rules.yaml b/clash/tests/data/config/rules.yaml index 1ea9cbf4..874b2139 100644 --- a/clash/tests/data/config/rules.yaml +++ b/clash/tests/data/config/rules.yaml @@ -6,7 +6,7 @@ mixed-port: 8899 tun: enable: true device-id: "dev://utun1989" - route-all: true + route-all: false gateway: "198.19.0.1/32" so-mark: 3389 # routes: @@ -53,7 +53,7 @@ dns: allow-lan: true mode: rule -log-level: debug +log-level: trace external-controller: :9090 external-ui: "public" # secret: "clash-rs" diff --git a/clash_lib/src/app/dispatcher/dispatcher_impl.rs b/clash_lib/src/app/dispatcher/dispatcher_impl.rs index 52cb517c..c0bf618a 100644 --- a/clash_lib/src/app/dispatcher/dispatcher_impl.rs +++ b/clash_lib/src/app/dispatcher/dispatcher_impl.rs @@ -10,7 +10,7 @@ use crate::{ internal::proxy::{PROXY_DIRECT, PROXY_GLOBAL}, }, proxy::{datagram::UdpPacket, AnyInboundDatagram}, - session::Session, + session::{Session, SocksAddr}, }; use futures::{SinkExt, StreamExt}; use std::{ @@ -75,40 +75,51 @@ impl Dispatcher { } #[instrument(skip(self, sess, lhs))] - pub async fn dispatch_stream(&self, sess: Session, mut lhs: S) + pub async fn dispatch_stream(&self, mut sess: Session, mut lhs: S) where S: AsyncRead + AsyncWrite + Unpin + Send, { - let sess = if self.resolver.fake_ip_enabled() { - match sess.destination { - crate::session::SocksAddr::Ip(addr) => { - let ip = addr.ip(); + let dest: SocksAddr = match &sess.destination { + crate::session::SocksAddr::Ip(socket_addr) => { + if self.resolver.fake_ip_enabled() { + trace!("looking up fake ip: {}", socket_addr.ip()); + let ip = socket_addr.ip(); if self.resolver.is_fake_ip(ip).await { let host = self.resolver.reverse_lookup(ip).await; match host { - Some(host) => { - let mut sess = sess; - sess.destination = crate::session::SocksAddr::Domain( - host, - addr.port(), - ); - sess - } + Some(host) => (host, socket_addr.port()) + .try_into() + .expect("must be valid domain"), None => { error!("failed to reverse lookup fake ip: {}", ip); return; } } } else { - sess + (*socket_addr).into() + } + } else { + trace!("looking up resolve cache ip: {}", socket_addr.ip()); + if let Some(resolved) = + self.resolver.cached_for(socket_addr.ip()).await + { + (resolved, socket_addr.port()) + .try_into() + .expect("must be valid domain") + } else { + (*socket_addr).into() } } - crate::session::SocksAddr::Domain(..) => sess, } - } else { - sess + crate::session::SocksAddr::Domain(host, port) => { + (host.to_owned(), *port) + .try_into() + .expect("must be valid domain") + } }; + sess.destination = dest.clone(); + let mode = *self.mode.lock().unwrap(); let (outbound_name, rule) = match mode { RunMode::Global => (PROXY_GLOBAL, None), @@ -253,28 +264,17 @@ impl Dispatcher { while let Some(packet) = local_r.next().await { let mut sess = sess.clone(); sess.source = packet.src_addr.clone().must_into_socket_addr(); - sess.destination = packet.dst_addr.clone(); - - // populate fake ip for route matching - let sess = if resolver.fake_ip_enabled() { - trace!("looking up fake ip for {sess}"); - match sess.destination { - crate::session::SocksAddr::Ip(addr) => { - let ip = addr.ip(); + + let dest: SocksAddr = match &packet.dst_addr { + crate::session::SocksAddr::Ip(socket_addr) => { + if resolver.fake_ip_enabled() { + let ip = socket_addr.ip(); if resolver.is_fake_ip(ip).await { - trace!("fake ip detected"); let host = resolver.reverse_lookup(ip).await; match host { - Some(host) => { - trace!("fake ip resolved to {}", host); - let mut sess = sess; - sess.destination = - crate::session::SocksAddr::Domain( - host, - addr.port(), - ); - sess - } + Some(host) => (host, socket_addr.port()) + .try_into() + .expect("must be valid domain"), None => { error!( "failed to reverse lookup fake ip: {}", @@ -284,18 +284,31 @@ impl Dispatcher { } } } else { - sess + (*socket_addr).into() + } + } else { + if let Some(resolved) = + resolver.cached_for(socket_addr.ip()).await + { + (resolved, socket_addr.port()) + .try_into() + .expect("must be valid domain") + } else { + (*socket_addr).into() } } - crate::session::SocksAddr::Domain(..) => sess, } - } else { - sess + crate::session::SocksAddr::Domain(host, port) => { + (host.to_owned(), *port) + .try_into() + .expect("must be valid domain") + } }; + sess.destination = dest.clone(); // mutate packet for fake ip let mut packet = packet; - packet.dst_addr = sess.destination.clone(); + packet.dst_addr = dest; let mode = *mode.lock().unwrap(); diff --git a/clash_lib/src/app/dns/mod.rs b/clash_lib/src/app/dns/mod.rs index 09b63aed..ff130904 100644 --- a/clash_lib/src/app/dns/mod.rs +++ b/clash_lib/src/app/dns/mod.rs @@ -63,6 +63,9 @@ pub trait ClashResolver: Sync + Send { enhanced: bool, ) -> anyhow::Result>; + async fn cached_for(&self, ip: std::net::IpAddr) -> Option; + + /// Used for DNS Server async fn exchange(&self, message: op::Message) -> anyhow::Result; /// Only used for look up fake IP diff --git a/clash_lib/src/app/dns/resolver/enhanced.rs b/clash_lib/src/app/dns/resolver/enhanced.rs index 218004ad..5443669f 100644 --- a/clash_lib/src/app/dns/resolver/enhanced.rs +++ b/clash_lib/src/app/dns/resolver/enhanced.rs @@ -10,7 +10,7 @@ use std::{ time::Duration, }; use tokio::sync::RwLock; -use tracing::{debug, error, instrument, warn}; +use tracing::{debug, error, instrument, trace, warn}; use hickory_proto::{op, rr}; @@ -46,6 +46,9 @@ pub struct EnhancedResolver { policy: Option>>, fake_dns: Option, + + reverse_lookup_cache: + Option>>>, } impl EnhancedResolver { @@ -75,6 +78,8 @@ impl EnhancedResolver { policy: None, fake_dns: None, + + reverse_lookup_cache: None, } } @@ -94,6 +99,8 @@ impl EnhancedResolver { policy: None, fake_dns: None, + + reverse_lookup_cache: None, }); Self { @@ -199,6 +206,121 @@ impl EnhancedResolver { } _ => None, }, + + reverse_lookup_cache: Some(Arc::new(RwLock::new( + lru_time_cache::LruCache::with_expiry_duration_and_capacity( + Duration::from_secs(3), /* should be shorter than TTL so + * client won't be connecting to a + * different server after the ip is + * reverse mapped to hostname and + * being resolved again */ + 4096, + ), + ))), + } + } + + async fn resolve_inner( + &self, + host: &str, + enhanced: bool, + ) -> anyhow::Result> { + match self.ipv6.load(Relaxed) { + true => { + let fut1 = self + .resolve_v6(host, enhanced) + .map(|x| x.map(|v6| v6.map(net::IpAddr::from))); + let fut2 = self + .resolve_v4(host, enhanced) + .map(|x| x.map(|v4| v4.map(net::IpAddr::from))); + + let futs = vec![fut1.boxed(), fut2.boxed()]; + let r = futures::future::select_ok(futs).await?; + if r.0.is_some() { + return Ok(r.0); + } + let r = futures::future::select_all(r.1).await; + return r.0; + } + false => self + .resolve_v4(host, enhanced) + .await + .map(|ip| ip.map(net::IpAddr::from)), + } + } + + async fn resolve_v4_inner( + &self, + host: &str, + enhanced: bool, + ) -> anyhow::Result> { + if enhanced { + if let Some(hosts) = &self.hosts { + if let Some(v) = hosts.search(host) { + return Ok(v.get_data().map(|v| match v { + net::IpAddr::V4(v4) => *v4, + _ => unreachable!("invalid IP family"), + })); + } + } + } + + if let Ok(ip) = host.parse::() { + return Ok(Some(ip)); + } + + if enhanced && self.fake_ip_enabled() { + let mut fake_dns = self.fake_dns.as_ref().unwrap().write().await; + if !fake_dns.should_skip(host) { + let ip = fake_dns.lookup(host).await; + debug!("fake dns lookup: {} -> {:?}", host, ip); + match ip { + net::IpAddr::V4(v4) => return Ok(Some(v4)), + _ => unreachable!("invalid IP family"), + } + } + } + + match self.lookup_ip(host, rr::RecordType::A).await { + Ok(result) => match result.choose(&mut rand::thread_rng()).unwrap() { + net::IpAddr::V4(v4) => Ok(Some(*v4)), + _ => unreachable!("invalid IP family"), + }, + Err(e) => Err(e), + } + } + + async fn resolve_v6_inner( + &self, + host: &str, + enhanced: bool, + ) -> anyhow::Result> { + if !self.ipv6.load(Relaxed) { + return Err(Error::DNSError("ipv6 disabled".into()).into()); + } + + if enhanced { + if let Some(hosts) = &self.hosts { + if let Some(v) = hosts.search(host) { + return Ok(v.get_data().map(|v| match v { + net::IpAddr::V6(v6) => *v6, + _ => unreachable!("invalid IP family"), + })); + } + } + } + + if let Ok(ip) = host.parse::() { + return Ok(Some(ip)); + } + + match self.lookup_ip(host, rr::RecordType::AAAA).await { + Ok(result) => match result.choose(&mut rand::thread_rng()).unwrap() { + net::IpAddr::V6(v6) => Ok(Some(*v6)), + _ => unreachable!("invalid IP family"), + }, + + Err(e) => Err(e), } } @@ -251,7 +373,7 @@ impl EnhancedResolver { m.add_query(q); m.set_recursion_desired(true); - match self.exchange(m).await { + match self.exchange(&m).await { Ok(result) => { let ip_list = EnhancedResolver::ip_list_of_message(&result); if !ip_list.is_empty() { @@ -264,7 +386,7 @@ impl EnhancedResolver { } } - async fn exchange(&self, message: op::Message) -> anyhow::Result { + async fn exchange(&self, message: &op::Message) -> anyhow::Result { if let Some(q) = message.query() { if let Some(lru) = &self.lru_cache { if let Some(cached) = lru.read().await.peek(q.to_string().as_str()) { @@ -436,6 +558,13 @@ impl EnhancedResolver { }) .collect() } + + async fn save_reverse_lookup(&self, ip: net::IpAddr, domain: String) { + if let Some(lru) = &self.reverse_lookup_cache { + trace!("reverse lookup cache insert: {} -> {}", ip, domain); + lru.write().await.insert(ip, domain); + } + } } #[async_trait] @@ -446,28 +575,7 @@ impl ClashResolver for EnhancedResolver { host: &str, enhanced: bool, ) -> anyhow::Result> { - match self.ipv6.load(Relaxed) { - true => { - let fut1 = self - .resolve_v6(host, enhanced) - .map(|x| x.map(|v6| v6.map(net::IpAddr::from))); - let fut2 = self - .resolve_v4(host, enhanced) - .map(|x| x.map(|v4| v4.map(net::IpAddr::from))); - - let futs = vec![fut1.boxed(), fut2.boxed()]; - let r = futures::future::select_ok(futs).await?; - if r.0.is_some() { - return Ok(r.0); - } - let r = futures::future::select_all(r.1).await; - return r.0; - } - false => self - .resolve_v4(host, enhanced) - .await - .map(|ip| ip.map(net::IpAddr::from)), - } + self.resolve_inner(host, enhanced).await } async fn resolve_v4( @@ -475,40 +583,7 @@ impl ClashResolver for EnhancedResolver { host: &str, enhanced: bool, ) -> anyhow::Result> { - if enhanced { - if let Some(hosts) = &self.hosts { - if let Some(v) = hosts.search(host) { - return Ok(v.get_data().map(|v| match v { - net::IpAddr::V4(v4) => *v4, - _ => unreachable!("invalid IP family"), - })); - } - } - } - - if let Ok(ip) = host.parse::() { - return Ok(Some(ip)); - } - - if enhanced && self.fake_ip_enabled() { - let mut fake_dns = self.fake_dns.as_ref().unwrap().write().await; - if !fake_dns.should_skip(host) { - let ip = fake_dns.lookup(host).await; - debug!("fake dns lookup: {} -> {:?}", host, ip); - match ip { - net::IpAddr::V4(v4) => return Ok(Some(v4)), - _ => unreachable!("invalid IP family"), - } - } - } - - match self.lookup_ip(host, rr::RecordType::A).await { - Ok(result) => match result.choose(&mut rand::thread_rng()).unwrap() { - net::IpAddr::V4(v4) => Ok(Some(*v4)), - _ => unreachable!("invalid IP family"), - }, - Err(e) => Err(e), - } + self.resolve_v4_inner(host, enhanced).await } async fn resolve_v6( @@ -516,37 +591,30 @@ impl ClashResolver for EnhancedResolver { host: &str, enhanced: bool, ) -> anyhow::Result> { - if !self.ipv6.load(Relaxed) { - return Err(Error::DNSError("ipv6 disabled".into()).into()); - } + self.resolve_v6_inner(host, enhanced).await + } - if enhanced { - if let Some(hosts) = &self.hosts { - if let Some(v) = hosts.search(host) { - return Ok(v.get_data().map(|v| match v { - net::IpAddr::V6(v6) => *v6, - _ => unreachable!("invalid IP family"), - })); - } + async fn cached_for(&self, ip: net::IpAddr) -> Option { + if let Some(lru) = &self.reverse_lookup_cache { + if let Some(cached) = lru.read().await.peek(&ip) { + trace!("reverse lookup cache hit: {} -> {}", ip, cached); + return Some(cached.clone()); } } - if let Ok(ip) = host.parse::() { - return Ok(Some(ip)); - } - - match self.lookup_ip(host, rr::RecordType::AAAA).await { - Ok(result) => match result.choose(&mut rand::thread_rng()).unwrap() { - net::IpAddr::V6(v6) => Ok(Some(*v6)), - _ => unreachable!("invalid IP family"), - }, - - Err(e) => Err(e), - } + None } async fn exchange(&self, message: op::Message) -> anyhow::Result { - self.exchange(message).await + let rv = self.exchange(&message).await?; + let hostname = message.query().unwrap().name().to_ascii(); + let ip_list = EnhancedResolver::ip_list_of_message(&rv); + if !ip_list.is_empty() { + for ip in ip_list { + self.save_reverse_lookup(ip, hostname.clone()).await; + } + } + Ok(rv) } fn ipv6(&self) -> bool { diff --git a/clash_lib/src/app/dns/resolver/system_linux.rs b/clash_lib/src/app/dns/resolver/system_linux.rs index c16ce87f..44567ccd 100644 --- a/clash_lib/src/app/dns/resolver/system_linux.rs +++ b/clash_lib/src/app/dns/resolver/system_linux.rs @@ -56,6 +56,10 @@ impl ClashResolver for SystemResolver { Ok(response.iter().map(|x| x.0).choose(&mut rand::thread_rng())) } + async fn cached_for(&self, _: std::net::IpAddr) -> Option { + None + } + async fn exchange( &self, _: hickory_proto::op::Message, diff --git a/clash_lib/src/app/dns/resolver/system_non_linux.rs b/clash_lib/src/app/dns/resolver/system_non_linux.rs index 081b08cf..c306860a 100644 --- a/clash_lib/src/app/dns/resolver/system_non_linux.rs +++ b/clash_lib/src/app/dns/resolver/system_non_linux.rs @@ -75,6 +75,10 @@ impl ClashResolver for SystemResolver { Ok(response.into_iter().choose(&mut rand::thread_rng())) } + async fn cached_for(&self, _: std::net::IpAddr) -> Option { + None + } + async fn exchange( &self, _: hickory_proto::op::Message, diff --git a/clash_lib/src/app/router/mod.rs b/clash_lib/src/app/router/mod.rs index d53c8ded..ff9019aa 100644 --- a/clash_lib/src/app/router/mod.rs +++ b/clash_lib/src/app/router/mod.rs @@ -93,10 +93,6 @@ impl Router { && r.should_resolve_ip() && !sess_resolved { - debug!( - "rule `{r}` resolving domain {} locally", - sess.destination.domain().unwrap() - ); if let Ok(Some(ip)) = self .dns_resolver .resolve(sess.destination.domain().unwrap(), false) @@ -114,7 +110,6 @@ impl Router { r.target(), r.type_name() ); - debug!("matched rule details: {}", r); return (r.target(), Some(r)); } } diff --git a/clash_lib/src/proxy/shadowsocks/datagram.rs b/clash_lib/src/proxy/shadowsocks/datagram.rs index af96acdc..c6acf251 100644 --- a/clash_lib/src/proxy/shadowsocks/datagram.rs +++ b/clash_lib/src/proxy/shadowsocks/datagram.rs @@ -1,5 +1,6 @@ use std::{ io, + net::SocketAddr, pin::Pin, task::{Context, Poll}, }; @@ -15,10 +16,9 @@ use shadowsocks::{ ProxySocket, }; use tokio::io::ReadBuf; -use tracing::{debug, instrument, trace}; +use tracing::{debug, instrument}; use crate::{ - app::dns::ThreadSafeDNSResolver, common::errors::new_io_error, proxy::{datagram::UdpPacket, AnyOutboundDatagram}, session::SocksAddr, @@ -28,26 +28,20 @@ use crate::{ /// as underlying I/O pub struct OutboundDatagramShadowsocks { inner: ProxySocket, - remote_addr: SocksAddr, + remote_addr: SocketAddr, flushed: bool, pkt: Option, buf: Vec, - resolver: ThreadSafeDNSResolver, } impl OutboundDatagramShadowsocks { - pub fn new( - inner: ProxySocket, - remote_addr: (String, u16), - resolver: ThreadSafeDNSResolver, - ) -> Self { + pub fn new(inner: ProxySocket, remote_addr: SocketAddr) -> Self { Self { inner, flushed: true, pkt: None, remote_addr: remote_addr.try_into().expect("must into socks addr"), buf: vec![0u8; 65535], - resolver, } } } @@ -93,33 +87,9 @@ where ref mut pkt, ref remote_addr, ref mut flushed, - ref mut resolver, .. } = *self; - let dst = match remote_addr.to_owned() { - SocksAddr::Domain(domain, port) => { - let domain = domain.to_string(); - let port = port.to_owned(); - - let mut fut = resolver.resolve(domain.as_str(), false); - let ip = ready!(fut.as_mut().poll(cx).map_err(|_| { - io::Error::new(io::ErrorKind::Other, "resolve domain failed") - }))?; - - if let Some(ip) = ip { - trace!("resolve domain {} to {}", domain, ip); - (ip, port).into() - } else { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::Other, - format!("resolve domain failed: {}", domain), - ))); - } - } - SocksAddr::Ip(addr) => addr, - }; - let pkt_container = pkt; if let Some(pkt) = pkt_container { @@ -127,12 +97,12 @@ where let addr: shadowsocks::relay::Address = (pkt.dst_addr.host(), pkt.dst_addr.port()).into(); - let n = ready!(inner.poll_send_to(dst, &addr, data, cx))?; + let n = ready!(inner.poll_send_to(*remote_addr, &addr, data, cx))?; debug!( "send udp packet to remote ss server, len: {}, remote_addr: {}, \ dst_addr: {}", - n, dst, addr + n, remote_addr, addr ); let wrote_all = n == data.len(); diff --git a/clash_lib/src/proxy/shadowsocks/mod.rs b/clash_lib/src/proxy/shadowsocks/mod.rs index 1de33dbd..a7a8dd02 100644 --- a/clash_lib/src/proxy/shadowsocks/mod.rs +++ b/clash_lib/src/proxy/shadowsocks/mod.rs @@ -17,6 +17,7 @@ use crate::{ }, dns::ThreadSafeDNSResolver, }, + common::errors::new_io_error, impl_default_connector, proxy::{HandlerCommonOptions, OutboundHandler}, session::Session, @@ -277,10 +278,22 @@ impl OutboundHandler for Handler { &cfg, ShadowsocksUdpIo::new(socket), ); + let server_addr = resolver + .resolve(&self.opts.server, false) + .await + .map_err(|x| { + new_io_error(format!( + "failed to resolve {}: {}", + self.opts.server, x + )) + })? + .ok_or(new_io_error(format!( + "failed to resolve {}", + self.opts.server + )))?; let d = OutboundDatagramShadowsocks::new( socket, - (self.opts.server.to_owned(), self.opts.port), - resolver, + (server_addr, self.opts.port).into(), ); let d = ChainedDatagramWrapper::new(d); d.append_to_chain(self.name()).await; diff --git a/clash_lib/src/proxy/utils/mod.rs b/clash_lib/src/proxy/utils/mod.rs index 52d3599c..a4dea3f9 100644 --- a/clash_lib/src/proxy/utils/mod.rs +++ b/clash_lib/src/proxy/utils/mod.rs @@ -17,7 +17,7 @@ pub use proxy_connector::*; use serde::{Deserialize, Serialize}; pub use socket_helpers::*; -use tracing::{debug, trace}; +use tracing::trace; #[derive(Debug)] pub struct OutboundInterface { @@ -101,7 +101,7 @@ pub fn get_outbound_interface() -> Option { left.cmp(&right) }); - debug!( + trace!( "sorted outbound interfaces: {:?}, took: {}ms", all_outbounds, now.elapsed().as_millis()