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

Support TUIC #332

Merged
merged 19 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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");
Itsusinn marked this conversation as resolved.
Show resolved Hide resolved
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