Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support geosite #466

Merged
merged 20 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,16 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: true

- name: Setup upterm session
uses: lhotari/action-upterm@v1
if: ${{ failure() }}
with:
## If no one connects after 5 minutes, shut down server.
wait-timeout-minutes: 5
## limits ssh access and adds the ssh public key for the user which triggered the workflow
limit-access-to-actor: true
## limits ssh access and adds the ssh public keys of the listed GitHub users
limit-access-to-users: ibigbug
- uses: actions/cache@v4
with:
path: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use tracing::{debug, trace};

use crate::common::geodata::GeoData;
use crate::{
app::{
remote_content_manager::providers::{
Expand Down Expand Up @@ -84,6 +85,7 @@ impl RuleProviderImpl {
interval: Duration,
vehicle: ThreadSafeProviderVehicle,
mmdb: Arc<Mmdb>,
geodata: Arc<GeoData>,
) -> Self {
let inner = Arc::new(tokio::sync::RwLock::new(Inner {
content: match behovior {
Expand Down Expand Up @@ -111,7 +113,7 @@ impl RuleProviderImpl {
let scheme: ProviderScheme = serde_yaml::from_slice(input).map_err(|x| {
Error::InvalidConfig(format!("proxy provider parse error {}: {}", n, x))
})?;
let rules = make_rules(behovior, scheme.payload, mmdb.clone())?;
let rules = make_rules(behovior, scheme.payload, mmdb.clone(), geodata.clone())?;
Ok(rules)
});

Expand Down Expand Up @@ -214,13 +216,14 @@ fn make_rules(
behavior: RuleSetBehavior,
rules: Vec<String>,
mmdb: Arc<Mmdb>,
geodata: Arc<GeoData>,
) -> Result<RuleContent, Error> {
match behavior {
RuleSetBehavior::Domain => Ok(RuleContent::Domain(make_domain_rules(rules)?)),
RuleSetBehavior::Ipcidr => Ok(RuleContent::Ipcidr(Box::new(make_ip_cidr_rules(rules)?))),
RuleSetBehavior::Classical => {
Ok(RuleContent::Classical(make_classical_rules(rules, mmdb)?))
}
RuleSetBehavior::Classical => Ok(RuleContent::Classical(make_classical_rules(
rules, mmdb, geodata,
)?)),
}
}

Expand All @@ -243,6 +246,7 @@ fn make_ip_cidr_rules(rules: Vec<String>) -> Result<CidrTrie, Error> {
fn make_classical_rules(
rules: Vec<String>,
mmdb: Arc<Mmdb>,
geodata: Arc<GeoData>,
) -> Result<Vec<Box<dyn RuleMatcher>>, Error> {
let mut rv = vec![];
for rule in rules {
Expand All @@ -259,7 +263,7 @@ fn make_classical_rules(
_ => Err(Error::InvalidConfig(format!("invalid rule line: {}", rule))),
}?;

let rule_matcher = map_rule_type(rule_type, mmdb.clone(), None);
let rule_matcher = map_rule_type(rule_type, mmdb.clone(), geodata.clone(), None);
rv.push(rule_matcher);
}
Ok(rv)
Expand Down
26 changes: 25 additions & 1 deletion clash_lib/src/app/router/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use super::remote_content_manager::providers::rule_provider::{
use super::remote_content_manager::providers::{file_vehicle, http_vehicle};

mod rules;

use crate::common::geodata::GeoData;
pub use rules::RuleMatcher;

pub struct Router {
Expand All @@ -45,6 +47,7 @@ impl Router {
rule_providers: HashMap<String, RuleProviderDef>,
dns_resolver: ThreadSafeDNSResolver,
mmdb: Arc<Mmdb>,
geodata: Arc<GeoData>,
cwd: String,
) -> Self {
let mut rule_provider_registry = HashMap::new();
Expand All @@ -54,6 +57,7 @@ impl Router {
&mut rule_provider_registry,
dns_resolver.clone(),
mmdb.clone(),
geodata.clone(),
cwd,
)
.await
Expand All @@ -62,7 +66,14 @@ impl Router {
Self {
rules: rules
.into_iter()
.map(|r| map_rule_type(r, mmdb.clone(), Some(&rule_provider_registry)))
.map(|r| {
map_rule_type(
r,
mmdb.clone(),
geodata.clone(),
Some(&rule_provider_registry),
)
})
.collect(),
dns_resolver,
rule_provider_registry,
Expand Down Expand Up @@ -99,6 +110,7 @@ impl Router {
r.target(),
r.type_name()
);
debug!("matched rule details: {}", r);
return (r.target(), Some(r));
}
}
Expand All @@ -111,6 +123,7 @@ impl Router {
rule_provider_registry: &mut HashMap<String, ThreadSafeRuleProvider>,
resolver: ThreadSafeDNSResolver,
mmdb: Arc<Mmdb>,
geodata: Arc<GeoData>,
cwd: String,
) -> Result<(), Error> {
for (name, provider) in rule_providers.into_iter() {
Expand All @@ -131,6 +144,7 @@ impl Router {
Duration::from_secs(http.interval),
Arc::new(vehicle),
mmdb.clone(),
geodata.clone(),
);

rule_provider_registry.insert(name, Arc::new(provider));
Expand All @@ -149,6 +163,7 @@ impl Router {
Duration::from_secs(file.interval.unwrap_or_default()),
Arc::new(vehicle),
mmdb.clone(),
geodata.clone(),
);

rule_provider_registry.insert(name, Arc::new(provider));
Expand Down Expand Up @@ -183,6 +198,7 @@ impl Router {
pub fn map_rule_type(
rule_type: RuleType,
mmdb: Arc<Mmdb>,
geodata: Arc<GeoData>,
rule_provider_registry: Option<&HashMap<String, ThreadSafeRuleProvider>>,
) -> Box<dyn RuleMatcher> {
match rule_type {
Expand Down Expand Up @@ -234,6 +250,14 @@ pub fn map_rule_type(
no_resolve,
mmdb: mmdb.clone(),
}),
RuleType::GeoSite {
target,
country_code,
} => {
let res = rules::geodata::GeoSiteMatcher::new(country_code, target, geodata.as_ref())
.unwrap();
Box::new(res) as _
}
RuleType::SRCPort { target, port } => Box::new(rules::port::Port {
port,
target,
Expand Down
51 changes: 51 additions & 0 deletions clash_lib/src/app/router/rules/geodata/attribute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::common::geodata::geodata_proto;

pub trait AttrMatcher {
fn matches(&self, domain: &geodata_proto::Domain) -> bool;
}

pub struct BooleanAttrMatcher(pub String);

impl AttrMatcher for BooleanAttrMatcher {
fn matches(&self, domain: &geodata_proto::Domain) -> bool {
for attr in &domain.attribute {
if attr.key.eq_ignore_ascii_case(&self.0) {
return true;
}
}
false
}
}

impl From<String> for BooleanAttrMatcher {
fn from(s: String) -> Self {
BooleanAttrMatcher(s)
}
}

// logical AND of multiple attribute matchers
pub struct AndAttrMatcher {
list: Vec<Box<dyn AttrMatcher>>,
}

impl From<Vec<String>> for AndAttrMatcher {
fn from(list: Vec<String>) -> Self {
AndAttrMatcher {
list: list
.into_iter()
.map(|s| Box::new(BooleanAttrMatcher(s)) as Box<dyn AttrMatcher>)
.collect(),
}
}
}

impl AttrMatcher for AndAttrMatcher {
fn matches(&self, domain: &geodata_proto::Domain) -> bool {
for matcher in &self.list {
if !matcher.matches(domain) {
return false;
}
}
true
}
}
64 changes: 64 additions & 0 deletions clash_lib/src/app/router/rules/geodata/geodata.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
syntax = "proto3";

package geodata;

// Domain for routing decision.
message Domain {
// Type of domain value.
enum Type {
// The value is used as is.
Plain = 0;
// The value is used as a regular expression.
Regex = 1;
// The value is a root domain.
Domain = 2;
// The value is a domain.
Full = 3;
}

// Domain matching type.
Type type = 1;

// Domain value.
string value = 2;

message Attribute {
string key = 1;

oneof typed_value {
bool bool_value = 2;
int64 int_value = 3;
}
}

// Attributes of this domain. May be used for filtering.
repeated Attribute attribute = 3;
}

// IP for routing decision, in CIDR form.
message CIDR {
// IP address, should be either 4 or 16 bytes.
bytes ip = 1;

// Number of leading ones in the network mask.
uint32 prefix = 2;
}

message GeoIP {
string country_code = 1;
repeated CIDR cidr = 2;
bool reverse_match = 3;
}

message GeoIPList {
repeated GeoIP entry = 1;
}

message GeoSite {
string country_code = 1;
repeated Domain domain = 2;
}

message GeoSiteList {
repeated GeoSite entry = 1;
}
61 changes: 61 additions & 0 deletions clash_lib/src/app/router/rules/geodata/matcher_group.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::app::router::rules::geodata::str_matcher::{try_new_matcher, Matcher};
use crate::common::geodata::geodata_proto::{domain::Type, Domain};
use crate::common::trie;
use std::sync::Arc;

pub trait DomainGroupMatcher: Send + Sync {
fn apply(&self, domain: &str) -> bool;
}

pub struct SuccinctMatcherGroup {
set: trie::StringTrie<()>,
other_matchers: Vec<Box<dyn Matcher>>,
not: bool,
}

impl SuccinctMatcherGroup {
pub fn try_new(domains: Vec<Domain>, not: bool) -> Result<Self, crate::Error> {
let mut set = trie::StringTrie::new();
let mut other_matchers = Vec::new();
for domain in domains {
let t = Type::try_from(domain.r#type)?;
match t {
Type::Plain | Type::Regex => {
let matcher = try_new_matcher(domain.value, t)?;
other_matchers.push(matcher);
}
Type::Domain => {
let domain = format!("+.{}", domain.value);
set.insert(&domain, Arc::new(()));
}
Type::Full => {
set.insert(&domain.value, Arc::new(()));
}
}
}
Ok(SuccinctMatcherGroup {
set,
other_matchers,
not,
})
}
}

impl DomainGroupMatcher for SuccinctMatcherGroup {
fn apply(&self, domain: &str) -> bool {
let mut is_matched = self.set.search(domain).is_some();
if !is_matched {
for matcher in &self.other_matchers {
if matcher.matches(domain) {
is_matched = true;
break;
}
}
}
if self.not {
!is_matched
} else {
is_matched
}
}
}
Loading
Loading