diff --git a/Cargo.lock b/Cargo.lock index 24c8904..948b3b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1172,7 +1172,7 @@ dependencies = [ [[package]] name = "shuttle" -version = "0.6.5" +version = "0.6.6" dependencies = [ "anyhow", "atoi", @@ -1209,6 +1209,7 @@ dependencies = [ "bytes", "futures", "log", + "rand", "rustls-pemfile 2.0.0", "socks5-proto", "tokio", @@ -1254,8 +1255,7 @@ dependencies = [ [[package]] name = "socks5-proto" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7216e2046a0bd0aa4c4170dab8e7b918e5f1230031abe9a62dc3da47a40543" +source = "git+https://github.com/cyejing/socks5-server.git#2318838e91f7208337eeec10619e4ae5aee3a958" dependencies = [ "bytes", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 6c82902..07c9c55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle" -version = "0.6.5" +version = "0.6.6" edition = "2021" publish = false @@ -67,3 +67,6 @@ futures= {workspace = true} [dev-dependencies] reqwest = { version = "0.11", default-features=false, features = ["socks","rustls-tls"] } + +[patch.crates-io] +socks5-proto = {git="https://github.com/cyejing/socks5-server.git"} diff --git a/examples/shuttlec-proxy.yaml b/examples/shuttlec-proxy.yaml index 3be9037..1add4b0 100644 --- a/examples/shuttlec-proxy.yaml +++ b/examples/shuttlec-proxy.yaml @@ -1,6 +1,6 @@ run_type: proxy proxy_mode: trojan # trojan,direct,websocket -proxy_addr: 0.0.0.0:4080 +proxy_addr: 0.0.0.0:1082 ssl_enable: true invalid_certs: true remote_addr: 127.0.0.1:4845 diff --git a/shuttle-station/Cargo.toml b/shuttle-station/Cargo.toml index 9fc6974..9d0b6fa 100644 --- a/shuttle-station/Cargo.toml +++ b/shuttle-station/Cargo.toml @@ -27,4 +27,5 @@ bytes = {workspace = true} socks5-proto = {workspace = true} async-trait = "0.1" +rand = "0.8.4" diff --git a/shuttle-station/src/dial.rs b/shuttle-station/src/dial.rs index dbf871c..ad6b2ad 100644 --- a/shuttle-station/src/dial.rs +++ b/shuttle-station/src/dial.rs @@ -11,6 +11,7 @@ use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::MaybeTlsStream; +use crate::proto::padding::Padding; use crate::proto::trojan; use crate::tls::make_server_name; use crate::tls::make_tls_connector; @@ -28,6 +29,7 @@ pub struct TrojanDial { remote_addr: String, hash: String, invalid_certs: bool, + padding: bool, } #[derive(Debug)] pub struct WebSocketDial { @@ -36,11 +38,12 @@ pub struct WebSocketDial { } impl TrojanDial { - pub fn new(remote_addr: String, hash: String, invalid_certs: bool) -> Self { + pub fn new(remote_addr: String, hash: String, invalid_certs: bool, padding: bool) -> Self { Self { remote_addr, hash, invalid_certs, + padding, } } } @@ -77,8 +80,14 @@ impl Dial for TrojanDial { .await .context(format!("Trojan can't connect remote {}", remote_addr))?; - let req = trojan::Request::new(self.hash.clone(), Command::Connect, addr); - req.write_to(&mut remote_ts).await?; + if self.padding { + let req = trojan::Request::new(self.hash.clone(), Command::Padding, addr); + req.write_to(&mut remote_ts).await?; + Padding::read_from(&mut remote_ts).await?; + } else { + let req = trojan::Request::new(self.hash.clone(), Command::Connect, addr); + req.write_to(&mut remote_ts).await?; + }; Ok(remote_ts) } @@ -98,8 +107,14 @@ impl Dial> for TrojanDial { .await .context("Trojan can't connect tls")?; - let req = trojan::Request::new(self.hash.clone(), Command::Connect, addr); - req.write_to(&mut remote_ts_ssl).await?; + if self.padding { + let req = trojan::Request::new(self.hash.clone(), Command::Padding, addr); + req.write_to(&mut remote_ts_ssl).await?; + Padding::read_from(&mut remote_ts_ssl).await?; + } else { + let req = trojan::Request::new(self.hash.clone(), Command::Connect, addr); + req.write_to(&mut remote_ts_ssl).await?; + } Ok(remote_ts_ssl) } diff --git a/shuttle-station/src/proto/mod.rs b/shuttle-station/src/proto/mod.rs index bd1c576..4a8a1a5 100644 --- a/shuttle-station/src/proto/mod.rs +++ b/shuttle-station/src/proto/mod.rs @@ -1,2 +1,3 @@ pub mod http_connect; +pub mod padding; pub mod trojan; diff --git a/shuttle-station/src/proto/padding.rs b/shuttle-station/src/proto/padding.rs new file mode 100644 index 0000000..54cae38 --- /dev/null +++ b/shuttle-station/src/proto/padding.rs @@ -0,0 +1,59 @@ +use anyhow::Context; +use bytes::{BufMut, BytesMut}; +use rand::{Rng, RngCore}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +#[derive(Debug)] +pub struct Padding { + len: u16, +} + +impl Padding { + pub async fn read_from(stream: &mut R) -> anyhow::Result + where + R: AsyncRead + Unpin, + { + let len = stream.read_u16().await?; + let mut buf = vec![0; len as usize]; + stream.read_exact(&mut buf).await?; + Ok(Padding { len }) + } + + pub async fn write_to(&self, w: &mut W) -> anyhow::Result<()> + where + W: AsyncWrite + Unpin, + { + let mut buf = BytesMut::with_capacity(self.serialized_len()); + self.write_to_buf(&mut buf); + w.write_all(&buf) + .await + .context("Padding Write buf failed")?; + + Ok(()) + } + + pub fn write_to_buf(&self, buf: &mut B) { + let rand_buf = self.rand_buf(); + buf.put_u16(self.len); + buf.put_slice(&rand_buf); + } + + pub fn serialized_len(&self) -> usize { + 2 + self.len as usize + } + + pub fn rand_buf(&self) -> Vec { + let mut rng = rand::thread_rng(); + let mut buf = vec![0; self.len as usize]; + rng.fill_bytes(&mut buf); + buf + } +} + +impl Default for Padding { + fn default() -> Self { + let mut rng = rand::thread_rng(); + let len = rng.gen_range(100..3000); + Self { len } + } +} diff --git a/shuttle-station/src/proto/trojan.rs b/shuttle-station/src/proto/trojan.rs index f0f63a7..330ebb9 100644 --- a/shuttle-station/src/proto/trojan.rs +++ b/shuttle-station/src/proto/trojan.rs @@ -7,6 +7,8 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use crate::{peekable::AsyncPeek, CRLF}; +use super::padding::Padding; + /// Trojan request /// /// ```plain @@ -90,6 +92,12 @@ impl Request { let _crlf = r.read_u16().await?; + if let Command::Padding = cmd { + let _padding = Padding::read_from(r) + .await + .context("trojan read padding failed")?; + } + Ok(Self::new(hash, cmd, addr)) } @@ -173,6 +181,9 @@ impl Request { buf.put_u8(u8::from(self.command)); self.write_to_buf_address(buf); buf.put_slice(&CRLF); + if self.is_padding() { + Padding::default().write_to_buf(buf) + } } pub fn write_to_buf_address(&self, buf: &mut B) { match &self.address { @@ -200,4 +211,8 @@ impl Request { pub fn serialized_len(&self) -> usize { 56 + 2 + 1 + self.address.serialized_len() + 2 } + + pub fn is_padding(&self) -> bool { + Command::Padding == self.command + } } diff --git a/src/client.rs b/src/client.rs index fe93407..29946dd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,8 @@ use std::{sync::Arc, time::Duration}; use shuttle_station::{ - dial::{Dial, DirectDial, TrojanDial, WebSocketDial}, + dial::{DirectDial, TrojanDial, WebSocketDial}, proxy::ProxyConnection, - Address, }; use tokio::net::{TcpListener, TcpStream}; use tokio_rustls::client::TlsStream; @@ -68,6 +67,7 @@ async fn proxy_handle(cc: Arc, ts: TcpStream) { cc.remote_addr.clone(), cc.hash.clone(), cc.invalid_certs, + cc.padding, )), ) .handle() @@ -80,6 +80,7 @@ async fn proxy_handle(cc: Arc, ts: TcpStream) { cc.remote_addr.clone(), cc.hash.clone(), cc.invalid_certs, + cc.padding, )), ) .handle() @@ -95,16 +96,3 @@ async fn proxy_handle(cc: Arc, ts: TcpStream) { } }; } - -#[allow(dead_code)] -fn websocket_heartbeat_open(remote_addr: String, hash: String) { - tokio::spawn(async move { - loop { - let ws_dial = WebSocketDial::new(remote_addr.clone(), hash.clone()); - let address = Address::DomainAddress("api.shuttle.rs".as_bytes().to_vec(), 443); - let wss = ws_dial.dial(address).await.ok(); - info!("heartbeat dial {remote_addr} [{}]", wss.is_some()); - tokio::time::sleep(Duration::from_secs(30)).await - } - }); -} diff --git a/src/config.rs b/src/config.rs index 6711e44..96d5853 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,8 @@ pub struct ClientConfig { pub ssl_enable: bool, #[serde(default = "default_false")] pub invalid_certs: bool, + #[serde(default = "default_true")] + pub padding: bool, #[serde(default)] pub holes: Vec, } diff --git a/src/server.rs b/src/server.rs index a35315e..0882d65 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Context}; use shuttle_station::dial::{Dial, DirectDial}; use shuttle_station::peekable::{AsyncPeek, PeekableStream}; +use shuttle_station::proto::padding::Padding; use shuttle_station::proto::{self, trojan}; use tracing::{info_span, Instrument}; @@ -124,6 +125,10 @@ where .context("Trojan request read failed")?; let addr = req.address.clone(); + if req.is_padding() { + Padding::default().write_to(stream).await?; + } + debug!("Trojan start connect {addr}"); let mut remote_ts = DirectDial::default()