diff --git a/.cargo/config.toml b/.cargo/config.toml index 456b19b..0805c38 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,4 +2,4 @@ protocol = "sparse" [build] -#target = "aarch64-linux-android" +# target = "aarch64-linux-android" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..79f7c71 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,106 @@ +name: Check + +on: + [push, pull_request] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: rustup component add rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - run: rustup component add clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + + build: + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu # arch: x86_64, os: linux + - aarch64-unknown-linux-gnu # arch: aarch64 + - armv7-unknown-linux-gnueabihf # arch: armv7 + - x86_64-apple-darwin # os: darwin + - aarch64-apple-darwin # os: darwin + - x86_64-pc-windows-msvc # os: windows + - i686-pc-windows-msvc + + include: + - target: x86_64-unknown-linux-gnu + host_os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + host_os: ubuntu-latest + - target: armv7-unknown-linux-gnueabihf + host_os: ubuntu-latest + - target: x86_64-apple-darwin + host_os: macos-latest + - target: aarch64-apple-darwin + host_os: macos-latest + - target: x86_64-pc-windows-msvc + host_os: windows-latest + - target: i686-pc-windows-msvc + host_os: windows-latest + + runs-on: ${{ matrix.host_os }} + + steps: + - uses: actions/checkout@v3 + + - name: Prepare + shell: bash + run: | + rustup target add ${{ matrix.target }} + + - name: Build + shell: bash + run: | + if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then + sudo .github/workflows/install-cross.sh + cross build --verbose --target ${{ matrix.target }} + else + cargo build --verbose --target ${{ matrix.target }} + fi + + - name: Run tests + run: cargo test --verbose --all-features + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8d888bf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,76 @@ +name: Build Releases +on: + push: + tags: + - "*" +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu + - x86_64-unknown-linux-musl + - aarch64-unknown-linux-gnu + - armv7-unknown-linux-gnueabihf + - x86_64-apple-darwin + - aarch64-apple-darwin + - x86_64-pc-windows-msvc + - i686-pc-windows-msvc + + include: + - target: x86_64-unknown-linux-gnu + host_os: ubuntu-latest + - target: x86_64-unknown-linux-musl + host_os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + host_os: ubuntu-latest + - target: armv7-unknown-linux-gnueabihf + host_os: ubuntu-latest + - target: x86_64-apple-darwin + host_os: macos-latest + - target: aarch64-apple-darwin + host_os: macos-latest + - target: x86_64-pc-windows-msvc + host_os: windows-latest + - target: i686-pc-windows-msvc + host_os: windows-latest + + runs-on: ${{ matrix.host_os }} + steps: + - uses: actions/checkout@v3 + + - name: Prepare + shell: bash + run: | + mkdir release + rustup target add ${{ matrix.target }} + if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then + sudo .github/workflows/install-cross.sh + fi + + - name: Build + shell: bash + run: | + if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then + cross build --all-features --release --target ${{ matrix.target }} + else + cargo build --all-features --release --target ${{ matrix.target }} + fi + cbindgen -c cbindgen.toml -l C -o target/${{ matrix.target }}/release/overtls-api.h + if [[ "${{ matrix.host_os }}" == "windows-latest" ]]; then + powershell Compress-Archive -Path target/${{ matrix.target }}/release/overtls.exe, ./config.json, target/${{ matrix.target }}/release/overtls-api.h, target/${{ matrix.target }}/release/overtls.dll -DestinationPath release/overtls-${{ matrix.target }}.zip + elif [[ "${{ matrix.host_os }}" == "macos-latest" ]]; then + zip -j release/overtls-${{ matrix.target }}.zip target/${{ matrix.target }}/release/overtls ./config.json target/${{ matrix.target }}/release/overtls-api.h target/${{ matrix.target }}/release/libovertls.dylib + elif [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then + zip -j release/overtls-${{ matrix.target }}.zip target/${{ matrix.target }}/release/overtls ./config.json target/${{ matrix.target }}/release/overtls-api.h target/${{ matrix.target }}/release/libovertls.so + fi + + - name: Upload + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: release/* diff --git a/.gitignore b/.gitignore index d9f29fb..375f42a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +examples/ nginx_signing.key overtls-daemon.sh project.xcworkspace/ diff --git a/Cargo.toml b/Cargo.toml index b6020f4..b063dbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "overtls" -version = "0.1.7" +version = "0.2.8" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -8,30 +8,44 @@ edition = "2021" crate-type = ["staticlib", "cdylib", "lib"] [dependencies] +async-shared-timeout = "0.2" base64 = "0.21" -bytes = "1.4" -clap = { version = "4.2", features = ["derive"] } +bytes = "1.5" +chrono = "0.4" +clap = { version = "4.4", features = ["derive"] } +ctrlc2 = { version = "3.5", features = ["tokio", "termination"] } dotenvy = "0.15" env_logger = "0.10" -futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } -http = "0.2" +futures-util = { version = "0.3", default-features = false, features = [ + "sink", + "std", +] } +http = "1.0" httparse = "1.8" lazy_static = "1.4" -log = "0.4" -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json"] } -rustls = "0.21" -rustls-pemfile = "1.0" -serde = { version = "1.0", features = [ "derive" ] } +log = { version = "0.4", features = ["std"] } +moka = { version = "0.12", features = ["future"] } +reqwest = { version = "0.11", default-features = false, features = [ + "rustls-tls", + "json", +] } +rustls = { version = "0.22" } +rustls-pemfile = "2.0" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -socks5-impl = "0.2" +socks5-impl = "0.5" thiserror = "1.0" -tokio = { version = "1.28", features = [ "full" ] } -tokio-rustls = "0.24" -tokio-tungstenite = { version = "0.19", features = [ "rustls-tls-webpki-roots" ] } -tungstenite = { version = "0.19", features = [ "rustls-tls-webpki-roots" ] } -url = "2.3" -webpki = { package = "rustls-webpki", version = "0.100", features = ["alloc", "std"] } -webpki-roots = "0.23" +tokio = { version = "1.35", features = ["full"] } +tokio-rustls = "0.25" +tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] } +trust-dns-proto = "0.23" +tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] } +url = "2.5" +webpki = { package = "rustls-webpki", version = "0.102", features = [ + "alloc", + "std", +] } +webpki-roots = "0.26" [target.'cfg(target_family="unix")'.dependencies] daemonize = "0.5" diff --git a/apple/readme.md b/apple/readme.md new file mode 100644 index 0000000..58db657 --- /dev/null +++ b/apple/readme.md @@ -0,0 +1,19 @@ +## Building iOS framework + +### Install **Rust** build tools +- Install Xcode Command Line Tools: `xcode-select --install` +- Install Rust programming language: `curl https://sh.rustup.rs -sSf | sh` +- Install iOS target support: `rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios` +- Install `cbindgen` tool: `cargo install cbindgen` + +### Building iOS framework +Due to an unknown reason at present, compiling Rust code from Xcode fails, so you have to manually compile it. +Please run the following command in zsh (or bash): +```bash +cd overtls + +cargo build --release --target aarch64-apple-ios +cargo build --release --target x86_64-apple-ios +lipo -create target/aarch64-apple-ios/release/libovertls.a target/x86_64-apple-ios/release/libovertls.a -output target/libovertls.a +cbindgen --config cbindgen.toml -l C -o target/overtls-ios.h +``` diff --git a/cbindgen.toml b/cbindgen.toml index f2276b7..f6b6598 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -1,3 +1,3 @@ [export] -include = ["over_tls_client_run", "over_tls_client_stop"] +include = ["over_tls_client_run", "over_tls_client_stop", "overtls_set_log_callback"] exclude = ["Java_com_github_shadowsocks_bg_OverTlsWrapper_runClient", "Java_com_github_shadowsocks_bg_OverTlsWrapper_stopClient"] diff --git a/config.json b/config.json index 3c48685..6bc14d1 100644 --- a/config.json +++ b/config.json @@ -3,7 +3,6 @@ "method": "none", "password": "password", "tunnel_path": "/secret-tunnel-path/", - "server_settings": { "disable_tls": false, "manage_clients": { @@ -19,7 +18,6 @@ "listen_host": "0.0.0.0", "listen_port": 443 }, - "client_settings": { "disable_tls": false, "client_id": "33959370-71e0-401d-9746-cda471fc5926", @@ -27,6 +25,8 @@ "server_port": 443, "server_domain": "example.com", "cafile": "", + "listen_user": "", + "listen_password": "", "listen_host": "127.0.0.1", "listen_port": 1080 } diff --git a/install/overtls-install.sh b/install/overtls-install.sh index 348a9e4..92a928c 100755 --- a/install/overtls-install.sh +++ b/install/overtls-install.sh @@ -711,6 +711,44 @@ function install_binary_as_systemd_service() { create_overtls_systemd_service "${role}" "${local_bin_file_path}" "${local_cfg_file_path}" } +function macos_install_binary_as_service() { + local role="${1}" + local local_bin_file_path=${2} + local local_cfg_file_path=${3} + local svc_daemon_file_path="~/Library/LaunchAgents/${service_name}.plist" + + cat > ${svc_daemon_file_path} < + + + + Label + ${service_name} + RunAtLoad + + KeepAlive + + StartInterval + 3 + ProgramArguments + + ${local_bin_file_path} + -r + ${role} + -c + ${local_cfg_file_path} + -d + + WorkingDirectory + /usr/local + + +EOF + + launchctl load ${svc_daemon_file_path} + launchctl start ${service_name} +} + # Uninstall overtls function uninstall_overtls() { printf "Are you sure uninstall ${service_name}? (y/n)\n" @@ -825,18 +863,35 @@ function main() { uninstall_overtls ;; service) - check_root_account local role="${2}" local customer_binary_path="$3" local customer_cfg_file_path="$4" check_install_systemd_svc_params "${role}" "${customer_binary_path}" "${customer_cfg_file_path}" - install_binary_as_systemd_service "${role}" "${customer_binary_path}" "${customer_cfg_file_path}" + if [[ "$(uname)" == "Linux" ]]; then + check_root_account + install_binary_as_systemd_service "${role}" "${customer_binary_path}" "${customer_cfg_file_path}" + elif [[ "$(uname)" == "Darwin" ]]; then + macos_install_binary_as_service "${role}" "${customer_binary_path}" "${customer_cfg_file_path}" + else + echo -e "${RedBG} Unsupported operating system! ${Font}" + exit 1 + fi ;; qrcode) local svc_bin_path="${2}" local cfg_path="${3}" - check_system - sudo ${INSTALL_CMD} -y install qrencode >/dev/null 2>&1 + if [[ "$(uname)" == "Darwin" ]]; then + if ! command -v qrencode &> /dev/null ; then + if ! command -v brew &> /dev/null ; then + echo -e "${Info} ${Yellow} Homebrew not found, please install it first! ${Font}" + exit 1 + fi + brew install qrencode >/dev/null 2>&1 + fi + elif [[ "$(uname)" == "Linux" ]]; then + check_system + sudo ${INSTALL_CMD} -y install qrencode >/dev/null 2>&1 + fi print_qrcode "${svc_bin_path}" "${cfg_path}" ;; *) diff --git a/install/rust.sh b/install/rust.sh index 64780aa..7d7c3a5 100755 --- a/install/rust.sh +++ b/install/rust.sh @@ -13,6 +13,7 @@ function linux_dependency_install() { if [[ "${ID}" == "ubuntu" && `echo "${VERSION_ID}" | cut -d '.' -f1` -ge 20 ]]; then sudo apt -y install inetutils-ping fi + sudo apt autoremove -y else echo -e "Current system is ${ID} ${VERSION_ID} is not in the list of supported systems, installation is interrupted " exit 1 diff --git a/readme-cn.md b/readme-cn.md index 8466f54..75514a6 100644 --- a/readme-cn.md +++ b/readme-cn.md @@ -4,7 +4,7 @@ overtls 是 [SOCKS5](https://en.wikipedia.org/wiki/SOCKS#SOCKS5) 型代理軟件 功能齊備且代碼精簡,核心功能總共也就大概 1200 行代碼。 -> `OverTLS` 相當於 `SSRoT` 去掉 `SSR` 和 `SS`, 唯獨保留 `oT` 的 Rust 實現,快如閃電,穩如老狗。 +> `OverTLS` 相當於 [SSRoT](https://github.com/ShadowsocksR-Live/shadowsocksr-native) 去掉 `SSR` 和 `SS`, 唯獨保留 `oT` 的 Rust 實現,快如閃電,穩如老狗。 > ```kotlin > fun isOverTLS() : Boolean = > over_tls_enable && method == "none" && obfs == "plain" && protocol == "origin" diff --git a/readme.md b/readme.md index 6be2e60..30a0d5c 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ overtls is [SOCKS5](https://en.wikipedia.org/wiki/SOCKS#SOCKS5) type proxy softw The function is complete and the code is concise, and the core function is 1200 lines of code in total. -> `OverTLS` is a Rust implementation of `SSRoT` without `SSR` and `SS`, only retaining `oT`, which is fast and stable. +> `OverTLS` is a Rust implementation of [SSRoT](https://github.com/ShadowsocksR-Live/shadowsocksr-native) without `SSR` and `SS`, only retaining `oT`, which is fast and stable. > ```kotlin > fun isOverTLS() : Boolean = > over_tls_enable && method == "none" && obfs == "plain" && protocol == "origin" @@ -109,24 +109,3 @@ Note the `tunnel_path` configuration, please make sure to change it to your own > For testing purposes, the `disable_tls` option is provided to have the ability to disable `TLS`; that is, if this option exists and is true, the software will transmit traffic in `plain text`; for security reasons, please do not use it on official occasions. This example shows the configuration file of the least entry, the complete configuration file can refer to [config.json](config.json). - - -## Building iOS framework - -### Install **Rust** build tools -- Install Xcode Command Line Tools: `xcode-select --install` -- Install Rust programming language: `curl https://sh.rustup.rs -sSf | sh` -- Install iOS target support: `rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios` -- Install `cbindgen` tool: `cargo install cbindgen` - -### Building iOS framework -Due to an unknown reason at present, compiling Rust code from Xcode fails, so you have to manually compile it. -Please run the following command in zsh (or bash): -``` -cd overtls - -cargo build --release --target aarch64-apple-ios -cargo build --release --target x86_64-apple-ios -lipo -create target/aarch64-apple-ios/release/libovertls.a target/x86_64-apple-ios/release/libovertls.a -output target/libovertls.a -cbindgen --config cbindgen.toml -l C -o target/overtls-ios.h -``` diff --git a/rustfmt.toml b/rustfmt.toml index 7530651..8449be0 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -max_width = 120 +max_width = 140 diff --git a/src/android.rs b/src/android.rs index ff1eee5..50bc47d 100644 --- a/src/android.rs +++ b/src/android.rs @@ -11,7 +11,7 @@ pub mod native { JNIEnv, JavaVM, }; use std::{ - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, RwLock, @@ -25,11 +25,7 @@ pub mod native { macro_rules! socket_protector { () => { - crate::android::native::SOCKET_PROTECTOR - .lock() - .unwrap() - .as_mut() - .unwrap() + crate::android::native::SOCKET_PROTECTOR.lock().unwrap().as_mut().unwrap() }; } @@ -222,8 +218,12 @@ pub mod native { EXITING_FLAG.store(true, Ordering::SeqCst); let l_addr = *LISTEN_ADDR.lock().unwrap(); - let addr = if l_addr.is_ipv6() { "::1" } else { crate::LOCAL_HOST_V4 }; - let _ = std::net::TcpStream::connect((addr, l_addr.port())); + let addr = if l_addr.is_ipv6() { + SocketAddr::from((Ipv6Addr::LOCALHOST, l_addr.port())) + } else { + SocketAddr::from((Ipv4Addr::LOCALHOST, l_addr.port())) + }; + let _ = std::net::TcpStream::connect(addr); log::trace!("stopClient on listen address {l_addr}"); SocketProtector::release(); @@ -273,12 +273,8 @@ pub mod native { let return_type = ReturnType::Primitive(Primitive::Boolean); let arguments = [JValue::Int(socket).as_jni()]; let result = unsafe { - self.jni_env.call_method_unchecked( - self.vpn_service, - self.protect_method_id, - return_type, - &arguments[..], - ) + self.jni_env + .call_method_unchecked(self.vpn_service, self.protect_method_id, return_type, &arguments[..]) }; match result { Ok(value) => { diff --git a/src/api.rs b/src/api.rs index 2a70b58..bcd52bc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,10 +1,9 @@ #![cfg(not(target_os = "android"))] use crate::error::{Error, Result}; -use crate::LOCAL_HOST_V4; -use std::os::raw::{c_char, c_int, c_void}; use std::{ - net::SocketAddr, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + os::raw::{c_char, c_int, c_void}, sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, @@ -27,7 +26,7 @@ unsafe impl Sync for CCallback {} lazy_static::lazy_static! { static ref EXITING_FLAG: Arc = Arc::new(AtomicBool::new(false)); - static ref LISTEN_ADDR: Arc> = Arc::new(Mutex::new(format!("{}:0", LOCAL_HOST_V4).parse::().unwrap())); + static ref LISTEN_ADDR: Arc> = Arc::new(Mutex::new(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))); } /// # Safety @@ -39,20 +38,29 @@ pub unsafe extern "C" fn over_tls_client_run( verbose: c_char, callback: Option, ctx: *mut c_void, +) -> c_int { + use log::LevelFilter; + let log_level = if verbose != 0 { LevelFilter::Trace } else { LevelFilter::Info }; + log::set_max_level(log_level); + log::set_boxed_logger(Box::::default()).unwrap(); + + _over_tls_client_run(config_path, callback, ctx) +} + +unsafe fn _over_tls_client_run( + config_path: *const c_char, + callback: Option, + ctx: *mut c_void, ) -> c_int { let ccb = CCallback(callback, ctx); let block = || -> Result<()> { let config_path = std::ffi::CStr::from_ptr(config_path).to_str()?; - let log_level = if verbose != 0 { "trace" } else { "info" }; - let root = module_path!().split("::").next().ok_or("module path error")?; - let default = format!("off,{}={}", root, log_level); - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default)).init(); let cb = |addr: SocketAddr| { log::trace!("Listening on {}", addr); let port = addr.port(); - let addr = format!("{}:{}", LOCAL_HOST_V4, port).parse::().unwrap(); + let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port)); *LISTEN_ADDR.lock().unwrap() = addr; unsafe { ccb.call(port as c_int); @@ -83,8 +91,12 @@ pub unsafe extern "C" fn over_tls_client_stop() -> c_int { EXITING_FLAG.store(true, Ordering::SeqCst); let l_addr = *LISTEN_ADDR.lock().unwrap(); - let addr = if l_addr.is_ipv6() { "::1" } else { LOCAL_HOST_V4 }; - let _ = std::net::TcpStream::connect((addr, l_addr.port())); + let addr = if l_addr.is_ipv6() { + SocketAddr::from((Ipv6Addr::LOCALHOST, l_addr.port())) + } else { + SocketAddr::from((Ipv4Addr::LOCALHOST, l_addr.port())) + }; + let _ = std::net::TcpStream::connect(addr); log::trace!("Client stop on listen address {}", l_addr); 0 } diff --git a/src/client.rs b/src/client.rs index da36566..05aa06f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,7 +10,11 @@ use bytes::BytesMut; use futures_util::{SinkExt, StreamExt}; use socks5_impl::{ protocol::{Address, Reply}, - server::{auth::NoAuth, connection::connect::NeedReply, Connect, Connection, IncomingConnection, Server}, + server::{ + auth::{NoAuth, UserKeyAuth}, + connection::connect::NeedReply, + AuthAdaptor, ClientConnection, Connect, IncomingConnection, Server, + }, }; use std::{ net::SocketAddr, @@ -42,9 +46,28 @@ where log::trace!("with following settings:"); log::trace!("{}", serde_json::to_string_pretty(config)?); + let client = config.client.as_ref().ok_or("client")?; + + let listen_user = client.listen_user.as_deref().filter(|s| !s.is_empty()); + if let Some(user) = listen_user { + let listen_password = client.listen_password.as_deref().unwrap_or(""); + let key = UserKeyAuth::new(user, listen_password); + _run_client(config, Arc::new(key), exiting_flag, callback).await?; + } else { + _run_client(config, Arc::new(NoAuth), exiting_flag, callback).await?; + } + Ok(()) +} + +async fn _run_client(config: &Config, auth: AuthAdaptor, exiting_flag: Option>, callback: Option) -> Result<()> +where + F: FnOnce(SocketAddr) + Send + Sync + 'static, + O: Send + Sync + 'static, +{ let client = config.client.as_ref().ok_or("client")?; let addr = SocketAddr::new(client.listen_host.parse()?, client.listen_port); - let server = Server::bind(addr, std::sync::Arc::new(NoAuth)).await?; + + let server = Server::::bind(addr, auth).await?; if let Some(callback) = callback { callback(server.local_addr()?); @@ -73,15 +96,16 @@ where Ok(()) } -async fn handle_incoming( - conn: IncomingConnection, +async fn handle_incoming( + conn: IncomingConnection, config: Config, udp_tx: Option, - incomings: udprelay::SocketAddrSet, + incomings: udprelay::SocketAddrHashSet, ) -> Result<()> { let peer_addr = conn.peer_addr()?; - match conn.handshake().await? { - Connection::Associate(asso, _) => { + let (conn, _res) = conn.authenticate().await?; + match conn.wait_request().await? { + ClientConnection::UdpAssociate(asso, _) => { if let Some(udp_tx) = udp_tx { if let Err(e) = udprelay::handle_s5_upd_associate(asso, udp_tx, incomings).await { log::debug!("{peer_addr} handle_s5_upd_associate \"{e}\""); @@ -91,11 +115,11 @@ async fn handle_incoming( conn.shutdown().await?; } } - Connection::Bind(bind, _) => { + ClientConnection::Bind(bind, _) => { let mut conn = bind.reply(Reply::CommandNotSupported, Address::unspecified()).await?; conn.shutdown().await?; } - Connection::Connect(connect, addr) => { + ClientConnection::Connect(connect, addr) => { if let Err(e) = handle_socks5_cmd_connection(connect, addr.clone(), config).await { log::debug!("{} <> {} {}", peer_addr, addr, e); } @@ -116,7 +140,7 @@ async fn handle_socks5_cmd_connection(connect: Connect, target_addr: let client = config.client.as_ref().ok_or("client not exist")?; let (ip_addr, port) = (client.server_host.as_str(), client.server_port); - let addr = &SocketAddr::new(ip_addr.parse()?, port); + let addr = SocketAddr::new(ip_addr.parse()?, port); if !config.disable_tls() { let ws_stream = create_tls_ws_stream(addr, Some(target_addr.clone()), &config, None).await?; @@ -134,6 +158,7 @@ async fn client_traffic_loop Result<()> { + let mut timer = tokio::time::interval(std::time::Duration::from_secs(30)); loop { let mut buf = BytesMut::with_capacity(crate::STREAM_BUFFER_SIZE); tokio::select! { @@ -171,9 +196,16 @@ async fn client_traffic_loop { + log::trace!("{} <- {} Websocket pong from remote", peer_addr, target_addr); + }, _ => {} } } + _ = timer.tick() => { + ws_stream.send(Message::Ping(vec![])).await?; + log::trace!("{} -> {} Websocket ping from local", peer_addr, target_addr); + } } } Ok(()) @@ -182,7 +214,7 @@ async fn client_traffic_loop>; pub(crate) async fn create_tls_ws_stream( - svr_addr: &SocketAddr, + svr_addr: SocketAddr, dst_addr: Option
, config: &Config, udp_tunnel: Option, @@ -199,7 +231,7 @@ pub(crate) async fn create_tls_ws_stream( } pub(crate) async fn create_plaintext_ws_stream( - server_addr: &SocketAddr, + server_addr: SocketAddr, dst_addr: Option
, config: &Config, udp_tunnel: Option, @@ -232,14 +264,11 @@ pub(crate) async fn create_ws_stream( stream.read_buf(&mut buf).await?; let response = Response::try_parse(&buf)?.ok_or("response parse failed")?.1; - let remote_key = response - .headers() - .get("Sec-WebSocket-Accept") - .ok_or(format!("{:?}", response))?; + let remote_key = response.headers().get("Sec-WebSocket-Accept").ok_or(format!("{:?}", response))?; let accept_key = tungstenite::handshake::derive_accept_key(key.as_bytes()); - if accept_key.as_str() != remote_key.to_str()? { + if accept_key.as_str() != remote_key.to_str().map_err(|e| e.to_string())? { return Err(Error::from("accept key error")); } diff --git a/src/cmdopt.rs b/src/cmdopt.rs index 5cce3d8..d3e07a1 100644 --- a/src/cmdopt.rs +++ b/src/cmdopt.rs @@ -4,6 +4,16 @@ pub enum Role { Client, } +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +pub enum ArgVerbosity { + Off, + Error, + Warn, + Info, + Debug, + Trace, +} + /// Proxy tunnel over tls #[derive(clap::Parser, Debug, Clone, PartialEq, Eq)] #[command(author, version, about = "Proxy tunnel over tls.", long_about = None)] @@ -16,9 +26,13 @@ pub struct CmdOpt { #[arg(short, long, value_name = "file path")] pub config: std::path::PathBuf, - /// Verbose mode. - #[arg(short, long)] - pub verbose: bool, + /// Cache DNS Query result + #[arg(long)] + pub cache_dns: bool, + + /// Verbosity level + #[arg(short, long, value_name = "level", value_enum, default_value = "info")] + pub verbosity: ArgVerbosity, #[cfg(target_family = "unix")] #[arg(short, long)] diff --git a/src/config.rs b/src/config.rs index f830cc7..8280545 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use crate::error::{Error, Result}; use serde::{Deserialize, Serialize}; use std::{ - net::{SocketAddr, ToSocketAddrs}, + net::{Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}, path::PathBuf, }; @@ -52,6 +52,10 @@ pub struct Client { pub cafile: Option, pub listen_host: String, pub listen_port: u16, + pub listen_user: Option, + pub listen_password: Option, + #[serde(skip)] + pub cache_dns: bool, } impl Default for Config { @@ -119,7 +123,7 @@ impl Config { pub fn forward_addr(&self) -> Option { if self.is_server { let f = |s: &Server| s.forward_addr.clone(); - let default = Some(format!("http://{}:80", crate::LOCAL_HOST_V4)); + let default = Some(format!("http://{}:80", Ipv4Addr::LOCALHOST)); self.server.as_ref().map(f).unwrap_or(default) } else { None @@ -129,16 +133,10 @@ impl Config { pub fn listen_addr(&self) -> Result { if self.is_server { let f = |s: &Server| SocketAddr::new(s.listen_host.parse().unwrap(), s.listen_port); - self.server - .as_ref() - .map(f) - .ok_or_else(|| "Server listen address is not set".into()) + self.server.as_ref().map(f).ok_or_else(|| "Server listen address is not set".into()) } else { let f = |c: &Client| SocketAddr::new(c.listen_host.parse().unwrap(), c.listen_port); - self.client - .as_ref() - .map(f) - .ok_or_else(|| "Client listen address is not set".into()) + self.client.as_ref().map(f).ok_or_else(|| "Client listen address is not set".into()) } } @@ -153,6 +151,16 @@ impl Config { false } + pub fn cache_dns(&self) -> bool { + self.client.as_ref().map_or(false, |c| c.cache_dns) + } + + pub fn set_cache_dns(&mut self, cache_dns: bool) { + if let Some(c) = &mut self.client { + c.cache_dns = cache_dns; + } + } + pub fn check_correctness(&mut self, is_server: bool) -> Result<()> { self.is_server = is_server; if self.is_server { @@ -175,7 +183,7 @@ impl Config { if let Some(server) = &mut self.server { if server.listen_host.is_empty() { - server.listen_host = "0.0.0.0".to_string(); + server.listen_host = Ipv4Addr::UNSPECIFIED.to_string(); } if server.listen_port == 0 { server.listen_port = 443; @@ -191,9 +199,6 @@ impl Config { if client.server_domain.is_none() || client.server_domain.as_ref().unwrap_or(&"".to_string()).is_empty() { client.server_domain = Some(client.server_host.clone()); } - if client.listen_host.is_empty() { - client.listen_host = crate::LOCAL_HOST_V4.to_string(); - } if !self.is_server { let mut addr = (client.server_host.clone(), client.server_port).to_socket_addrs()?; @@ -203,7 +208,13 @@ impl Config { let timeout = std::time::Duration::from_secs(self.test_timeout_secs); std::net::TcpStream::connect_timeout(&addr, timeout)?; } - client.server_host = addr.ip().to_string(); + if client.listen_host.is_empty() { + client.listen_host = if addr.is_ipv4() { + Ipv4Addr::LOCALHOST.to_string() + } else { + Ipv6Addr::LOCALHOST.to_string() + }; + } } } Ok(()) @@ -230,7 +241,9 @@ impl Config { let host = &client.server_host; let port = client.server_port; - let url = format!("{host}:{port}:origin:{method}:plain:{password}/?remarks={remarks}&ot_enable=1&ot_domain={domain}&ot_path={tunnel_path}"); + let url = format!( + "{host}:{port}:origin:{method}:plain:{password}/?remarks={remarks}&ot_enable=1&ot_domain={domain}&ot_path={tunnel_path}" + ); Ok(format!("ssr://{}", crate::base64_encode(url.as_bytes(), engine))) } } diff --git a/src/dns.rs b/src/dns.rs new file mode 100644 index 0000000..0dd2d32 --- /dev/null +++ b/src/dns.rs @@ -0,0 +1,73 @@ +use moka::future::Cache; +use std::{net::IpAddr, time::Duration}; +use trust_dns_proto::{ + op::{Message, Query, ResponseCode::NoError}, + rr::RData, +}; + +pub(crate) fn parse_data_to_dns_message(data: &[u8], used_by_tcp: bool) -> std::io::Result { + if used_by_tcp { + let err = std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid DNS data"); + if data.len() < 2 { + return Err(err); + } + let len = u16::from_be_bytes([data[0], data[1]]) as usize; + let data = data.get(2..len + 2).ok_or(err)?; + return parse_data_to_dns_message(data, false); + } + let message = Message::from_vec(data).map_err(std::io::Error::from)?; + Ok(message) +} + +pub(crate) fn extract_ipaddr_from_dns_message(message: &Message) -> std::io::Result { + if message.response_code() != NoError { + let msg = format!("{:?}", message.response_code()); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, msg)); + } + let mut cname = None; + for answer in message.answers() { + let err = std::io::Error::new(std::io::ErrorKind::Other, "DNS response not contains answer data"); + match answer.data().ok_or(err)? { + RData::A(addr) => { + return Ok(IpAddr::V4((*addr).into())); + } + RData::AAAA(addr) => { + return Ok(IpAddr::V6((*addr).into())); + } + RData::CNAME(name) => { + cname = Some(name.to_utf8()); + } + _ => {} + } + } + if let Some(cname) = cname { + return Err(std::io::Error::new(std::io::ErrorKind::Other, cname)); + } + Err(std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", message.answers()))) +} + +pub(crate) fn extract_domain_from_dns_message(message: &Message) -> std::io::Result { + let err = std::io::Error::new(std::io::ErrorKind::Other, "DNS request not contains query body"); + let query = message.queries().first().ok_or(err)?; + let name = query.name().to_string(); + Ok(name) +} + +pub(crate) fn create_dns_cache() -> Cache, Message> { + Cache::builder() + .time_to_live(Duration::from_secs(30 * 60)) + .time_to_idle(Duration::from_secs(5 * 60)) + .build() +} + +pub(crate) async fn dns_cache_get_message(cache: &Cache, Message>, message: &Message) -> Option { + if let Some(mut cached_message) = cache.get(&message.queries().to_vec()).await { + cached_message.set_id(message.id()); + return Some(cached_message); + } + None +} + +pub(crate) async fn dns_cache_put_message(cache: &Cache, Message>, message: &Message) { + cache.insert(message.queries().to_vec(), message.clone()).await; +} diff --git a/src/dump_logger.rs b/src/dump_logger.rs new file mode 100644 index 0000000..46cf3c5 --- /dev/null +++ b/src/dump_logger.rs @@ -0,0 +1,73 @@ +use std::{ + os::raw::{c_char, c_int, c_void}, + sync::Mutex, +}; + +lazy_static::lazy_static! { + pub static ref DUMP_CALLBACK: Mutex> = Mutex::new(None); +} + +/// # Safety +/// +/// set dump log info callback. +#[no_mangle] +pub unsafe extern "C" fn overtls_set_log_callback( + callback: Option, + ctx: *mut c_void, +) { + *DUMP_CALLBACK.lock().unwrap() = Some(DumpCallback(callback, ctx)); +} + +#[derive(Clone)] +pub struct DumpCallback(Option, *mut c_void); + +impl DumpCallback { + unsafe fn call(self, dump_level: c_int, info: *const c_char) { + if let Some(cb) = self.0 { + cb(dump_level, info, self.1); + } + } +} + +unsafe impl Send for DumpCallback {} +unsafe impl Sync for DumpCallback {} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct DumpLogger {} + +impl log::Log for DumpLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Trace + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + let current_crate_name = env!("CARGO_CRATE_NAME"); + if record.module_path().unwrap_or("").starts_with(current_crate_name) { + self.do_dump_log(record); + } + } + } + + fn flush(&self) {} +} + +impl DumpLogger { + fn do_dump_log(&self, record: &log::Record) { + let timestamp: chrono::DateTime = chrono::Local::now(); + let msg = format!( + "[{} {:<5} {}] - {}", + timestamp.format("%Y-%m-%d %H:%M:%S"), + record.level(), + record.module_path().unwrap_or(""), + record.args() + ); + let c_msg = std::ffi::CString::new(msg).unwrap(); + let ptr = c_msg.as_ptr(); + if let Some(cb) = DUMP_CALLBACK.lock().unwrap().clone() { + unsafe { + cb.call(record.level() as c_int, ptr); + } + } + } +} diff --git a/src/error.rs b/src/error.rs index 1554b41..5cd937c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -36,8 +36,8 @@ pub enum Error { #[error("rustls::error::Error {0}")] Rustls(#[from] rustls::Error), - #[error("tokio_rustls::rustls::client::InvalidDnsNameError {0}")] - InvalidDnsName(#[from] tokio_rustls::rustls::client::InvalidDnsNameError), + #[error("rustls::pki_types::InvalidDnsNameError {0}")] + InvalidDnsName(#[from] rustls::pki_types::InvalidDnsNameError), #[error("httparse::Error {0}")] Httparse(#[from] httparse::Error), @@ -56,19 +56,16 @@ pub enum Error { #[error("std::str::Utf8Error {0}")] Utf8(#[from] std::str::Utf8Error), - #[error("&str error: {0}")] - Str(String), + #[error("socks5_impl::Error {0}")] + Socks5(#[from] socks5_impl::Error), #[error("String error: {0}")] String(String), - - #[error("&String error: {0}")] - RefString(String), } impl From<&str> for Error { fn from(s: &str) -> Self { - Error::Str(s.to_string()) + Error::String(s.to_string()) } } @@ -80,8 +77,8 @@ impl From for Error { impl From<&String> for Error { fn from(s: &String) -> Self { - Error::RefString(s.to_string()) + Error::String(s.to_string()) } } -pub type Result = std::result::Result; +pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 7d4442f..0dbe79e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ pub(crate) mod api; pub(crate) mod base64_wrapper; pub mod client; pub mod config; +pub(crate) mod dns; +pub mod dump_logger; pub mod error; pub mod server; pub(crate) mod tcp_stream; @@ -15,15 +17,13 @@ pub(crate) mod weirduri; use base64_wrapper::{base64_decode, base64_encode, Base64Engine}; use bytes::BytesMut; pub use error::{Error, Result}; -use socks5_impl::protocol::Address; +use socks5_impl::protocol::{Address, StreamOperation}; #[cfg(target_os = "windows")] pub(crate) const STREAM_BUFFER_SIZE: usize = 1024 * 32; #[cfg(not(target_os = "windows"))] pub(crate) const STREAM_BUFFER_SIZE: usize = 1024 * 32 * 3; -pub const LOCAL_HOST_V4: &str = "127.0.0.1"; - pub(crate) fn addess_to_b64str(addr: &Address, url_safe: bool) -> String { let mut buf = BytesMut::with_capacity(1024); addr.write_to_buf(&mut buf); @@ -51,7 +51,7 @@ pub(crate) fn b64str_to_address(s: &str, url_safe: bool) -> Result
{ result? } }; - Address::from_data(&buf).map_err(|e| e.into()) + Address::try_from(&buf[..]).map_err(|e| e.into()) } pub(crate) fn combine_addr_and_port(addr: &str, port: u16) -> String { diff --git a/src/main.rs b/src/main.rs index 2c0cd6d..de263dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,8 @@ -use overtls::{client, config, server, Error, Result, LOCAL_HOST_V4}; -use std::sync::{atomic::AtomicBool, Arc}; +use overtls::{client, config, server, Error, Result}; +use std::{ + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + sync::{atomic::AtomicBool, Arc}, +}; mod cmdopt; @@ -8,13 +11,13 @@ fn main() -> Result<()> { dotenvy::dotenv().ok(); - let level = if opt.verbose { "trace" } else { "info" }; - let default = format!("{}={}", module_path!(), level); - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default)).init(); + let level = format!("{}={:?}", module_path!(), opt.verbosity); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(level)).init(); let is_server = opt.is_server(); let mut config = config::Config::from_config_file(&opt.config)?; + config.set_cache_dns(opt.cache_dns); if opt.qrcode { let qrcode = config.generate_ssr_qrcode()?; @@ -66,30 +69,18 @@ async fn async_main(config: config::Config) -> Result<()> { let local_addr = config.listen_addr()?; - tokio::spawn(async move { - #[cfg(unix)] - { - use tokio::signal::unix::{signal, SignalKind}; - let mut kill_signal = signal(SignalKind::terminate())?; - tokio::select! { - _ = tokio::signal::ctrl_c() => log::info!("Ctrl-C received, shutting down..."), - _ = kill_signal.recv() => log::info!("Kill signal received, shutting down..."), - } - } - - #[cfg(not(unix))] - { - tokio::signal::ctrl_c().await?; - log::info!("Ctrl-C received, shutting down..."); - } - + ctrlc2::set_async_handler(async move { exiting_flag.store(true, std::sync::atomic::Ordering::Relaxed); - let addr = if local_addr.is_ipv6() { "::1" } else { LOCAL_HOST_V4 }; - let _ = std::net::TcpStream::connect((addr, local_addr.port())); - - Ok::<(), Error>(()) - }); + let addr = if local_addr.is_ipv6() { + SocketAddr::from((Ipv6Addr::LOCALHOST, local_addr.port())) + } else { + SocketAddr::from((Ipv4Addr::LOCALHOST, local_addr.port())) + }; + let _ = std::net::TcpStream::connect(addr); + log::info!(""); + }) + .await; if let Err(e) = main_body.await { log::error!("{}", e); diff --git a/src/server.rs b/src/server.rs index b5d6574..c7ce803 100644 --- a/src/server.rs +++ b/src/server.rs @@ -8,10 +8,10 @@ use crate::{ }; use bytes::{BufMut, BytesMut}; use futures_util::{SinkExt, StreamExt}; -use socks5_impl::protocol::Address; +use socks5_impl::protocol::{Address, StreamOperation}; use std::{ collections::HashMap, - net::{SocketAddr, ToSocketAddrs}, + net::{Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -43,29 +43,30 @@ pub async fn run_server(config: &Config, exiting_flag: Option>) let p = server.listen_port; let addr: SocketAddr = (h, p).to_socket_addrs()?.next().ok_or("Invalid server address")?; - let certs = if let Some(ref cert) = server.certfile { + let certs = server.certfile.as_ref().and_then(|cert| { if !config.disable_tls() { server_load_certs(cert).ok() } else { None } - } else { - None - }; + }); - let keys = if let Some(ref key) = server.keyfile { + let keys = server.keyfile.as_ref().and_then(|key| { if !config.disable_tls() { - server_load_keys(key).ok() + let keys = server_load_keys(key).ok(); + if keys.as_ref().map(|keys| keys.len()).unwrap_or(0) > 0 { + keys + } else { + None + } } else { None } - } else { - None - }; + }); let svr_cfg = if let (Some(certs), Some(mut keys)) = (certs, keys) { + let _key = keys.first().ok_or("no keys")?; rustls::ServerConfig::builder() - .with_safe_defaults() .with_no_client_auth() .with_single_cert(certs, keys.remove(0)) .ok() @@ -99,20 +100,16 @@ pub async fn run_server(config: &Config, exiting_flag: Option>) let incoming_task = async move { if let Some(acceptor) = acceptor { let stream = acceptor.accept(stream).await?; - if let Err(e) = handle_incoming(stream, peer_addr, config, traffic_audit).await { - log::debug!("{}: {}", peer_addr, e); - } - } else if let Err(e) = handle_incoming(stream, peer_addr, config, traffic_audit).await { - log::debug!("{}: {}", peer_addr, e); + handle_incoming(stream, peer_addr, config, traffic_audit).await?; } else { - log::debug!("some unknown error with {}", peer_addr); + handle_incoming(stream, peer_addr, config, traffic_audit).await?; } Ok::<_, Error>(()) }; tokio::spawn(async move { - if let Err(err) = incoming_task.await { - log::debug!("{:?}", err); + if let Err(e) = incoming_task.await { + log::debug!("{peer_addr}: {e}"); } }); } @@ -189,7 +186,7 @@ where let tls_enable = scheme == "https"; let host = url.host_str().ok_or("url host not exist")?; let port = url.port_or_known_default().ok_or("port not exist")?; - let forward_addr = &SocketAddr::new(host.parse()?, port); + let forward_addr = SocketAddr::new(host.parse()?, port); if tls_enable { let cert_store = retrieve_root_cert_store_for_client(&None)?; @@ -283,7 +280,7 @@ async fn websocket_traffic_handler( log::trace!("[UDP] {} tunneling established", peer); result = create_udp_tunnel(ws_stream, config, traffic_audit, &client_id).await; if let Err(ref e) = result { - log::debug!("[UDP] {} closed error: {}", peer, e); + log::debug!("[UDP] {} closed with error \"{}\"", peer, e); } else { log::trace!("[UDP] {} closed.", peer); } @@ -291,7 +288,7 @@ async fn websocket_traffic_handler( let addr_str = b64str_to_address(&target_address, false)?.to_string(); let dst_addr = addr_str.to_socket_addrs()?.next().ok_or("addr string parse failed")?; log::trace!("{} -> {} {client_id:?} uri path: \"{}\"", peer, dst_addr, uri_path); - result = normal_tunnel(ws_stream, config, traffic_audit, &client_id, &dst_addr).await; + result = normal_tunnel(ws_stream, peer, config, traffic_audit, &client_id, dst_addr).await; if let Err(ref e) = result { log::debug!("{} <> {} connection closed error: {}", peer, dst_addr, e); } else { @@ -303,79 +300,58 @@ async fn websocket_traffic_handler( async fn normal_tunnel( mut ws_stream: WebSocketStream, + peer: SocketAddr, _config: Config, traffic_audit: TrafficAuditPtr, client_id: &Option, - dst_addr: &SocketAddr, + dst_addr: SocketAddr, ) -> Result<()> { let mut outgoing = crate::tcp_stream::create(dst_addr).await?; - - let (ws_stream_tx, mut ws_stream_rx) = tokio::sync::mpsc::channel(1024); - let (outgoing_tx, mut outgoing_rx) = tokio::sync::mpsc::channel(1024); - - let ws_stream_to_outgoing = async move { - loop { - tokio::select! { - Some(msg) = ws_stream.next() => { - let msg = msg?; - if let Some(client_id) = &client_id { - let len = (msg.len() + WS_MSG_HEADER_LEN) as u64; - traffic_audit.lock().await.add_upstream_traffic_of(client_id, len); - } - if msg.is_close() { + let mut buffer = [0; crate::STREAM_BUFFER_SIZE]; + loop { + tokio::select! { + msg = ws_stream.next() => { + let msg = msg.ok_or(format!("{peer} -> {dst_addr} no Websocket message"))??; + let len = (msg.len() + WS_MSG_HEADER_LEN) as u64; + log::trace!("{peer} -> {dst_addr} length {}", len); + if let Some(client_id) = &client_id { + traffic_audit.lock().await.add_upstream_traffic_of(client_id, len); + } + match msg { + Message::Close(_) => { + log::trace!("{peer} <> {dst_addr} incoming connection closed normally"); break; } - if msg.is_text() || msg.is_binary() { - outgoing_tx.send(msg.into_data()).await?; + Message::Text(_) | Message::Binary(_) => { + outgoing.write_all(&msg.into_data()).await?; } - } - Some(data) = ws_stream_rx.recv() => { - let msg = Message::binary(data); - if let Some(client_id) = &client_id { - let len = (msg.len() + WS_MSG_HEADER_LEN) as u64; - traffic_audit.lock().await.add_downstream_traffic_of(client_id, len); - } - ws_stream.send(msg).await?; - } - else => { - break; + _ => {} } } - } - Ok::<_, Error>(()) - }; - - let outgoing_to_ws_stream = async move { - loop { - tokio::select! { - Ok(data) = async { - let mut b2 = [0; crate::STREAM_BUFFER_SIZE]; - let n = outgoing.read(&mut b2).await?; - Ok::<_, Error>(Some(b2[..n].to_vec())) - } => { - if let Some(data) = data { - if data.is_empty() { - break; + len = outgoing.read(&mut buffer) => { + match len { + Ok(0) => { + ws_stream.send(Message::Close(None)).await?; + log::trace!("{} <> {} outgoing connection reached EOF", peer, dst_addr); + break; + } + Ok(n) => { + let msg = Message::Binary(buffer[..n].to_vec()); + let len = (msg.len() + WS_MSG_HEADER_LEN) as u64; + log::trace!("{peer} <- {dst_addr} length {}", len); + if let Some(client_id) = &client_id { + traffic_audit.lock().await.add_downstream_traffic_of(client_id, len); } - ws_stream_tx.send(data).await?; - } else { + ws_stream.send(msg).await?; + } + Err(e) => { + ws_stream.send(Message::Close(None)).await?; + log::debug!("{} <> {} outgoing connection closed \"{}\"", peer, dst_addr, e); break; } } - Some(msg) = outgoing_rx.recv() => { - outgoing.write_all(&msg).await?; - } - else => { - break; - } } } - Ok::<_, Error>(()) - }; - - tokio::select! { - r = ws_stream_to_outgoing => { if let Err(e) = r { log::debug!("{} ws_stream_to_outgoing \"{}\"", dst_addr, e); } } - r = outgoing_to_ws_stream => { if let Err(e) = r { log::debug!("{} outgoing_to_ws_stream \"{}\"", dst_addr, e); } } } Ok(()) } @@ -386,8 +362,8 @@ async fn create_udp_tunnel( traffic_audit: TrafficAuditPtr, client_id: &Option, ) -> Result<()> { - let udp_socket = UdpSocket::bind("0.0.0.0:0").await?; - let udp_socket_v6 = UdpSocket::bind("[::]:0").await?; + let udp_socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).await?; + let udp_socket_v6 = UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0)).await?; let mut buf = vec![0u8; crate::STREAM_BUFFER_SIZE]; let mut buf_v6 = vec![0u8; crate::STREAM_BUFFER_SIZE]; @@ -408,16 +384,22 @@ async fn create_udp_tunnel( if msg.is_text() || msg.is_binary() { let data = msg.into_data(); let mut buf = BytesMut::from(&data[..]); - let dst_addr = Address::from_data(&buf)?; - let _ = buf.split_to(dst_addr.serialized_len()); - let src_addr = Address::from_data(&buf)?; - let _ = buf.split_to(src_addr.serialized_len()); + let dst_addr = Address::try_from(&buf[..])?; + let _ = buf.split_to(dst_addr.len()); + let src_addr = Address::try_from(&buf[..])?; + let _ = buf.split_to(src_addr.len()); let pkt = buf.to_vec(); log::trace!("[UDP] {src_addr} -> {dst_addr} length {}", pkt.len()); dst_src_pairs.lock().await.insert(dst_addr.clone(), src_addr); - let dst_addr = SocketAddr::try_from(dst_addr)?; + let mut dst_addr = dst_addr.to_socket_addrs()?.next().ok_or("invalid address")?; + if dst_addr.port() == 53 && addr_is_private(&dst_addr) { + match dst_addr { + SocketAddr::V4(_) => dst_addr = "8.8.8.8:53".parse::()?, + SocketAddr::V6(_) => dst_addr = "[2001:4860:4860::8888]:53".parse::()?, + } + } if dst_addr.is_ipv4() { udp_socket.send_to(&pkt, &dst_addr).await?; @@ -442,6 +424,20 @@ async fn create_udp_tunnel( Ok(()) } +// TODO: use IpAddr::is_global() instead when it's stable +fn addr_is_private(addr: &SocketAddr) -> bool { + fn is_benchmarking(addr: &Ipv4Addr) -> bool { + addr.octets()[0] == 198 && (addr.octets()[1] & 0xfe) == 18 + } + fn addr_v4_is_private(addr: &Ipv4Addr) -> bool { + is_benchmarking(addr) || addr.is_private() || addr.is_loopback() || addr.is_link_local() + } + match addr { + SocketAddr::V4(addr) => addr_v4_is_private(addr.ip()), + SocketAddr::V6(_) => false, + } +} + async fn _write_ws_stream( pkt: &[u8], ws_stream: &mut WebSocketStream, diff --git a/src/tcp_stream.rs b/src/tcp_stream.rs index 23e9886..bccb09f 100644 --- a/src/tcp_stream.rs +++ b/src/tcp_stream.rs @@ -2,7 +2,7 @@ use crate::error::Result; use std::net::SocketAddr; use tokio::net::TcpStream; -pub(crate) async fn create(addr: &SocketAddr) -> Result { +pub(crate) async fn create(addr: SocketAddr) -> Result { #[cfg(target_os = "android")] { let socket = if addr.is_ipv4() { @@ -16,7 +16,7 @@ pub(crate) async fn create(addr: &SocketAddr) -> Result { use std::os::unix::io::AsRawFd; crate::android::tun_callbacks::on_socket_created(socket.as_raw_fd()); - Ok(socket.connect(*addr).await?) + Ok(socket.connect(addr).await?) } #[cfg(not(target_os = "android"))] diff --git a/src/tls.rs b/src/tls.rs index 22913cd..40fc797 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,5 +1,8 @@ -use crate::error::{Error, Result}; -use rustls_pemfile::{certs, rsa_private_keys}; +use crate::error::Result; +use rustls::{ + pki_types::{CertificateDer, PrivateKeyDer, ServerName}, + RootCertStore, +}; use std::{ fs::File, io::BufReader, @@ -7,11 +10,7 @@ use std::{ path::{Path, PathBuf}, }; use tokio::net::TcpStream; -use tokio_rustls::{ - client::TlsStream, - rustls::{self, Certificate, OwnedTrustAnchor, PrivateKey, RootCertStore}, - TlsConnector, -}; +use tokio_rustls::{client::TlsStream, TlsConnector}; pub(crate) fn retrieve_root_cert_store_for_client(cafile: &Option) -> Result { let mut root_cert_store = rustls::RootCertStore::empty(); @@ -19,56 +18,49 @@ pub(crate) fn retrieve_root_cert_store_for_client(cafile: &Option) -> R if let Some(cafile) = cafile { if cafile.exists() { let mut pem = BufReader::new(File::open(cafile)?); - let certs = rustls_pemfile::certs(&mut pem)?; - let trust_anchors = certs.iter().map(|cert| { - if let Ok(ta) = webpki::TrustAnchor::try_from_cert_der(&cert[..]) { - OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints) - } else { - OwnedTrustAnchor::from_subject_spki_name_constraints(vec![], vec![], Some(vec![])) - } - }); - root_cert_store.add_server_trust_anchors(trust_anchors); + for cert in rustls_pemfile::certs(&mut pem) { + root_cert_store.add(cert?)?; + } done = true; } } if !done { - root_cert_store.add_server_trust_anchors( - webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { - OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints) - }), - ); + root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); } Ok(root_cert_store) } pub(crate) async fn create_tls_client_stream( root_cert_store: RootCertStore, - addr: &SocketAddr, + addr: SocketAddr, domain: &str, ) -> Result> { let config = rustls::ClientConfig::builder() - .with_safe_defaults() .with_root_certificates(root_cert_store) .with_no_client_auth(); let connector = TlsConnector::from(std::sync::Arc::new(config)); let stream = crate::tcp_stream::create(addr).await?; - let domain = rustls::ServerName::try_from(domain)?; + let domain = ServerName::try_from(domain)?.to_owned(); let stream = connector.connect(domain, stream).await?; Ok(stream) } -pub(crate) fn server_load_certs(path: &Path) -> Result> { - certs(&mut BufReader::new(File::open(path)?)) - .map_err(|e| Error::from(format!("Certificate error: {e}"))) - .map(|mut certs| certs.drain(..).map(Certificate).collect()) +pub(crate) fn server_load_certs(path: &Path) -> Result>> { + let mut res = vec![]; + for cert in rustls_pemfile::certs(&mut BufReader::new(File::open(path)?)) { + res.push(cert?); + } + Ok(res) } -pub(crate) fn server_load_keys(path: &Path) -> Result> { - rsa_private_keys(&mut BufReader::new(File::open(path)?)) - .map_err(|e| Error::from(format!("PrivateKey error: {e}"))) - .map(|mut keys| keys.drain(..).map(PrivateKey).collect()) +pub(crate) fn server_load_keys(path: &Path) -> Result>> { + let mut res = vec![]; + for key in rustls_pemfile::rsa_private_keys(&mut BufReader::new(File::open(path)?)) { + res.push(PrivateKeyDer::from(key?)); + } + Ok(res) } diff --git a/src/traffic_audit.rs b/src/traffic_audit.rs index b38c8fd..8be6c66 100644 --- a/src/traffic_audit.rs +++ b/src/traffic_audit.rs @@ -113,10 +113,7 @@ impl TrafficAudit { } pub fn set_enable_of(&mut self, client_id: &str, enable: bool) { - self.client_map - .entry(client_id.to_string()) - .or_default() - .set_enable(enable); + self.client_map.entry(client_id.to_string()).or_default().set_enable(enable); } pub fn get_enable_of(&self, client_id: &str) -> bool { @@ -125,9 +122,7 @@ impl TrafficAudit { } pub fn reset(&mut self) { - self.client_map - .iter_mut() - .for_each(|(_, client_node)| client_node.reset()); + self.client_map.iter_mut().for_each(|(_, client_node)| client_node.reset()); } pub fn reset_of(&mut self, client_id: &str) { diff --git a/src/udprelay.rs b/src/udprelay.rs index 06352d7..21bcdc9 100644 --- a/src/udprelay.rs +++ b/src/udprelay.rs @@ -1,15 +1,17 @@ use crate::{ client, config::Config, + dns, error::{Error, Result}, }; +use async_shared_timeout::{runtime, Timeout}; use bytes::{BufMut, Bytes, BytesMut}; use futures_util::{SinkExt, StreamExt}; use socks5_impl::{ - protocol::{Address, Reply, UdpHeader}, + protocol::{Address, Reply, StreamOperation, UdpHeader}, server::{ connection::associate::{AssociatedUdpSocket, NeedReply as UdpNeedReply}, - Associate, + UdpAssociate, }, }; use std::{ @@ -32,12 +34,12 @@ use tungstenite::protocol::Message; pub(crate) type UdpRequestReceiver = broadcast::Receiver<(Bytes, Address, Address)>; pub(crate) type UdpRequestSender = broadcast::Sender<(Bytes, Address, Address)>; -pub(crate) type SocketAddrSet = Arc>>; +pub(crate) type SocketAddrHashSet = Arc>>; pub(crate) async fn handle_s5_upd_associate( - associate: Associate, + associate: UdpAssociate, udp_tx: UdpRequestSender, - incomings: SocketAddrSet, + incomings: SocketAddrHashSet, ) -> Result<()> { let listen_ip = associate.local_addr()?.ip(); @@ -45,7 +47,7 @@ pub(crate) async fn handle_s5_upd_associate( let udp_listener = UdpSocket::bind(SocketAddr::from((listen_ip, 0))).await; match udp_listener.and_then(|socket| socket.local_addr().map(|addr| (socket, addr))) { Ok((listen_udp, listen_addr)) => { - log::info!("[UDP] listen on {listen_addr}"); + log::trace!("[UDP] {listen_addr} listen on"); let s5_listen_addr = listen_addr.into(); let mut reply_listener = associate.reply(Reply::Succeeded, s5_listen_addr).await?; @@ -57,15 +59,20 @@ pub(crate) async fn handle_s5_upd_associate( let incoming_addr = Arc::new(Mutex::new(SocketAddr::from(([0, 0, 0, 0], 0)))); + let timeout_secs = Duration::from_secs(10); // TODO: configurable + let runtime = runtime::Tokio::new(); + let timeout = Timeout::new(runtime, timeout_secs); + let res = tokio::select! { - _ = reply_listener.wait_until_closed() => Ok::<_, Error>(()), - res = socks5_to_relay(listen_udp.clone(), incoming_addr.clone(), incomings.clone(), udp_tx) => res, - res = relay_to_socks5(listen_udp, incoming_addr.clone(), udp_rx) => res, + _ = timeout.wait() => Ok::<_, Error>(()), + res = reply_listener.wait_until_closed() => res.map_err(|e| e.into()), + res = socks5_to_relay(listen_udp.clone(), incoming_addr.clone(), incomings.clone(), udp_tx, &timeout) => res, + res = relay_to_socks5(listen_udp, incoming_addr.clone(), udp_rx, &timeout) => res, }; reply_listener.shutdown().await?; - log::trace!("[UDP] listener {listen_addr} closed with {res:?}"); + log::trace!("[UDP] {listen_addr} listener closed with {res:?}"); { let incoming = *incoming_addr.lock().await; @@ -92,11 +99,12 @@ pub(crate) const fn command_max_serialized_len() -> usize { async fn socks5_to_relay( listen_udp: Arc, incoming: Arc>, - incomings: SocketAddrSet, + incomings: SocketAddrHashSet, udp_tx: UdpRequestSender, + timeout: &Timeout, ) -> Result<()> { loop { - log::trace!("[UDP] waiting for incoming packet"); + // log::trace!("[UDP] waiting for incoming packet"); let buf_size = MAX_UDP_RELAY_PACKET_SIZE - UdpHeader::max_serialized_len(); listen_udp.set_max_packet_size(buf_size); @@ -110,9 +118,10 @@ async fn socks5_to_relay( incoming.lock().await.clone_from(&src_addr); incomings.lock().await.insert(src_addr); - log::trace!("[UDP] incoming packet {src_addr} -> {dst_addr} {} bytes", pkt.len()); + // log::trace!("[UDP] {src_addr} -> {dst_addr} incoming packet size {}", pkt.len()); let src_addr = src_addr.into(); let _ = udp_tx.send((pkt, dst_addr, src_addr)); + timeout.reset(); } log::trace!("[UDP] socks5_to_relay exiting."); Ok(()) @@ -122,52 +131,59 @@ async fn relay_to_socks5( listen_udp: Arc, incoming_addr: Arc>, mut udp_rx: UdpRequestReceiver, + timeout: &Timeout, ) -> Result<()> { - while let Ok((pkt, addr, _)) = udp_rx.recv().await { + while let Ok((pkt, addr, _from_addr)) = udp_rx.recv().await { let to_addr = SocketAddr::try_from(addr.clone())?; if *incoming_addr.lock().await == to_addr { - log::trace!("[UDP] feedback to incoming {to_addr}"); + // log::trace!("[UDP] {to_addr} <- {_from_addr} feedback to incoming"); listen_udp.send_to(pkt, 0, addr, to_addr).await?; + timeout.reset(); } } log::trace!("[UDP] relay_to_socks5 exiting."); Ok(()) } -pub(crate) fn create_udp_tunnel() -> (UdpRequestSender, UdpRequestReceiver, SocketAddrSet) { - let incomings = Arc::new(Mutex::new(HashSet::::new())); +pub(crate) fn create_udp_tunnel() -> (UdpRequestSender, UdpRequestReceiver, SocketAddrHashSet) { + let incomings: SocketAddrHashSet = Arc::new(Mutex::new(HashSet::::new())); let (tx, rx) = tokio::sync::broadcast::channel::<(Bytes, Address, Address)>(10); (tx, rx, incomings) } -pub(crate) async fn run_udp_loop(udp_tx: UdpRequestSender, incomings: SocketAddrSet, config: Config) -> Result<()> { +pub(crate) async fn run_udp_loop(udp_tx: UdpRequestSender, incomings: SocketAddrHashSet, config: Config) -> Result<()> { let client = config.client.as_ref().ok_or("config client not exist")?; let mut addr = (client.server_host.as_str(), client.server_port).to_socket_addrs()?; let svr_addr = addr.next().ok_or("client address not exist")?; if !config.disable_tls() { - let ws_stream = client::create_tls_ws_stream(&svr_addr, None, &config, Some(true)).await?; - _run_udp_loop(udp_tx, incomings, ws_stream).await?; + let ws_stream = client::create_tls_ws_stream(svr_addr, None, &config, Some(true)).await?; + _run_udp_loop(udp_tx, incomings, ws_stream, config.cache_dns()).await?; } else { - let ws_stream = client::create_plaintext_ws_stream(&svr_addr, None, &config, Some(true)).await?; - _run_udp_loop(udp_tx, incomings, ws_stream).await?; + let ws_stream = client::create_plaintext_ws_stream(svr_addr, None, &config, Some(true)).await?; + _run_udp_loop(udp_tx, incomings, ws_stream, config.cache_dns()).await?; } Ok(()) } async fn _run_udp_loop( udp_tx: UdpRequestSender, - incomings: SocketAddrSet, + incomings: SocketAddrHashSet, mut ws_stream: WebSocketStream, + cache_dns: bool, ) -> Result<()> { let mut udp_rx = udp_tx.subscribe(); + let mut timer = tokio::time::interval(Duration::from_secs(30)); + + let cache = dns::create_dns_cache(); + let mut res = Ok::<_, Error>(()); loop { let _res = tokio::select! { Ok((pkt, dst_addr, src_addr)) = udp_rx.recv() => { - let flag = { incomings.lock().await.contains(&SocketAddr::try_from(dst_addr.clone())?) }; - if !flag { + let direction = { incomings.lock().await.contains(&SocketAddr::try_from(dst_addr.clone())?) }; + if !direction { // packet send to remote server, format: dst_addr + src_addr + pkt let mut buf = BytesMut::new(); dst_addr.write_to_buf(&mut buf); @@ -179,11 +195,23 @@ async fn _run_udp_loop( log::error!("{}", e); } - log::trace!("[UDP] send to remote {src_addr} -> {dst_addr} {} bytes", buf.len()); + if dst_addr.port() == 53 { + let msg = dns::parse_data_to_dns_message(&pkt, false)?; + let domain = dns::extract_domain_from_dns_message(&msg)?; + if let (true, Some(cached_message)) = (cache_dns, dns::dns_cache_get_message(&cache, &msg).await) { + log::debug!("[UDP] {src_addr} -> {dst_addr} DNS query hit cache \"{}\"", domain); + let data = cached_message.to_vec().map_err(|e| e.to_string())?; + udp_tx.send((Bytes::from(data), src_addr, dst_addr))?; + continue; + } + log::debug!("[UDP] {src_addr} -> {dst_addr} DNS query \"{}\"", domain); + } else { + log::debug!("[UDP] {src_addr} -> {dst_addr} send to remote size {}", buf.len()); + } let msg = Message::Binary(buf.freeze().to_vec()); ws_stream.send(msg).await?; } else { - log::trace!("[UDP] skip feedback packet {src_addr} -> {dst_addr}"); + // log::trace!("[UDP] {dst_addr} <- {src_addr} skip feedback packet"); } Ok::<_, Error>(()) }, @@ -197,44 +225,64 @@ async fn _run_udp_loop( match msg { Some(Ok(Message::Binary(buf))) => { let mut buf = BytesMut::from(&buf[..]); - let incoming_addr = Address::from_data(&buf)?; - let _ = buf.split_to(incoming_addr.serialized_len()); - let remote_addr = Address::from_data(&buf)?; - let _ = buf.split_to(remote_addr.serialized_len()); + let incoming_addr = Address::try_from(&buf[..])?; + let _ = buf.split_to(incoming_addr.len()); + let remote_addr = Address::try_from(&buf[..])?; + let _ = buf.split_to(remote_addr.len()); let pkt = buf.to_vec(); - log::trace!("[UDP] {} <- {} length {}", incoming_addr, remote_addr, len); + + if remote_addr.port() == 53 { + let msg = dns::parse_data_to_dns_message(&pkt, false)?; + let domain = dns::extract_domain_from_dns_message(&msg)?; + let mut ipaddr = format!("{:?}", dns::extract_ipaddr_from_dns_message(&msg)); + ipaddr.truncate(48); + if cache_dns { + dns::dns_cache_put_message(&cache, &msg).await; + } + log::debug!("[UDP] {incoming_addr} <- {remote_addr} DNS response \"{}\" <==> \"{}\"", domain, ipaddr); + } else { + log::debug!("[UDP] {incoming_addr} <- {remote_addr} recv from remote size {}", len); + } udp_tx.send((Bytes::from(pkt), incoming_addr, remote_addr))?; }, Some(Ok(Message::Close(_))) => { log::trace!("[UDP] ws stream closed by remote"); break; }, + Some(Ok(Message::Pong(_))) => { + log::trace!("[UDP] Websocket pong from remote"); + }, Some(Ok(_)) => { - log::trace!("[UDP] unexpected ws message"); + log::trace!("[UDP] unexpected Websocket message"); }, Some(Err(err)) => { - log::trace!("[UDP] {err}"); + log::trace!("[UDP] error \"{err}\""); res = Err(err.into()); break; }, None => { - log::trace!("[UDP] ws stream closed by local"); + log::trace!("[UDP] Websocket stream closed by local"); break; } } Ok::<_, Error>(()) }, + _ = timer.tick() => { + ws_stream.send(Message::Ping(vec![])).await?; + log::trace!("[UDP] Websocket ping from local"); + Ok::<_, Error>(()) + } }; } - log::trace!("[UDP] run_udp_loop exiting..."); + log::trace!("[UDP] _run_udp_loop exiting..."); res } pub(crate) async fn udp_handler_watchdog( config: &Config, - incomings: &SocketAddrSet, + incomings: &SocketAddrHashSet, udp_tx: &UdpRequestSender, exiting_flag: Option>, ) -> Result<()> { diff --git a/src/weirduri.rs b/src/weirduri.rs index 1be7ad9..bbc475b 100644 --- a/src/weirduri.rs +++ b/src/weirduri.rs @@ -13,23 +13,18 @@ pub(crate) const CLIENT_ID: &str = "Client-Id"; /// For example, we can pass the remote server IP to the server. /// This is useful for servers that are behind a reverse proxy. #[derive(Debug, Clone)] -pub(crate) struct WeirdUri<'a> { - pub(crate) uri: &'a str, +pub(crate) struct WeirdUri { + pub(crate) uri: String, pub(crate) target_address: Option, pub(crate) sec_websocket_key: String, pub(crate) udp_tunnel: Option, pub(crate) client_id: Option, } -impl<'a> WeirdUri<'a> { - pub(crate) fn new( - uri: &'a str, - target_address: Option, - udp_tunnel: Option, - client_id: Option, - ) -> Self { +impl WeirdUri { + pub(crate) fn new(uri: &str, target_address: Option, udp_tunnel: Option, client_id: Option) -> Self { Self { - uri, + uri: uri.to_owned(), target_address, sec_websocket_key: generate_key(), udp_tunnel, @@ -38,9 +33,9 @@ impl<'a> WeirdUri<'a> { } } -impl<'a> IntoClientRequest for WeirdUri<'a> { +impl IntoClientRequest for WeirdUri { fn into_client_request(self) -> Result { - let uri = url::Url::parse(self.uri).map_err(|_| Error::Url(UrlError::NoPathOrQuery))?; + let uri = url::Url::parse(&self.uri).map_err(|_| Error::Url(UrlError::NoPathOrQuery))?; let host = uri.host_str().ok_or(Error::Url(UrlError::EmptyHostName))?; let host = crate::combine_addr_and_port(host, uri.port().unwrap_or(80)); @@ -72,7 +67,7 @@ impl<'a> IntoClientRequest for WeirdUri<'a> { } } -impl std::fmt::Display for WeirdUri<'_> { +impl std::fmt::Display for WeirdUri { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Ok(req) = self.clone().into_client_request() { write!(f, "{req:?}")