Skip to content

Commit

Permalink
Support TUIC (#332)
Browse files Browse the repository at this point in the history
* chore: ignore nix direnv files

* build: add tuic related deps

* feat: support parse tuic configuration

* fix(logging): shutdown should be logged after receiving signal

* feat: support TUIC TCP relay

* feat(tuic): allow more configuration and remove hardcode vars

* chore: clean logging and error handling

* feat(tuic): support UDP relay

* chore(tuic): rename symbols

* build: change tuic into git deps

* style: cargo fmt

* build: update tuic

* refactor: reduce spawn task

* refactor: use Arc<TuicConnection>

* style: make clippy happy

* fix: handle UDP dissociate

* refactor: remove redundant fast open

and fix typo
  • Loading branch information
iHsin authored Mar 25, 2024
1 parent ef2b845 commit bfddc52
Show file tree
Hide file tree
Showing 15 changed files with 1,087 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ venv/

# don't check in this real config
ignore*.yaml
config.yaml
cache.db
Country.mmdb
ruleset/

rust-project.json

# for NixOS direnv
.envrc
shell.nix
84 changes: 84 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions clash_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ once_cell = "1.18.0"
# opentelemetry
opentelemetry = "0.22"
opentelemetry_sdk = { version = "0.22", features = ["rt-tokio"] }
tracing-opentelemetry = "0.23"
tracing-opentelemetry = "0.23"
opentelemetry-jaeger-propagator = "0.1.0"
opentelemetry-jaeger = { version = "0.21", features = ["collector_client", "hyper_collector_client", "rt-tokio"] }
opentelemetry-otlp = { version = "0.15.0", features = ["http-proto"] }
Expand Down Expand Up @@ -84,7 +84,7 @@ hickory-proto = { version = "0.24", features = ["dns-over-rustls", "dns-over-htt

# DoH
# ideally we should make a CryptoProvider with boringssl and get rid of rings
rustls = { version = "0.21", features=["dangerous_configuration"] }
rustls = { version = "0.21", features=["dangerous_configuration", "quic"] }
rustls-pemfile = "1.0.4"
webpki-roots = "0.25"
dhcproto = "0.11"
Expand All @@ -108,6 +108,12 @@ murmur3 = "0.5.2"
arti-client = { version = "0.14.0", default-features = false, features = ["tokio", "rustls", "compression", "static-sqlite"] }
tor-rtcompat = { version = "0.10.0" }

# tuic
tuic = { rev = "82fab62", git = "https://github.com/Itsusinn/tuic.git" }
tuic-quinn = { rev = "82fab62", git = "https://github.com/Itsusinn/tuic.git" }
quinn = { version = "0.10", default-features = false, features = ["futures-io", "runtime-tokio", "tls-rustls"] }
register-count = "0.1.0"

console-subscriber = { version = "0.2.0" }
tracing-timing = { version = "0.6.0" }
criterion = { version = "0.5", features = ["html_reports", "async_tokio"], optional = true }
Expand Down
4 changes: 3 additions & 1 deletion clash_lib/src/app/outbound/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ impl OutboundManager {
OutboundProxyProtocol::Tor(tor) => {
handlers.insert(tor.name.clone(), tor.try_into()?);
}

OutboundProxyProtocol::Tuic(tuic) => {
handlers.insert(tuic.name.clone(), tuic.try_into()?);
}
p => {
unimplemented!("proto {} not supported yet", p);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ impl ProxySetProvider {
OutboundProxyProtocol::Vmess(vm) => vm.try_into(),
OutboundProxyProtocol::Wireguard(wg) => wg.try_into(),
OutboundProxyProtocol::Tor(tor) => tor.try_into(),
OutboundProxyProtocol::Tuic(tuic) => tuic.try_into(),
})
.collect::<Result<Vec<_>, _>>();
Ok(proxies?)
Expand Down
38 changes: 38 additions & 0 deletions clash_lib/src/config/internal/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::Deserialize;
use serde_yaml::Value;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use uuid::Uuid;

pub const PROXY_DIRECT: &str = "DIRECT";
pub const PROXY_REJECT: &str = "REJECT";
Expand Down Expand Up @@ -61,6 +62,8 @@ pub enum OutboundProxyProtocol {
Wireguard(OutboundWireguard),
#[serde(rename = "tor")]
Tor(OutboundTor),
#[serde(rename = "tuic")]
Tuic(OutboundTuic),
}

impl OutboundProxyProtocol {
Expand All @@ -74,6 +77,7 @@ impl OutboundProxyProtocol {
OutboundProxyProtocol::Vmess(vmess) => &vmess.name,
OutboundProxyProtocol::Wireguard(wireguard) => &wireguard.name,
OutboundProxyProtocol::Tor(tor) => &tor.name,
OutboundProxyProtocol::Tuic(tuic) => &tuic.name,
}
}
}
Expand Down Expand Up @@ -105,6 +109,7 @@ impl Display for OutboundProxyProtocol {
OutboundProxyProtocol::Vmess(_) => write!(f, "Vmess"),
OutboundProxyProtocol::Wireguard(_) => write!(f, "Wireguard"),
OutboundProxyProtocol::Tor(_) => write!(f, "Tor"),
OutboundProxyProtocol::Tuic(_) => write!(f, "Tuic"),
}
}
}
Expand Down Expand Up @@ -217,6 +222,39 @@ pub struct OutboundTor {
pub name: String,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Default)]
#[serde(rename_all = "kebab-case")]
pub struct OutboundTuic {
pub name: String,
pub server: String,
pub port: u16,
pub uuid: Uuid,
pub password: String,
/// override field 'server' dns record, not used for now
pub ip: Option<String>,
pub heartbeat_interval: Option<u64>,
/// h3
pub alpn: Option<Vec<String>>,
pub disable_sni: Option<bool>,
pub reduce_rtt: Option<bool>,
/// millis
pub request_timeout: Option<u64>,
pub udp_relay_mode: Option<String>,
pub congestion_controller: Option<String>,
/// bytes
pub max_udp_relay_packet_size: Option<u64>,
pub fast_open: Option<bool>,
pub skip_cert_verify: Option<bool>,
pub max_open_stream: Option<u64>,
pub sni: Option<String>,
/// millis
pub gc_interval: Option<u64>,
/// millis
pub gc_lifetime: Option<u64>,
pub send_window: Option<u64>,
pub receive_window: Option<u64>,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum OutboundGroupProtocol {
Expand Down
2 changes: 1 addition & 1 deletion clash_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,8 @@ async fn start_async(opts: Options) -> Result<(), Error> {
}

runners.push(Box::pin(async move {
info!("receiving shutdown signal");
shutdown_rx.recv().await;
info!("receiving shutdown signal");
Ok(())
}));

Expand Down
1 change: 1 addition & 0 deletions clash_lib/src/proxy/converters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod shadowsocks;
pub mod tor;
pub mod trojan;
pub mod tuic;
pub mod vmess;
pub mod wireguard;
59 changes: 59 additions & 0 deletions clash_lib/src/proxy/converters/tuic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::time::Duration;

use quinn::VarInt;

use crate::{
config::internal::proxy::OutboundTuic,
proxy::{
tuic::{types::CongestionControl, Handler, HandlerOptions},
AnyOutboundHandler,
},
};

impl TryFrom<OutboundTuic> for AnyOutboundHandler {
type Error = crate::Error;

fn try_from(value: OutboundTuic) -> Result<Self, Self::Error> {
(&value).try_into()
}
}

impl TryFrom<&OutboundTuic> for AnyOutboundHandler {
type Error = crate::Error;

fn try_from(s: &OutboundTuic) -> Result<Self, Self::Error> {
Handler::new(HandlerOptions {
name: s.name.to_owned(),
server: s.server.to_owned(),
port: s.port,
uuid: s.uuid.to_owned(),
password: s.password.to_owned(),
udp_relay_mode: s.udp_relay_mode.to_owned().unwrap_or("native".to_string()),
disable_sni: s.disable_sni.unwrap_or(false),
alpn: s
.alpn
.clone()
.map(|v| v.into_iter().map(|alpn| alpn.into_bytes()).collect())
.unwrap_or_default(),
heartbeat_interval: Duration::from_millis(s.heartbeat_interval.unwrap_or(3000)),
reduce_rtt: s.reduce_rtt.unwrap_or(false) || s.fast_open.unwrap_or(false),
request_timeout: Duration::from_millis(s.request_timeout.unwrap_or(8000)),
congestion_controller: s
.congestion_controller
.clone()
.map(|v| CongestionControl::from(v.as_str()))
.unwrap_or_default(),
max_udp_relay_packet_size: s.max_udp_relay_packet_size.unwrap_or(1500),
max_open_stream: VarInt::from_u64(s.max_open_stream.unwrap_or(32))
.unwrap_or(VarInt::MAX),
ip: s.ip.clone(),
skip_cert_verify: s.skip_cert_verify.unwrap_or(false),
sni: s.sni.clone(),
gc_interval: Duration::from_millis(s.gc_interval.unwrap_or(3000)),
gc_lifetime: Duration::from_millis(s.gc_lifetime.unwrap_or(15000)),
send_window: s.send_window.unwrap_or(8 * 1024 * 1024 * 2),
receive_window: VarInt::from_u64(s.receive_window.unwrap_or(8 * 1024 * 1024))
.unwrap_or(VarInt::MAX),
})
}
}
2 changes: 2 additions & 0 deletions clash_lib/src/proxy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub mod shadowsocks;
pub mod socks;
pub mod tor;
pub mod trojan;
pub mod tuic;
pub mod tun;
pub mod utils;
pub mod vmess;
Expand Down Expand Up @@ -110,6 +111,7 @@ pub enum OutboundType {
Trojan,
WireGuard,
Tor,
Tuic,

#[serde(rename = "URLTest")]
UrlTest,
Expand Down
41 changes: 41 additions & 0 deletions clash_lib/src/proxy/tuic/compat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::{
pin::Pin,
task::{Context, Poll},
};

use futures::{Sink, SinkExt, Stream};

use crate::{common::errors::new_io_error, proxy::datagram::UdpPacket};

use super::TuicDatagramOutbound;

impl Sink<UdpPacket> for TuicDatagramOutbound {
type Error = std::io::Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.send_tx
.poll_ready_unpin(cx)
.map_err(|v| new_io_error(&format!("{v:?}")))
}
fn start_send(mut self: Pin<&mut Self>, item: UdpPacket) -> Result<(), Self::Error> {
self.send_tx
.start_send_unpin(item)
.map_err(|v| new_io_error(&format!("{v:?}")))
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.send_tx
.poll_flush_unpin(cx)
.map_err(|v| new_io_error(&format!("{v:?}")))
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.send_tx
.poll_close_unpin(cx)
.map_err(|v| new_io_error(&format!("{v:?}")))
}
}

impl Stream for TuicDatagramOutbound {
type Item = UdpPacket;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.recv_rx.poll_recv(cx)
}
}
Loading

0 comments on commit bfddc52

Please sign in to comment.