diff --git a/clash/tests/data/config/Country-asn.mmdb b/clash/tests/data/config/Country-asn.mmdb new file mode 100644 index 00000000..f6ce9eed Binary files /dev/null and b/clash/tests/data/config/Country-asn.mmdb differ diff --git a/clash/tests/data/config/GeoLite2-ASN.mmdb b/clash/tests/data/config/GeoLite2-ASN.mmdb new file mode 100644 index 00000000..6a925726 Binary files /dev/null and b/clash/tests/data/config/GeoLite2-ASN.mmdb differ diff --git a/clash/tests/data/config/rules.yaml b/clash/tests/data/config/rules.yaml index 67ea642f..29528100 100644 --- a/clash/tests/data/config/rules.yaml +++ b/clash/tests/data/config/rules.yaml @@ -73,6 +73,8 @@ external-ui: "public" experimental: ignore-resolve-fail: true +asn-mmdb: Country-asn.mmdb + profile: store-selected: true store-fake-ip: false diff --git a/clash_lib/src/app/dispatcher/dispatcher_impl.rs b/clash_lib/src/app/dispatcher/dispatcher_impl.rs index 51583397..2d942f82 100644 --- a/clash_lib/src/app/dispatcher/dispatcher_impl.rs +++ b/clash_lib/src/app/dispatcher/dispatcher_impl.rs @@ -123,7 +123,7 @@ impl Dispatcher { let mode = *self.mode.lock().unwrap(); let (outbound_name, rule) = match mode { RunMode::Global => (PROXY_GLOBAL, None), - RunMode::Rule => self.router.match_route(&sess).await, + RunMode::Rule => self.router.match_route(&mut sess).await, RunMode::Direct => (PROXY_DIRECT, None), }; @@ -315,7 +315,7 @@ impl Dispatcher { let (outbound_name, rule) = match mode { RunMode::Global => (PROXY_GLOBAL, None), - RunMode::Rule => router.match_route(&sess).await, + RunMode::Rule => router.match_route(&mut sess).await, RunMode::Direct => (PROXY_DIRECT, None), }; diff --git a/clash_lib/src/app/dns/filters.rs b/clash_lib/src/app/dns/filters.rs index 97c8026f..0de03fa9 100644 --- a/clash_lib/src/app/dns/filters.rs +++ b/clash_lib/src/app/dns/filters.rs @@ -17,7 +17,7 @@ impl GeoIPFilter { impl FallbackIPFilter for GeoIPFilter { fn apply(&self, ip: &net::IpAddr) -> bool { self.1 - .lookup(*ip) + .lookup_contry(*ip) .map(|x| x.country) .is_ok_and(|x| x.is_some_and(|x| x.iso_code == Some(self.0.as_str()))) } diff --git a/clash_lib/src/app/dns/resolver/enhanced.rs b/clash_lib/src/app/dns/resolver/enhanced.rs index 9d4c215b..3426a79c 100644 --- a/clash_lib/src/app/dns/resolver/enhanced.rs +++ b/clash_lib/src/app/dns/resolver/enhanced.rs @@ -583,7 +583,13 @@ impl ClashResolver for EnhancedResolver { async fn exchange(&self, message: op::Message) -> anyhow::Result { let rv = self.exchange(&message).await?; - let hostname = message.query().unwrap().name().to_ascii(); + let hostname = message + .query() + .unwrap() + .name() + .to_utf8() + .trim_end_matches('.') + .to_owned(); let ip_list = EnhancedResolver::ip_list_of_message(&rv); if !ip_list.is_empty() { for ip in ip_list { diff --git a/clash_lib/src/app/router/mod.rs b/clash_lib/src/app/router/mod.rs index a7ea8bda..29a51ace 100644 --- a/clash_lib/src/app/router/mod.rs +++ b/clash_lib/src/app/router/mod.rs @@ -16,7 +16,7 @@ use crate::app::router::rules::final_::Final; use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration}; use hyper::Uri; -use tracing::{error, info}; +use tracing::{error, info, trace}; use super::{ dns::ThreadSafeDNSResolver, @@ -33,9 +33,9 @@ pub use rules::RuleMatcher; pub struct Router { rules: Vec>, - #[allow(dead_code)] - rule_provider_registry: HashMap, dns_resolver: ThreadSafeDNSResolver, + + asn_mmdb: Option>, } pub type ThreadSafeRouter = Arc; @@ -47,7 +47,8 @@ impl Router { rules: Vec, rule_providers: HashMap, dns_resolver: ThreadSafeDNSResolver, - mmdb: Arc, + country_mmdb: Arc, + asn_mmdb: Option>, geodata: Arc, cwd: String, ) -> Self { @@ -57,7 +58,7 @@ impl Router { rule_providers, &mut rule_provider_registry, dns_resolver.clone(), - mmdb.clone(), + country_mmdb.clone(), geodata.clone(), cwd, ) @@ -70,23 +71,24 @@ impl Router { .map(|r| { map_rule_type( r, - mmdb.clone(), + country_mmdb.clone(), geodata.clone(), Some(&rule_provider_registry), ) }) .collect(), dns_resolver, - rule_provider_registry, + + asn_mmdb, } } + /// this mutates the session, attaching resolved IP and ASN pub async fn match_route( &self, - sess: &Session, + sess: &mut Session, ) -> (&str, Option<&Box>) { let mut sess_resolved = false; - let mut sess_dup = sess.clone(); for r in self.rules.iter() { if sess.destination.is_domain() @@ -98,15 +100,40 @@ impl Router { .resolve(sess.destination.domain().unwrap(), false) .await { - sess_dup.resolved_ip = Some(ip); + sess.resolved_ip = Some(ip); sess_resolved = true; } } - if r.apply(&sess_dup) { + let mayby_ip = sess.resolved_ip.or(sess.destination.ip()); + if let (Some(ip), Some(asn_mmdb)) = (mayby_ip, &self.asn_mmdb) { + // try simplified mmdb first + let rv = asn_mmdb.lookup_contry(ip); + if let Ok(country) = rv { + sess.asn = country + .country + .and_then(|c| c.iso_code) + .map(|s| s.to_string()); + } + if sess.asn.is_none() { + match asn_mmdb.lookup_asn(ip) { + Ok(asn) => { + trace!("asn for {} is {:?}", ip, asn); + sess.asn = asn + .autonomous_system_organization + .map(|s| s.to_string()); + } + Err(e) => { + trace!("failed to lookup ASN for {}: {}", ip, e); + } + } + } + } + + if r.apply(sess) { info!( "matched {} to target {}[{}]", - &sess_dup, + &sess, r.target(), r.type_name() ); @@ -303,6 +330,8 @@ pub fn map_rule_type( .clone(), )), None => { + // this is called in remote rule provider with no rule provider + // registry, in this case, we should panic unreachable!("you shouldn't nest rule-set within another rule-set") } }, @@ -390,6 +419,7 @@ mod tests { Default::default(), mock_resolver, Arc::new(mmdb), + None, Arc::new(geodata), temp_dir.path().to_str().unwrap().to_string(), ) @@ -397,7 +427,7 @@ mod tests { assert_eq!( router - .match_route(&Session { + .match_route(&mut Session { destination: crate::session::SocksAddr::Domain( "china.com".to_string(), 1111, @@ -412,7 +442,7 @@ mod tests { assert_eq!( router - .match_route(&Session { + .match_route(&mut Session { destination: crate::session::SocksAddr::Domain( "t.me".to_string(), 1111, @@ -427,7 +457,7 @@ mod tests { assert_eq!( router - .match_route(&Session { + .match_route(&mut Session { destination: crate::session::SocksAddr::Domain( "git.io".to_string(), 1111 @@ -443,7 +473,7 @@ mod tests { assert_eq!( router - .match_route(&Session { + .match_route(&mut Session { destination: crate::session::SocksAddr::Domain( "no-match".to_string(), 1111 diff --git a/clash_lib/src/app/router/rules/geoip.rs b/clash_lib/src/app/router/rules/geoip.rs index 93d6ad20..deff1b8d 100644 --- a/clash_lib/src/app/router/rules/geoip.rs +++ b/clash_lib/src/app/router/rules/geoip.rs @@ -29,7 +29,7 @@ impl RuleMatcher for GeoIP { }; if let Some(ip) = ip { - match self.mmdb.lookup(ip) { + match self.mmdb.lookup_contry(ip) { Ok(country) => { country .country diff --git a/clash_lib/src/common/mmdb.rs b/clash_lib/src/common/mmdb.rs index 5c9515fe..116fead6 100644 --- a/clash_lib/src/common/mmdb.rs +++ b/clash_lib/src/common/mmdb.rs @@ -92,9 +92,13 @@ impl Mmdb { } } - pub fn lookup(&self, ip: IpAddr) -> std::io::Result { + pub fn lookup_contry(&self, ip: IpAddr) -> std::io::Result { self.reader .lookup::(ip) .map_err(map_io_error) } + + pub fn lookup_asn(&self, ip: IpAddr) -> std::io::Result { + self.reader.lookup::(ip).map_err(map_io_error) + } } diff --git a/clash_lib/src/config/def.rs b/clash_lib/src/config/def.rs index 1b7e9fcd..948df383 100644 --- a/clash_lib/src/config/def.rs +++ b/clash_lib/src/config/def.rs @@ -12,6 +12,7 @@ fn default_tun_address() -> String { #[serde(rename_all = "kebab-case")] pub struct TunConfig { pub enable: bool, + #[serde(alias = "device-url")] pub device_id: String, /// tun interface address #[serde(default = "default_tun_address")] @@ -293,6 +294,10 @@ pub struct Config { pub mmdb: String, /// Country database download url pub mmdb_download_url: Option, + /// Optional ASN database path relative to the $CWD + pub asn_mmdb: String, + /// Optional ASN database download url + pub asn_mmdb_download_url: Option, /// Geosite database path relative to the $CWD pub geosite: String, /// Geosite database download url @@ -396,6 +401,8 @@ impl Default for Config { "https://github.com/Loyalsoldier/geoip/releases/download/202307271745/Country.mmdb" .to_owned(), ), + asn_mmdb: "Country-asn.mmdb".to_string(), + asn_mmdb_download_url: None, // can be downloaded from the same release but let's not make it default geosite: "geosite.dat".to_string(), geosite_download_url: Some("https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/202406182210/geosite.dat".to_owned()), tun: Default::default(), diff --git a/clash_lib/src/config/internal/config.rs b/clash_lib/src/config/internal/config.rs index 8f273d4e..d10cc194 100644 --- a/clash_lib/src/config/internal/config.rs +++ b/clash_lib/src/config/internal/config.rs @@ -92,6 +92,8 @@ impl TryFrom for Config { routing_mask: c.routing_mask, mmdb: c.mmdb.to_owned(), mmdb_download_url: c.mmdb_download_url.to_owned(), + asn_mmdb: c.asn_mmdb.to_owned(), + asn_mmdb_download_url: c.asn_mmdb_download_url.to_owned(), geosite: c.geosite.to_owned(), geosite_download_url: c.geosite_download_url.to_owned(), }, @@ -283,6 +285,8 @@ pub struct General { pub routing_mask: Option, pub mmdb: String, pub mmdb_download_url: Option, + pub asn_mmdb: String, + pub asn_mmdb_download_url: Option, pub geosite: String, pub geosite_download_url: Option, diff --git a/clash_lib/src/lib.rs b/clash_lib/src/lib.rs index 1d559e22..59b05879 100644 --- a/clash_lib/src/lib.rs +++ b/clash_lib/src/lib.rs @@ -332,22 +332,20 @@ async fn create_components( .map_err(|x| Error::DNSError(x.to_string()))?; debug!("initializing mmdb"); - let mmdb = Arc::new( + let country_mmdb = Arc::new( mmdb::Mmdb::new( cwd.join(&config.general.mmdb), config.general.mmdb_download_url, - client, + client.clone(), ) .await?, ); - let client = new_http_client(system_resolver) - .map_err(|x| Error::DNSError(x.to_string()))?; let geodata = Arc::new( geodata::GeoData::new( cwd.join(&config.general.geosite), config.general.geosite_download_url, - client, + client.clone(), ) .await?, ); @@ -362,7 +360,7 @@ async fn create_components( let dns_resolver = dns::new_resolver( &config.dns, Some(cache_store.clone()), - Some(mmdb.clone()), + Some(country_mmdb.clone()), ) .await; @@ -394,13 +392,25 @@ async fn create_components( .await?, ); + debug!("initializing country asn mmdb"); + let p = cwd.join(&config.general.asn_mmdb); + let asn_mmdb = if p.exists() || config.general.asn_mmdb_download_url.is_some() { + Some(Arc::new( + mmdb::Mmdb::new(p, config.general.asn_mmdb_download_url, client.clone()) + .await?, + )) + } else { + None + }; + debug!("initializing router"); let router = Arc::new( Router::new( config.rules, config.rule_providers, dns_resolver.clone(), - mmdb, + country_mmdb, + asn_mmdb, geodata, cwd.to_string_lossy().to_string(), ) diff --git a/clash_lib/src/session.rs b/clash_lib/src/session.rs index c8d5c07c..ddce0385 100644 --- a/clash_lib/src/session.rs +++ b/clash_lib/src/session.rs @@ -403,6 +403,8 @@ pub struct Session { pub so_mark: Option, /// The bind interface pub iface: Option, + /// The ASN of the destination IP address. Only for display. + pub asn: Option, } impl Session { @@ -412,16 +414,23 @@ impl Session { rv.insert("type".to_string(), Box::new(self.typ) as _); rv.insert("sourceIP".to_string(), Box::new(self.source.ip()) as _); rv.insert("sourcePort".to_string(), Box::new(self.source.port()) as _); - rv.insert( - "destinationIP".to_string(), - Box::new(self.destination.ip()) as _, - ); + rv.insert("destinationIP".to_string(), { + let ip = self.resolved_ip.or(self.destination.ip()); + let asn = self.asn.clone(); + + let rv = match (ip, asn) { + (Some(ip), Some(asn)) => format!("{}({})", ip, asn), + (Some(ip), None) => ip.to_string(), + (None, _) => "".to_string(), + }; + Box::new(rv) as _ + }); rv.insert( "destinationPort".to_string(), Box::new(self.destination.port()) as _, ); rv.insert("host".to_string(), Box::new(self.destination.host()) as _); - + rv.insert("asn".to_string(), Box::new(self.asn.clone()) as _); rv } } @@ -436,6 +445,7 @@ impl Default for Session { resolved_ip: None, so_mark: None, iface: None, + asn: None, } } } @@ -458,6 +468,7 @@ impl Debug for Session { .field("destination", &self.destination) .field("packet_mark", &self.so_mark) .field("iface", &self.iface) + .field("asn", &self.asn) .finish() } } @@ -472,6 +483,7 @@ impl Clone for Session { resolved_ip: self.resolved_ip, so_mark: self.so_mark, iface: self.iface.as_ref().cloned(), + asn: self.asn.clone(), } } }