From 7c154d2340849cdabb5971b3b6982f187a6b7efe Mon Sep 17 00:00:00 2001 From: ty Date: Tue, 19 Apr 2022 11:48:43 +0800 Subject: [PATCH] AEAD-2022 ciphers (new protocol) (#811) --- .github/workflows/build-and-test.yml | 8 +- Cargo.lock | 173 ++++-- Cargo.toml | 7 +- README.md | 2 + bin/ssurl.rs | 2 +- crates/shadowsocks-service/Cargo.toml | 13 +- crates/shadowsocks-service/src/config.rs | 2 +- .../src/local/http/connector.rs | 4 +- .../src/local/net/tcp/auto_proxy_stream.rs | 97 +--- .../src/local/net/udp/association.rs | 101 +++- .../src/local/tunnel/udprelay.rs | 101 +++- .../shadowsocks-service/src/manager/server.rs | 2 +- crates/shadowsocks-service/src/net/mod.rs | 1 + .../shadowsocks-service/src/net/mon_socket.rs | 71 ++- .../src/net/packet_window.rs | 191 +++++++ .../shadowsocks-service/src/server/server.rs | 1 + .../src/server/tcprelay.rs | 19 +- .../src/server/udprelay.rs | 363 +++++++++++-- crates/shadowsocks/Cargo.toml | 12 +- crates/shadowsocks/src/config.rs | 42 +- crates/shadowsocks/src/relay/tcprelay/aead.rs | 18 +- .../src/relay/tcprelay/aead_2022.rs | 371 +++++++++++++ .../src/relay/tcprelay/crypto_io.rs | 153 +++++- crates/shadowsocks/src/relay/tcprelay/mod.rs | 11 + .../src/relay/tcprelay/proxy_listener.rs | 2 +- .../src/relay/tcprelay/proxy_stream/client.rs | 307 +++++++---- .../src/relay/tcprelay/proxy_stream/mod.rs | 11 +- .../tcprelay/proxy_stream/protocol/mod.rs | 173 ++++++ .../tcprelay/proxy_stream/protocol/v1.rs | 70 +++ .../tcprelay/proxy_stream/protocol/v2.rs | 235 ++++++++ .../src/relay/tcprelay/proxy_stream/server.rs | 163 +++--- .../shadowsocks/src/relay/tcprelay/stream.rs | 20 +- .../shadowsocks/src/relay/tcprelay/utils.rs | 6 +- crates/shadowsocks/src/relay/udprelay/aead.rs | 120 ++++ .../src/relay/udprelay/aead_2022.rs | 514 ++++++++++++++++++ .../src/relay/udprelay/crypto_io.rs | 248 ++++----- crates/shadowsocks/src/relay/udprelay/mod.rs | 6 + .../shadowsocks/src/relay/udprelay/options.rs | 15 + .../src/relay/udprelay/proxy_socket.rs | 172 +++++- .../shadowsocks/src/relay/udprelay/stream.rs | 100 ++++ crates/shadowsocks/tests/tcp.rs | 42 +- crates/shadowsocks/tests/tcp_tfo.rs | 4 +- crates/shadowsocks/tests/udp.rs | 42 +- src/service/local.rs | 2 +- src/service/manager.rs | 2 +- src/service/server.rs | 2 +- tests/socks4.rs | 2 +- tests/socks5.rs | 2 +- tests/udp.rs | 2 +- 49 files changed, 3397 insertions(+), 630 deletions(-) create mode 100644 crates/shadowsocks-service/src/net/packet_window.rs create mode 100644 crates/shadowsocks/src/relay/tcprelay/aead_2022.rs create mode 100644 crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/mod.rs create mode 100644 crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/v1.rs create mode 100644 crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/v2.rs create mode 100644 crates/shadowsocks/src/relay/udprelay/aead.rs create mode 100644 crates/shadowsocks/src/relay/udprelay/aead_2022.rs create mode 100644 crates/shadowsocks/src/relay/udprelay/options.rs create mode 100644 crates/shadowsocks/src/relay/udprelay/stream.rs diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 4036f5825a56..9789c3b243ca 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -47,19 +47,19 @@ jobs: run: cargo test --manifest-path ./crates/shadowsocks/Cargo.toml --verbose --no-default-features --no-fail-fast - name: Build with All Features Enabled (Unix) if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} - run: cargo build --verbose --features "local-http-rustls local-redir local-dns dns-over-tls dns-over-https stream-cipher local-tun" + run: cargo build --verbose --features "local-http-rustls local-redir local-dns local-tun dns-over-tls dns-over-https stream-cipher aead-cipher-2022" - name: Build with All Features Enabled (Windows) if: ${{ runner.os == 'Windows' }} - run: cargo build --verbose --features "local-http-rustls local-dns dns-over-tls dns-over-https stream-cipher" + run: cargo build --verbose --features "local-http-rustls local-dns dns-over-tls dns-over-https stream-cipher aead-cipher-2022" - name: Build with All Features Enabled - shadowsocks - run: cargo build --manifest-path ./crates/shadowsocks/Cargo.toml --verbose --features "stream-cipher" + run: cargo build --manifest-path ./crates/shadowsocks/Cargo.toml --verbose --features "stream-cipher aead-cipher-2022" - name: Clippy Check uses: actions-rs/clippy-check@v1 with: name: clippy-${{ matrix.platform }} token: ${{ secrets.GITHUB_TOKEN }} args: | - --verbose --features "local-http-rustls local-redir local-dns dns-over-tls dns-over-https stream-cipher" -- -Z macro-backtrace + --features "local-http-rustls local-redir local-dns dns-over-tls dns-over-https stream-cipher aead-cipher-2022" -- -Z macro-backtrace -W clippy::absurd_extreme_comparisons -W clippy::erasing_op -A clippy::collapsible_else_if diff --git a/Cargo.lock b/Cargo.lock index 4f716f0eac0d..d48b5c3f9cbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,12 +18,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if", - "cipher", + "cipher 0.3.0", "cpufeatures", "ctr", "opaque-debug 0.3.0", ] +[[package]] +name = "aes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe0133578c0986e1fe3dfcd4af1cc5b2dd6c3dbf534d69916ce16a2701d40ba" +dependencies = [ + "cfg-if", + "cipher 0.4.3", + "cpufeatures", +] + [[package]] name = "aes-gcm" version = "0.9.4" @@ -31,8 +42,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" dependencies = [ "aead", - "aes", - "cipher", + "aes 0.7.5", + "cipher 0.3.0", "ctr", "ghash", "subtle", @@ -45,8 +56,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589c637f0e68c877bbd59a4599bbe849cac8e5f3e4b5a3ebae8f528cd218dcdc" dependencies = [ "aead", - "aes", - "cipher", + "aes 0.7.5", + "cipher 0.3.0", "ctr", "polyval", "subtle", @@ -80,6 +91,18 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "async-trait" version = "0.1.53" @@ -132,6 +155,20 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake3" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.3", +] + [[package]] name = "block-buffer" version = "0.7.3" @@ -235,7 +272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a9cf981c7e62b6fb02225592ee7ebf221e0b0b5317984a57a1e9d21af20e317" dependencies = [ "aead", - "cipher", + "cipher 0.3.0", "ctr", "subtle", ] @@ -253,7 +290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91" dependencies = [ "cfg-if", - "cipher", + "cipher 0.3.0", "cpufeatures", "zeroize", ] @@ -266,7 +303,7 @@ checksum = "3b84ed6d1d5f7aa9bdde921a5090e0ca4d934d250ea3b402a5fab3a994e28a2a" dependencies = [ "aead", "chacha20", - "cipher", + "cipher 0.3.0", "poly1305", "zeroize", ] @@ -299,22 +336,41 @@ dependencies = [ "generic-array 0.14.5", ] +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" -version = "3.1.8" +version = "3.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c" +checksum = "6aad2534fad53df1cc12519c5cda696dd3e20e6118a027e24054aea14a0bdcbe" dependencies = [ "atty", "bitflags", + "clap_lex", "indexmap", - "os_str_bytes", "strsim", "termcolor", "terminal_size", "textwrap", ] +[[package]] +name = "clap_lex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cmake" version = "0.1.48" @@ -330,6 +386,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation" version = "0.9.3" @@ -403,7 +465,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" dependencies = [ - "cipher", + "cipher 0.3.0", ] [[package]] @@ -590,9 +652,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" dependencies = [ "cfg-if", "libc", @@ -868,9 +930,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" +checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" [[package]] name = "httpdate" @@ -949,6 +1011,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array 0.14.5", +] + [[package]] name = "instant" version = "0.1.12" @@ -978,9 +1049,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "iprange" @@ -1040,9 +1111,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058a107a784f8be94c7d35c1300f4facced2e93d2fbe5b1452b44e905ddca4a9" +checksum = "97caf428b83f7c86809b7450722cd1f2b1fc7fb23aa7b9dee7e72ed14d048352" dependencies = [ "kqueue-sys", "libc", @@ -1066,9 +1137,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" [[package]] name = "libmimalloc-sys" @@ -1395,9 +1466,6 @@ name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] [[package]] name = "p256" @@ -1611,9 +1679,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -1765,9 +1833,9 @@ dependencies = [ [[package]] name = "rpmalloc-sys" -version = "0.2.2+1.4.1" +version = "0.2.3+b097fd0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370e623bf2ca97dd497b7dd0e2889ec953a46c8c268489a818a5e305633e8609" +checksum = "8d4b7d5e225a53887ee57fcec492eaf114b8e290f7072d035adc6ddd6810b67b" dependencies = [ "cc", "libc", @@ -1788,30 +1856,30 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" +checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ "openssl-probe", - "rustls-pemfile 0.2.1", + "rustls-pemfile 1.0.0", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" dependencies = [ "base64", ] [[package]] name = "rustls-pemfile" -version = "0.3.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" +checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" dependencies = [ "base64", ] @@ -2002,8 +2070,9 @@ dependencies = [ [[package]] name = "shadowsocks" -version = "1.14.3" +version = "1.15.0" dependencies = [ + "aes 0.8.1", "arc-swap 1.5.0", "async-trait", "base64", @@ -2015,6 +2084,7 @@ dependencies = [ "futures", "libc", "log", + "lru_time_cache", "nix", "notify", "once_cell", @@ -2026,7 +2096,7 @@ dependencies = [ "serde_urlencoded", "shadowsocks-crypto", "socket2", - "spin 0.9.2", + "spin 0.9.3", "thiserror", "tokio", "tokio-tfo", @@ -2037,13 +2107,14 @@ dependencies = [ [[package]] name = "shadowsocks-crypto" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd381517e3eb8fec5090696debfdea972d8afe6fc926c26c7bfd5fee9053efbd" +version = "0.4.0" +source = "git+https://github.com/shadowsocks/shadowsocks-crypto.git#d0d9914fbf01ac85cf1120cbc7888de6bd0e5817" dependencies = [ - "aes", + "aes 0.7.5", "aes-gcm", "aes-gcm-siv", + "blake3", + "bytes", "ccm", "cfg-if", "chacha20", @@ -2057,7 +2128,7 @@ dependencies = [ [[package]] name = "shadowsocks-rust" -version = "1.14.4" +version = "1.15.0" dependencies = [ "build-time", "byte_string", @@ -2090,7 +2161,7 @@ dependencies = [ [[package]] name = "shadowsocks-service" -version = "1.14.3" +version = "1.15.0" dependencies = [ "arc-swap 1.5.0", "async-trait", @@ -2120,7 +2191,7 @@ dependencies = [ "shadowsocks", "smoltcp", "socket2", - "spin 0.9.2", + "spin 0.9.3", "thiserror", "tokio", "tokio-native-tls", @@ -2230,9 +2301,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" +checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" dependencies = [ "lock_api", ] @@ -2495,9 +2566,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if", "log", @@ -2519,9 +2590,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90442985ee2f57c9e1b548ee72ae842f4a9a20e3f417cc38dbc5dc684d9bb4ee" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" dependencies = [ "lazy_static", ] diff --git a/Cargo.toml b/Cargo.toml index a17a299d6005..77fc88c0c38f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shadowsocks-rust" -version = "1.14.4" +version = "1.15.0" authors = ["Shadowsocks Contributors"] description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls." repository = "https://github.com/shadowsocks/shadowsocks-rust" @@ -126,6 +126,9 @@ stream-cipher = ["shadowsocks-service/stream-cipher"] # WARN: These non-standard AEAD ciphers are not officially supported by shadowsocks community aead-cipher-extra = ["shadowsocks-service/aead-cipher-extra"] +# Enable AEAD 2022 +aead-cipher-2022 = ["shadowsocks-service/aead-cipher-2022"] + # Enable detection against replay attack security-replay-attack-detect = ["shadowsocks-service/security-replay-attack-detect"] replay-attack-detect = ["security-replay-attack-detect"] # Backward compatibility. DO NOT USE. @@ -165,7 +168,7 @@ jemallocator = { version = "0.3", optional = true } snmalloc-rs = { version = "0.2", optional = true } rpmalloc = { version = "0.2", optional = true } -shadowsocks-service = { version = "1.14.1", path = "./crates/shadowsocks-service" } +shadowsocks-service = { version = "1.15.0", path = "./crates/shadowsocks-service" } [target.'cfg(unix)'.dependencies] daemonize = "0.4" diff --git a/README.md b/README.md index d5edf8d2eb19..2266cb06f9bd 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ Related Projects: - `aead-cipher-extra` - Enable non-standard AEAD ciphers +- `aead-cipher-2022` - Enable AEAD-2022 ciphers ([Draft](https://github.com/shadowsocks/shadowsocks-org/issues/194#issuecomment-1065833908)) + #### Memory Allocators This project uses system (libc) memory allocator (Rust's default). But it also allows you to use other famous allocators by features: diff --git a/bin/ssurl.rs b/bin/ssurl.rs index 7284f10a78e9..178e81210732 100644 --- a/bin/ssurl.rs +++ b/bin/ssurl.rs @@ -3,7 +3,7 @@ //! SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ] //! userinfo = websafe-base64-encode-utf8(method ":" password) -use clap::{Command, Arg}; +use clap::{Arg, Command}; use qrcode::{types::Color, QrCode}; use shadowsocks_service::{ diff --git a/crates/shadowsocks-service/Cargo.toml b/crates/shadowsocks-service/Cargo.toml index ec0774719050..9703860873cf 100644 --- a/crates/shadowsocks-service/Cargo.toml +++ b/crates/shadowsocks-service/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shadowsocks-service" -version = "1.14.3" +version = "1.15.0" authors = ["Shadowsocks Contributors"] description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls." repository = "https://github.com/shadowsocks/shadowsocks-rust" @@ -38,7 +38,7 @@ dns-over-tls = ["trust-dns", "trust-dns-resolver/dns-over-tls", "trust-dns-resol dns-over-https = ["trust-dns", "trust-dns-resolver/dns-over-https", "trust-dns-resolver/dns-over-https-rustls"] # Enable DNS-relay -local-dns = ["local", "trust-dns", "rand"] +local-dns = ["local", "trust-dns"] # Backward compatibility, DO NOT USE local-dns-relay = ["local-dns"] # Enable client flow statistic report @@ -56,7 +56,7 @@ local-tunnel = ["local"] # Enable socks4 protocol for sslocal local-socks4 = ["local"] # Enable Tun interface protocol for sslocal -local-tun = ["local", "etherparse", "tun", "rand", "smoltcp"] +local-tun = ["local", "etherparse", "tun", "smoltcp"] # Enable Stream Cipher Protocol # WARN: Stream Cipher Protocol is proved to be insecure @@ -68,6 +68,9 @@ stream-cipher = ["shadowsocks/stream-cipher"] # WARN: These non-standard AEAD ciphers are not officially supported by shadowsocks community aead-cipher-extra = ["shadowsocks/aead-cipher-extra"] +# Enable AEAD 2022 +aead-cipher-2022 = ["shadowsocks/aead-cipher-2022"] + # Enable detection against replay attack security-replay-attack-detect = ["shadowsocks/security-replay-attack-detect"] # Enable IV printable prefix @@ -92,7 +95,7 @@ lru_time_cache = "0.11" bytes = "1.0" byte_string = "1.0" byteorder = "1.3" -rand = { version = "0.8", optional = true } +rand = { version = "0.8", features = ["small_rng"] } futures = "0.3" tokio = { version = "1.5", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } @@ -123,7 +126,7 @@ smoltcp = { version = "0.8", optional = true, default-features = false, features serde = { version = "1.0", features = ["derive"] } json5 = "0.4" -shadowsocks = { version = "1.14.1", path = "../shadowsocks", default-features = false } +shadowsocks = { version = "1.15.0", path = "../shadowsocks", default-features = false } # Just for the ioctl call macro [target.'cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] diff --git a/crates/shadowsocks-service/src/config.rs b/crates/shadowsocks-service/src/config.rs index 86fa37d66d1a..d5f735526373 100644 --- a/crates/shadowsocks-service/src/config.rs +++ b/crates/shadowsocks-service/src/config.rs @@ -66,7 +66,7 @@ use serde::{Deserialize, Serialize}; use shadowsocks::relay::socks5::Address; use shadowsocks::{ config::{ManagerAddr, Mode, ReplayAttackPolicy, ServerAddr, ServerConfig, ServerWeight}, - crypto::v1::CipherKind, + crypto::CipherKind, plugin::PluginConfig, }; #[cfg(feature = "trust-dns")] diff --git a/crates/shadowsocks-service/src/local/http/connector.rs b/crates/shadowsocks-service/src/local/http/connector.rs index c9afec080d90..414595e1383e 100644 --- a/crates/shadowsocks-service/src/local/http/connector.rs +++ b/crates/shadowsocks-service/src/local/http/connector.rs @@ -31,9 +31,9 @@ impl Connector { } impl Service for Connector { - type Response = ProxyHttpStream; type Error = io::Error; type Future = Connecting; + type Response = ProxyHttpStream; fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll> { Poll::Ready(Ok(())) @@ -67,7 +67,7 @@ impl Service for Connector { } } } - .boxed(), + .boxed(), } } } diff --git a/crates/shadowsocks-service/src/local/net/tcp/auto_proxy_stream.rs b/crates/shadowsocks-service/src/local/net/tcp/auto_proxy_stream.rs index 330d55a4e6c5..1c04e9ae5131 100644 --- a/crates/shadowsocks-service/src/local/net/tcp/auto_proxy_stream.rs +++ b/crates/shadowsocks-service/src/local/net/tcp/auto_proxy_stream.rs @@ -11,12 +11,9 @@ use std::{ use pin_project::pin_project; use shadowsocks::{ net::TcpStream, - relay::{ - socks5::Address, - tcprelay::proxy_stream::{ProxyClientStream, ProxyClientStreamReadHalf, ProxyClientStreamWriteHalf}, - }, + relay::{socks5::Address, tcprelay::proxy_stream::ProxyClientStream}, }; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf, ReadHalf, WriteHalf}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use crate::{ local::{context::ServiceContext, loadbalancing::ServerIdent}, @@ -160,93 +157,3 @@ impl From>> for AutoProxyClientStrea AutoProxyClientStream::Proxied(s) } } - -impl AutoProxyClientStream { - pub fn into_split(self) -> (AutoProxyClientStreamReadHalf, AutoProxyClientStreamWriteHalf) { - match self { - AutoProxyClientStream::Proxied(s) => { - let (r, w) = s.into_split(); - ( - AutoProxyClientStreamReadHalf::Proxied(r), - AutoProxyClientStreamWriteHalf::Proxied(w), - ) - } - AutoProxyClientStream::Bypassed(s) => { - let (r, w) = tokio::io::split(s); - ( - AutoProxyClientStreamReadHalf::Bypassed(r), - AutoProxyClientStreamWriteHalf::Bypassed(w), - ) - } - } - } -} - -#[allow(clippy::large_enum_variant)] -#[pin_project(project = AutoProxyClientStreamReadHalfProj)] -pub enum AutoProxyClientStreamReadHalf { - Proxied(#[pin] ProxyClientStreamReadHalf>), - Bypassed(#[pin] ReadHalf), -} - -impl AutoProxyIo for AutoProxyClientStreamReadHalf { - fn is_proxied(&self) -> bool { - matches!(*self, AutoProxyClientStreamReadHalf::Proxied(..)) - } -} - -impl AsyncRead for AutoProxyClientStreamReadHalf { - fn poll_read(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { - match self.project() { - AutoProxyClientStreamReadHalfProj::Proxied(s) => s.poll_read(cx, buf), - AutoProxyClientStreamReadHalfProj::Bypassed(s) => s.poll_read(cx, buf), - } - } -} - -#[allow(clippy::large_enum_variant)] -#[pin_project(project = AutoProxyClientStreamWriteHalfProj)] -pub enum AutoProxyClientStreamWriteHalf { - Proxied(#[pin] ProxyClientStreamWriteHalf>), - Bypassed(#[pin] WriteHalf), -} - -impl AutoProxyIo for AutoProxyClientStreamWriteHalf { - fn is_proxied(&self) -> bool { - matches!(*self, AutoProxyClientStreamWriteHalf::Proxied(..)) - } -} - -impl AsyncWrite for AutoProxyClientStreamWriteHalf { - fn poll_write(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll> { - match self.project() { - AutoProxyClientStreamWriteHalfProj::Proxied(s) => s.poll_write(cx, buf), - AutoProxyClientStreamWriteHalfProj::Bypassed(s) => s.poll_write(cx, buf), - } - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - match self.project() { - AutoProxyClientStreamWriteHalfProj::Proxied(s) => s.poll_flush(cx), - AutoProxyClientStreamWriteHalfProj::Bypassed(s) => s.poll_flush(cx), - } - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - match self.project() { - AutoProxyClientStreamWriteHalfProj::Proxied(s) => s.poll_shutdown(cx), - AutoProxyClientStreamWriteHalfProj::Bypassed(s) => s.poll_shutdown(cx), - } - } - - fn poll_write_vectored( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - bufs: &[IoSlice<'_>], - ) -> Poll> { - match self.project() { - AutoProxyClientStreamWriteHalfProj::Proxied(s) => s.poll_write_vectored(cx, bufs), - AutoProxyClientStreamWriteHalfProj::Bypassed(s) => s.poll_write_vectored(cx, bufs), - } - } -} diff --git a/crates/shadowsocks-service/src/local/net/udp/association.rs b/crates/shadowsocks-service/src/local/net/udp/association.rs index b18fb950ccf0..f679f0411005 100644 --- a/crates/shadowsocks-service/src/local/net/udp/association.rs +++ b/crates/shadowsocks-service/src/local/net/udp/association.rs @@ -1,6 +1,7 @@ //! UDP Association Managing use std::{ + cell::RefCell, io::{self, ErrorKind}, marker::PhantomData, net::SocketAddr, @@ -13,20 +14,26 @@ use bytes::Bytes; use futures::future; use log::{debug, error, trace, warn}; use lru_time_cache::LruCache; +use rand::{rngs::SmallRng, Rng, SeedableRng}; use tokio::{sync::mpsc, task::JoinHandle, time}; use shadowsocks::{ lookup_then, net::UdpSocket as ShadowUdpSocket, relay::{ - udprelay::{ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE}, + udprelay::{options::UdpSocketControlData, ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE}, Address, }, }; use crate::{ local::{context::ServiceContext, loadbalancing::PingBalancer}, - net::{MonProxySocket, UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE, UDP_ASSOCIATION_SEND_CHANNEL_SIZE}, + net::{ + packet_window::PacketWindowFilter, + MonProxySocket, + UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE, + UDP_ASSOCIATION_SEND_CHANNEL_SIZE, + }, }; /// Writer for sending packets back to client @@ -170,6 +177,24 @@ where } } +#[derive(Debug, Clone)] +struct ServerContext { + packet_window_filter: PacketWindowFilter, +} + +#[derive(Clone)] +struct ServerSessionContext { + server_session_map: LruCache, +} + +impl ServerSessionContext { + fn new() -> ServerSessionContext { + ServerSessionContext { + server_session_map: LruCache::with_expiry_duration_and_capacity(Duration::from_secs(30 * 60), 5), + } + } +} + struct UdpAssociationContext where W: UdpInboundWrite + Send + Sync + Unpin + 'static, @@ -183,6 +208,9 @@ where keepalive_flag: bool, balancer: PingBalancer, respond_writer: W, + client_session_id: u64, + client_packet_id: u64, + server_session: Option, } impl Drop for UdpAssociationContext @@ -194,6 +222,15 @@ where } } +thread_local! { + static CLIENT_SESSION_RNG: RefCell = RefCell::new(SmallRng::from_entropy()); +} + +#[inline] +fn generate_client_session_id() -> u64 { + CLIENT_SESSION_RNG.with(|rng| rng.borrow_mut().gen()) +} + impl UdpAssociationContext where W: UdpInboundWrite + Send + Sync + Unpin + 'static, @@ -220,6 +257,11 @@ where keepalive_flag: false, balancer, respond_writer, + // client_session_id must be random generated, + // server use this ID to identify every independent clients. + client_session_id: generate_client_session_id(), + client_packet_id: 1, + server_session: None, }; let handle = tokio::spawn(async move { assoc.dispatch_packet(receiver).await }); @@ -277,7 +319,7 @@ where } received_opt = receive_from_proxied_opt(&self.proxied_socket, &mut proxied_buffer) => { - let (n, addr) = match received_opt { + let (n, addr, control_opt) = match received_opt { Ok(r) => r, Err(err) => { error!("udp relay {} <- ... (proxied) failed, error: {}", self.peer_addr, err); @@ -287,6 +329,33 @@ where } }; + if let Some(control) = control_opt { + // Check if Packet ID is in the window + + let session = self.server_session.get_or_insert_with(ServerSessionContext::new); + + let packet_id = control.packet_id; + let session_context = session + .server_session_map + .entry(control.server_session_id) + .or_insert_with(|| { + trace!( + "udp server with session {} for {} created", + control.client_session_id, + self.peer_addr, + ); + + ServerContext { + packet_window_filter: PacketWindowFilter::new() + } + }); + + if !session_context.packet_window_filter.validate_packet_id(packet_id, u64::MAX) { + error!("udp {} packet_id {} out of window", self.peer_addr, packet_id); + continue; + } + } + self.send_received_respond_packet(&addr, &proxied_buffer[..n], false).await; } @@ -322,14 +391,14 @@ where async fn receive_from_proxied_opt( socket: &Option, buf: &mut Vec, - ) -> io::Result<(usize, Address)> { + ) -> io::Result<(usize, Address, Option)> { match *socket { None => future::pending().await, Some(ref s) => { if buf.is_empty() { buf.resize(MAXIMUM_UDP_PAYLOAD_SIZE, 0); } - s.recv(buf).await + s.recv_with_ctrl(buf).await } } } @@ -434,7 +503,27 @@ where } }; - match socket.send(target_addr, data).await { + // Increase Packet ID before send + self.client_packet_id = match self.client_packet_id.checked_add(1) { + Some(i) => i, + None => { + warn!( + "{} -> {} (proxied) sending {} bytes failed, packet id overflowed", + self.peer_addr, + target_addr, + data.len(), + ); + return Ok(()); + } + }; + + let control = UdpSocketControlData { + client_session_id: self.client_session_id, + server_session_id: 0, + packet_id: self.client_packet_id, + }; + + match socket.send_with_ctrl(target_addr, &control, data).await { Ok(..) => return Ok(()), Err(err) => { debug!( diff --git a/crates/shadowsocks-service/src/local/tunnel/udprelay.rs b/crates/shadowsocks-service/src/local/tunnel/udprelay.rs index 514053a78d8d..f22aa4a19067 100644 --- a/crates/shadowsocks-service/src/local/tunnel/udprelay.rs +++ b/crates/shadowsocks-service/src/local/tunnel/udprelay.rs @@ -1,6 +1,7 @@ //! UDP Tunnel server use std::{ + cell::RefCell, io::{self, ErrorKind}, net::SocketAddr, sync::Arc, @@ -11,12 +12,13 @@ use bytes::Bytes; use futures::future; use log::{debug, error, info, trace, warn}; use lru_time_cache::LruCache; +use rand::{rngs::SmallRng, Rng, SeedableRng}; use shadowsocks::{ lookup_then, net::UdpSocket as ShadowUdpSocket, relay::{ socks5::Address, - udprelay::{ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE}, + udprelay::{options::UdpSocketControlData, ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE}, }, ServerAddr, }; @@ -24,7 +26,12 @@ use tokio::{net::UdpSocket, sync::mpsc, task::JoinHandle, time}; use crate::{ local::{context::ServiceContext, loadbalancing::PingBalancer}, - net::{MonProxySocket, UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE, UDP_ASSOCIATION_SEND_CHANNEL_SIZE}, + net::{ + packet_window::PacketWindowFilter, + MonProxySocket, + UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE, + UDP_ASSOCIATION_SEND_CHANNEL_SIZE, + }, }; type AssociationMap = LruCache; @@ -197,6 +204,24 @@ impl UdpAssociation { } } +#[derive(Debug, Clone)] +struct ServerContext { + packet_window_filter: PacketWindowFilter, +} + +#[derive(Clone)] +struct ServerSessionContext { + server_session_map: LruCache, +} + +impl ServerSessionContext { + fn new() -> ServerSessionContext { + ServerSessionContext { + server_session_map: LruCache::with_expiry_duration_and_capacity(Duration::from_secs(30 * 60), 5), + } + } +} + struct UdpAssociationContext { context: Arc, peer_addr: SocketAddr, @@ -206,6 +231,9 @@ struct UdpAssociationContext { keepalive_flag: bool, balancer: PingBalancer, inbound: Arc, + client_session_id: u64, + client_packet_id: u64, + server_session: Option, } impl Drop for UdpAssociationContext { @@ -214,6 +242,15 @@ impl Drop for UdpAssociationContext { } } +thread_local! { + static CLIENT_SESSION_RNG: RefCell = RefCell::new(SmallRng::from_entropy()); +} + +#[inline] +fn generate_client_session_id() -> u64 { + CLIENT_SESSION_RNG.with(|rng| rng.borrow_mut().gen()) +} + impl UdpAssociationContext { fn create( context: Arc, @@ -237,6 +274,11 @@ impl UdpAssociationContext { keepalive_flag: false, balancer, inbound, + // client_session_id must be random generated, + // server use this ID to identify every independent clients. + client_session_id: generate_client_session_id(), + client_packet_id: 1, + server_session: None, }; let handle = tokio::spawn(async move { assoc.dispatch_packet(receiver).await }); @@ -262,7 +304,7 @@ impl UdpAssociationContext { } received_opt = receive_from_proxied_opt(&self.proxied_socket, &mut proxied_buffer) => { - let (n, addr) = match received_opt { + let (n, addr, control_opt) = match received_opt { Ok(r) => r, Err(err) => { error!("udp relay {} <- ... failed, error: {}", self.peer_addr, err); @@ -272,6 +314,33 @@ impl UdpAssociationContext { } }; + if let Some(control) = control_opt { + // Check if Packet ID is in the window + + let session = self.server_session.get_or_insert_with(ServerSessionContext::new); + + let packet_id = control.packet_id; + let session_context = session + .server_session_map + .entry(control.server_session_id) + .or_insert_with(|| { + trace!( + "udp server with session {} for {} created", + control.client_session_id, + self.peer_addr, + ); + + ServerContext { + packet_window_filter: PacketWindowFilter::new() + } + }); + + if !session_context.packet_window_filter.validate_packet_id(packet_id, u64::MAX) { + error!("udp {} packet_id {} out of window", self.peer_addr, packet_id); + continue; + } + } + self.send_received_respond_packet(&addr, &proxied_buffer[..n]).await; } @@ -291,14 +360,14 @@ impl UdpAssociationContext { async fn receive_from_proxied_opt( socket: &Option, buf: &mut Vec, - ) -> io::Result<(usize, Address)> { + ) -> io::Result<(usize, Address, Option)> { match *socket { None => future::pending().await, Some(ref s) => { if buf.is_empty() { buf.resize(MAXIMUM_UDP_PAYLOAD_SIZE, 0); } - s.recv(buf).await + s.recv_with_ctrl(buf).await } } } @@ -341,7 +410,27 @@ impl UdpAssociationContext { } }; - match socket.send(&self.forward_addr, data).await { + // Increase Packet ID before send + self.client_packet_id = match self.client_packet_id.checked_add(1) { + Some(i) => i, + None => { + warn!( + "{} -> {} (proxied) sending {} bytes failed, packet id overflowed", + self.peer_addr, + self.forward_addr, + data.len(), + ); + return Ok(()); + } + }; + + let control = UdpSocketControlData { + client_session_id: self.client_session_id, + server_session_id: 0, + packet_id: self.client_packet_id, + }; + + match socket.send_with_ctrl(&self.forward_addr, &control, data).await { Ok(..) => return Ok(()), Err(err) => { debug!( diff --git a/crates/shadowsocks-service/src/manager/server.rs b/crates/shadowsocks-service/src/manager/server.rs index 1ccac048372d..3455cc76700a 100644 --- a/crates/shadowsocks-service/src/manager/server.rs +++ b/crates/shadowsocks-service/src/manager/server.rs @@ -8,7 +8,7 @@ use log::{error, info, trace}; use shadowsocks::{ config::{Mode, ServerConfig, ServerType}, context::{Context, SharedContext}, - crypto::v1::CipherKind, + crypto::CipherKind, dns_resolver::DnsResolver, manager::protocol::{ self, diff --git a/crates/shadowsocks-service/src/net/mod.rs b/crates/shadowsocks-service/src/net/mod.rs index 1d0f32769017..abb64434bf9a 100644 --- a/crates/shadowsocks-service/src/net/mod.rs +++ b/crates/shadowsocks-service/src/net/mod.rs @@ -5,6 +5,7 @@ pub use self::{flow::FlowStat, mon_socket::MonProxySocket, mon_stream::MonProxyS pub mod flow; pub mod mon_socket; pub mod mon_stream; +pub mod packet_window; pub mod utils; /// Packet size for all UDP associations' send queue diff --git a/crates/shadowsocks-service/src/net/mon_socket.rs b/crates/shadowsocks-service/src/net/mon_socket.rs index 0b4a1ab08c91..7bf541d2fe13 100644 --- a/crates/shadowsocks-service/src/net/mon_socket.rs +++ b/crates/shadowsocks-service/src/net/mon_socket.rs @@ -2,7 +2,10 @@ use std::{io, net::SocketAddr, sync::Arc}; -use shadowsocks::{relay::socks5::Address, ProxySocket}; +use shadowsocks::{ + relay::{socks5::Address, udprelay::options::UdpSocketControlData}, + ProxySocket, +}; use tokio::net::ToSocketAddrs; use super::flow::FlowStat; @@ -28,6 +31,20 @@ impl MonProxySocket { Ok(()) } + /// Send a UDP packet to addr through proxy + #[inline] + pub async fn send_with_ctrl( + &self, + addr: &Address, + control: &UdpSocketControlData, + payload: &[u8], + ) -> io::Result<()> { + let n = self.socket.send_with_ctrl(addr, control, payload).await?; + self.flow_stat.incr_tx(n as u64); + + Ok(()) + } + /// Send a UDP packet to target from proxy #[inline] pub async fn send_to(&self, target: A, addr: &Address, payload: &[u8]) -> io::Result<()> { @@ -37,6 +54,21 @@ impl MonProxySocket { Ok(()) } + /// Send a UDP packet to target from proxy + #[inline] + pub async fn send_to_with_ctrl( + &self, + target: A, + addr: &Address, + control: &UdpSocketControlData, + payload: &[u8], + ) -> io::Result<()> { + let n = self.socket.send_to_with_ctrl(target, addr, control, payload).await?; + self.flow_stat.incr_tx(n as u64); + + Ok(()) + } + /// Receive packet from Shadowsocks' UDP server /// /// This function will use `recv_buf` to store intermediate data, so it has to be big enough to store the whole shadowsocks' packet @@ -50,6 +82,22 @@ impl MonProxySocket { Ok((n, addr)) } + /// Receive packet from Shadowsocks' UDP server + /// + /// This function will use `recv_buf` to store intermediate data, so it has to be big enough to store the whole shadowsocks' packet + /// + /// It is recommended to allocate a buffer to have at least 65536 bytes. + #[inline] + pub async fn recv_with_ctrl( + &self, + recv_buf: &mut [u8], + ) -> io::Result<(usize, Address, Option)> { + let (n, addr, recv_n, control) = self.socket.recv_with_ctrl(recv_buf).await?; + self.flow_stat.incr_rx(recv_n as u64); + + Ok((n, addr, control)) + } + /// Receive packet from Shadowsocks' UDP server /// /// This function will use `recv_buf` to store intermediate data, so it has to be big enough to store the whole shadowsocks' packet @@ -63,8 +111,29 @@ impl MonProxySocket { Ok((n, peer_addr, addr)) } + /// Receive packet from Shadowsocks' UDP server + /// + /// This function will use `recv_buf` to store intermediate data, so it has to be big enough to store the whole shadowsocks' packet + /// + /// It is recommended to allocate a buffer to have at least 65536 bytes. + #[inline] + pub async fn recv_from_with_ctrl( + &self, + recv_buf: &mut [u8], + ) -> io::Result<(usize, SocketAddr, Address, Option)> { + let (n, peer_addr, addr, recv_n, control) = self.socket.recv_from_with_ctrl(recv_buf).await?; + self.flow_stat.incr_rx(recv_n as u64); + + Ok((n, peer_addr, addr, control)) + } + #[inline] pub fn get_ref(&self) -> &ProxySocket { &self.socket } + + #[inline] + pub fn flow_stat(&self) -> &FlowStat { + &self.flow_stat + } } diff --git a/crates/shadowsocks-service/src/net/packet_window.rs b/crates/shadowsocks-service/src/net/packet_window.rs new file mode 100644 index 000000000000..4ff8275c8e20 --- /dev/null +++ b/crates/shadowsocks-service/src/net/packet_window.rs @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2017-2021 WireGuard LLC. All Rights Reserved. + +//! Packet window +//! +//! https://github.com/WireGuard/wireguard-go/blob/master/replay/replay.go + +const BLOCK_BIT_LOG: u64 = 6; // 1<<6 == 64 bits +const BLOCK_BITS: u64 = 1 << BLOCK_BIT_LOG; // must be power of 2 +const RING_BLOCKS: u64 = 1 << 7; // must be power of 2 +const WINDOW_SIZE: u64 = (RING_BLOCKS - 1) * BLOCK_BITS; +const BLOCK_MASK: u64 = RING_BLOCKS - 1; +const BIT_MASK: u64 = BLOCK_BITS - 1; + +/// Packet window for checking `packet_id` is in the sliding window +#[derive(Debug, Clone)] +pub struct PacketWindowFilter { + last_packet_id: u64, + packet_ring: [u64; RING_BLOCKS as usize], +} + +impl Default for PacketWindowFilter { + fn default() -> PacketWindowFilter { + PacketWindowFilter::new() + } +} + +impl PacketWindowFilter { + /// Create an empty filter + pub fn new() -> PacketWindowFilter { + PacketWindowFilter { + last_packet_id: 0, + packet_ring: [0u64; RING_BLOCKS as usize], + } + } + + /// Reset filter to the initial state + pub fn reset(&mut self) { + self.last_packet_id = 0; + self.packet_ring[0] = 0; + } + + /// Check and remember the `packet_id` + /// + /// Overlimit `packet_id >= limit` are always rejected + pub fn validate_packet_id(&mut self, packet_id: u64, limit: u64) -> bool { + if packet_id >= limit { + return false; + } + + let mut index_block = packet_id >> BLOCK_BIT_LOG; + if packet_id > self.last_packet_id { + // Move the window forward + + let current = self.last_packet_id >> BLOCK_BIT_LOG; + let mut diff = index_block - current; + if diff > RING_BLOCKS { + // Clear the whole filter + diff = RING_BLOCKS; + } + for d in 1..=diff { + let i = current + d; + self.packet_ring[(i & BLOCK_MASK) as usize] = 0; + } + self.last_packet_id = packet_id; + } else if self.last_packet_id - packet_id > WINDOW_SIZE { + // Behind the current window + return false; + } + + // Check and set bit + index_block &= BLOCK_MASK; + let index_bit = packet_id & BIT_MASK; + let old = self.packet_ring[index_block as usize]; + let new = old | (1 << index_bit); + self.packet_ring[index_block as usize] = new; + old != new + } +} + +#[cfg(test)] +mod test { + use super::*; + + use std::cell::RefCell; + + #[test] + fn test_packet_window() { + const REJECT_AFTER_MESSAGES: u64 = u64::MAX - (1u64 << 13); + + let filter = RefCell::new(PacketWindowFilter::new()); + + let test_number = RefCell::new(0); + #[allow(non_snake_case)] + let T = |n: u64, expected: bool| { + *(test_number.borrow_mut()) += 1; + if filter.borrow_mut().validate_packet_id(n, REJECT_AFTER_MESSAGES) != expected { + panic!("Test {} failed, {} {}", test_number.borrow(), n, expected); + } + }; + + const T_LIM: u64 = WINDOW_SIZE + 1; + + T(0, true); // 1 + T(1, true); // 2 + T(1, false); // 3 + T(9, true); // 4 + T(8, true); // 5 + T(7, true); // 6 + T(7, false); // 7 + T(T_LIM, true); // 8 + T(T_LIM - 1, true); // 9 + T(T_LIM - 1, false); // 10 + T(T_LIM - 2, true); // 11 + T(2, true); // 12 + T(2, false); // 13 + T(T_LIM + 16, true); // 14 + T(3, false); // 15 + T(T_LIM + 16, false); // 16 + T(T_LIM * 4, true); // 17 + T(T_LIM * 4 - (T_LIM - 1), true); // 18 + T(10, false); // 19 + T(T_LIM * 4 - T_LIM, false); // 20 + T(T_LIM * 4 - (T_LIM + 1), false); // 21 + T(T_LIM * 4 - (T_LIM - 2), true); // 22 + T(T_LIM * 4 + 1 - T_LIM, false); // 23 + T(0, false); // 24 + T(REJECT_AFTER_MESSAGES, false); // 25 + T(REJECT_AFTER_MESSAGES - 1, true); // 26 + T(REJECT_AFTER_MESSAGES, false); // 27 + T(REJECT_AFTER_MESSAGES - 1, false); // 28 + T(REJECT_AFTER_MESSAGES - 2, true); // 29 + T(REJECT_AFTER_MESSAGES + 1, false); // 30 + T(REJECT_AFTER_MESSAGES + 2, false); // 31 + T(REJECT_AFTER_MESSAGES - 2, false); // 32 + T(REJECT_AFTER_MESSAGES - 3, true); // 33 + T(0, false); // 34 + + println!("Bulk test 1"); + filter.borrow_mut().reset(); + *(test_number.borrow_mut()) = 0; + for i in 1..=WINDOW_SIZE { + T(i, true); + } + T(0, true); + T(0, false); + + println!("Bulk test 2"); + filter.borrow_mut().reset(); + *(test_number.borrow_mut()) = 0; + for i in 2..=WINDOW_SIZE + 1 { + T(i, true); + } + T(1, true); + T(0, false); + + println!("Bulk test 3"); + filter.borrow_mut().reset(); + *(test_number.borrow_mut()) = 0; + for i in (1..=WINDOW_SIZE + 1).rev() { + T(i, true); + } + + println!("Bulk test 4"); + filter.borrow_mut().reset(); + *(test_number.borrow_mut()) = 0; + for i in (2..=WINDOW_SIZE + 2).rev() { + T(i, true); + } + T(0, false); + + println!("Bulk test 5"); + filter.borrow_mut().reset(); + *(test_number.borrow_mut()) = 0; + for i in (1..=WINDOW_SIZE).rev() { + T(i, true); + } + T(WINDOW_SIZE + 1, true); + T(0, false); + + println!("Bulk test 6"); + filter.borrow_mut().reset(); + *(test_number.borrow_mut()) = 0; + for i in (1..=WINDOW_SIZE).rev() { + T(i, true); + } + T(0, true); + T(WINDOW_SIZE + 1, true); + } +} diff --git a/crates/shadowsocks-service/src/server/server.rs b/crates/shadowsocks-service/src/server/server.rs index 0fa4f06fa20d..86030afe8bfb 100644 --- a/crates/shadowsocks-service/src/server/server.rs +++ b/crates/shadowsocks-service/src/server/server.rs @@ -171,6 +171,7 @@ impl Server { async fn run_udp_server(&self) -> io::Result<()> { let server = UdpServer::new( self.context.clone(), + self.svr_cfg.method(), self.udp_expiry_duration, self.udp_capacity, self.accept_opts.clone(), diff --git a/crates/shadowsocks-service/src/server/tcprelay.rs b/crates/shadowsocks-service/src/server/tcprelay.rs index 3c8c0a20e716..54cf6c990a19 100644 --- a/crates/shadowsocks-service/src/server/tcprelay.rs +++ b/crates/shadowsocks-service/src/server/tcprelay.rs @@ -10,12 +10,9 @@ use std::{ use log::{debug, error, info, trace, warn}; use shadowsocks::{ - crypto::v1::CipherKind, + crypto::CipherKind, net::{AcceptOpts, TcpStream as OutboundTcpStream}, - relay::{ - socks5::{Address, Error as Socks5Error}, - tcprelay::{utils::copy_encrypted_bidirectional, ProxyServerStream}, - }, + relay::tcprelay::{utils::copy_encrypted_bidirectional, ProxyServerStream}, ProxyListener, ServerConfig, }; @@ -107,9 +104,17 @@ struct TcpServerClient { impl TcpServerClient { async fn serve(mut self) -> io::Result<()> { - let target_addr = match Address::read_from(&mut self.stream).await { + // let target_addr = match Address::read_from(&mut self.stream).await { + let target_addr = match self.stream.handshake().await { Ok(a) => a, - Err(Socks5Error::IoError(ref err)) if err.kind() == ErrorKind::UnexpectedEof => { + // Err(Socks5Error::IoError(ref err)) if err.kind() == ErrorKind::UnexpectedEof => { + // debug!( + // "handshake failed, received EOF before a complete target Address, peer: {}", + // self.peer_addr + // ); + // return Ok(()); + // } + Err(err) if err.kind() == ErrorKind::UnexpectedEof => { debug!( "handshake failed, received EOF before a complete target Address, peer: {}", self.peer_addr diff --git a/crates/shadowsocks-service/src/server/udprelay.rs b/crates/shadowsocks-service/src/server/udprelay.rs index db0286a27029..b7af8dcde8b6 100644 --- a/crates/shadowsocks-service/src/server/udprelay.rs +++ b/crates/shadowsocks-service/src/server/udprelay.rs @@ -1,6 +1,7 @@ //! Shadowsocks UDP server use std::{ + cell::RefCell, io::{self, ErrorKind}, net::SocketAddr, sync::Arc, @@ -11,28 +12,78 @@ use bytes::Bytes; use futures::future; use log::{debug, error, info, trace, warn}; use lru_time_cache::LruCache; +use rand::{rngs::SmallRng, Rng, SeedableRng}; use shadowsocks::{ + crypto::{CipherCategory, CipherKind}, lookup_then, net::{AcceptOpts, UdpSocket as OutboundUdpSocket}, relay::{ socks5::Address, - udprelay::{ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE}, + udprelay::{options::UdpSocketControlData, ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE}, }, ServerConfig, }; use tokio::{sync::mpsc, task::JoinHandle, time}; -use crate::net::{MonProxySocket, UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE, UDP_ASSOCIATION_SEND_CHANNEL_SIZE}; +use crate::net::{ + packet_window::PacketWindowFilter, + MonProxySocket, + UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE, + UDP_ASSOCIATION_SEND_CHANNEL_SIZE, +}; use super::context::ServiceContext; +#[derive(Debug, Clone, Copy)] +enum NatKey { + PeerAddr(SocketAddr), + #[cfg(feature = "aead-cipher-2022")] + SessionId(u64), +} + type AssociationMap = LruCache; +#[cfg(feature = "aead-cipher-2022")] +type SessionMap = LruCache; + +enum NatMap { + Association(AssociationMap), + #[cfg(feature = "aead-cipher-2022")] + Session(SessionMap), +} + +impl NatMap { + fn cleanup_expired(&mut self) { + match *self { + NatMap::Association(ref mut m) => { + m.iter(); + } + #[cfg(feature = "aead-cipher-2022")] + NatMap::Session(ref mut m) => { + m.iter(); + } + } + } + + fn keep_alive(&mut self, key: &NatKey) { + match (self, key) { + (NatMap::Association(ref mut m), NatKey::PeerAddr(ref peer_addr)) => { + m.get(peer_addr); + } + #[cfg(feature = "aead-cipher-2022")] + (NatMap::Session(ref mut m), NatKey::SessionId(ref session_id)) => { + m.get(session_id); + } + #[allow(unreachable_patterns)] + _ => unreachable!("NatMap & NatKey mismatch"), + } + } +} pub struct UdpServer { context: Arc, - assoc_map: AssociationMap, - keepalive_tx: mpsc::Sender, - keepalive_rx: mpsc::Receiver, + assoc_map: NatMap, + keepalive_tx: mpsc::Sender, + keepalive_rx: mpsc::Receiver, time_to_live: Duration, accept_opts: AcceptOpts, } @@ -40,14 +91,31 @@ pub struct UdpServer { impl UdpServer { pub fn new( context: Arc, + method: CipherKind, time_to_live: Option, capacity: Option, accept_opts: AcceptOpts, ) -> UdpServer { let time_to_live = time_to_live.unwrap_or(crate::DEFAULT_UDP_EXPIRY_DURATION); - let assoc_map = match capacity { - Some(capacity) => LruCache::with_expiry_duration_and_capacity(time_to_live, capacity), - None => LruCache::with_expiry_duration(time_to_live), + + fn create_assoc_map(time_to_live: Duration, capacity: Option) -> LruCache + where + K: Ord + Clone, + { + match capacity { + Some(capacity) => LruCache::with_expiry_duration_and_capacity(time_to_live, capacity), + None => LruCache::with_expiry_duration(time_to_live), + } + } + + let assoc_map = match method.category() { + CipherCategory::None | CipherCategory::Aead => { + NatMap::Association(create_assoc_map(time_to_live, capacity)) + } + #[cfg(feature = "stream-cipher")] + CipherCategory::Stream => NatMap::Association(create_assoc_map(time_to_live, capacity)), + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => NatMap::Session(create_assoc_map(time_to_live, capacity)), }; let (keepalive_tx, keepalive_rx) = mpsc::channel(UDP_ASSOCIATION_KEEP_ALIVE_CHANNEL_SIZE); @@ -80,16 +148,16 @@ impl UdpServer { tokio::select! { _ = cleanup_timer.tick() => { // cleanup expired associations. iter() will remove expired elements - let _ = self.assoc_map.iter(); + let _ = self.assoc_map.cleanup_expired(); } peer_addr_opt = self.keepalive_rx.recv() => { let peer_addr = peer_addr_opt.expect("keep-alive channel closed unexpectly"); - self.assoc_map.get(&peer_addr); + self.assoc_map.keep_alive(&peer_addr); } - recv_result = listener.recv_from(&mut buffer) => { - let (n, peer_addr, target_addr) = match recv_result { + recv_result = listener.recv_from_with_ctrl(&mut buffer) => { + let (n, peer_addr, target_addr, control) = match recv_result { Ok(s) => s, Err(err) => { error!("udp server recv_from failed with error: {}", err); @@ -122,7 +190,7 @@ impl UdpServer { } let data = &buffer[..n]; - if let Err(err) = self.send_packet(&listener, peer_addr, target_addr, data).await { + if let Err(err) = self.send_packet(&listener, peer_addr, target_addr, control, data).await { error!( "udp packet relay {} with {} bytes failed, error: {}", peer_addr, @@ -140,31 +208,70 @@ impl UdpServer { listener: &Arc, peer_addr: SocketAddr, target_addr: Address, + control: Option, data: &[u8], ) -> io::Result<()> { - if let Some(assoc) = self.assoc_map.get(&peer_addr) { - return assoc.try_send((target_addr, Bytes::copy_from_slice(data))); - } + match self.assoc_map { + NatMap::Association(ref mut m) => { + if let Some(assoc) = m.get(&peer_addr) { + return assoc.try_send((peer_addr, target_addr, Bytes::copy_from_slice(data), control)); + } - let assoc = UdpAssociation::new( - self.context.clone(), - listener.clone(), - peer_addr, - self.keepalive_tx.clone(), - ); + let assoc = UdpAssociation::new_association( + self.context.clone(), + listener.clone(), + peer_addr, + self.keepalive_tx.clone(), + ); - debug!("created udp association for {}", peer_addr); + debug!("created udp association for {}", peer_addr); - assoc.try_send((target_addr, Bytes::copy_from_slice(data)))?; - self.assoc_map.insert(peer_addr, assoc); + assoc.try_send((peer_addr, target_addr, Bytes::copy_from_slice(data), control))?; + m.insert(peer_addr, assoc); + } + #[cfg(feature = "aead-cipher-2022")] + NatMap::Session(ref mut m) => { + let xcontrol = match control { + None => { + error!("control is required for session based NAT, from {}", peer_addr); + return Err(io::Error::new(ErrorKind::Other, "control data missing in packet")); + } + Some(ref c) => c, + }; + + let client_session_id = xcontrol.client_session_id; + + if let Some(assoc) = m.get(&client_session_id) { + return assoc.try_send((peer_addr, target_addr, Bytes::copy_from_slice(data), control)); + } + + let assoc = UdpAssociation::new_session( + self.context.clone(), + listener.clone(), + peer_addr, + self.keepalive_tx.clone(), + client_session_id, + ); + + debug!( + "created udp association for {} with session {}", + peer_addr, client_session_id + ); + + assoc.try_send((peer_addr, target_addr, Bytes::copy_from_slice(data), control))?; + m.insert(client_session_id, assoc); + } + } Ok(()) } } +type UdpAssociationSendMessage = (SocketAddr, Address, Bytes, Option); + struct UdpAssociation { assoc_handle: JoinHandle<()>, - sender: mpsc::Sender<(Address, Bytes)>, + sender: mpsc::Sender, } impl Drop for UdpAssociation { @@ -174,17 +281,30 @@ impl Drop for UdpAssociation { } impl UdpAssociation { - fn new( + fn new_association( context: Arc, inbound: Arc, peer_addr: SocketAddr, - keepalive_tx: mpsc::Sender, + keepalive_tx: mpsc::Sender, ) -> UdpAssociation { - let (assoc_handle, sender) = UdpAssociationContext::create(context, inbound, peer_addr, keepalive_tx); + let (assoc_handle, sender) = UdpAssociationContext::create(context, inbound, peer_addr, keepalive_tx, None); UdpAssociation { assoc_handle, sender } } - fn try_send(&self, data: (Address, Bytes)) -> io::Result<()> { + #[cfg(feature = "aead-cipher-2022")] + fn new_session( + context: Arc, + inbound: Arc, + peer_addr: SocketAddr, + keepalive_tx: mpsc::Sender, + client_session_id: u64, + ) -> UdpAssociation { + let (assoc_handle, sender) = + UdpAssociationContext::create(context, inbound, peer_addr, keepalive_tx, Some(client_session_id)); + UdpAssociation { assoc_handle, sender } + } + + fn try_send(&self, data: UdpAssociationSendMessage) -> io::Result<()> { if let Err(..) = self.sender.try_send(data) { let err = io::Error::new(ErrorKind::Other, "udp relay channel full"); return Err(err); @@ -193,14 +313,36 @@ impl UdpAssociation { } } +struct ClientContext { + packet_window_filter: PacketWindowFilter, +} + +struct ClientSessionContext { + client_session_id: u64, + client_context_map: LruCache, +} + +impl ClientSessionContext { + fn new(client_session_id: u64) -> ClientSessionContext { + ClientSessionContext { + client_session_id, + client_context_map: LruCache::with_expiry_duration_and_capacity(Duration::from_secs(30 * 60), 10), + } + } +} + struct UdpAssociationContext { context: Arc, peer_addr: SocketAddr, outbound_ipv4_socket: Option, outbound_ipv6_socket: Option, - keepalive_tx: mpsc::Sender, + keepalive_tx: mpsc::Sender, keepalive_flag: bool, inbound: Arc, + // AEAD 2022 + client_session: Option, + server_session_id: u64, + server_packet_id: u64, } impl Drop for UdpAssociationContext { @@ -209,13 +351,23 @@ impl Drop for UdpAssociationContext { } } +thread_local! { + static CLIENT_SESSION_RNG: RefCell = RefCell::new(SmallRng::from_entropy()); +} + +#[inline] +fn generate_server_session_id() -> u64 { + CLIENT_SESSION_RNG.with(|rng| rng.borrow_mut().gen()) +} + impl UdpAssociationContext { fn create( context: Arc, inbound: Arc, peer_addr: SocketAddr, - keepalive_tx: mpsc::Sender, - ) -> (JoinHandle<()>, mpsc::Sender<(Address, Bytes)>) { + keepalive_tx: mpsc::Sender, + client_session_id: Option, + ) -> (JoinHandle<()>, mpsc::Sender) { // Pending packets UDP_ASSOCIATION_SEND_CHANNEL_SIZE for each association should be good enough for a server. // If there are plenty of packets stuck in the channel, dropping excessive packets is a good way to protect the server from // being OOM. @@ -229,13 +381,17 @@ impl UdpAssociationContext { keepalive_tx, keepalive_flag: false, inbound, + client_session: client_session_id.map(ClientSessionContext::new), + // server_session_id must be generated randomly + server_session_id: generate_server_session_id(), + server_packet_id: 0, }; let handle = tokio::spawn(async move { assoc.dispatch_packet(receiver).await }); (handle, sender) } - async fn dispatch_packet(&mut self, mut receiver: mpsc::Receiver<(Address, Bytes)>) { + async fn dispatch_packet(&mut self, mut receiver: mpsc::Receiver) { let mut outbound_ipv4_buffer = Vec::new(); let mut outbound_ipv6_buffer = Vec::new(); let mut keepalive_interval = time::interval(Duration::from_secs(1)); @@ -243,7 +399,7 @@ impl UdpAssociationContext { loop { tokio::select! { packet_received_opt = receiver.recv() => { - let (target_addr, data) = match packet_received_opt { + let (peer_addr, target_addr, data, control) = match packet_received_opt { Some(d) => d, None => { trace!("udp association for {} -> ... channel closed", self.peer_addr); @@ -251,7 +407,7 @@ impl UdpAssociationContext { } }; - self.dispatch_received_packet(&target_addr, &data).await; + self.dispatch_received_packet(peer_addr, &target_addr, &data, &control).await; } received_opt = receive_from_outbound_opt(&self.outbound_ipv4_socket, &mut outbound_ipv4_buffer) => { @@ -286,8 +442,16 @@ impl UdpAssociationContext { _ = keepalive_interval.tick() => { if self.keepalive_flag { - if let Err(..) = self.keepalive_tx.try_send(self.peer_addr) { - debug!("udp relay {} keep-alive failed, channel full or closed", self.peer_addr); + let nat_key = match self.client_session { + None => NatKey::PeerAddr(self.peer_addr), + #[cfg(feature = "aead-cipher-2022")] + Some(ref s) => NatKey::SessionId(s.client_session_id), + #[cfg(not(feature = "aead-cipher-2022"))] + Some(..) => unreachable!("client_session_id is not None but aead-cipher-2022 is not enabled"), + }; + + if let Err(..) = self.keepalive_tx.try_send(nat_key) { + debug!("udp relay {:?} keep-alive failed, channel full or closed", nat_key); } else { self.keepalive_flag = false; } @@ -313,12 +477,29 @@ impl UdpAssociationContext { } } - async fn dispatch_received_packet(&mut self, target_addr: &Address, data: &[u8]) { + async fn dispatch_received_packet( + &mut self, + peer_addr: SocketAddr, + target_addr: &Address, + data: &[u8], + control: &Option, + ) { + if let Some(ref mut session) = self.client_session { + if peer_addr != self.peer_addr { + debug!( + "udp relay for {} changed to {}, session: {:?}", + self.peer_addr, peer_addr, session.client_session_id + ); + self.peer_addr = peer_addr; + } + } + trace!( - "udp relay {} -> {} with {} bytes", + "udp relay {} -> {} with {} bytes, control: {:?}", self.peer_addr, target_addr, - data.len() + data.len(), + control, ); if self.context.check_outbound_blocked(target_addr).await { @@ -329,6 +510,34 @@ impl UdpAssociationContext { return; } + if let Some(control) = control { + // Check if Packet ID is in the window + + let session = self + .client_session + .get_or_insert_with(|| ClientSessionContext::new(control.client_session_id)); + + let session_context = session.client_context_map.entry(self.peer_addr).or_insert_with(|| { + trace!( + "udp client {} with session {} created", + self.peer_addr, + control.client_session_id + ); + ClientContext { + packet_window_filter: PacketWindowFilter::new(), + } + }); + + let packet_id = control.packet_id; + if !session_context + .packet_window_filter + .validate_packet_id(packet_id, u64::MAX) + { + error!("udp client {} packet_id {} out of window", self.peer_addr, packet_id); + return; + } + } + if let Err(err) = self.dispatch_received_outbound_packet(target_addr, data).await { error!( "udp relay {} -> {} with {} bytes, error: {}", @@ -392,17 +601,67 @@ impl UdpAssociationContext { // Keep association alive in map self.keepalive_flag = true; - // Send back to client - if let Err(err) = self.inbound.send_to(self.peer_addr, addr, data).await { - warn!( - "udp failed to send back {} bytes to client {}, from target {}, error: {}", - data.len(), - self.peer_addr, - addr, - err - ); - } else { - trace!("udp relay {} <- {} with {} bytes", self.peer_addr, addr, data.len()); + match self.client_session { + None => { + // Naive route, send data directly back to client without session + if let Err(err) = self.inbound.send_to(self.peer_addr, addr, data).await { + warn!( + "udp failed to send back {} bytes to client {}, from target {}, error: {}", + data.len(), + self.peer_addr, + addr, + err + ); + } else { + trace!("udp relay {} <- {} with {} bytes", self.peer_addr, addr, data.len()); + } + } + Some(ref client_session) => { + // AEAD 2022, client session + + // Increase Packet ID before send + self.server_packet_id = match self.server_packet_id.checked_add(1) { + Some(i) => i, + None => { + warn!( + "udp failed to send back {} bytes to client {}, from target {}, server packet id overflowed", + data.len(), + self.peer_addr, + addr + ); + return; + } + }; + + let control = UdpSocketControlData { + client_session_id: client_session.client_session_id, + server_session_id: self.server_session_id, + packet_id: self.server_packet_id, + }; + + if let Err(err) = self + .inbound + .send_to_with_ctrl(self.peer_addr, addr, &control, data) + .await + { + warn!( + "udp failed to send back {} bytes to client {}, from target {}, control: {:?}, error: {}", + data.len(), + self.peer_addr, + addr, + control, + err + ); + } else { + trace!( + "udp relay {} <- {} with {} bytes, control {:?}", + self.peer_addr, + addr, + data.len(), + control + ); + } + } } } } diff --git a/crates/shadowsocks/Cargo.toml b/crates/shadowsocks/Cargo.toml index 1c106a567806..c7f4e18cfb5d 100644 --- a/crates/shadowsocks/Cargo.toml +++ b/crates/shadowsocks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shadowsocks" -version = "1.14.3" +version = "1.15.0" authors = ["Shadowsocks Contributors"] description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls." repository = "https://github.com/shadowsocks/shadowsocks-rust" @@ -31,6 +31,9 @@ stream-cipher = ["shadowsocks-crypto/v1-stream"] # WARN: These non-standard AEAD ciphers are not officially supported by shadowsocks community aead-cipher-extra = ["shadowsocks-crypto/v1-aead-extra"] +# Enable AEAD 2022 +aead-cipher-2022 = ["shadowsocks-crypto/v2", "rand/small_rng", "aes", "lru_time_cache"] + # Enable detection against replay attack security-replay-attack-detect = ["bloomfilter", "spin"] # Enable IV printable prefix @@ -56,6 +59,7 @@ pin-project = "1.0" bloomfilter = { version = "1.0.8", optional = true } thiserror = "1.0" rand = { version = "0.8", optional = true } +lru_time_cache = { version = "0.11", optional = true } serde = { version = "1.0", features = ["derive"] } serde_urlencoded = "0.7" @@ -72,11 +76,13 @@ trust-dns-resolver = { version = "0.21", optional = true } arc-swap = { version = "1.3", optional = true } notify = { version = "5.0.0-pre.13", optional = true } +aes = { version = "0.8", optional = true } + [target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies] -shadowsocks-crypto = { version = "0.3.3", features = ["ring"] } +shadowsocks-crypto = { version = "0.4", git = "https://github.com/shadowsocks/shadowsocks-crypto.git", features = ["ring"] } [target.'cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))'.dependencies] -shadowsocks-crypto = { version = "0.3.3", features = [] } +shadowsocks-crypto = { version = "0.4", git = "https://github.com/shadowsocks/shadowsocks-crypto.git", features = [] } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["mswsock", "winsock2", "netioapi"] } diff --git a/crates/shadowsocks/src/config.rs b/crates/shadowsocks/src/config.rs index 4be50f62e177..25c9cc413f84 100644 --- a/crates/shadowsocks/src/config.rs +++ b/crates/shadowsocks/src/config.rs @@ -15,7 +15,7 @@ use log::error; use url::{self, Url}; use crate::{ - crypto::v1::{openssl_bytes_to_key, CipherKind}, + crypto::{v1::openssl_bytes_to_key, CipherKind}, plugin::PluginConfig, relay::socks5::Address, }; @@ -175,6 +175,39 @@ pub struct ServerConfig { weight: ServerWeight, } +#[cfg(feature = "aead-cipher-2022")] +#[inline] +fn make_derived_key(method: CipherKind, password: &str, enc_key: &mut [u8]) { + if method.is_aead_2022() { + // AEAD 2022 password is a base64 form of enc_key + match base64::decode_config(password, base64::STANDARD) { + Ok(v) => { + if v.len() != enc_key.len() { + panic!( + "{} is expecting a {} bytes key, but password: {} ({} bytes after decode)", + method, + enc_key.len(), + password, + v.len() + ); + } + enc_key.copy_from_slice(&v); + } + Err(err) => { + panic!("{} password {} is not base64 encoded, error: {}", method, password, err); + } + } + } else { + openssl_bytes_to_key(password.as_bytes(), enc_key); + } +} + +#[cfg(not(feature = "aead-cipher-2022"))] +#[inline] +fn make_derived_key(_method: CipherKind, password: &str, enc_key: &mut [u8]) { + openssl_bytes_to_key(password.as_bytes(), enc_key); +} + impl ServerConfig { /// Create a new `ServerConfig` pub fn new(addr: A, password: P, method: CipherKind) -> ServerConfig @@ -185,7 +218,7 @@ impl ServerConfig { let password = password.into(); let mut enc_key = vec![0u8; method.key_len()].into_boxed_slice(); - openssl_bytes_to_key(password.as_bytes(), &mut enc_key); + make_derived_key(method, &password, &mut enc_key); ServerConfig { addr: addr.into(), @@ -211,7 +244,7 @@ impl ServerConfig { self.password = password.into(); let mut enc_key = vec![0u8; method.key_len()].into_boxed_slice(); - openssl_bytes_to_key(self.password.as_bytes(), &mut enc_key); + make_derived_key(method, &self.password, &mut enc_key); self.enc_key = enc_key; } @@ -399,7 +432,8 @@ impl ServerConfig { } }; - let mut svrconfig = ServerConfig::new(addr, pwd.to_owned(), method.parse().unwrap()); + let method = method.parse().expect("method"); + let mut svrconfig = ServerConfig::new(addr, pwd.to_owned(), method); if let Some(q) = parsed.query() { let query = match serde_urlencoded::from_bytes::>(q.as_bytes()) { diff --git a/crates/shadowsocks/src/relay/tcprelay/aead.rs b/crates/shadowsocks/src/relay/tcprelay/aead.rs index c3e579165495..6dc3461dfa95 100644 --- a/crates/shadowsocks/src/relay/tcprelay/aead.rs +++ b/crates/shadowsocks/src/relay/tcprelay/aead.rs @@ -48,7 +48,7 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use crate::{ context::Context, - crypto::v1::{Cipher, CipherKind}, + crypto::{v1::Cipher, CipherKind}, }; /// AEAD packet payload must be smaller than 0x3FFF @@ -93,6 +93,10 @@ impl DecryptedReader { } } + pub fn salt(&self) -> Option<&[u8]> { + self.salt.as_deref() + } + /// Attempt to read decrypted data from stream pub fn poll_read_decrypted( &mut self, @@ -219,9 +223,8 @@ impl DecryptedReader { } // Check repeated salt after first successful decryption #442 - if self.salt.is_some() { - let salt = self.salt.take().unwrap(); - context.check_nonce_replay(&salt)?; + if let Some(ref salt) = self.salt { + context.check_nonce_replay(salt)?; } // Remote TAG @@ -298,6 +301,7 @@ pub struct EncryptedWriter { cipher: Cipher, buffer: BytesMut, state: EncryptWriteState, + salt: Bytes, } impl EncryptedWriter { @@ -311,9 +315,15 @@ impl EncryptedWriter { cipher: Cipher::new(method, key, nonce), buffer, state: EncryptWriteState::AssemblePacket, + salt: Bytes::copy_from_slice(nonce), } } + /// Salt (nonce) + pub fn salt(&self) -> &[u8] { + self.salt.as_ref() + } + /// Attempt to write encrypted data into the writer pub fn poll_write_encrypted( &mut self, diff --git a/crates/shadowsocks/src/relay/tcprelay/aead_2022.rs b/crates/shadowsocks/src/relay/tcprelay/aead_2022.rs new file mode 100644 index 000000000000..ce78dce06f1a --- /dev/null +++ b/crates/shadowsocks/src/relay/tcprelay/aead_2022.rs @@ -0,0 +1,371 @@ +//! AEAD 2022 packet I/O facilities +//! +//! ```plain +//! TCP request (before encryption) +//! +------+---------------------+------------------+ +//! | ATYP | Destination Address | Destination Port | +//! +------+---------------------+------------------+ +//! | 1 | Variable | 2 | +//! +------+---------------------+------------------+ +//! +//! TCP request (after encryption, *ciphertext*) +//! +--------+--------------+------------------+--------------+---------------+ +//! | NONCE | *HeaderLen* | HeaderLen_TAG | *Header* | Header_TAG | +//! +--------+--------------+------------------+--------------+---------------+ +//! | Fixed | 2 | Fixed | Variable | Fixed | +//! +--------+--------------+------------------+--------------+---------------+ +//! +//! TCP Chunk (before encryption) +//! +----------+ +//! | DATA | +//! +----------+ +//! | Variable | +//! +----------+ +//! +//! TCP Chunk (after encryption, *ciphertext*) +//! +--------------+---------------+--------------+------------+ +//! | *DataLen* | DataLen_TAG | *Data* | Data_TAG | +//! +--------------+---------------+--------------+------------+ +//! | 2 | Fixed | Variable | Fixed | +//! +--------------+---------------+--------------+------------+ +//! ``` +use std::{ + io::{self, ErrorKind}, + marker::Unpin, + pin::Pin, + slice, + task::{self, Poll}, + u16, +}; + +use byte_string::ByteStr; +use bytes::{BufMut, Bytes, BytesMut}; +use futures::ready; +use log::trace; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +use crate::{ + context::Context, + crypto::{v2::tcp::TcpCipher, CipherKind}, +}; + +/// AEAD packet payload must be smaller than 0xFFFF (u16::MAX) +pub const MAX_PACKET_SIZE: usize = 0xFFFF; + +enum DecryptReadState { + WaitSalt { key: Bytes }, + ReadLength, + ReadData { length: usize }, + BufferedData { pos: usize }, +} + +/// Reader wrapper that will decrypt data automatically +pub struct DecryptedReader { + state: DecryptReadState, + cipher: Option, + buffer: BytesMut, + method: CipherKind, + salt: Option, +} + +impl DecryptedReader { + pub fn new(method: CipherKind, key: &[u8]) -> DecryptedReader { + if method.salt_len() > 0 { + DecryptedReader { + state: DecryptReadState::WaitSalt { + key: Bytes::copy_from_slice(key), + }, + cipher: None, + buffer: BytesMut::with_capacity(method.salt_len()), + method, + salt: None, + } + } else { + DecryptedReader { + state: DecryptReadState::ReadLength, + cipher: Some(TcpCipher::new(method, key, &[])), + buffer: BytesMut::with_capacity(2 + method.tag_len()), + method, + salt: None, + } + } + } + + pub fn salt(&self) -> Option<&[u8]> { + self.salt.as_deref() + } + + /// Attempt to read decrypted data from stream + pub fn poll_read_decrypted( + &mut self, + cx: &mut task::Context<'_>, + context: &Context, + stream: &mut S, + buf: &mut ReadBuf<'_>, + ) -> Poll> + where + S: AsyncRead + Unpin + ?Sized, + { + loop { + match self.state { + DecryptReadState::WaitSalt { ref key } => { + let key = unsafe { &*(key.as_ref() as *const _) }; + ready!(self.poll_read_salt(cx, stream, key))?; + + self.buffer.clear(); + self.state = DecryptReadState::ReadLength; + self.buffer.reserve(2 + self.method.tag_len()); + } + DecryptReadState::ReadLength => match ready!(self.poll_read_length(cx, stream))? { + None => { + return Ok(()).into(); + } + Some(length) => { + self.buffer.clear(); + self.state = DecryptReadState::ReadData { length }; + self.buffer.reserve(length + self.method.tag_len()); + } + }, + DecryptReadState::ReadData { length } => { + ready!(self.poll_read_data(cx, context, stream, length))?; + + self.state = DecryptReadState::BufferedData { pos: 0 }; + } + DecryptReadState::BufferedData { ref mut pos } => { + if *pos < self.buffer.len() { + let buffered = &self.buffer[*pos..]; + + let consumed = usize::min(buffered.len(), buf.remaining()); + buf.put_slice(&buffered[..consumed]); + + *pos += consumed; + + return Ok(()).into(); + } + + self.buffer.clear(); + self.state = DecryptReadState::ReadLength; + self.buffer.reserve(2 + self.method.tag_len()); + } + } + } + } + + fn poll_read_salt(&mut self, cx: &mut task::Context<'_>, stream: &mut S, key: &[u8]) -> Poll> + where + S: AsyncRead + Unpin + ?Sized, + { + let salt_len = self.method.salt_len(); + + let n = ready!(self.poll_read_exact(cx, stream, salt_len))?; + if n < salt_len { + return Err(ErrorKind::UnexpectedEof.into()).into(); + } + + let salt = &self.buffer[..salt_len]; + // #442 Remember salt in filter after first successful decryption. + // + // If we check salt right here will allow attacker to flood our filter and eventually block all of our legitimate clients' requests. + self.salt = Some(Bytes::copy_from_slice(salt)); + + trace!("got AEAD salt {:?}", ByteStr::new(salt)); + + let cipher = TcpCipher::new(self.method, key, salt); + + self.cipher = Some(cipher); + + Ok(()).into() + } + + fn poll_read_length(&mut self, cx: &mut task::Context<'_>, stream: &mut S) -> Poll>> + where + S: AsyncRead + Unpin + ?Sized, + { + let length_len = 2 + self.method.tag_len(); + + let n = ready!(self.poll_read_exact(cx, stream, length_len))?; + if n == 0 { + return Ok(None).into(); + } + + let cipher = self.cipher.as_mut().expect("cipher is None"); + + let m = &mut self.buffer[..length_len]; + let length = DecryptedReader::decrypt_length(cipher, m)?; + + Ok(Some(length)).into() + } + + fn poll_read_data( + &mut self, + cx: &mut task::Context<'_>, + context: &Context, + stream: &mut S, + size: usize, + ) -> Poll> + where + S: AsyncRead + Unpin + ?Sized, + { + let data_len = size + self.method.tag_len(); + + let n = ready!(self.poll_read_exact(cx, stream, data_len))?; + if n == 0 { + return Err(ErrorKind::UnexpectedEof.into()).into(); + } + + let cipher = self.cipher.as_mut().expect("cipher is None"); + + let m = &mut self.buffer[..data_len]; + if !cipher.decrypt_packet(m) { + return Err(io::Error::new(ErrorKind::Other, "invalid tag-in")).into(); + } + + // Check repeated salt after first successful decryption #442 + if let Some(ref salt) = self.salt { + context.check_nonce_replay(salt)?; + } + + // Remote TAG + self.buffer.truncate(size); + + Ok(()).into() + } + + fn poll_read_exact(&mut self, cx: &mut task::Context<'_>, stream: &mut S, size: usize) -> Poll> + where + S: AsyncRead + Unpin + ?Sized, + { + assert!(size != 0); + + while self.buffer.len() < size { + let remaining = size - self.buffer.len(); + let buffer = &mut self.buffer.chunk_mut()[..remaining]; + + let mut read_buf = + ReadBuf::uninit(unsafe { slice::from_raw_parts_mut(buffer.as_mut_ptr() as *mut _, remaining) }); + ready!(Pin::new(&mut *stream).poll_read(cx, &mut read_buf))?; + + let n = read_buf.filled().len(); + if n == 0 { + if !self.buffer.is_empty() { + return Err(ErrorKind::UnexpectedEof.into()).into(); + } else { + return Ok(0).into(); + } + } + + unsafe { + self.buffer.advance_mut(n); + } + } + + Ok(size).into() + } + + fn decrypt_length(cipher: &mut TcpCipher, m: &mut [u8]) -> io::Result { + let plen = { + if !cipher.decrypt_packet(m) { + return Err(io::Error::new(ErrorKind::Other, "invalid tag-in")); + } + + u16::from_be_bytes([m[0], m[1]]) as usize + }; + + Ok(plen) + } +} + +enum EncryptWriteState { + AssemblePacket, + Writing { pos: usize }, +} + +/// Writer wrapper that will encrypt data automatically +pub struct EncryptedWriter { + cipher: TcpCipher, + buffer: BytesMut, + state: EncryptWriteState, + salt: Bytes, +} + +impl EncryptedWriter { + /// Creates a new EncryptedWriter + pub fn new(method: CipherKind, key: &[u8], nonce: &[u8]) -> EncryptedWriter { + // nonce should be sent with the first packet + let mut buffer = BytesMut::with_capacity(nonce.len()); + buffer.put(nonce); + + EncryptedWriter { + cipher: TcpCipher::new(method, key, nonce), + buffer, + state: EncryptWriteState::AssemblePacket, + salt: Bytes::copy_from_slice(nonce), + } + } + + /// Salt (nonce) + pub fn salt(&self) -> &[u8] { + self.salt.as_ref() + } + + /// Attempt to write encrypted data into the writer + pub fn poll_write_encrypted( + &mut self, + cx: &mut task::Context<'_>, + stream: &mut S, + mut buf: &[u8], + ) -> Poll> + where + S: AsyncWrite + Unpin + ?Sized, + { + if buf.len() > MAX_PACKET_SIZE { + buf = &buf[..MAX_PACKET_SIZE]; + } + + loop { + match self.state { + EncryptWriteState::AssemblePacket => { + // Step 1. Append Length + let length_size = 2 + self.cipher.tag_len(); + self.buffer.reserve(length_size); + + let mbuf = &mut self.buffer.chunk_mut()[..length_size]; + let mbuf = unsafe { slice::from_raw_parts_mut(mbuf.as_mut_ptr(), mbuf.len()) }; + + self.buffer.put_u16(buf.len() as u16); + self.cipher.encrypt_packet(mbuf); + unsafe { self.buffer.advance_mut(self.cipher.tag_len()) }; + + // Step 2. Append data + let data_size = buf.len() + self.cipher.tag_len(); + self.buffer.reserve(data_size); + + let mbuf = &mut self.buffer.chunk_mut()[..data_size]; + let mbuf = unsafe { slice::from_raw_parts_mut(mbuf.as_mut_ptr(), mbuf.len()) }; + + self.buffer.put_slice(buf); + self.cipher.encrypt_packet(mbuf); + unsafe { self.buffer.advance_mut(self.cipher.tag_len()) }; + + // Step 3. Write all + self.state = EncryptWriteState::Writing { pos: 0 }; + } + EncryptWriteState::Writing { ref mut pos } => { + while *pos < self.buffer.len() { + let n = ready!(Pin::new(&mut *stream).poll_write(cx, &self.buffer[*pos..]))?; + if n == 0 { + return Err(ErrorKind::UnexpectedEof.into()).into(); + } + *pos += n; + } + + // Reset state + self.state = EncryptWriteState::AssemblePacket; + self.buffer.clear(); + + return Ok(buf.len()).into(); + } + } + } + } +} diff --git a/crates/shadowsocks/src/relay/tcprelay/crypto_io.rs b/crates/shadowsocks/src/relay/tcprelay/crypto_io.rs index 14ee6d17eb9f..e31a8e7c585f 100644 --- a/crates/shadowsocks/src/relay/tcprelay/crypto_io.rs +++ b/crates/shadowsocks/src/relay/tcprelay/crypto_io.rs @@ -13,10 +13,12 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf, ReadHalf, WriteHalf}; use crate::{ context::Context, - crypto::v1::{CipherCategory, CipherKind}, + crypto::{CipherCategory, CipherKind}, }; use super::aead::{DecryptedReader as AeadDecryptedReader, EncryptedWriter as AeadEncryptedWriter}; +#[cfg(feature = "aead-cipher-2022")] +use super::aead_2022::{DecryptedReader as Aead2022DecryptedReader, EncryptedWriter as Aead2022EncryptedWriter}; #[cfg(feature = "stream-cipher")] use super::stream::{DecryptedReader as StreamDecryptedReader, EncryptedWriter as StreamEncryptedWriter}; @@ -27,6 +29,8 @@ pub enum DecryptedReader { Aead(AeadDecryptedReader), #[cfg(feature = "stream-cipher")] Stream(StreamDecryptedReader), + #[cfg(feature = "aead-cipher-2022")] + Aead2022(Aead2022DecryptedReader), } impl DecryptedReader { @@ -37,6 +41,8 @@ impl DecryptedReader { CipherCategory::Stream => DecryptedReader::Stream(StreamDecryptedReader::new(method, key)), CipherCategory::Aead => DecryptedReader::Aead(AeadDecryptedReader::new(method, key)), CipherCategory::None => DecryptedReader::None, + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => DecryptedReader::Aead2022(Aead2022DecryptedReader::new(method, key)), } } @@ -57,6 +63,20 @@ impl DecryptedReader { DecryptedReader::Stream(ref mut reader) => reader.poll_read_decrypted(cx, context, stream, buf), DecryptedReader::Aead(ref mut reader) => reader.poll_read_decrypted(cx, context, stream, buf), DecryptedReader::None => Pin::new(stream).poll_read(cx, buf), + #[cfg(feature = "aead-cipher-2022")] + DecryptedReader::Aead2022(ref mut reader) => reader.poll_read_decrypted(cx, context, stream, buf), + } + } + + /// Get received IV (Stream) or Salt (AEAD, AEAD2022) + pub fn nonce(&self) -> Option<&[u8]> { + match *self { + #[cfg(feature = "stream-cipher")] + DecryptedReader::Stream(ref reader) => reader.iv(), + DecryptedReader::Aead(ref reader) => reader.salt(), + DecryptedReader::None => None, + #[cfg(feature = "aead-cipher-2022")] + DecryptedReader::Aead2022(ref reader) => reader.salt(), } } } @@ -67,6 +87,8 @@ pub enum EncryptedWriter { Aead(AeadEncryptedWriter), #[cfg(feature = "stream-cipher")] Stream(StreamEncryptedWriter), + #[cfg(feature = "aead-cipher-2022")] + Aead2022(Aead2022EncryptedWriter), } impl EncryptedWriter { @@ -77,6 +99,8 @@ impl EncryptedWriter { CipherCategory::Stream => EncryptedWriter::Stream(StreamEncryptedWriter::new(method, key, nonce)), CipherCategory::Aead => EncryptedWriter::Aead(AeadEncryptedWriter::new(method, key, nonce)), CipherCategory::None => EncryptedWriter::None, + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => EncryptedWriter::Aead2022(Aead2022EncryptedWriter::new(method, key, nonce)), } } @@ -96,6 +120,20 @@ impl EncryptedWriter { EncryptedWriter::Stream(ref mut writer) => writer.poll_write_encrypted(cx, stream, buf), EncryptedWriter::Aead(ref mut writer) => writer.poll_write_encrypted(cx, stream, buf), EncryptedWriter::None => Pin::new(stream).poll_write(cx, buf), + #[cfg(feature = "aead-cipher-2022")] + EncryptedWriter::Aead2022(ref mut writer) => writer.poll_write_encrypted(cx, stream, buf), + } + } + + /// Get sent IV (Stream) or Salt (AEAD, AEAD2022) + pub fn nonce(&self) -> &[u8] { + match *self { + #[cfg(feature = "stream-cipher")] + EncryptedWriter::Stream(ref writer) => writer.iv(), + EncryptedWriter::Aead(ref writer) => writer.salt(), + EncryptedWriter::None => &[], + #[cfg(feature = "aead-cipher-2022")] + EncryptedWriter::Aead2022(ref writer) => writer.salt(), } } } @@ -123,6 +161,8 @@ impl CryptoStream { CipherCategory::Stream => method.iv_len(), CipherCategory::Aead => method.salt_len(), CipherCategory::None => 0, + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => method.salt_len(), }; let iv = match category { @@ -140,6 +180,13 @@ impl CryptoStream { local_salt } CipherCategory::None => Vec::new(), + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => { + let mut local_salt = vec![0u8; prev_len]; + context.generate_nonce(&mut local_salt, true); + trace!("generated AEAD cipher salt {:?}", ByteStr::new(&local_salt)); + local_salt + } }; CryptoStream { @@ -173,6 +220,31 @@ impl CryptoStream { pub fn into_inner(self) -> S { self.stream } + + /// Get received IV (Stream) or Salt (AEAD, AEAD2022) + pub fn received_nonce(&self) -> Option<&[u8]> { + self.dec.nonce() + } + + /// Get sent IV (Stream) or Salt (AEAD, AEAD2022) + pub fn sent_nonce(&self) -> &[u8] { + self.enc.nonce() + } +} + +/// Cryptographic reader trait +pub trait CryptoRead { + fn poll_read_decrypted( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + context: &Context, + buf: &mut ReadBuf<'_>, + ) -> Poll>; +} + +/// Cryptographic writer trait +pub trait CryptoWrite { + fn poll_write_encrypted(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll>; } impl CryptoStream { @@ -182,32 +254,51 @@ impl CryptoStream { } } -impl CryptoStream +impl CryptoRead for CryptoStream where S: AsyncRead + AsyncWrite + Unpin, { /// Attempt to read decrypted data from `stream` #[inline] - pub fn poll_read_decrypted( - &mut self, + fn poll_read_decrypted( + mut self: Pin<&mut Self>, cx: &mut task::Context<'_>, context: &Context, buf: &mut ReadBuf<'_>, ) -> Poll> { - self.dec.poll_read_decrypted(cx, context, &mut self.stream, buf) + let CryptoStream { + ref mut dec, + ref mut stream, + .. + } = *self; + dec.poll_read_decrypted(cx, context, stream, buf) } } -impl CryptoStream +impl CryptoWrite for CryptoStream where S: AsyncRead + AsyncWrite + Unpin, { /// Attempt to write encrypted data to `stream` #[inline] - pub fn poll_write_encrypted(&mut self, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll> { - self.enc.poll_write_encrypted(cx, &mut self.stream, buf) + fn poll_write_encrypted( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let CryptoStream { + ref mut enc, + ref mut stream, + .. + } = *self; + enc.poll_write_encrypted(cx, stream, buf) } +} +impl CryptoStream +where + S: AsyncRead + AsyncWrite + Unpin, +{ /// Polls `flush` on the underlying stream #[inline] pub fn poll_flush(&mut self, cx: &mut task::Context<'_>) -> Poll> { @@ -257,18 +348,33 @@ impl CryptoStreamReadHalf { } impl CryptoStreamReadHalf +where + S: AsyncRead + Unpin, +{ + /// Get received IV (Stream) or Salt (AEAD, AEAD2022) + pub fn nonce(&self) -> Option<&[u8]> { + self.dec.nonce() + } +} + +impl CryptoRead for CryptoStreamReadHalf where S: AsyncRead + Unpin, { /// Attempt to read decrypted data from `stream` #[inline] - pub fn poll_read_decrypted( - &mut self, + fn poll_read_decrypted( + mut self: Pin<&mut Self>, cx: &mut task::Context<'_>, context: &Context, buf: &mut ReadBuf<'_>, ) -> Poll> { - self.dec.poll_read_decrypted(cx, context, &mut self.reader, buf) + let CryptoStreamReadHalf { + ref mut dec, + ref mut reader, + .. + } = *self; + dec.poll_read_decrypted(cx, context, reader, buf) } } @@ -283,18 +389,37 @@ impl CryptoStreamWriteHalf { pub fn method(&self) -> CipherKind { self.method } + + /// Get sent IV (Stream) or Salt (AEAD, AEAD2022) + pub fn sent_nonce(&self) -> &[u8] { + self.enc.nonce() + } } -impl CryptoStreamWriteHalf +impl CryptoWrite for CryptoStreamWriteHalf where S: AsyncWrite + Unpin, { /// Attempt to write encrypted data to `stream` #[inline] - pub fn poll_write_encrypted(&mut self, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll> { - self.enc.poll_write_encrypted(cx, &mut self.writer, buf) + fn poll_write_encrypted( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let CryptoStreamWriteHalf { + ref mut enc, + ref mut writer, + .. + } = *self; + enc.poll_write_encrypted(cx, writer, buf) } +} +impl CryptoStreamWriteHalf +where + S: AsyncWrite + Unpin, +{ /// Polls `flush` on the underlying stream #[inline] pub fn poll_flush(&mut self, cx: &mut task::Context<'_>) -> Poll> { diff --git a/crates/shadowsocks/src/relay/tcprelay/mod.rs b/crates/shadowsocks/src/relay/tcprelay/mod.rs index 465b1aa7e5fb..8aeda85b1399 100644 --- a/crates/shadowsocks/src/relay/tcprelay/mod.rs +++ b/crates/shadowsocks/src/relay/tcprelay/mod.rs @@ -6,9 +6,20 @@ pub use self::{ }; mod aead; +#[cfg(feature = "aead-cipher-2022")] +mod aead_2022; pub mod crypto_io; pub mod proxy_listener; pub mod proxy_stream; #[cfg(feature = "stream-cipher")] mod stream; pub mod utils; + +/// Connection direction type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StreamType { + /// Connection initiated from client to server + Client, + /// Connection initiated from server to client + Server, +} diff --git a/crates/shadowsocks/src/relay/tcprelay/proxy_listener.rs b/crates/shadowsocks/src/relay/tcprelay/proxy_listener.rs index c3f1e23bca76..4bbd2bd99c50 100644 --- a/crates/shadowsocks/src/relay/tcprelay/proxy_listener.rs +++ b/crates/shadowsocks/src/relay/tcprelay/proxy_listener.rs @@ -11,7 +11,7 @@ use tokio::{ use crate::{ config::{ServerAddr, ServerConfig}, context::SharedContext, - crypto::v1::CipherKind, + crypto::CipherKind, net::{AcceptOpts, TcpListener}, relay::tcprelay::proxy_stream::server::ProxyServerStream, }; diff --git a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/client.rs b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/client.rs index 955c1bdd32e8..d77a9e1dd834 100644 --- a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/client.rs +++ b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/client.rs @@ -16,13 +16,16 @@ use tokio::{ time, }; +#[cfg(feature = "aead-cipher-2022")] +use crate::context::Context; use crate::{ config::ServerConfig, context::SharedContext, + crypto::CipherKind, net::{ConnectOpts, TcpStream as OutboundTcpStream}, relay::{ socks5::Address, - tcprelay::crypto_io::{CryptoStream, CryptoStreamReadHalf, CryptoStreamWriteHalf}, + tcprelay::crypto_io::{CryptoRead, CryptoStream, CryptoWrite}, }, }; @@ -32,12 +35,19 @@ enum ProxyClientStreamWriteState { Connected, } +enum ProxyClientStreamReadState { + #[cfg(feature = "aead-cipher-2022")] + WaitHeader(BytesMut, usize), + Established, +} + /// A stream for sending / receiving data stream from remote server via shadowsocks' proxy server #[pin_project] pub struct ProxyClientStream { #[pin] stream: CryptoStream, - state: ProxyClientStreamWriteState, + writer_state: ProxyClientStreamWriteState, + reader_state: ProxyClientStreamReadState, context: SharedContext, } @@ -141,9 +151,21 @@ where let addr = addr.into(); let stream = CryptoStream::from_stream(&context, stream, svr_cfg.method(), svr_cfg.key()); + #[cfg(not(feature = "aead-cipher-2022"))] + let reader_state = ProxyClientStreamReadState::Established; + + #[cfg(feature = "aead-cipher-2022")] + let reader_state = if svr_cfg.method().is_aead_2022() { + // AEAD 2022 has a respond header + ProxyClientStreamReadState::WaitHeader(BytesMut::new(), 0) + } else { + ProxyClientStreamReadState::Established + }; + ProxyClientStream { stream, - state: ProxyClientStreamWriteState::Connect(addr), + writer_state: ProxyClientStreamWriteState::Connect(addr), + reader_state, context, } } @@ -164,164 +186,213 @@ where } } -impl AsyncRead for ProxyClientStream +#[cfg(feature = "aead-cipher-2022")] +fn poll_read_aead_2022_header( + context: &Context, + mut stream: Pin<&mut CryptoStream>, + cx: &mut task::Context<'_>, + header_buf: &mut BytesMut, + header_pos: &mut usize, +) -> Poll> where S: AsyncRead + AsyncWrite + Unpin, { - #[inline] - fn poll_read(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { - let mut this = self.project(); - this.stream.poll_read_decrypted(cx, this.context, buf) + use bytes::Buf; + use std::time::SystemTime; + + // AEAD 2022 TCP Response Header + // + // +-------+-------+-------+-------+-------+-------+-------+-------+-------+ + // | TYPE | UNIX TIMESTAMP | + // +-------+-------+-------+-------+-------+-------+-------+-------+-------+ + // | Request SALT (Variable ...) + // +-------+-------+-------+-------+-------+-------+-------+-------+-------+ + + const SERVER_STREAM_TYPE: u8 = 1; + const SERVER_STREAM_TIMESTAMP_MAX_DIFF: u64 = 30; + + // Initialize buffer + let method = stream.method(); + if header_buf.is_empty() { + header_buf.resize(1 + 8 + method.salt_len(), 0); + *header_pos = 0; + } + + while *header_pos < header_buf.len() { + let remaining_buf = &mut header_buf[*header_pos..]; + let mut read_buf = ReadBuf::new(remaining_buf); + + ready!(stream.as_mut().poll_read_decrypted(cx, context, &mut read_buf))?; + + *header_pos += read_buf.filled().len(); } + + // Done reading TCP header, check all the fields + + let stream_type = header_buf.get_u8(); + if stream_type != SERVER_STREAM_TYPE { + return Err(io::Error::new( + ErrorKind::Other, + format!("received TCP response header with wrong type {}", stream_type), + )) + .into(); + } + + let timestamp = header_buf.get_u64(); + let now = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(n) => n.as_secs(), + Err(_) => panic!("SystemTime::now() is before UNIX Epoch!"), + }; + + if now.abs_diff(timestamp) > SERVER_STREAM_TIMESTAMP_MAX_DIFF { + return Err(io::Error::new( + ErrorKind::Other, + format!("received TCP response header with aged timestamp: {}", timestamp), + )) + .into(); + } + + let salt = &header_buf[..]; + if salt != stream.sent_nonce() { + return Err(io::Error::new( + ErrorKind::Other, + "received TCP response header with unmatched salt", + )) + .into(); + } + + Ok(()).into() } -impl AsyncWrite for ProxyClientStream +impl AsyncRead for ProxyClientStream where S: AsyncRead + AsyncWrite + Unpin, { - fn poll_write(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll> { + #[inline] + fn poll_read(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + #[allow(unused_mut)] let mut this = self.project(); + #[allow(clippy::never_loop)] loop { - match this.state { - ProxyClientStreamWriteState::Connect(ref addr) => { - // Target Address should be sent with the first packet together, - // which would prevent from being detected by connection features. + match this.reader_state { + ProxyClientStreamReadState::Established => { + return this.stream.poll_read_decrypted(cx, this.context, buf); + } + #[cfg(feature = "aead-cipher-2022")] + ProxyClientStreamReadState::WaitHeader(ref mut buf, ref mut buf_pos) => { + ready!(poll_read_aead_2022_header( + this.context, + this.stream.as_mut(), + cx, + buf, + buf_pos, + ))?; + *(this.reader_state) = ProxyClientStreamReadState::Established; + } + } + } + } +} - let addr_length = addr.serialized_len(); +#[inline] +fn make_first_packet_buffer(method: CipherKind, addr: &Address, buf: &[u8]) -> BytesMut { + // Target Address should be sent with the first packet together, + // which would prevent from being detected. + + let addr_length = addr.serialized_len(); + let mut buffer = BytesMut::new(); + + #[cfg(feature = "aead-cipher-2022")] + if method.is_aead_2022() { + // TCP Request Header + // + // +-------+-------+-------+-------+-------+-------+-------+-------+-------+ + // | TYPE | UNIX TIMESTAMP | + // +-------+-------+-------+-------+-------+-------+-------+-------+-------+ + // | ADDR (Variable ...) + // +-------+-------+-------+-------+-------+-------+-------+-------+-------+ + // | PADDING SIZE | PADDING (Variable ...) + // +-------+-------+-------+-------+-------+-------+-------+-------+-------+ + // + // Client -> Server TYPE=0 + + use rand::{rngs::SmallRng, Rng, SeedableRng}; + use std::{cell::RefCell, time::SystemTime}; + + const CLIENT_STREAM_TYPE: u8 = 0; + const MAX_PADDING_SIZE: usize = 900; + + thread_local! { + static PADDING_RNG: RefCell = RefCell::new(SmallRng::from_entropy()); + } - let mut buffer = BytesMut::with_capacity(addr_length + buf.len()); - addr.write_to_buf(&mut buffer); - buffer.put_slice(buf); + let padding_size = if buf.is_empty() { + PADDING_RNG.with(|rng| rng.borrow_mut().gen::() % MAX_PADDING_SIZE) + } else { + // If handshake with data buffer, then padding is not required and should be 0 for letting TFO work properly. + 0 + }; - // Save the concatenated buffer before it is written successfully. - // APIs require buffer to be kept alive before Poll::Ready - // - // Proactor APIs like IOCP on Windows, pointers of buffers have to be kept alive - // before IO completion. - *(this.state) = ProxyClientStreamWriteState::Connecting(buffer); - } - ProxyClientStreamWriteState::Connecting(ref buffer) => { - let n = ready!(this.stream.poll_write_encrypted(cx, buffer))?; + buffer.reserve(1 + 8 + addr_length + 2 + padding_size); + buffer.put_u8(CLIENT_STREAM_TYPE); - // In general, poll_write_encrypted should perform like write_all. - debug_assert!(n == buffer.len()); + let timestamp = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(n) => n.as_secs(), + Err(_) => panic!("SystemTime::now() is before UNIX Epoch!"), + }; + buffer.put_u64(timestamp); - *(this.state) = ProxyClientStreamWriteState::Connected; + addr.write_to_buf(&mut buffer); - // NOTE: - // poll_write will return Ok(0) if buf.len() == 0 - // But for the first call, this function will eventually send the handshake packet (IV/Salt + ADDR) to the remote address. - // - // https://github.com/shadowsocks/shadowsocks-rust/issues/232 - // - // For protocols that requires *Server Hello* message, like FTP, clients won't send anything to the server until server sends handshake messages. - // This could be achieved by calling poll_write with an empty input buffer. - return Ok(buf.len()).into(); - } - ProxyClientStreamWriteState::Connected => { - return this.stream.poll_write_encrypted(cx, buf); - } + buffer.put_u16(padding_size as u16); + + if padding_size > 0 { + unsafe { + buffer.advance_mut(padding_size); } } } - #[inline] - fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().stream.poll_flush(cx) - } + let _ = method; - #[inline] - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().stream.poll_shutdown(cx) + // STREAM / AEAD protocol, append the Address before payload + if buffer.is_empty() { + buffer.reserve(addr_length + buf.len()); + addr.write_to_buf(&mut buffer); } -} -impl ProxyClientStream -where - S: AsyncRead + AsyncWrite + Unpin, -{ - /// Splits into reader and writer halves - pub fn into_split(self) -> (ProxyClientStreamReadHalf, ProxyClientStreamWriteHalf) { - // Cannot split if stream is still pending - assert!( - !matches!(self.state, ProxyClientStreamWriteState::Connecting(..)), - "stream is pending on writing the first packet" - ); - let (reader, writer) = self.stream.into_split(); - ( - ProxyClientStreamReadHalf { - reader, - context: self.context, - }, - ProxyClientStreamWriteHalf { - writer, - state: self.state, - }, - ) - } -} + buffer.put_slice(buf); -/// Owned read half produced by `ProxyClientStream::into_split` -#[pin_project] -pub struct ProxyClientStreamReadHalf { - #[pin] - reader: CryptoStreamReadHalf, - context: SharedContext, -} - -impl AsyncRead for ProxyClientStreamReadHalf -where - S: AsyncRead + Unpin, -{ - #[inline] - fn poll_read(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { - let mut this = self.project(); - this.reader.poll_read_decrypted(cx, this.context, buf) - } -} - -/// Owned write half produced by `ProxyClientStream::into_split` -#[pin_project] -pub struct ProxyClientStreamWriteHalf { - #[pin] - writer: CryptoStreamWriteHalf, - state: ProxyClientStreamWriteState, + buffer } -impl AsyncWrite for ProxyClientStreamWriteHalf +impl AsyncWrite for ProxyClientStream where - S: AsyncWrite + Unpin, + S: AsyncRead + AsyncWrite + Unpin, { fn poll_write(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll> { - let mut this = self.project(); + let this = self.project(); loop { - match this.state { + match this.writer_state { ProxyClientStreamWriteState::Connect(ref addr) => { - // Target Address should be sent with the first packet together, - // which would prevent from being detected by connection features. - - let addr_length = addr.serialized_len(); - - let mut buffer = BytesMut::with_capacity(addr_length + buf.len()); - addr.write_to_buf(&mut buffer); - buffer.put_slice(buf); + let buffer = make_first_packet_buffer(this.stream.method(), addr, buf); // Save the concatenated buffer before it is written successfully. // APIs require buffer to be kept alive before Poll::Ready // // Proactor APIs like IOCP on Windows, pointers of buffers have to be kept alive // before IO completion. - *(this.state) = ProxyClientStreamWriteState::Connecting(buffer); + *(this.writer_state) = ProxyClientStreamWriteState::Connecting(buffer); } ProxyClientStreamWriteState::Connecting(ref buffer) => { - let n = ready!(this.writer.poll_write_encrypted(cx, buffer))?; + let n = ready!(this.stream.poll_write_encrypted(cx, buffer))?; // In general, poll_write_encrypted should perform like write_all. debug_assert!(n == buffer.len()); - *(this.state) = ProxyClientStreamWriteState::Connected; + *(this.writer_state) = ProxyClientStreamWriteState::Connected; // NOTE: // poll_write will return Ok(0) if buf.len() == 0 @@ -334,7 +405,7 @@ where return Ok(buf.len()).into(); } ProxyClientStreamWriteState::Connected => { - return this.writer.poll_write_encrypted(cx, buf); + return this.stream.poll_write_encrypted(cx, buf); } } } @@ -342,11 +413,11 @@ where #[inline] fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().writer.poll_flush(cx) + self.project().stream.poll_flush(cx) } #[inline] fn poll_shutdown(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().writer.poll_shutdown(cx) + self.project().stream.poll_shutdown(cx) } } diff --git a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/mod.rs b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/mod.rs index b9715785d5ab..de8d20a3fd79 100644 --- a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/mod.rs +++ b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/mod.rs @@ -1,9 +1,12 @@ //! Stream interface for communicating with shadowsocks proxy servers -pub use self::{ - client::{ProxyClientStream, ProxyClientStreamReadHalf, ProxyClientStreamWriteHalf}, - server::{ProxyServerStream, ProxyServerStreamReadHalf, ProxyServerStreamWriteHalf}, -}; +// pub use self::{ +// client::{ProxyClientStream, ProxyClientStreamReadHalf, ProxyClientStreamWriteHalf}, +// server::{ProxyServerStream, ProxyServerStreamReadHalf, ProxyServerStreamWriteHalf}, +// }; + +pub use self::{client::ProxyClientStream, server::ProxyServerStream}; pub mod client; +pub mod protocol; pub mod server; diff --git a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/mod.rs b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/mod.rs new file mode 100644 index 000000000000..dffa4b327fef --- /dev/null +++ b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/mod.rs @@ -0,0 +1,173 @@ +//! Shadowsocks TCP protocol + +use std::io; + +use bytes::BufMut; +use tokio::io::AsyncRead; + +use crate::{ + crypto::{CipherCategory, CipherKind}, + relay::socks5::Address, +}; + +pub use self::v1::{ + StreamTcpRequestHeader, + StreamTcpRequestHeaderRef, + StreamTcpResponseHeader, + StreamTcpResponseHeaderRef, +}; +#[cfg(feature = "aead-cipher-2022")] +pub use self::v2::{ + Aead2022TcpRequestHeader, + Aead2022TcpRequestHeaderRef, + Aead2022TcpResponseHeader, + Aead2022TcpResponseHeaderRef, +}; + +pub mod v1; +#[cfg(feature = "aead-cipher-2022")] +pub mod v2; + +pub enum TcpRequestHeader { + Stream(StreamTcpRequestHeader), + #[cfg(feature = "aead-cipher-2022")] + Aead2022(Aead2022TcpRequestHeader), +} + +impl TcpRequestHeader { + pub async fn read_from(method: CipherKind, reader: &mut R) -> io::Result { + match method.category() { + CipherCategory::None | CipherCategory::Aead => Ok(TcpRequestHeader::Stream( + StreamTcpRequestHeader::read_from(reader).await?, + )), + #[cfg(feature = "stream-cipher")] + CipherCategory::Stream => Ok(TcpRequestHeader::Stream( + StreamTcpRequestHeader::read_from(reader).await?, + )), + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => Ok(TcpRequestHeader::Aead2022( + Aead2022TcpRequestHeader::read_from(reader).await?, + )), + } + } + + pub fn write_to_buf(&self, buf: &mut B) { + match *self { + TcpRequestHeader::Stream(ref h) => h.write_to_buf(buf), + #[cfg(feature = "aead-cipher-2022")] + TcpRequestHeader::Aead2022(ref h) => h.write_to_buf(buf), + } + } + + pub fn addr(self) -> Address { + match self { + TcpRequestHeader::Stream(h) => h.addr, + #[cfg(feature = "aead-cipher-2022")] + TcpRequestHeader::Aead2022(h) => h.addr, + } + } + + pub fn addr_ref(&self) -> &Address { + match *self { + TcpRequestHeader::Stream(ref h) => &h.addr, + #[cfg(feature = "aead-cipher-2022")] + TcpRequestHeader::Aead2022(ref h) => &h.addr, + } + } + + pub fn serialized_len(&self) -> usize { + match *self { + TcpRequestHeader::Stream(ref h) => h.serialized_len(), + #[cfg(feature = "aead-cipher-2022")] + TcpRequestHeader::Aead2022(ref h) => h.serialized_len(), + } + } +} + +pub enum TcpRequestHeaderRef<'a> { + Stream(StreamTcpRequestHeaderRef<'a>), + #[cfg(feature = "aead-cipher-2022")] + Aead2022(Aead2022TcpRequestHeaderRef<'a>), +} + +impl<'a> TcpRequestHeaderRef<'a> { + pub fn write_to_buf(&self, buf: &mut B) { + match *self { + TcpRequestHeaderRef::Stream(ref h) => h.write_to_buf(buf), + #[cfg(feature = "aead-cipher-2022")] + TcpRequestHeaderRef::Aead2022(ref h) => h.write_to_buf(buf), + } + } + + pub fn serialized_len(&self) -> usize { + match *self { + TcpRequestHeaderRef::Stream(ref h) => h.serialized_len(), + #[cfg(feature = "aead-cipher-2022")] + TcpRequestHeaderRef::Aead2022(ref h) => h.serialized_len(), + } + } +} + +pub enum TcpResponseHeader { + Stream(StreamTcpResponseHeader), + #[cfg(feature = "aead-cipher-2022")] + Aead2022(Aead2022TcpResponseHeader), +} + +impl TcpResponseHeader { + pub async fn read_from(method: CipherKind, reader: &mut R) -> io::Result { + match method.category() { + CipherCategory::None | CipherCategory::Aead => Ok(TcpResponseHeader::Stream( + StreamTcpResponseHeader::read_from(reader).await?, + )), + #[cfg(feature = "stream-cipher")] + CipherCategory::Stream => Ok(TcpResponseHeader::Stream( + StreamTcpResponseHeader::read_from(reader).await?, + )), + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => Ok(TcpResponseHeader::Aead2022( + Aead2022TcpResponseHeader::read_from(method, reader).await?, + )), + } + } + + pub fn write_to_buf(&self, buf: &mut B) { + match *self { + TcpResponseHeader::Stream(ref h) => h.write_to_buf(buf), + #[cfg(feature = "aead-cipher-2022")] + TcpResponseHeader::Aead2022(ref h) => h.write_to_buf(buf), + } + } + + pub fn serialized_len(&self) -> usize { + match *self { + TcpResponseHeader::Stream(ref h) => h.serialized_len(), + #[cfg(feature = "aead-cipher-2022")] + TcpResponseHeader::Aead2022(ref h) => h.serialized_len(), + } + } +} + +pub enum TcpResponseHeaderRef<'a> { + Stream(StreamTcpResponseHeaderRef<'a>), + #[cfg(feature = "aead-cipher-2022")] + Aead2022(Aead2022TcpResponseHeaderRef<'a>), +} + +impl<'a> TcpResponseHeaderRef<'a> { + pub fn write_to_buf(&self, buf: &mut B) { + match *self { + TcpResponseHeaderRef::Stream(ref h) => h.write_to_buf(buf), + #[cfg(feature = "aead-cipher-2022")] + TcpResponseHeaderRef::Aead2022(ref h) => h.write_to_buf(buf), + } + } + + pub fn serialized_len(&self) -> usize { + match *self { + TcpResponseHeaderRef::Stream(ref h) => h.serialized_len(), + #[cfg(feature = "aead-cipher-2022")] + TcpResponseHeaderRef::Aead2022(ref h) => h.serialized_len(), + } + } +} diff --git a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/v1.rs b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/v1.rs new file mode 100644 index 000000000000..11fa020ac03d --- /dev/null +++ b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/v1.rs @@ -0,0 +1,70 @@ +//! Shadowsocks Stream / AEAD header protocol + +use std::{io, marker::PhantomData}; + +use bytes::BufMut; +use tokio::io::AsyncRead; + +use crate::relay::socks5::Address; + +pub struct StreamTcpRequestHeader { + pub addr: Address, +} + +impl StreamTcpRequestHeader { + pub async fn read_from(reader: &mut R) -> io::Result { + Ok(StreamTcpRequestHeader { + addr: Address::read_from(reader).await?, + }) + } + + pub fn write_to_buf(&self, buf: &mut B) { + StreamTcpRequestHeaderRef { addr: &self.addr }.write_to_buf(buf) + } + + pub fn serialized_len(&self) -> usize { + StreamTcpRequestHeaderRef { addr: &self.addr }.serialized_len() + } +} + +pub struct StreamTcpRequestHeaderRef<'a> { + pub addr: &'a Address, +} + +impl<'a> StreamTcpRequestHeaderRef<'a> { + pub fn write_to_buf(&self, buf: &mut B) { + self.addr.write_to_buf(buf); + } + + pub fn serialized_len(&self) -> usize { + self.addr.serialized_len() + } +} + +pub struct StreamTcpResponseHeader; + +impl StreamTcpResponseHeader { + pub async fn read_from(_reader: &mut R) -> io::Result { + Ok(StreamTcpResponseHeader) + } + + pub fn write_to_buf(&self, buf: &mut B) { + StreamTcpResponseHeaderRef { data: PhantomData }.write_to_buf(buf) + } + + pub fn serialized_len(&self) -> usize { + 0 + } +} + +pub struct StreamTcpResponseHeaderRef<'a> { + data: PhantomData<&'a ()>, +} + +impl<'a> StreamTcpResponseHeaderRef<'a> { + pub fn write_to_buf(&self, _buf: &mut B) {} + + pub fn serialized_len(&self) -> usize { + 0 + } +} diff --git a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/v2.rs b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/v2.rs new file mode 100644 index 000000000000..648d2c7054ee --- /dev/null +++ b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/protocol/v2.rs @@ -0,0 +1,235 @@ +//! Shadowsocks AEAD 2022 header protocol + +use std::{ + io::{self, ErrorKind}, + time::SystemTime, +}; + +use bytes::BufMut; +use tokio::io::{AsyncRead, AsyncReadExt}; + +use crate::{crypto::CipherKind, relay::Address}; + +pub const MAX_PADDING_SIZE: usize = 900; +pub const SERVER_STREAM_TIMESTAMP_MAX_DIFF: u64 = 30; + +#[inline] +pub fn get_now_timestamp() -> u64 { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(n) => n.as_secs(), + Err(_) => panic!("SystemTime::now() is before UNIX Epoch!"), + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Aead2022TcpStreamType { + Client = 0, + Server = 1, +} + +/// TCP Request Header +// +/// +-------+-------+-------+-------+-------+-------+-------+-------+-------+ +/// | TYPE | UNIX TIMESTAMP | +/// +-------+-------+-------+-------+-------+-------+-------+-------+-------+ +/// | ADDR (Variable ...) +/// +-------+-------+-------+-------+-------+-------+-------+-------+-------+ +/// | PADDING SIZE | PADDING (Variable ...) +/// +-------+-------+-------+-------+-------+-------+-------+-------+-------+ +#[derive(Debug, Clone)] +pub struct Aead2022TcpRequestHeader { + pub addr: Address, + pub timestamp: u64, + pub padding_size: u16, +} + +impl Aead2022TcpRequestHeader { + pub async fn read_from(reader: &mut R) -> io::Result { + use std::slice; + + let mut fix_header1 = [0u8; 1 + 8]; + reader.read_exact(&mut fix_header1).await?; + + let stream_type = fix_header1[0]; + if stream_type != Aead2022TcpStreamType::Client as u8 { + return Err(io::Error::new( + ErrorKind::Other, + format!("TCP request type {} invalid", stream_type), + )); + } + + let timestamp_slice = &fix_header1[1..]; + let timestamp_buffer: &[u64] = unsafe { slice::from_raw_parts(timestamp_slice.as_ptr() as *const _, 1) }; + let timestamp = u64::from_be(timestamp_buffer[0]); + + let now = get_now_timestamp(); + + if now.abs_diff(timestamp) > SERVER_STREAM_TIMESTAMP_MAX_DIFF { + return Err(io::Error::new( + ErrorKind::Other, + format!("received TCP request header with aged timestamp: {}", timestamp), + )); + } + + let addr = Address::read_from(reader).await?; + + let mut padding_size_buffer = [0u8; 2]; + reader.read_exact(&mut padding_size_buffer).await?; + + let padding_size = u16::from_be_bytes(padding_size_buffer); + if padding_size > 0 { + let mut take_reader = reader.take(padding_size as u64); + let mut buffer = [0u8; 64]; + loop { + match take_reader.read(&mut buffer).await { + Ok(0) => break, + Ok(..) => continue, + Err(err) => return Err(err), + } + } + } + + Ok(Aead2022TcpRequestHeader { + addr, + timestamp, + padding_size, + }) + } + + pub fn write_to_buf(&self, buf: &mut B) { + Aead2022TcpRequestHeaderRef { + addr: &self.addr, + timestamp: self.timestamp, + padding_size: self.padding_size, + } + .write_to_buf(buf) + } + + pub fn serialized_len(&self) -> usize { + Aead2022TcpRequestHeaderRef { + addr: &self.addr, + timestamp: self.timestamp, + padding_size: self.padding_size, + } + .serialized_len() + } +} + +pub struct Aead2022TcpRequestHeaderRef<'a> { + pub addr: &'a Address, + pub timestamp: u64, + pub padding_size: u16, +} + +impl<'a> Aead2022TcpRequestHeaderRef<'a> { + pub fn write_to_buf(&self, buf: &mut B) { + buf.put_u8(Aead2022TcpStreamType::Client as u8); + buf.put_u64(self.timestamp); + self.addr.write_to_buf(buf); + + assert!( + self.padding_size as usize <= MAX_PADDING_SIZE, + "padding length must be in [0, {}]", + MAX_PADDING_SIZE + ); + + buf.put_u16(self.padding_size); + if self.padding_size > 0 { + unsafe { + buf.advance_mut(self.padding_size as usize); + } + } + } + + pub fn serialized_len(&self) -> usize { + 1 + 8 + self.addr.serialized_len() + 2 + self.padding_size as usize + } +} + +/// AEAD 2022 TCP Response Header +/// +/// +-------+-------+-------+-------+-------+-------+-------+-------+-------+ +/// | TYPE | UNIX TIMESTAMP | +/// +-------+-------+-------+-------+-------+-------+-------+-------+-------+ +/// | Request SALT (Variable ...) +/// +-------+-------+-------+-------+-------+-------+-------+-------+-------+ +pub struct Aead2022TcpResponseHeader { + pub timestamp: u64, + pub request_salt: Vec, +} + +impl Aead2022TcpResponseHeader { + pub async fn read_from( + method: CipherKind, + reader: &mut R, + ) -> io::Result { + use std::slice; + + let mut fix_header1 = [0u8; 1 + 8]; + reader.read_exact(&mut fix_header1).await?; + + let stream_type = fix_header1[0]; + if stream_type != Aead2022TcpStreamType::Server as u8 { + return Err(io::Error::new( + ErrorKind::Other, + format!("TCP request type {} invalid", stream_type), + )); + } + + let timestamp_slice = &fix_header1[1..]; + let timestamp_buffer: &[u64] = unsafe { slice::from_raw_parts(timestamp_slice.as_ptr() as *const _, 1) }; + let timestamp = u64::from_be(timestamp_buffer[0]); + + let now = get_now_timestamp(); + + if now.abs_diff(timestamp) > SERVER_STREAM_TIMESTAMP_MAX_DIFF { + return Err(io::Error::new( + ErrorKind::Other, + format!("received TCP response header with aged timestamp: {}", timestamp), + )); + } + + let salt_size = method.salt_len(); + let mut request_salt = vec![0u8; salt_size]; + reader.read_exact(&mut request_salt).await?; + + Ok(Aead2022TcpResponseHeader { + timestamp, + request_salt, + }) + } + + pub fn write_to_buf(&self, buf: &mut B) { + Aead2022TcpResponseHeaderRef { + timestamp: self.timestamp, + request_salt: &self.request_salt, + } + .write_to_buf(buf) + } + + pub fn serialized_len(&self) -> usize { + Aead2022TcpResponseHeaderRef { + timestamp: self.timestamp, + request_salt: &self.request_salt, + } + .serialized_len() + } +} + +pub struct Aead2022TcpResponseHeaderRef<'a> { + pub timestamp: u64, + pub request_salt: &'a [u8], +} + +impl<'a> Aead2022TcpResponseHeaderRef<'a> { + pub fn write_to_buf(&self, buf: &mut B) { + buf.put_u8(Aead2022TcpStreamType::Server as u8); + buf.put_u64(self.timestamp); + buf.put_slice(self.request_salt); + } + + pub fn serialized_len(&self) -> usize { + 1 + 8 + self.request_salt.len() + } +} diff --git a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/server.rs b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/server.rs index e9788d675d5d..7a4bd7786a2d 100644 --- a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/server.rs +++ b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/server.rs @@ -6,21 +6,39 @@ use std::{ task::{self, Poll}, }; +#[cfg(feature = "aead-cipher-2022")] +use bytes::BytesMut; +use futures::ready; use pin_project::pin_project; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use crate::{ context::SharedContext, - crypto::v1::CipherKind, - relay::tcprelay::crypto_io::{CryptoStream, CryptoStreamReadHalf, CryptoStreamWriteHalf}, + crypto::CipherKind, + relay::{ + socks5::Address, + tcprelay::{ + crypto_io::{CryptoRead, CryptoStream, CryptoWrite}, + proxy_stream::protocol::TcpRequestHeader, + }, + }, }; +enum ProxyServerStreamWriteState { + #[cfg(feature = "aead-cipher-2022")] + PrepareHeader(Option), + #[cfg(feature = "aead-cipher-2022")] + WriteHeader(BytesMut, usize), + Established, +} + /// A stream for communicating with shadowsocks' proxy client #[pin_project] pub struct ProxyServerStream { #[pin] stream: CryptoStream, context: SharedContext, + writer_state: ProxyServerStreamWriteState, } impl ProxyServerStream { @@ -30,9 +48,20 @@ impl ProxyServerStream { method: CipherKind, key: &[u8], ) -> ProxyServerStream { + #[cfg(feature = "aead-cipher-2022")] + let writer_state = if method.is_aead_2022() { + ProxyServerStreamWriteState::PrepareHeader(None) + } else { + ProxyServerStreamWriteState::Established + }; + + #[cfg(not(feature = "aead-cipher-2022"))] + let writer_state = ProxyServerStreamWriteState::Established; + ProxyServerStream { stream: CryptoStream::from_stream(&context, stream, method, key), context, + writer_state, } } @@ -56,17 +85,13 @@ impl ProxyServerStream where S: AsyncRead + AsyncWrite + Unpin, { - /// Splits into reader and writer halves - pub fn into_split(self) -> (ProxyServerStreamReadHalf, ProxyServerStreamWriteHalf) { - let (reader, writer) = self.stream.into_split(); - - ( - ProxyServerStreamReadHalf { - reader, - context: self.context, - }, - ProxyServerStreamWriteHalf { writer }, - ) + /// Handshaking. Getting the destination address from client + /// + /// This method should be called only once after accepted. + pub async fn handshake(&mut self) -> io::Result
{ + let header = TcpRequestHeader::read_from(self.stream.method(), self).await?; + // TODO: Check header is not in a standalone AEAD package + Ok(header.addr()) } } @@ -76,8 +101,18 @@ where { #[inline] fn poll_read(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { - let mut this = self.project(); - this.stream.poll_read_decrypted(cx, this.context, buf) + let this = self.project(); + ready!(this.stream.poll_read_decrypted(cx, this.context, buf))?; + + // Wakeup writer task because we have already received the salt + #[cfg(feature = "aead-cipher-2022")] + if let ProxyServerStreamWriteState::PrepareHeader(ref mut waker) = this.writer_state { + if let Some(waker) = waker.take() { + waker.wake(); + } + } + + Ok(()).into() } } @@ -87,62 +122,66 @@ where { #[inline] fn poll_write(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll> { - self.project().stream.poll_write_encrypted(cx, buf) - } - - #[inline] - fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().stream.poll_flush(cx) - } - - #[inline] - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().stream.poll_shutdown(cx) - } -} - -/// Owned read half produced by `ProxyServerStream::into_split` -#[pin_project] -pub struct ProxyServerStreamReadHalf { - #[pin] - reader: CryptoStreamReadHalf, - context: SharedContext, -} - -impl AsyncRead for ProxyServerStreamReadHalf -where - S: AsyncRead + Unpin, -{ - #[inline] - fn poll_read(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + #[allow(unused_mut)] let mut this = self.project(); - this.reader.poll_read_decrypted(cx, this.context, buf) - } -} -/// Owned write half produced by `ProxyServerStream::into_split` -#[pin_project] -pub struct ProxyServerStreamWriteHalf { - #[pin] - writer: CryptoStreamWriteHalf, -} - -impl AsyncWrite for ProxyServerStreamWriteHalf -where - S: AsyncWrite + Unpin, -{ - #[inline] - fn poll_write(self: Pin<&mut Self>, cx: &mut task::Context<'_>, buf: &[u8]) -> Poll> { - self.project().writer.poll_write_encrypted(cx, buf) + #[allow(clippy::never_loop)] + loop { + match *this.writer_state { + ProxyServerStreamWriteState::Established => { + return this.stream.poll_write_encrypted(cx, buf); + } + #[cfg(feature = "aead-cipher-2022")] + ProxyServerStreamWriteState::PrepareHeader(ref mut waker) => { + match this.stream.received_nonce() { + None => { + // Reader didn't receive the salt from client yet. + if let Some(waker) = waker.take() { + if !waker.will_wake(cx.waker()) { + waker.wake(); + } + } + *waker = Some(cx.waker().clone()); + return Poll::Pending; + } + Some(nonce) => { + use crate::relay::tcprelay::proxy_stream::protocol::v2::{ + get_now_timestamp, + Aead2022TcpResponseHeaderRef, + }; + + let header = Aead2022TcpResponseHeaderRef { + timestamp: get_now_timestamp(), + request_salt: nonce, + }; + + let mut buffer = BytesMut::with_capacity(header.serialized_len()); + header.write_to_buf(&mut buffer); + + *(this.writer_state) = ProxyServerStreamWriteState::WriteHeader(buffer, 0); + } + } + } + #[cfg(feature = "aead-cipher-2022")] + ProxyServerStreamWriteState::WriteHeader(ref buf, ref mut buf_pos) => { + let n = ready!(this.stream.as_mut().poll_write_encrypted(cx, &buf[*buf_pos..]))?; + *buf_pos += n; + + if *buf_pos >= buf.len() { + *(this.writer_state) = ProxyServerStreamWriteState::Established; + } + } + } + } } #[inline] fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().writer.poll_flush(cx) + self.project().stream.poll_flush(cx) } #[inline] fn poll_shutdown(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { - self.project().writer.poll_shutdown(cx) + self.project().stream.poll_shutdown(cx) } } diff --git a/crates/shadowsocks/src/relay/tcprelay/stream.rs b/crates/shadowsocks/src/relay/tcprelay/stream.rs index ff48438f3a2b..e91f3d7d0a1e 100644 --- a/crates/shadowsocks/src/relay/tcprelay/stream.rs +++ b/crates/shadowsocks/src/relay/tcprelay/stream.rs @@ -15,7 +15,7 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use crate::{ context::Context, - crypto::v1::{Cipher, CipherKind}, + crypto::{v1::Cipher, CipherKind}, }; enum DecryptReadState { @@ -29,6 +29,7 @@ pub struct DecryptedReader { cipher: Option, buffer: BytesMut, method: CipherKind, + iv: Option, } impl DecryptedReader { @@ -41,6 +42,7 @@ impl DecryptedReader { cipher: None, buffer: BytesMut::with_capacity(method.iv_len()), method, + iv: None, } } else { DecryptedReader { @@ -48,10 +50,15 @@ impl DecryptedReader { cipher: Some(Cipher::new(method, key, &[])), buffer: BytesMut::new(), method, + iv: Some(Bytes::new()), } } } + pub fn iv(&self) -> Option<&[u8]> { + self.iv.as_deref() + } + /// Attempt to read decrypted data from reader pub fn poll_read_decrypted( &mut self, @@ -116,8 +123,10 @@ impl DecryptedReader { trace!("got stream iv {:?}", ByteStr::new(iv)); - let cipher = Cipher::new(self.method, key, iv); + // Stores IV + self.iv = Some(Bytes::copy_from_slice(iv)); + let cipher = Cipher::new(self.method, key, iv); self.cipher = Some(cipher); Ok(()).into() @@ -165,6 +174,7 @@ pub struct EncryptedWriter { cipher: Cipher, buffer: BytesMut, state: EncryptWriteState, + iv: Bytes, } impl EncryptedWriter { @@ -178,9 +188,15 @@ impl EncryptedWriter { cipher: Cipher::new(method, key, nonce), buffer, state: EncryptWriteState::AssemblePacket, + iv: Bytes::copy_from_slice(nonce), } } + /// IV + pub fn iv(&self) -> &[u8] { + self.iv.as_ref() + } + /// Attempt to write encrypted data into the writer pub fn poll_write_encrypted( &mut self, diff --git a/crates/shadowsocks/src/relay/tcprelay/utils.rs b/crates/shadowsocks/src/relay/tcprelay/utils.rs index f01b96b78f05..252ab223bd20 100644 --- a/crates/shadowsocks/src/relay/tcprelay/utils.rs +++ b/crates/shadowsocks/src/relay/tcprelay/utils.rs @@ -14,7 +14,7 @@ use futures::ready; use pin_project::pin_project; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use crate::crypto::v1::{CipherCategory, CipherKind}; +use crate::crypto::{CipherCategory, CipherKind}; #[derive(Debug)] struct CopyBuffer { @@ -146,6 +146,8 @@ fn encrypted_read_buffer_size(method: CipherKind) -> usize { #[cfg(feature = "stream-cipher")] CipherCategory::Stream => 1 << 14, CipherCategory::None => 1 << 14, + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => super::aead_2022::MAX_PACKET_SIZE + method.tag_len(), } } @@ -155,6 +157,8 @@ fn plain_read_buffer_size(method: CipherKind) -> usize { #[cfg(feature = "stream-cipher")] CipherCategory::Stream => 1 << 14, CipherCategory::None => 1 << 14, + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => super::aead_2022::MAX_PACKET_SIZE, } } diff --git a/crates/shadowsocks/src/relay/udprelay/aead.rs b/crates/shadowsocks/src/relay/udprelay/aead.rs new file mode 100644 index 000000000000..fd16be20b320 --- /dev/null +++ b/crates/shadowsocks/src/relay/udprelay/aead.rs @@ -0,0 +1,120 @@ +//! Shadowsocks UDP AEAD protocol +//! +//! Payload with AEAD cipher +//! +//! ```plain +//! UDP (after encryption, *ciphertext*) +//! +--------+-----------+-----------+ +//! | NONCE | *Data* | Data_TAG | +//! +--------+-----------+-----------+ +//! | Fixed | Variable | Fixed | +//! +--------+-----------+-----------+ +//! ``` + +use std::io::{self, Cursor, ErrorKind}; + +use byte_string::ByteStr; +use bytes::{BufMut, BytesMut}; +use log::trace; + +use crate::{ + context::Context, + crypto::{v1::Cipher, CipherKind}, + relay::socks5::Address, +}; + +/// Encrypt UDP AEAD protocol packet +pub fn encrypt_payload_aead( + context: &Context, + method: CipherKind, + key: &[u8], + addr: &Address, + payload: &[u8], + dst: &mut BytesMut, +) { + let salt_len = method.salt_len(); + let addr_len = addr.serialized_len(); + + // Packet = IV + ADDRESS + PAYLOAD + TAG + dst.reserve(salt_len + addr_len + payload.len() + method.tag_len()); + + // Generate IV + dst.resize(salt_len, 0); + let salt = &mut dst[..salt_len]; + + if salt_len > 0 { + context.generate_nonce(salt, false); + trace!("UDP packet generated aead salt {:?}", ByteStr::new(salt)); + } + + let mut cipher = Cipher::new(method, key, salt); + + addr.write_to_buf(dst); + dst.put_slice(payload); + + unsafe { + dst.advance_mut(method.tag_len()); + } + + let m = &mut dst[salt_len..]; + cipher.encrypt_packet(m); +} + +/// Decrypt UDP AEAD protocol packet +pub async fn decrypt_payload_aead( + _context: &Context, + method: CipherKind, + key: &[u8], + payload: &mut [u8], +) -> io::Result<(usize, Address)> { + let plen = payload.len(); + let salt_len = method.salt_len(); + if plen < salt_len { + let err = io::Error::new(ErrorKind::InvalidData, "udp packet too short for salt"); + return Err(err); + } + + let (salt, data) = payload.split_at_mut(salt_len); + // context.check_nonce_replay(salt)?; + + trace!("UDP packet got AEAD salt {:?}", ByteStr::new(salt)); + + let mut cipher = Cipher::new(method, key, salt); + let tag_len = cipher.tag_len(); + + if data.len() < tag_len { + return Err(io::Error::new(io::ErrorKind::Other, "udp packet too short for tag")); + } + + if !cipher.decrypt_packet(data) { + return Err(io::Error::new(io::ErrorKind::Other, "invalid tag-in")); + } + + // Truncate TAG + let data_len = data.len() - tag_len; + let data = &mut data[..data_len]; + + let (dn, addr) = parse_packet(data).await?; + + let data_length = data_len - dn; + let data_start_idx = salt_len + dn; + let data_end_idx = data_start_idx + data_length; + + payload.copy_within(data_start_idx..data_end_idx, 0); + + Ok((data_length, addr)) +} + +async fn parse_packet(buf: &[u8]) -> io::Result<(usize, Address)> { + let mut cur = Cursor::new(buf); + match Address::read_from(&mut cur).await { + Ok(address) => { + let pos = cur.position() as usize; + Ok((pos, address)) + } + Err(..) => { + let err = io::Error::new(ErrorKind::InvalidData, "parse udp packet Address failed"); + Err(err) + } + } +} diff --git a/crates/shadowsocks/src/relay/udprelay/aead_2022.rs b/crates/shadowsocks/src/relay/udprelay/aead_2022.rs new file mode 100644 index 000000000000..1a8335d24ada --- /dev/null +++ b/crates/shadowsocks/src/relay/udprelay/aead_2022.rs @@ -0,0 +1,514 @@ +//! Shadowsocks UDP AEAD 2022 protocol +//! +//! Payload with AEAD 2022 cipher +//! +//! Client -> Server +//! +//! ```plain +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Client Session ID | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Packet ID | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | TYPE | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | UNIX Epoch Timestamp | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | PADDING SIZE | Padding (Variable ...) +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Address (Variable ...) +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Payload (Variable ...) +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! ``` +//! +//! Server -> Client +//! +//! ```plain +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Server Session ID | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Packet ID | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | TYPE | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | UNIX Epoch Timestamp | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Client Session ID | +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | PADDING SIZE | Padding (Variable ...) +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Address (Variable ...) +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! | Payload (Variable ...) +//! +-------+-------+-------+-------+-------+-------+-------+-------+ +//! ``` + +use std::{ + cell::RefCell, + cmp::Ordering, + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + io::{self, Cursor, ErrorKind, Seek, SeekFrom}, + rc::Rc, + slice, + time::{Duration, SystemTime}, +}; + +use aes::{ + cipher::{BlockDecrypt, BlockEncrypt, KeyInit}, + Aes128, + Aes256, + Block, +}; +use byte_string::ByteStr; +use bytes::{Buf, BufMut, BytesMut}; +use log::{error, trace}; +use lru_time_cache::LruCache; +use rand::{rngs::SmallRng, Rng, SeedableRng}; + +use crate::{ + context::Context, + crypto::{ + v2::udp::{ChaCha20Poly1305Cipher, UdpCipher}, + CipherKind, + }, + relay::socks5::Address, +}; + +use super::options::UdpSocketControlData; + +const CLIENT_SOCKET_TYPE: u8 = 0; +const SERVER_SOCKET_TYPE: u8 = 1; +const MAX_PADDING_SIZE: usize = 900; +const SERVER_PACKET_TIMESTAMP_MAX_DIFF: u64 = 30; + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +struct CipherKey { + method: CipherKind, + key: usize, + session_id: u64, +} + +impl PartialOrd for CipherKey { + fn partial_cmp(&self, other: &CipherKey) -> Option { + let hash1 = { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + }; + let hash2 = { + let mut hasher = DefaultHasher::new(); + other.hash(&mut hasher); + hasher.finish() + }; + + hash1.partial_cmp(&hash2) + } +} + +impl Ord for CipherKey { + fn cmp(&self, other: &CipherKey) -> Ordering { + let hash1 = { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + }; + let hash2 = { + let mut hasher = DefaultHasher::new(); + other.hash(&mut hasher); + hasher.finish() + }; + + hash1.cmp(&hash2) + } +} + +thread_local! { + static PADDING_RNG: RefCell = RefCell::new(SmallRng::from_entropy()); + static CIPHER_CACHE: RefCell>> = + RefCell::new(LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60), 102400)); +} + +#[inline] +fn get_padding_size(payload: &[u8]) -> usize { + if payload.is_empty() { + PADDING_RNG.with(|rng| rng.borrow_mut().gen::() % MAX_PADDING_SIZE) + } else { + 0 + } +} + +#[inline] +pub fn get_now_timestamp() -> u64 { + match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { + Ok(n) => n.as_secs(), + Err(_) => panic!("SystemTime::now() is before UNIX Epoch!"), + } +} + +fn get_cipher(method: CipherKind, key: &[u8], session_id: u64) -> Rc { + CIPHER_CACHE.with(|cache| { + let mut cache = cache.borrow_mut(); + + let cache_key = CipherKey { + method, + // The key is stored in ServerConfig structure, so the address of it won't change. + key: key.as_ptr() as usize, + session_id, + }; + + cache + .entry(cache_key) + .or_insert_with(|| Rc::new(UdpCipher::new(method, key, session_id))) + .clone() + }) +} + +fn encrypt_message(_context: &Context, method: CipherKind, key: &[u8], packet: &mut BytesMut, session_id: u64) { + unsafe { + packet.advance_mut(method.tag_len()); + } + + match method { + CipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305 => { + // ChaCha20-Poly1305 uses PSK as key, prepended nonce in packet + let nonce_size = ChaCha20Poly1305Cipher::nonce_size(); + + let cipher = get_cipher(method, key, session_id); + + let (nonce, message) = packet.split_at_mut(nonce_size); + cipher.encrypt_packet(nonce, message); + } + CipherKind::AEAD2022_BLAKE3_AES_128_GCM | CipherKind::AEAD2022_BLAKE3_AES_256_GCM => { + // AES-*-GCM uses derived key, and part of the packet header as nonce + + let cipher = get_cipher(method, key, session_id); + + // Encrypt the rest of the packet with AEAD cipher (AES-*-GCM) + let (packet_header, message) = packet.split_at_mut(16); + let nonce = &packet_header[4..16]; + cipher.encrypt_packet(nonce, message); + + // [SessionID + PacketID] is encrypted with AES-ECB with PSK + // No padding is required because these 2 fields are 128-bits, which is exactly the same as AES's block size + match method { + CipherKind::AEAD2022_BLAKE3_AES_128_GCM => { + let cipher = Aes128::new_from_slice(key).expect("AES-128 init"); + let block = Block::from_mut_slice(&mut packet[0..16]); + cipher.encrypt_block(block); + } + CipherKind::AEAD2022_BLAKE3_AES_256_GCM => { + let cipher = Aes256::new_from_slice(key).expect("AES-256 init"); + let block = Block::from_mut_slice(&mut packet[0..16]); + cipher.encrypt_block(block); + } + _ => unreachable!("{} is not an AES-*-GCM cipher", method), + } + } + _ => unreachable!("{} is not an AEAD 2022 cipher", method), + } +} + +fn decrypt_message(context: &Context, method: CipherKind, key: &[u8], packet: &mut [u8]) -> bool { + match method { + CipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305 => { + // ChaCha20-Poly1305 uses PSK as key, prepended nonce in packet + let nonce_size = ChaCha20Poly1305Cipher::nonce_size(); + + let (nonce, message) = packet.split_at_mut(nonce_size); + if let Err(..) = context.check_nonce_replay(nonce) { + error!("detected replayed nonce: {:?}", ByteStr::new(nonce)); + return false; + } + + // NOTE: ChaCha20-Poly1305's session_id is not required because it uses PSK directly + // + // But still, we get the session_id for cache + let session_id = { + let session_id_buf = &message[0..8]; + let session_id_slice: &[u64] = unsafe { slice::from_raw_parts(session_id_buf.as_ptr() as *const _, 1) }; + u64::from_be(session_id_slice[0]) + }; + + let cipher = get_cipher(method, key, session_id); + + if !cipher.decrypt_packet(nonce, message) { + return false; + } + } + CipherKind::AEAD2022_BLAKE3_AES_128_GCM | CipherKind::AEAD2022_BLAKE3_AES_256_GCM => { + // AES-*-GCM uses derived key, and part of the packet header as nonce + // + // Decrypt the header block first + // [SessionID + PacketID] is encrypted with AES-ECB with PSK + // No padding is required because these 2 fields are 128-bits, which is exactly the same as AES's block size + + let (packet_header, message) = packet.split_at_mut(16); + + match method { + CipherKind::AEAD2022_BLAKE3_AES_128_GCM => { + let cipher = Aes128::new_from_slice(key).expect("AES-128 init"); + let block = Block::from_mut_slice(packet_header); + cipher.decrypt_block(block); + } + CipherKind::AEAD2022_BLAKE3_AES_256_GCM => { + let cipher = Aes256::new_from_slice(key).expect("AES-256 init"); + let block = Block::from_mut_slice(packet_header); + cipher.decrypt_block(block); + } + _ => unreachable!("{} is not an AES-*-GCM cipher", method), + } + + // Session ID is the first 64-bits + + let session_id = { + let session_id_buf = &packet_header[0..8]; + let session_id_slice: &[u64] = unsafe { slice::from_raw_parts(session_id_buf.as_ptr() as *const _, 1) }; + u64::from_be(session_id_slice[0]) + }; + + let nonce = &packet_header[4..16]; + + let cipher = { + if let Err(..) = context.check_nonce_replay(nonce) { + error!("detected replayed nonce: {:?}", ByteStr::new(nonce)); + return false; + } + + get_cipher(method, key, session_id) + }; + + if !cipher.decrypt_packet(nonce, message) { + return false; + } + } + _ => unreachable!("{} is not an AEAD 2022 cipher", method), + } + + true +} + +#[inline] +fn get_nonce_len(method: CipherKind) -> usize { + match method { + CipherKind::AEAD2022_BLAKE3_AES_128_GCM | CipherKind::AEAD2022_BLAKE3_AES_256_GCM => 0, + CipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305 => method.nonce_len(), + _ => unreachable!("{} is not an AEAD 2022 cipher", method), + } +} + +/// Encrypt `Client -> Server` UDP AEAD protocol packet +pub fn encrypt_client_payload_aead_2022( + context: &Context, + method: CipherKind, + key: &[u8], + addr: &Address, + control: &UdpSocketControlData, + payload: &[u8], + dst: &mut BytesMut, +) { + let padding_size = get_padding_size(payload); + let nonce_size = get_nonce_len(method); + + dst.reserve( + nonce_size + 8 + 8 + 1 + 8 + 2 + padding_size + addr.serialized_len() + payload.len() + method.tag_len(), + ); + + // Generate IV + if nonce_size > 0 { + unsafe { + dst.advance_mut(nonce_size); + } + let nonce = &mut dst[..nonce_size]; + + context.generate_nonce(nonce, false); + trace!("UDP packet generated aead nonce {:?}", ByteStr::new(nonce)); + } + + // Add header fields + dst.put_u64(control.client_session_id); + dst.put_u64(control.packet_id); + dst.put_u8(CLIENT_SOCKET_TYPE); + dst.put_u64(get_now_timestamp()); + dst.put_u16(padding_size as u16); + if padding_size > 0 { + unsafe { + dst.advance_mut(padding_size); + } + } + addr.write_to_buf(dst); + dst.put_slice(payload); + + encrypt_message(context, method, key, dst, control.client_session_id); +} + +/// Decrypt `Client -> Server` UDP AEAD protocol packet +pub async fn decrypt_client_payload_aead_2022( + context: &Context, + method: CipherKind, + key: &[u8], + payload: &mut [u8], +) -> io::Result<(usize, Address, UdpSocketControlData)> { + let nonce_len = get_nonce_len(method); + let tag_len = method.tag_len(); + if payload.len() < nonce_len + tag_len + 8 + 8 + 1 + 8 + 2 { + let err = io::Error::new(ErrorKind::InvalidData, "udp packet too short"); + return Err(err); + } + + if !decrypt_message(context, method, key, payload) { + return Err(io::Error::new(io::ErrorKind::Other, "invalid tag-in")); + } + + let data = &payload[nonce_len..payload.len() - tag_len]; + let mut cursor = Cursor::new(data); + + let client_session_id = cursor.get_u64(); + let packet_id = cursor.get_u64(); + let socket_type = cursor.get_u8(); + if socket_type != CLIENT_SOCKET_TYPE { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("invalid socket type {}", socket_type), + )); + } + let timestamp = cursor.get_u64(); + + let now = get_now_timestamp(); + if now.abs_diff(timestamp) > SERVER_PACKET_TIMESTAMP_MAX_DIFF { + return Err(io::Error::new( + ErrorKind::Other, + format!("received TCP response header with aged timestamp: {}", timestamp), + )); + } + + let padding_size = cursor.get_u16() as usize; + if padding_size > 0 { + cursor.seek(SeekFrom::Current(padding_size as i64))?; + } + + let control = UdpSocketControlData { + client_session_id, + server_session_id: 0, + packet_id, + }; + + let addr = Address::read_from(&mut cursor).await?; + + let payload_start = cursor.position() as usize; + let payload_len = data.len() - payload_start; + + payload.copy_within(nonce_len + payload_start..nonce_len + payload_start + payload_len, 0); + + Ok((payload_len, addr, control)) +} + +/// Encrypt `Server -> Client` UDP AEAD protocol packet +pub fn encrypt_server_payload_aead_2022( + context: &Context, + method: CipherKind, + key: &[u8], + addr: &Address, + control: &UdpSocketControlData, + payload: &[u8], + dst: &mut BytesMut, +) { + let padding_size = get_padding_size(payload); + let nonce_size = get_nonce_len(method); + + dst.reserve( + nonce_size + 8 + 8 + 1 + 8 + 8 + 2 + padding_size + addr.serialized_len() + payload.len() + method.tag_len(), + ); + + // Generate IV + if nonce_size > 0 { + unsafe { + dst.advance_mut(nonce_size); + } + let nonce = &mut dst[..nonce_size]; + + context.generate_nonce(nonce, false); + trace!("UDP packet generated aead nonce {:?}", ByteStr::new(nonce)); + } + + // Add header fields + dst.put_u64(control.server_session_id); + dst.put_u64(control.packet_id); + dst.put_u8(SERVER_SOCKET_TYPE); + dst.put_u64(get_now_timestamp()); + dst.put_u64(control.client_session_id); + dst.put_u16(padding_size as u16); + if padding_size > 0 { + unsafe { + dst.advance_mut(padding_size); + } + } + addr.write_to_buf(dst); + dst.put_slice(payload); + + encrypt_message(context, method, key, dst, control.server_session_id); +} + +/// Decrypt `Server -> Client` UDP AEAD protocol packet +pub async fn decrypt_server_payload_aead_2022( + context: &Context, + method: CipherKind, + key: &[u8], + payload: &mut [u8], +) -> io::Result<(usize, Address, UdpSocketControlData)> { + let nonce_len = get_nonce_len(method); + let tag_len = method.tag_len(); + if payload.len() < nonce_len + tag_len + 8 + 8 + 1 + 8 + 2 { + let err = io::Error::new(ErrorKind::InvalidData, "udp packet too short"); + return Err(err); + } + + if !decrypt_message(context, method, key, payload) { + return Err(io::Error::new(io::ErrorKind::Other, "invalid tag-in")); + } + + let data = &payload[nonce_len..payload.len() - tag_len]; + let mut cursor = Cursor::new(data); + + let server_session_id = cursor.get_u64(); + let packet_id = cursor.get_u64(); + let socket_type = cursor.get_u8(); + if socket_type != SERVER_SOCKET_TYPE { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("invalid socket type {}", socket_type), + )); + } + let timestamp = cursor.get_u64(); + + let now = get_now_timestamp(); + if now.abs_diff(timestamp) > SERVER_PACKET_TIMESTAMP_MAX_DIFF { + return Err(io::Error::new( + ErrorKind::Other, + format!("received TCP response header with aged timestamp: {}", timestamp), + )); + } + + let client_session_id = cursor.get_u64(); + + let padding_size = cursor.get_u16() as usize; + if padding_size > 0 { + cursor.seek(SeekFrom::Current(padding_size as i64))?; + } + + let control = UdpSocketControlData { + client_session_id, + server_session_id, + packet_id, + }; + + let addr = Address::read_from(&mut cursor).await?; + + let payload_start = cursor.position() as usize; + let payload_len = data.len() - payload_start; + + payload.copy_within(nonce_len + payload_start..nonce_len + payload_start + payload_len, 0); + + Ok((payload_len, addr, control)) +} diff --git a/crates/shadowsocks/src/relay/udprelay/crypto_io.rs b/crates/shadowsocks/src/relay/udprelay/crypto_io.rs index ac231beed8d6..ae5e7e59c6e1 100644 --- a/crates/shadowsocks/src/relay/udprelay/crypto_io.rs +++ b/crates/shadowsocks/src/relay/udprelay/crypto_io.rs @@ -21,112 +21,97 @@ //! ``` use std::io::{self, Cursor, ErrorKind}; -use byte_string::ByteStr; use bytes::{BufMut, BytesMut}; -use log::trace; use crate::{ context::Context, - crypto::v1::{Cipher, CipherCategory, CipherKind}, + crypto::{CipherCategory, CipherKind}, relay::socks5::Address, }; -/// Encrypt payload into ShadowSocks UDP encrypted packet -pub fn encrypt_payload( +#[cfg(feature = "aead-cipher-2022")] +use super::aead_2022::{ + decrypt_client_payload_aead_2022, + decrypt_server_payload_aead_2022, + encrypt_client_payload_aead_2022, + encrypt_server_payload_aead_2022, +}; +#[cfg(feature = "stream-cipher")] +use super::stream::{decrypt_payload_stream, encrypt_payload_stream}; +use super::{ + aead::{decrypt_payload_aead, encrypt_payload_aead}, + options::UdpSocketControlData, +}; + +/// Encrypt `Client -> Server` payload into ShadowSocks UDP encrypted packet +pub fn encrypt_client_payload( context: &Context, method: CipherKind, key: &[u8], addr: &Address, + control: &UdpSocketControlData, payload: &[u8], dst: &mut BytesMut, ) { match method.category() { CipherCategory::None => { + let _ = control; dst.reserve(addr.serialized_len() + payload.len()); addr.write_to_buf(dst); dst.put_slice(payload); } #[cfg(feature = "stream-cipher")] - CipherCategory::Stream => encrypt_payload_stream(context, method, key, addr, payload, dst), - CipherCategory::Aead => encrypt_payload_aead(context, method, key, addr, payload, dst), - } -} - -#[cfg(feature = "stream-cipher")] -fn encrypt_payload_stream( - context: &Context, - method: CipherKind, - key: &[u8], - addr: &Address, - payload: &[u8], - dst: &mut BytesMut, -) { - let iv_len = method.iv_len(); - let addr_len = addr.serialized_len(); - - // Packet = IV + ADDRESS + PAYLOAD - dst.reserve(iv_len + addr_len + payload.len()); - - // Generate IV - dst.resize(iv_len, 0); - let iv = &mut dst[..iv_len]; - - if iv_len > 0 { - context.generate_nonce(iv, false); - trace!("UDP packet generated stream iv {:?}", ByteStr::new(iv)); + CipherCategory::Stream => { + let _ = control; + encrypt_payload_stream(context, method, key, addr, payload, dst) + } + CipherCategory::Aead => { + let _ = control; + encrypt_payload_aead(context, method, key, addr, payload, dst) + } + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => encrypt_client_payload_aead_2022(context, method, key, addr, control, payload, dst), } - - let mut cipher = Cipher::new(method, key, iv); - - addr.write_to_buf(dst); - dst.put_slice(payload); - let m = &mut dst[iv_len..]; - cipher.encrypt_packet(m); } -fn encrypt_payload_aead( +/// Encrypt `Server -> Client` payload into ShadowSocks UDP encrypted packet +pub fn encrypt_server_payload( context: &Context, method: CipherKind, key: &[u8], addr: &Address, + control: &UdpSocketControlData, payload: &[u8], dst: &mut BytesMut, ) { - let salt_len = method.salt_len(); - let addr_len = addr.serialized_len(); - - // Packet = IV + ADDRESS + PAYLOAD + TAG - dst.reserve(salt_len + addr_len + payload.len() + method.tag_len()); - - // Generate IV - dst.resize(salt_len, 0); - let salt = &mut dst[..salt_len]; - - if salt_len > 0 { - context.generate_nonce(salt, false); - trace!("UDP packet generated aead salt {:?}", ByteStr::new(salt)); - } - - let mut cipher = Cipher::new(method, key, salt); - - addr.write_to_buf(dst); - dst.put_slice(payload); - - unsafe { - dst.advance_mut(method.tag_len()); + match method.category() { + CipherCategory::None => { + let _ = control; + dst.reserve(addr.serialized_len() + payload.len()); + addr.write_to_buf(dst); + dst.put_slice(payload); + } + #[cfg(feature = "stream-cipher")] + CipherCategory::Stream => { + let _ = control; + encrypt_payload_stream(context, method, key, addr, payload, dst) + } + CipherCategory::Aead => { + let _ = control; + encrypt_payload_aead(context, method, key, addr, payload, dst) + } + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => encrypt_server_payload_aead_2022(context, method, key, addr, control, payload, dst), } - - let m = &mut dst[salt_len..]; - cipher.encrypt_packet(m); } -/// Decrypt payload from ShadowSocks UDP encrypted packet -pub async fn decrypt_payload( +/// Decrypt `Client -> Server` payload from ShadowSocks UDP encrypted packet +pub async fn decrypt_client_payload( context: &Context, method: CipherKind, key: &[u8], payload: &mut [u8], -) -> io::Result<(usize, Address)> { +) -> io::Result<(usize, Address, Option)> { match method.category() { CipherCategory::None => { let mut cur = Cursor::new(payload); @@ -135,7 +120,7 @@ pub async fn decrypt_payload( let pos = cur.position() as usize; let payload = cur.into_inner(); payload.copy_within(pos.., 0); - Ok((payload.len() - pos, address)) + Ok((payload.len() - pos, address, None)) } Err(..) => { let err = io::Error::new(ErrorKind::InvalidData, "parse udp packet Address failed"); @@ -144,97 +129,52 @@ pub async fn decrypt_payload( } } #[cfg(feature = "stream-cipher")] - CipherCategory::Stream => decrypt_payload_stream(context, method, key, payload).await, - CipherCategory::Aead => decrypt_payload_aead(context, method, key, payload).await, + CipherCategory::Stream => decrypt_payload_stream(context, method, key, payload) + .await + .map(|(n, a)| (n, a, None)), + CipherCategory::Aead => decrypt_payload_aead(context, method, key, payload) + .await + .map(|(n, a)| (n, a, None)), + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => decrypt_client_payload_aead_2022(context, method, key, payload) + .await + .map(|(n, a, c)| (n, a, Some(c))), } } -#[cfg(feature = "stream-cipher")] -async fn decrypt_payload_stream( - _context: &Context, - method: CipherKind, - key: &[u8], - payload: &mut [u8], -) -> io::Result<(usize, Address)> { - let plen = payload.len(); - let iv_len = method.iv_len(); - - if plen < iv_len { - let err = io::Error::new(ErrorKind::InvalidData, "udp packet too short for iv"); - return Err(err); - } - - let (iv, data) = payload.split_at_mut(iv_len); - // context.check_nonce_replay(iv)?; - - trace!("UDP packet got stream IV {:?}", ByteStr::new(iv)); - let mut cipher = Cipher::new(method, key, iv); - - assert!(cipher.decrypt_packet(data)); - - let (dn, addr) = parse_packet(data).await?; - - let data_start_idx = iv_len + dn; - let data_length = payload.len() - data_start_idx; - payload.copy_within(data_start_idx.., 0); - - Ok((data_length, addr)) -} - -async fn decrypt_payload_aead( - _context: &Context, +/// Decrypt `Server -> Client` payload from ShadowSocks UDP encrypted packet +pub async fn decrypt_server_payload( + context: &Context, method: CipherKind, key: &[u8], payload: &mut [u8], -) -> io::Result<(usize, Address)> { - let plen = payload.len(); - let salt_len = method.salt_len(); - if plen < salt_len { - let err = io::Error::new(ErrorKind::InvalidData, "udp packet too short for salt"); - return Err(err); - } - - let (salt, data) = payload.split_at_mut(salt_len); - // context.check_nonce_replay(salt)?; - - trace!("UDP packet got AEAD salt {:?}", ByteStr::new(salt)); - - let mut cipher = Cipher::new(method, key, salt); - let tag_len = cipher.tag_len(); - - if data.len() < tag_len { - return Err(io::Error::new(io::ErrorKind::Other, "udp packet too short for tag")); - } - - if !cipher.decrypt_packet(data) { - return Err(io::Error::new(io::ErrorKind::Other, "invalid tag-in")); - } - - // Truncate TAG - let data_len = data.len() - tag_len; - let data = &mut data[..data_len]; - - let (dn, addr) = parse_packet(data).await?; - - let data_length = data_len - dn; - let data_start_idx = salt_len + dn; - let data_end_idx = data_start_idx + data_length; - - payload.copy_within(data_start_idx..data_end_idx, 0); - - Ok((data_length, addr)) -} - -async fn parse_packet(buf: &[u8]) -> io::Result<(usize, Address)> { - let mut cur = Cursor::new(buf); - match Address::read_from(&mut cur).await { - Ok(address) => { - let pos = cur.position() as usize; - Ok((pos, address)) - } - Err(..) => { - let err = io::Error::new(ErrorKind::InvalidData, "parse udp packet Address failed"); - Err(err) +) -> io::Result<(usize, Address, Option)> { + match method.category() { + CipherCategory::None => { + let mut cur = Cursor::new(payload); + match Address::read_from(&mut cur).await { + Ok(address) => { + let pos = cur.position() as usize; + let payload = cur.into_inner(); + payload.copy_within(pos.., 0); + Ok((payload.len() - pos, address, None)) + } + Err(..) => { + let err = io::Error::new(ErrorKind::InvalidData, "parse udp packet Address failed"); + Err(err) + } + } } + #[cfg(feature = "stream-cipher")] + CipherCategory::Stream => decrypt_payload_stream(context, method, key, payload) + .await + .map(|(n, a)| (n, a, None)), + CipherCategory::Aead => decrypt_payload_aead(context, method, key, payload) + .await + .map(|(n, a)| (n, a, None)), + #[cfg(feature = "aead-cipher-2022")] + CipherCategory::Aead2022 => decrypt_server_payload_aead_2022(context, method, key, payload) + .await + .map(|(n, a, c)| (n, a, Some(c))), } } diff --git a/crates/shadowsocks/src/relay/udprelay/mod.rs b/crates/shadowsocks/src/relay/udprelay/mod.rs index 7d17ab133c92..31c8a6c0e9f4 100644 --- a/crates/shadowsocks/src/relay/udprelay/mod.rs +++ b/crates/shadowsocks/src/relay/udprelay/mod.rs @@ -51,8 +51,14 @@ use std::time::Duration; pub use self::proxy_socket::ProxySocket; +mod aead; +#[cfg(feature = "aead-cipher-2022")] +mod aead_2022; mod crypto_io; +pub mod options; pub mod proxy_socket; +#[cfg(feature = "stream-cipher")] +mod stream; /// The maximum UDP payload size (defined in the original shadowsocks Python) /// diff --git a/crates/shadowsocks/src/relay/udprelay/options.rs b/crates/shadowsocks/src/relay/udprelay/options.rs new file mode 100644 index 000000000000..eb4d57678e3d --- /dev/null +++ b/crates/shadowsocks/src/relay/udprelay/options.rs @@ -0,0 +1,15 @@ +//! UDP Socket options and extra data + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub struct UdpSocketControlData { + /// Session ID in client. + /// + /// For identifying an unique association in client + pub client_session_id: u64, + /// Session ID in server. + /// + /// For identifying an unique association in server + pub server_session_id: u64, + /// Packet counter + pub packet_id: u64, +} diff --git a/crates/shadowsocks/src/relay/udprelay/proxy_socket.rs b/crates/shadowsocks/src/relay/udprelay/proxy_socket.rs index 566a03319352..c155e8d666b4 100644 --- a/crates/shadowsocks/src/relay/udprelay/proxy_socket.rs +++ b/crates/shadowsocks/src/relay/udprelay/proxy_socket.rs @@ -13,17 +13,33 @@ use tokio::{ use crate::{ config::{ServerAddr, ServerConfig}, context::SharedContext, - crypto::v1::CipherKind, + crypto::CipherKind, net::{AcceptOpts, ConnectOpts, UdpSocket as ShadowUdpSocket}, - relay::socks5::Address, + relay::{socks5::Address, udprelay::options::UdpSocketControlData}, }; -use super::crypto_io::{decrypt_payload, encrypt_payload}; +use super::crypto_io::{ + decrypt_client_payload, + decrypt_server_payload, + encrypt_client_payload, + encrypt_server_payload, +}; static DEFAULT_CONNECT_OPTS: Lazy = Lazy::new(Default::default); +static DEFAULT_SOCKET_CONTROL: Lazy = Lazy::new(UdpSocketControlData::default); + +/// UDP socket type, defining whether the socket is used in Client or Server +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UdpSocketType { + /// Socket used for `Client -> Server` + Client, + /// Socket used for `Server -> Client` + Server, +} /// UDP client for communicating with ShadowSocks' server pub struct ProxySocket { + socket_type: UdpSocketType, socket: UdpSocket, method: CipherKind, key: Box<[u8]>, @@ -50,17 +66,28 @@ impl ProxySocket { trace!("connected udp remote {} with {:?}", svr_cfg.addr(), opts); - Ok(ProxySocket::from_socket(context, svr_cfg, socket.into())) + Ok(ProxySocket::from_socket( + UdpSocketType::Client, + context, + svr_cfg, + socket.into(), + )) } /// Create a `ProxySocket` from a `UdpSocket` - pub fn from_socket(context: SharedContext, svr_cfg: &ServerConfig, socket: UdpSocket) -> ProxySocket { + pub fn from_socket( + socket_type: UdpSocketType, + context: SharedContext, + svr_cfg: &ServerConfig, + socket: UdpSocket, + ) -> ProxySocket { let key = svr_cfg.key().to_vec().into_boxed_slice(); let method = svr_cfg.method(); // NOTE: svr_cfg.timeout() is not for this socket, but for associations. ProxySocket { + socket_type, socket, method, key, @@ -91,17 +118,54 @@ impl ProxySocket { .1 } }; - Ok(ProxySocket::from_socket(context, svr_cfg, socket.into())) + Ok(ProxySocket::from_socket( + UdpSocketType::Server, + context, + svr_cfg, + socket.into(), + )) } /// Send a UDP packet to addr through proxy + #[inline] pub async fn send(&self, addr: &Address, payload: &[u8]) -> io::Result { + self.send_with_ctrl(addr, &DEFAULT_SOCKET_CONTROL, payload).await + } + + /// Send a UDP packet to addr through proxy + pub async fn send_with_ctrl( + &self, + addr: &Address, + control: &UdpSocketControlData, + payload: &[u8], + ) -> io::Result { let mut send_buf = BytesMut::new(); - encrypt_payload(&self.context, self.method, &self.key, addr, payload, &mut send_buf); + + match self.socket_type { + UdpSocketType::Client => encrypt_client_payload( + &self.context, + self.method, + &self.key, + addr, + control, + payload, + &mut send_buf, + ), + UdpSocketType::Server => encrypt_server_payload( + &self.context, + self.method, + &self.key, + addr, + control, + payload, + &mut send_buf, + ), + } trace!( - "UDP server client send to {}, payload length {} bytes, packet length {} bytes", + "UDP server client send to {}, control: {:?}, payload length {} bytes, packet length {} bytes", addr, + control, payload.len(), send_buf.len() ); @@ -128,12 +192,45 @@ impl ProxySocket { /// Send a UDP packet to target from proxy pub async fn send_to(&self, target: A, addr: &Address, payload: &[u8]) -> io::Result { + self.send_to_with_ctrl(target, addr, &DEFAULT_SOCKET_CONTROL, payload) + .await + } + + /// Send a UDP packet to target from proxy + pub async fn send_to_with_ctrl( + &self, + target: A, + addr: &Address, + control: &UdpSocketControlData, + payload: &[u8], + ) -> io::Result { let mut send_buf = BytesMut::new(); - encrypt_payload(&self.context, self.method, &self.key, addr, payload, &mut send_buf); + + match self.socket_type { + UdpSocketType::Client => encrypt_client_payload( + &self.context, + self.method, + &self.key, + addr, + control, + payload, + &mut send_buf, + ), + UdpSocketType::Server => encrypt_server_payload( + &self.context, + self.method, + &self.key, + addr, + control, + payload, + &mut send_buf, + ), + } trace!( - "UDP server client send to, addr {}, payload length {} bytes, packet length {} bytes", + "UDP server client send to, addr {}, control: {:?}, payload length {} bytes, packet length {} bytes", addr, + control, payload.len(), send_buf.len() ); @@ -164,6 +261,18 @@ impl ProxySocket { /// /// It is recommended to allocate a buffer to have at least 65536 bytes. pub async fn recv(&self, recv_buf: &mut [u8]) -> io::Result<(usize, Address, usize)> { + self.recv_with_ctrl(recv_buf).await.map(|(n, a, rn, _)| (n, a, rn)) + } + + /// Receive packet from Shadowsocks' UDP server + /// + /// This function will use `recv_buf` to store intermediate data, so it has to be big enough to store the whole shadowsocks' packet + /// + /// It is recommended to allocate a buffer to have at least 65536 bytes. + pub async fn recv_with_ctrl( + &self, + recv_buf: &mut [u8], + ) -> io::Result<(usize, Address, usize, Option)> { // Waiting for response from server SERVER -> CLIENT let recv_n = match self.recv_timeout { None => self.socket.recv(recv_buf).await?, @@ -174,16 +283,24 @@ impl ProxySocket { }, }; - let (n, addr) = decrypt_payload(&self.context, self.method, &self.key, &mut recv_buf[..recv_n]).await?; + let (n, addr, control) = match self.socket_type { + UdpSocketType::Client => { + decrypt_server_payload(&self.context, self.method, &self.key, &mut recv_buf[..recv_n]).await? + } + UdpSocketType::Server => { + decrypt_client_payload(&self.context, self.method, &self.key, &mut recv_buf[..recv_n]).await? + } + }; trace!( - "UDP server client receive from {}, packet length {} bytes, payload length {} bytes", + "UDP server client receive from {}, control: {:?}, packet length {} bytes, payload length {} bytes", addr, + control, recv_n, n ); - Ok((n, addr, recv_n)) + Ok((n, addr, recv_n, control)) } /// Receive packet from Shadowsocks' UDP server @@ -192,6 +309,20 @@ impl ProxySocket { /// /// It is recommended to allocate a buffer to have at least 65536 bytes. pub async fn recv_from(&self, recv_buf: &mut [u8]) -> io::Result<(usize, SocketAddr, Address, usize)> { + self.recv_from_with_ctrl(recv_buf) + .await + .map(|(n, sa, a, rn, _)| (n, sa, a, rn)) + } + + /// Receive packet from Shadowsocks' UDP server + /// + /// This function will use `recv_buf` to store intermediate data, so it has to be big enough to store the whole shadowsocks' packet + /// + /// It is recommended to allocate a buffer to have at least 65536 bytes. + pub async fn recv_from_with_ctrl( + &self, + recv_buf: &mut [u8], + ) -> io::Result<(usize, SocketAddr, Address, usize, Option)> { // Waiting for response from server SERVER -> CLIENT let (recv_n, target_addr) = match self.recv_timeout { None => self.socket.recv_from(recv_buf).await?, @@ -201,17 +332,26 @@ impl ProxySocket { Err(..) => return Err(io::ErrorKind::TimedOut.into()), }, }; - let (n, addr) = decrypt_payload(&self.context, self.method, &self.key, &mut recv_buf[..recv_n]).await?; + + let (n, addr, control) = match self.socket_type { + UdpSocketType::Client => { + decrypt_server_payload(&self.context, self.method, &self.key, &mut recv_buf[..recv_n]).await? + } + UdpSocketType::Server => { + decrypt_client_payload(&self.context, self.method, &self.key, &mut recv_buf[..recv_n]).await? + } + }; trace!( - "UDP server client receive from {}, addr {}, packet length {} bytes, payload length {} bytes", + "UDP server client receive from {}, addr {}, control: {:?}, packet length {} bytes, payload length {} bytes", target_addr, addr, + control, recv_n, n, ); - Ok((n, target_addr, addr, recv_n)) + Ok((n, target_addr, addr, recv_n, control)) } /// Get local addr of socket diff --git a/crates/shadowsocks/src/relay/udprelay/stream.rs b/crates/shadowsocks/src/relay/udprelay/stream.rs new file mode 100644 index 000000000000..34c741196fce --- /dev/null +++ b/crates/shadowsocks/src/relay/udprelay/stream.rs @@ -0,0 +1,100 @@ +//! Shadowsocks UDP Stream Protocol +//! +//! Payload with stream cipher +//! ```plain +//! +-------+----------+ +//! | IV | Payload | +//! +-------+----------+ +//! | Fixed | Variable | +//! +-------+----------+ +//! ``` + +use std::io::{self, Cursor, ErrorKind}; + +use byte_string::ByteStr; +use bytes::{BufMut, BytesMut}; +use log::trace; + +use crate::{ + context::Context, + crypto::{v1::Cipher, CipherKind}, + relay::socks5::Address, +}; + +/// Encrypt UDP stream protocol packet +pub fn encrypt_payload_stream( + context: &Context, + method: CipherKind, + key: &[u8], + addr: &Address, + payload: &[u8], + dst: &mut BytesMut, +) { + let iv_len = method.iv_len(); + let addr_len = addr.serialized_len(); + + // Packet = IV + ADDRESS + PAYLOAD + dst.reserve(iv_len + addr_len + payload.len()); + + // Generate IV + dst.resize(iv_len, 0); + let iv = &mut dst[..iv_len]; + + if iv_len > 0 { + context.generate_nonce(iv, false); + trace!("UDP packet generated stream iv {:?}", ByteStr::new(iv)); + } + + let mut cipher = Cipher::new(method, key, iv); + + addr.write_to_buf(dst); + dst.put_slice(payload); + let m = &mut dst[iv_len..]; + cipher.encrypt_packet(m); +} + +/// Decrypt UDP stream protocol packet +pub async fn decrypt_payload_stream( + _context: &Context, + method: CipherKind, + key: &[u8], + payload: &mut [u8], +) -> io::Result<(usize, Address)> { + let plen = payload.len(); + let iv_len = method.iv_len(); + + if plen < iv_len { + let err = io::Error::new(ErrorKind::InvalidData, "udp packet too short for iv"); + return Err(err); + } + + let (iv, data) = payload.split_at_mut(iv_len); + // context.check_nonce_replay(iv)?; + + trace!("UDP packet got stream IV {:?}", ByteStr::new(iv)); + let mut cipher = Cipher::new(method, key, iv); + + assert!(cipher.decrypt_packet(data)); + + let (dn, addr) = parse_packet(data).await?; + + let data_start_idx = iv_len + dn; + let data_length = payload.len() - data_start_idx; + payload.copy_within(data_start_idx.., 0); + + Ok((data_length, addr)) +} + +async fn parse_packet(buf: &[u8]) -> io::Result<(usize, Address)> { + let mut cur = Cursor::new(buf); + match Address::read_from(&mut cur).await { + Ok(address) => { + let pos = cur.position() as usize; + Ok((pos, address)) + } + Err(..) => { + let err = io::Error::new(ErrorKind::InvalidData, "parse udp packet Address failed"); + Err(err) + } + } +} diff --git a/crates/shadowsocks/tests/tcp.rs b/crates/shadowsocks/tests/tcp.rs index 5fe100ead908..8153ce8ff104 100644 --- a/crates/shadowsocks/tests/tcp.rs +++ b/crates/shadowsocks/tests/tcp.rs @@ -16,7 +16,7 @@ use tokio::{ use shadowsocks::{ config::{ServerConfig, ServerType}, context::Context, - crypto::v1::CipherKind, + crypto::CipherKind, relay::{ socks5::Address, tcprelay::{ @@ -32,7 +32,7 @@ async fn handle_tcp_tunnel_server_client( method: CipherKind, mut stream: ProxyServerStream, ) -> io::Result<()> { - let addr = Address::read_from(&mut stream).await?; + let addr = stream.handshake().await?; let mut remote = { let remote = match addr { @@ -44,7 +44,7 @@ async fn handle_tcp_tunnel_server_client( remote }; - let (mut sr, mut sw) = stream.into_split(); + let (mut sr, mut sw) = tokio::io::split(stream); let (mut mr, mut mw) = remote.split(); let l2r = copy_from_encrypted(method, &mut sr, &mut mw); @@ -70,7 +70,7 @@ async fn handle_tcp_tunnel_local_client( let remote = ProxyClientStream::connect(context, &svr_cfg, target_addr).await?; let (mut lr, mut lw) = stream.split(); - let (mut sr, mut sw) = remote.into_split(); + let (mut sr, mut sw) = tokio::io::split(remote); let l2s = copy_to_encrypted(svr_cfg.method(), &mut lr, &mut sw); let s2l = copy_from_encrypted(svr_cfg.method(), &mut sr, &mut lw); @@ -185,3 +185,37 @@ async fn tcp_tunnel_none() { .await .unwrap(); } + +#[cfg(feature = "aead-cipher-2022")] +#[tokio::test] +async fn tcp_tunnel_aead_2022_aes() { + let _ = env_logger::try_init(); + + let server_addr = "127.0.0.1:34001".parse::().unwrap(); + let local_addr = "127.0.0.1:34101".parse::().unwrap(); + tcp_tunnel_example( + server_addr, + local_addr, + "3L69X4PF2eSL/JSLkoWnXg==", + CipherKind::AEAD2022_BLAKE3_AES_128_GCM, + ) + .await + .unwrap(); +} + +#[cfg(feature = "aead-cipher-2022")] +#[tokio::test] +async fn tcp_tunnel_aead_2022_chacha20() { + let _ = env_logger::try_init(); + + let server_addr = "127.0.0.1:35001".parse::().unwrap(); + let local_addr = "127.0.0.1:35101".parse::().unwrap(); + tcp_tunnel_example( + server_addr, + local_addr, + "VUw3mGWIpil2z2DKiyauE2Sp9KyE2ab8dulciawe74o", + CipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305, + ) + .await + .unwrap(); +} diff --git a/crates/shadowsocks/tests/tcp_tfo.rs b/crates/shadowsocks/tests/tcp_tfo.rs index 058a01cd51ea..7dc6f954666b 100644 --- a/crates/shadowsocks/tests/tcp_tfo.rs +++ b/crates/shadowsocks/tests/tcp_tfo.rs @@ -15,7 +15,7 @@ use log::debug; use shadowsocks::{ config::ServerType, context::Context, - crypto::v1::CipherKind, + crypto::CipherKind, net::{AcceptOpts, ConnectOpts}, relay::{ socks5::Address, @@ -57,7 +57,7 @@ async fn tcp_tunnel_tfo() { Address::DomainNameAddress(name, port) => TcpStream::connect((name.as_str(), port)).await.unwrap(), }; - let (mut lr, mut lw) = stream.into_split(); + let (mut lr, mut lw) = tokio::io::split(stream); let (mut rr, mut rw) = remote.into_split(); let l2r = copy_from_encrypted(CipherKind::NONE, &mut lr, &mut rw); diff --git a/crates/shadowsocks/tests/udp.rs b/crates/shadowsocks/tests/udp.rs index c548ecd61e93..1895215d5f39 100644 --- a/crates/shadowsocks/tests/udp.rs +++ b/crates/shadowsocks/tests/udp.rs @@ -7,7 +7,7 @@ use tokio::{net::UdpSocket, sync::Barrier}; use shadowsocks::{ config::{ServerConfig, ServerType}, context::{Context, SharedContext}, - crypto::v1::CipherKind, + crypto::CipherKind, relay::{socks5::Address, udprelay::ProxySocket}, }; @@ -180,3 +180,43 @@ async fn udp_tunnel_none() { .await .unwrap(); } + +#[cfg(feature = "aead-cipher-2022")] +#[tokio::test] +async fn udp_tunnel_aead_2022_aes() { + let _ = env_logger::try_init(); + + let server_addr = "127.0.0.1:24001".parse::().unwrap(); + let local_addr = "127.0.0.1:24101".parse::().unwrap(); + let target_addr = "127.0.0.1:24201".parse::().unwrap(); + + udp_tunnel_echo( + server_addr, + local_addr, + target_addr, + "D1HJFfvRIxpklHLeKvjCDQ==", + CipherKind::AEAD2022_BLAKE3_AES_128_GCM, + ) + .await + .unwrap(); +} + +#[cfg(feature = "aead-cipher-2022")] +#[tokio::test] +async fn udp_tunnel_aead_2022_chacha20() { + let _ = env_logger::try_init(); + + let server_addr = "127.0.0.1:25001".parse::().unwrap(); + let local_addr = "127.0.0.1:25101".parse::().unwrap(); + let target_addr = "127.0.0.1:25201".parse::().unwrap(); + + udp_tunnel_echo( + server_addr, + local_addr, + target_addr, + "4wYfDniq4N6kMqFajRO03PPZLfPkl469eNYY9Wz0E78=", + CipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305, + ) + .await + .unwrap(); +} diff --git a/src/service/local.rs b/src/service/local.rs index 58894b4bfad3..8395422d7acd 100644 --- a/src/service/local.rs +++ b/src/service/local.rs @@ -18,7 +18,7 @@ use shadowsocks_service::{ local::loadbalancing::PingBalancer, shadowsocks::{ config::{Mode, ServerAddr, ServerConfig}, - crypto::v1::{available_ciphers, CipherKind}, + crypto::{available_ciphers, CipherKind}, plugin::PluginConfig, }, }; diff --git a/src/service/manager.rs b/src/service/manager.rs index 1134b29a1d1d..52a306b4fa90 100644 --- a/src/service/manager.rs +++ b/src/service/manager.rs @@ -15,7 +15,7 @@ use shadowsocks_service::{ run_manager, shadowsocks::{ config::{ManagerAddr, Mode}, - crypto::v1::{available_ciphers, CipherKind}, + crypto::{available_ciphers, CipherKind}, plugin::PluginConfig, }, }; diff --git a/src/service/server.rs b/src/service/server.rs index 3a4d57bb14d8..0ff42e673f44 100644 --- a/src/service/server.rs +++ b/src/service/server.rs @@ -13,7 +13,7 @@ use shadowsocks_service::{ run_server, shadowsocks::{ config::{ManagerAddr, Mode, ServerAddr, ServerConfig}, - crypto::v1::{available_ciphers, CipherKind}, + crypto::{available_ciphers, CipherKind}, plugin::PluginConfig, }, }; diff --git a/tests/socks4.rs b/tests/socks4.rs index 246b67502263..326a08ff178c 100644 --- a/tests/socks4.rs +++ b/tests/socks4.rs @@ -17,7 +17,7 @@ use shadowsocks_service::{ run_server, shadowsocks::{ config::{ServerAddr, ServerConfig}, - crypto::v1::CipherKind, + crypto::CipherKind, }, }; diff --git a/tests/socks5.rs b/tests/socks5.rs index 1872e6eb1239..f6357b733c87 100644 --- a/tests/socks5.rs +++ b/tests/socks5.rs @@ -17,7 +17,7 @@ use shadowsocks_service::{ run_server, shadowsocks::{ config::{Mode, ServerAddr, ServerConfig}, - crypto::v1::CipherKind, + crypto::CipherKind, relay::socks5::Address, }, }; diff --git a/tests/udp.rs b/tests/udp.rs index e6d526e403c6..a12515d981a5 100644 --- a/tests/udp.rs +++ b/tests/udp.rs @@ -11,7 +11,7 @@ use shadowsocks_service::{ local::socks::client::socks5::Socks5UdpClient, run_local, run_server, - shadowsocks::{config::Mode, crypto::v1::CipherKind, relay::socks5::Address, ServerConfig}, + shadowsocks::{config::Mode, crypto::CipherKind, relay::socks5::Address, ServerConfig}, }; const SERVER_ADDR: &str = "127.0.0.1:8093";