Skip to content

Commit

Permalink
feat(api): show ASN in connection metadata (#616)
Browse files Browse the repository at this point in the history
* show asn

* show asn

* clippy
  • Loading branch information
ibigbug authored Oct 1, 2024
1 parent 4c1a753 commit 801972b
Show file tree
Hide file tree
Showing 13 changed files with 109 additions and 34 deletions.
Binary file added clash/tests/data/config/Country-asn.mmdb
Binary file not shown.
Binary file added clash/tests/data/config/GeoLite2-ASN.mmdb
Binary file not shown.
2 changes: 2 additions & 0 deletions clash/tests/data/config/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions clash_lib/src/app/dispatcher/dispatcher_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};

Expand Down Expand Up @@ -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),
};

Expand Down
2 changes: 1 addition & 1 deletion clash_lib/src/app/dns/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())))
}
Expand Down
8 changes: 7 additions & 1 deletion clash_lib/src/app/dns/resolver/enhanced.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,13 @@ impl ClashResolver for EnhancedResolver {

async fn exchange(&self, message: op::Message) -> anyhow::Result<op::Message> {
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 {
Expand Down
62 changes: 46 additions & 16 deletions clash_lib/src/app/router/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,9 +33,9 @@ pub use rules::RuleMatcher;

pub struct Router {
rules: Vec<Box<dyn RuleMatcher>>,
#[allow(dead_code)]
rule_provider_registry: HashMap<String, ThreadSafeRuleProvider>,
dns_resolver: ThreadSafeDNSResolver,

asn_mmdb: Option<Arc<Mmdb>>,
}

pub type ThreadSafeRouter = Arc<Router>;
Expand All @@ -47,7 +47,8 @@ impl Router {
rules: Vec<RuleType>,
rule_providers: HashMap<String, RuleProviderDef>,
dns_resolver: ThreadSafeDNSResolver,
mmdb: Arc<Mmdb>,
country_mmdb: Arc<Mmdb>,
asn_mmdb: Option<Arc<Mmdb>>,
geodata: Arc<GeoData>,
cwd: String,
) -> Self {
Expand All @@ -57,7 +58,7 @@ impl Router {
rule_providers,
&mut rule_provider_registry,
dns_resolver.clone(),
mmdb.clone(),
country_mmdb.clone(),
geodata.clone(),
cwd,
)
Expand All @@ -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<dyn RuleMatcher>>) {
let mut sess_resolved = false;
let mut sess_dup = sess.clone();

for r in self.rules.iter() {
if sess.destination.is_domain()
Expand All @@ -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()
);
Expand Down Expand Up @@ -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")
}
},
Expand Down Expand Up @@ -390,14 +419,15 @@ mod tests {
Default::default(),
mock_resolver,
Arc::new(mmdb),
None,
Arc::new(geodata),
temp_dir.path().to_str().unwrap().to_string(),
)
.await;

assert_eq!(
router
.match_route(&Session {
.match_route(&mut Session {
destination: crate::session::SocksAddr::Domain(
"china.com".to_string(),
1111,
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion clash_lib/src/app/router/rules/geoip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion clash_lib/src/common/mmdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,13 @@ impl Mmdb {
}
}

pub fn lookup(&self, ip: IpAddr) -> std::io::Result<geoip2::Country> {
pub fn lookup_contry(&self, ip: IpAddr) -> std::io::Result<geoip2::Country> {
self.reader
.lookup::<geoip2::Country>(ip)
.map_err(map_io_error)
}

pub fn lookup_asn(&self, ip: IpAddr) -> std::io::Result<geoip2::Asn> {
self.reader.lookup::<geoip2::Asn>(ip).map_err(map_io_error)
}
}
7 changes: 7 additions & 0 deletions clash_lib/src/config/def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -293,6 +294,10 @@ pub struct Config {
pub mmdb: String,
/// Country database download url
pub mmdb_download_url: Option<String>,
/// Optional ASN database path relative to the $CWD
pub asn_mmdb: String,
/// Optional ASN database download url
pub asn_mmdb_download_url: Option<String>,
/// Geosite database path relative to the $CWD
pub geosite: String,
/// Geosite database download url
Expand Down Expand Up @@ -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(),
Expand Down
4 changes: 4 additions & 0 deletions clash_lib/src/config/internal/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ impl TryFrom<def::Config> 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(),
},
Expand Down Expand Up @@ -283,6 +285,8 @@ pub struct General {
pub routing_mask: Option<u32>,
pub mmdb: String,
pub mmdb_download_url: Option<String>,
pub asn_mmdb: String,
pub asn_mmdb_download_url: Option<String>,

pub geosite: String,
pub geosite_download_url: Option<String>,
Expand Down
24 changes: 17 additions & 7 deletions clash_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
);
Expand All @@ -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;

Expand Down Expand Up @@ -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(),
)
Expand Down
Loading

0 comments on commit 801972b

Please sign in to comment.