Skip to content

Commit

Permalink
tor outboud support (#265)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibigbug authored Jan 19, 2024
1 parent fb9a420 commit 7e75412
Show file tree
Hide file tree
Showing 22 changed files with 2,713 additions and 515 deletions.
2,678 changes: 2,405 additions & 273 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A custom protocol, rule based network proxy software.
- 🌈 Flexible traffic routing rules based off source/destination IP/Domain/GeoIP etc.
- 📦 Local anti spoofing DNS with support of UDP/TCP/DoH/DoT remote.
- 🛡 Run as a HTTP/Socks5 proxy, or utun device as a home network gateway.
- ⚙️ Shadowsocks/Trojan/Vmess/Wireguard(userspace) outbound support with different underlying trasports.
- ⚙️ Shadowsocks/Trojan/Vmess/Wireguard(userspace)/Tor outbound support with different underlying trasports(gRPC/TLS/H2/WebSocket/etc.).
- 🌍 Dynamic remote rule/proxy loader.
- 🎵 Tracing with Jaeger

Expand Down
24 changes: 24 additions & 0 deletions clash/tests/data/config/tor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
port: 8888
socks-port: 8889
mixed-port: 8899

mode: rule
log-level: debug
external-controller: 127.0.0.1:6170


proxies:
- name: "tor"
type: tor
- name: "ss-02"
type: ss
server: 10.0.0.13
port: 8388
cipher: aes-256-gcm
password: "password"
udp: true

rules:
- MATCH, tor
...
16 changes: 7 additions & 9 deletions clash_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = { workspace = true }
default = ["shadowsocks"]
tracing = []
bench = ["criterion"]
onion = ["arti-client/onion-service-client"]

[dependencies]
tokio = { version = "1", features = ["full"] }
Expand Down Expand Up @@ -63,9 +64,11 @@ chrono = { version = "0.4.26", features = ["serde"] }
tun = { git = "https://github.com/Watfaq/rust-tun.git", rev = "8f7568190f1200d3e272ca534baf8d1578147e18", features = ["async"] }
netstack-lwip = { git = "https://github.com/Watfaq/netstack-lwip.git", rev = "2817bf82740e04bbee6b7bf1165f55657a6ed163" }

boringtun = { version = "0.6.0" }
boringtun = { version = "0.6.0", git = "https://github.com/cloudflare/boringtun.git", rev = "f672bb6c1e1e371240a8d151f15854687eb740bb" }
smoltcp = { version = "0.11", default-features = false, features = ["std", "log", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-udp", "socket-tcp"] }

tokio-rustls = "0.24"
hyper-rustls = { version = "0.24", features = ["http1", "http2"] }

serde = { version = "1.0", features=["derive"] }
serde_yaml = "0.9"
Expand Down Expand Up @@ -100,6 +103,9 @@ maxminddb = "0.23.0"
public-suffix = "0.1.0"
murmur3 = "0.5.2"

arti-client = { version = "0.13.0", default-features = false, features = ["tokio", "rustls", "compression", "static-sqlite"] }
tor-rtcompat = { version = "0.9" }

console-subscriber = { version = "0.2.0" }
tracing-timing = { version = "0.6.0" }
criterion = { version = "0.5", features = ["html_reports", "async_tokio"], optional = true }
Expand All @@ -114,11 +120,3 @@ axum-macros = "0.4.0"

[target.'cfg(macos)'.dependencies]
security-framework = "2.8.0"

[target.'cfg(windows)'.dependencies]
tokio-rustls = "0.24"
hyper-rustls = { version = "0.24", features = ["http1", "http2"] }

[target.'cfg(not(windows))'.dependencies]
hyper-boring = "4.2.0"
tokio-boring = "4.2.0"
23 changes: 10 additions & 13 deletions clash_lib/src/app/dispatcher/tracked.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{fmt::Debug, pin::Pin, sync::Arc, task::Poll};

use async_trait::async_trait;
use futures::{Sink, Stream};
use hyper::client::connect::{Connected, Connection};
use tokio::{
Expand All @@ -8,11 +9,7 @@ use tokio::{
};
use tracing::debug;

use crate::{
app::router::RuleMatcher,
proxy::{datagram::UdpPacket, OutboundDatagram, ProxyStream},
session::Session,
};
use crate::{app::router::RuleMatcher, proxy::datagram::UdpPacket, session::Session};

use super::statistics_manager::{Manager, ProxyChain, TrackerInfo};

Expand All @@ -28,8 +25,8 @@ impl Tracked {
}
}

#[async_trait::async_trait]
pub trait ChainedStream: ProxyStream {
#[async_trait]
pub trait ChainedStream: AsyncRead + AsyncWrite + Unpin + Debug + Send + Sync {
fn chain(&self) -> &ProxyChain;
async fn append_to_chain(&self, name: &str);
}
Expand All @@ -40,7 +37,7 @@ impl Connection for BoxedChainedStream {
}
}

pub type BoxedChainedStream = Box<dyn ChainedStream + Send + Sync>;
pub type BoxedChainedStream = Box<dyn ChainedStream>;

#[derive(Debug)]
pub struct ChainedStreamWrapper<T> {
Expand All @@ -57,10 +54,10 @@ impl<T> ChainedStreamWrapper<T> {
}
}

#[async_trait::async_trait]
#[async_trait]
impl<T> ChainedStream for ChainedStreamWrapper<T>
where
T: AsyncRead + AsyncWrite + Unpin + Debug + Sync + Send,
T: AsyncRead + AsyncWrite + Unpin + Debug + Send + Sync,
{
fn chain(&self) -> &ProxyChain {
&self.chain
Expand Down Expand Up @@ -264,17 +261,17 @@ impl AsyncWrite for TrackedStream {
}
}

#[async_trait::async_trait]
#[async_trait]
pub trait ChainedDatagram:
OutboundDatagram<UdpPacket, Item = UdpPacket, Error = std::io::Error>
Stream<Item = UdpPacket> + Sink<UdpPacket, Error = std::io::Error> + Unpin
{
fn chain(&self) -> &ProxyChain;
async fn append_to_chain(&self, name: &str);
}

pub type BoxedChainedDatagram = Box<dyn ChainedDatagram + Send + Sync>;

#[async_trait::async_trait]
#[async_trait]
impl<T> ChainedDatagram for ChainedDatagramWrapper<T>
where
T: Sink<UdpPacket, Error = std::io::Error> + Unpin + Send + Sync + 'static,
Expand Down
4 changes: 4 additions & 0 deletions clash_lib/src/app/outbound/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ impl OutboundManager {
handlers.insert(wg.name.clone(), wg.try_into()?);
}

OutboundProxyProtocol::Tor(tor) => {
handlers.insert(tor.name.clone(), tor.try_into()?);
}

p => {
unimplemented!("proto {} not supported yet", p);
}
Expand Down
20 changes: 0 additions & 20 deletions clash_lib/src/app/remote_content_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@ pub struct ProxyManager {
proxy_state: Arc<RwLock<HashMap<String, ProxyState>>>,
dns_resolver: ThreadSafeDNSResolver,

#[cfg(windows)]
connector_map: Arc<RwLock<HashMap<String, hyper_rustls::HttpsConnector<LocalConnector>>>>,
#[cfg(not(windows))]
connector_map: Arc<RwLock<HashMap<String, hyper_boring::HttpsConnector<LocalConnector>>>>,
}

impl ProxyManager {
Expand Down Expand Up @@ -139,7 +136,6 @@ impl ProxyManager {
let name = name_clone;
let connector = LocalConnector(proxy.clone(), dns_resolver);

#[cfg(windows)]
let connector = {
use crate::common::tls::GLOBAL_ROOT_STORE;

Expand All @@ -160,22 +156,6 @@ impl ProxyManager {
let connector = g.entry(name.clone()).or_insert(connector);
connector.clone()
};
#[cfg(not(windows))]
let connector = {
use crate::common::errors::map_io_error;
use boring::ssl::{SslConnector, SslMethod};

let mut ssl = SslConnector::builder(SslMethod::tls()).map_err(map_io_error)?;
ssl.set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(map_io_error)?;

let connector = hyper_boring::HttpsConnector::with_connector(connector, ssl)
.map_err(map_io_error)?;

let mut g = self.connector_map.write().await;
let connector = g.entry(name.clone()).or_insert(connector);
connector.clone()
};

let client = hyper::Client::builder().build::<_, hyper::Body>(connector);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl ProxySetProvider {
OutboundProxyProtocol::Trojan(tr) => tr.try_into(),
OutboundProxyProtocol::Vmess(vm) => vm.try_into(),
OutboundProxyProtocol::Wireguard(wg) => wg.try_into(),
OutboundProxyProtocol::Tor(tor) => tor.try_into(),
})
.collect::<Result<Vec<_>, _>>();
Ok(proxies?)
Expand Down
20 changes: 0 additions & 20 deletions clash_lib/src/common/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,8 @@ impl Connection for AnyStream {
}
}

#[cfg(not(windows))]
pub type HttpClient = hyper::Client<hyper_boring::HttpsConnector<LocalConnector>>;
#[cfg(windows)]
pub type HttpClient = hyper::Client<hyper_rustls::HttpsConnector<LocalConnector>>;

#[cfg(not(windows))]
pub fn new_http_client(dns_resolver: ThreadSafeDNSResolver) -> std::io::Result<HttpClient> {
use super::errors::map_io_error;
use boring::ssl::{SslConnector, SslMethod};

let connector = LocalConnector(dns_resolver);

let mut ssl = SslConnector::builder(SslMethod::tls()).map_err(map_io_error)?;
ssl.set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(map_io_error)?;

let connector =
hyper_boring::HttpsConnector::with_connector(connector, ssl).map_err(map_io_error)?;
Ok(hyper::Client::builder().build::<_, hyper::Body>(connector))
}

#[cfg(windows)]
pub fn new_http_client(dns_resolver: ThreadSafeDNSResolver) -> std::io::Result<HttpClient> {
use std::sync::Arc;

Expand Down
9 changes: 8 additions & 1 deletion clash_lib/src/config/internal/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,14 @@ impl TryFrom<HashMap<String, Value>> for RuleProviderDef {
type Error = crate::Error;

fn try_from(mapping: HashMap<String, Value>) -> Result<Self, Self::Error> {
let name = mapping
.get("name")
.and_then(|x| x.as_str())
.ok_or(Error::InvalidConfig(
"rule provider name is required".to_owned(),
))?
.to_owned();
RuleProviderDef::deserialize(MapDeserializer::new(mapping.into_iter()))
.map_err(map_serde_error)
.map_err(map_serde_error(name))
}
}
57 changes: 48 additions & 9 deletions clash_lib/src/config/internal/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,20 @@ impl OutboundProxy {
}
}

pub fn map_serde_error(x: serde_yaml::Error) -> crate::Error {
Error::InvalidConfig(if let Some(loc) = x.location() {
format!("{}, line, {}, column: {}", x, loc.line(), loc.column())
} else {
x.to_string()
})
pub fn map_serde_error(name: String) -> impl FnOnce(serde_yaml::Error) -> crate::Error {
move |x| {
if let Some(loc) = x.location() {
Error::InvalidConfig(format!(
"invalid config for {} at line {}, column {} while parsing {}",
name,
loc.line(),
loc.column(),
name
))
} else {
Error::InvalidConfig(format!("error while parsine {}: {}", name, x))
}
}
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
Expand All @@ -51,6 +59,8 @@ pub enum OutboundProxyProtocol {
Vmess(OutboundVmess),
#[serde(rename = "wireguard")]
Wireguard(OutboundWireguard),
#[serde(rename = "tor")]
Tor(OutboundTor),
}

impl OutboundProxyProtocol {
Expand All @@ -63,6 +73,7 @@ impl OutboundProxyProtocol {
OutboundProxyProtocol::Trojan(trojan) => &trojan.name,
OutboundProxyProtocol::Vmess(vmess) => &vmess.name,
OutboundProxyProtocol::Wireguard(wireguard) => &wireguard.name,
OutboundProxyProtocol::Tor(tor) => &tor.name,
}
}
}
Expand All @@ -71,8 +82,15 @@ impl TryFrom<HashMap<String, Value>> for OutboundProxyProtocol {
type Error = crate::Error;

fn try_from(mapping: HashMap<String, Value>) -> Result<Self, Self::Error> {
let name = mapping
.get("name")
.and_then(|x| x.as_str())
.ok_or(Error::InvalidConfig(
"missing field `name` in outbound proxy protocol".to_owned(),
))?
.to_owned();
OutboundProxyProtocol::deserialize(MapDeserializer::new(mapping.into_iter()))
.map_err(map_serde_error)
.map_err(map_serde_error(name))
}
}

Expand All @@ -86,6 +104,7 @@ impl Display for OutboundProxyProtocol {
OutboundProxyProtocol::Trojan(_) => write!(f, "Trojan"),
OutboundProxyProtocol::Vmess(_) => write!(f, "Vmess"),
OutboundProxyProtocol::Wireguard(_) => write!(f, "Wireguard"),
OutboundProxyProtocol::Tor(_) => write!(f, "Tor"),
}
}
}
Expand Down Expand Up @@ -191,6 +210,12 @@ pub struct OutboundWireguard {
pub allowed_ips: Option<Vec<String>>,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Default)]
#[serde(rename_all = "kebab-case")]
pub struct OutboundTor {
pub name: String,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum OutboundGroupProtocol {
Expand Down Expand Up @@ -232,8 +257,15 @@ impl TryFrom<HashMap<String, Value>> for OutboundGroupProtocol {
type Error = Error;

fn try_from(mapping: HashMap<String, Value>) -> Result<Self, Self::Error> {
let name = mapping
.get("name")
.and_then(|x| x.as_str())
.ok_or(Error::InvalidConfig(
"missing field `name` in outbound proxy grouop".to_owned(),
))?
.to_owned();
OutboundGroupProtocol::deserialize(MapDeserializer::new(mapping.into_iter()))
.map_err(map_serde_error)
.map_err(map_serde_error(name))
}
}

Expand Down Expand Up @@ -360,7 +392,14 @@ impl TryFrom<HashMap<String, Value>> for OutboundProxyProviderDef {
type Error = crate::Error;

fn try_from(mapping: HashMap<String, Value>) -> Result<Self, Self::Error> {
let name = mapping
.get("name")
.and_then(|x| x.as_str())
.ok_or(Error::InvalidConfig(
"missing field `name` in outbound proxy provider".to_owned(),
))?
.to_owned();
OutboundProxyProviderDef::deserialize(MapDeserializer::new(mapping.into_iter()))
.map_err(map_serde_error)
.map_err(map_serde_error(name))
}
}
1 change: 1 addition & 0 deletions clash_lib/src/proxy/converters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod shadowsocks;
pub mod tor;
pub mod trojan;
pub mod vmess;
pub mod wireguard;
Loading

0 comments on commit 7e75412

Please sign in to comment.