Skip to content

Commit

Permalink
group dag order
Browse files Browse the repository at this point in the history
  • Loading branch information
ibigbug committed Aug 13, 2023
1 parent a02a3f7 commit 8d24eed
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 36 deletions.
60 changes: 32 additions & 28 deletions clash/tests/data/config/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,39 +47,43 @@ proxy-groups:
type: relay
proxies:
- "ss"
- "auto"
- "fallback-auto"
- "load-balance"
- "select"
- DIRECT

# - name: "auto"
# type: url-test
# proxies:
# - "ss"
# - DIRECT
# url: "http://www.gstatic.com/generate_204"
# interval: 300
- name: "auto"
type: url-test
proxies:
- "ss"
- DIRECT
url: "http://www.gstatic.com/generate_204"
interval: 300

# - name: "fallback-auto"
# type: fallback
# proxies:
# - "ss"
# - DIRECT
# url: "http://www.gstatic.com/generate_204"
# interval: 300
- name: "fallback-auto"
type: fallback
proxies:
- "ss"
- DIRECT
url: "http://www.gstatic.com/generate_204"
interval: 300

# - name: "load-balance"
# type: load-balance
# proxies:
# - "ss"
# - DIRECT
# strategy: round-robin
# url: "http://www.gstatic.com/generate_204"
# interval: 300
- name: "load-balance"
type: load-balance
proxies:
- "ss"
- DIRECT
strategy: round-robin
url: "http://www.gstatic.com/generate_204"
interval: 300

# - name: select
# type: select
# proxies:
# - "ss"
# - DIRECT
# - REJECT
- name: select
type: select
proxies:
- "ss"
- DIRECT
- REJECT


proxies:
Expand Down
5 changes: 5 additions & 0 deletions clash_lib/src/app/outbound/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ use crate::{
Error,
};

use super::utils::proxy_groups_dag_sort;

pub struct OutboundManager {
handlers: HashMap<String, AnyOutboundHandler>,
proxy_manager: ThreadSafeProxyManager,
Expand Down Expand Up @@ -112,6 +114,9 @@ impl OutboundManager {
}
}

let mut outbound_groups = outbound_groups;
proxy_groups_dag_sort(&mut outbound_groups)?;

for outbound_group in outbound_groups.iter() {
fn make_provider_from_proxies(
name: &str,
Expand Down
2 changes: 2 additions & 0 deletions clash_lib/src/app/outbound/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod manager;

mod utils;
270 changes: 270 additions & 0 deletions clash_lib/src/app/outbound/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
use std::{cell::RefCell, collections::HashMap};

use crate::{config::internal::proxy::OutboundGroupProtocol, Error};

// copy paste from https://github.com/Dreamacro/clash/blob/6a661bff0c185f38c4bd9d21c91a3233ba5fdb97/config/utils.go#L21
pub fn proxy_groups_dag_sort(groups: &mut Vec<OutboundGroupProtocol>) -> Result<(), Error> {
struct Node {
in_degree: i32,

// could be either group/proxy
proto: Option<OutboundGroupProtocol>,

outdegree: i32,

from: Vec<String>,
}

let mut graph: HashMap<String, RefCell<Node>> = HashMap::new();

for group in groups.iter() {
let group_name = group.name().to_owned();

let node = graph.get(&group_name);

if let Some(node) = node {
if node.borrow().proto.is_some() {
return Err(Error::InvalidConfig(format!(
"duplicate proxy group name: {}",
group_name
)));
} else {
node.borrow_mut().proto = Some(group.clone());
}
} else {
graph.insert(
group_name.clone(),
RefCell::new(Node {
in_degree: 0,
proto: Some(group.clone()),
outdegree: 0,
from: vec![],
}),
);
}

if let Some(proxies) = group.proxies() {
for proxy in proxies {
if let Some(node) = graph.get(proxy) {
node.borrow_mut().in_degree += 1;
} else {
graph.insert(
proxy.clone(),
RefCell::new(Node {
in_degree: 1,
proto: None,
outdegree: 0,
from: vec![],
}),
);
}
}
}
}

let mut index = 0;
let mut queue = vec![];

for (name, node) in graph.iter() {
if node.borrow_mut().in_degree == 0 {
queue.push(name.clone());
}
}

let group_len = groups.len();

while !queue.is_empty() {
let name = queue.first().unwrap().to_owned();
let node = graph.get(&name).unwrap();

if node.borrow().proto.is_some() {
index += 1;
groups[group_len - index] = node.borrow_mut().proto.take().unwrap();
if groups[group_len - index].proxies().is_none() {
graph.remove(&name);
continue;
}

for proxy in groups[group_len - index].proxies().unwrap() {
let node = graph.get(proxy.as_str()).unwrap();
node.borrow_mut().in_degree -= 1;
if node.borrow().in_degree == 0 {
queue.push(proxy.clone());
}
}
}

graph.remove(&name);

queue.remove(0);
}

if graph.len() == 0 {
return Ok(());
}

for (name, node) in graph.iter() {
if node.borrow().proto.is_none() {
continue;
}

if node.borrow().proto.as_ref().unwrap().proxies().is_none() {
continue;
}

let proxies = node
.borrow()
.proto
.as_ref()
.unwrap()
.proxies()
.unwrap()
.clone();

for proxy in proxies.iter() {
node.borrow_mut().outdegree += 1;
graph
.get(proxy)
.unwrap()
.borrow_mut()
.from
.push(name.to_owned());
}
}

let mut queue = vec![];
for (name, node) in graph.iter() {
if node.borrow_mut().outdegree == 0 {
queue.push(name.to_owned());
}
}

while queue.len() > 0 {
let name = queue.first().unwrap().to_owned();
let node = graph.get(&name).unwrap();

let parents = node.borrow().from.clone();

for parent in parents {
graph.get(parent.as_str()).unwrap().borrow_mut().outdegree -= 1;
if graph.get(parent.as_str()).unwrap().borrow_mut().outdegree == 0 {
queue.push(parent);
}
}

graph.remove(&name);

queue.remove(0);
}

let looped_groups: Vec<String> = graph.keys().map(|s| s.to_owned()).collect();

return Err(Error::InvalidConfig(format!(
"looped detected in proxy groups: {:?}",
looped_groups
)));
}

#[cfg(test)]
mod tests {
use crate::config::internal::proxy::{
OutboundGroupFallback, OutboundGroupLoadBalance, OutboundGroupProtocol, OutboundGroupRelay,
OutboundGroupSelect, OutboundGroupUrlTest,
};

#[test]
fn test_proxy_groups_dag_sort_ok() {
let g1 = OutboundGroupRelay {
name: "relay".to_owned(),
proxies: Some(vec![
"ss".to_owned(),
"auto".to_owned(),
"fallback-auto".to_owned(),
"load-balance".to_owned(),
"select".to_owned(),
"DIRECT".to_owned(),
]),
..Default::default()
};
let g2 = OutboundGroupUrlTest {
name: "auto".to_owned(),
proxies: Some(vec!["ss".to_owned(), "DIRECT".to_owned()]),
..Default::default()
};
let g3 = OutboundGroupFallback {
name: "fallback-auto".to_owned(),
proxies: Some(vec!["ss".to_owned(), "DIRECT".to_owned()]),
..Default::default()
};
let g4 = OutboundGroupLoadBalance {
name: "load-balance".to_owned(),
proxies: Some(vec!["ss".to_owned(), "DIRECT".to_owned()]),
..Default::default()
};
let g5 = OutboundGroupSelect {
name: "select".to_owned(),
proxies: Some(vec![
"ss".to_owned(),
"DIRECT".to_owned(),
"REJECT".to_owned(),
]),
..Default::default()
};

let mut groups = vec![
OutboundGroupProtocol::Relay(g1),
OutboundGroupProtocol::UrlTest(g2),
OutboundGroupProtocol::Fallback(g3),
OutboundGroupProtocol::LoadBalance(g4),
OutboundGroupProtocol::Select(g5),
];

super::proxy_groups_dag_sort(&mut groups).unwrap();

assert_eq!(groups.first().unwrap().name(), "auto");
assert_eq!(groups.last().unwrap().name(), "relay");
}

#[test]
fn test_proxy_groups_dag_sort_cycle() {
let g1 = OutboundGroupRelay {
name: "relay".to_owned(),
proxies: Some(vec![
"ss".to_owned(),
"auto".to_owned(),
"fallback-auto".to_owned(),
]),
..Default::default()
};
let g2 = OutboundGroupUrlTest {
name: "auto".to_owned(),
proxies: Some(vec![
"ss".to_owned(),
"DIRECT".to_owned(),
"cycle".to_owned(),
]),
..Default::default()
};
let g3 = OutboundGroupFallback {
name: "cycle".to_owned(),
proxies: Some(vec![
"ss".to_owned(),
"DIRECT".to_owned(),
"relay".to_owned(),
]),
..Default::default()
};

let mut groups = vec![
OutboundGroupProtocol::Relay(g1),
OutboundGroupProtocol::UrlTest(g2),
OutboundGroupProtocol::Fallback(g3),
];

let e = super::proxy_groups_dag_sort(&mut groups).unwrap_err();
assert_eq!(
e.to_string(),
"invalid config: looped detected in proxy groups: [\"relay\", \"auto\", \"cycle\"]"
);
}
}
Loading

0 comments on commit 8d24eed

Please sign in to comment.