0.2.0 update

This commit is contained in:
cfal
2025-11-26 22:59:30 +04:00
parent 2c92cd754b
commit ced60d27e3
87 changed files with 21202 additions and 2015 deletions
+523 -350
View File
File diff suppressed because it is too large Load Diff
Generated
+404 -150
View File
@@ -22,6 +22,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
version = "0.6.21"
@@ -54,22 +60,22 @@ dependencies = [
[[package]]
name = "anstyle-query"
version = "1.1.4"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -96,6 +102,45 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "asn1-rs"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror 2.0.17",
"time",
]
[[package]]
name = "asn1-rs-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -108,10 +153,22 @@ dependencies = [
]
[[package]]
name = "aws-lc-rs"
version = "1.14.1"
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -119,11 +176,10 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.32.3"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c"
checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6"
dependencies = [
"bindgen",
"cc",
"cmake",
"dunce",
@@ -142,26 +198,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.10.0",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -213,15 +249,15 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.44"
version = "1.2.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -235,24 +271,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfb-mode"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330"
dependencies = [
"cipher",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -275,17 +293,6 @@ dependencies = [
"inout",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "cmake"
version = "0.1.54"
@@ -350,9 +357,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
@@ -372,6 +379,35 @@ dependencies = [
"parking_lot_core",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "der-parser"
version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
dependencies = [
"asn1-rs",
"displaydoc",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -383,18 +419,23 @@ dependencies = [
"subtle",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "0.1.4"
@@ -442,15 +483,15 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "fnv"
version = "1.0.7"
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "fs_extra"
@@ -558,9 +599,9 @@ dependencies = [
[[package]]
name = "generic-array"
version = "0.14.9"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
@@ -593,12 +634,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "h3"
version = "0.0.8"
@@ -635,38 +670,104 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.16.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"digest",
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "http"
version = "1.3.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "indexmap"
version = "2.12.0"
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
]
[[package]]
name = "hyper-util"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"hyper",
"pin-project-lite",
"tokio",
]
[[package]]
name = "indexmap"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
dependencies = [
"equivalent",
"hashbrown 0.16.0",
"hashbrown 0.16.1",
]
[[package]]
@@ -704,15 +805,6 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@@ -721,22 +813,22 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
dependencies = [
"proc-macro2",
"quote",
@@ -814,22 +906,18 @@ dependencies = [
"libc",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -851,6 +939,15 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "lru"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f"
dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -925,6 +1022,49 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "oid-registry"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
dependencies = [
"asn1-rs",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -977,6 +1117,16 @@ dependencies = [
"subtle",
]
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64",
"serde_core",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1010,6 +1160,12 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1019,16 +1175,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
@@ -1098,9 +1244,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.41"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
@@ -1146,6 +1292,20 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rcgen"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5"
dependencies = [
"pem",
"ring",
"rustls-pki-types",
"time",
"x509-parser",
"yasna",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1210,6 +1370,15 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom",
]
[[package]]
name = "rustix"
version = "1.1.2"
@@ -1225,9 +1394,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.34"
version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [
"aws-lc-rs",
"log",
@@ -1250,6 +1419,15 @@ dependencies = [
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
@@ -1419,7 +1597,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "shoes"
version = "0.1.8"
version = "0.2.0"
dependencies = [
"aes",
"argon2",
@@ -1428,16 +1606,18 @@ dependencies = [
"base64",
"blake3",
"bytes",
"cfb-mode",
"dashmap",
"digest",
"env_logger",
"futures",
"h3",
"h3-quinn",
"hmac",
"http",
"http-body-util",
"hyper",
"hyper-util",
"log",
"lru",
"md-5",
"memchr",
"notify",
@@ -1445,17 +1625,22 @@ dependencies = [
"percent-encoding",
"quinn",
"rand",
"rand_core 0.6.4",
"rcgen",
"rustc-hash 1.1.0",
"rustls",
"rustls-pemfile",
"serde",
"serde_yaml",
"sha3",
"socket2",
"subtle",
"tempfile",
"tikv-jemallocator",
"tokio",
"tokio-rustls",
"webpki-roots",
"x509-parser",
]
[[package]]
@@ -1488,15 +1673,26 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.108"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tempfile"
version = "3.23.0"
@@ -1570,6 +1766,37 @@ dependencies = [
"tikv-jemalloc-sys",
]
[[package]]
name = "time"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "time-macros"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
@@ -2032,19 +2259,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "zerocopy"
version = "0.8.27"
name = "x509-parser"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425"
dependencies = [
"asn1-rs",
"data-encoding",
"der-parser",
"lazy_static",
"nom",
"oid-registry",
"ring",
"rusticata-macros",
"thiserror 2.0.17",
"time",
]
[[package]]
name = "yasna"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time",
]
[[package]]
name = "zerocopy"
version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5"
dependencies = [
"proc-macro2",
"quote",
+13 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "shoes"
version = "0.1.8"
version = "0.2.0"
edition = "2021"
license = "MIT"
description = "A multi-protocol proxy server."
@@ -18,16 +18,15 @@ aws-lc-rs = { version = "*", default-features = false }
base64 = "*"
blake3 = "*"
bytes = "*"
cfb-mode = "*"
dashmap = "*"
digest = "*"
env_logger = "*"
futures = "*"
h3 = "*"
h3-quinn = "*"
hmac = "*"
http = "*"
log = "*"
log = { version = "*", features = ["release_max_level_info"] }
lru = "*"
md-5 = "*"
memchr = "*"
notify = "*"
@@ -35,9 +34,12 @@ parking_lot = "*"
percent-encoding = "*"
quinn = { version = "*", default-features = false, features = ["log", "platform-verifier", "runtime-tokio", "rustls-aws-lc-rs"] }
rand = "*"
rand_core = "*"
rcgen = { version = "*", features = ["x509-parser"] }
rustc-hash = "1"
rustls = "*"
serde = { version = "*", features = ["derive", "std"] }
subtle = "*"
serde_yaml = "*"
sha3 = "*"
socket2 = "*"
@@ -50,11 +52,17 @@ tokio = { version = "*", features = [
"sync",
"time",
] }
tokio-rustls = { version = "*", default-features = false, features = ["logging", "tls12", "aws-lc-rs"] }
webpki-roots = { version = "*" }
x509-parser = "*"
[dev-dependencies]
tempfile = "*"
hyper = { version = "1", features = ["server", "http1"] }
hyper-util = { version = "0.1", features = ["tokio", "server"] }
http-body-util = "0.1"
rustls-pemfile = "*"
rcgen = "*"
tokio-rustls = "*"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = "0.6"
+163 -122
View File
@@ -1,123 +1,52 @@
# shoes
shoes is a multi-protocol proxy server written in Rust.
shoes is a high-performance multi-protocol proxy server written in Rust.
## Supported protocols
## Supported Protocols
- **HTTP/HTTPS** (TCP, QUIC)
- **SOCKS5** (TCP, QUIC)
- **Vmess** (TCP, QUIC, UDP-over-TCP)
- AEAD and Legacy modes
- Supported ciphers:
- aes-128-gcm
- chacha20-poly1305
- **Vless** (TCP, QUIC, UDP-over-TCP)
- **Snell** v3 (TCP, QUIC, UDP-over-TCP)
- Supported ciphers:
- aes-128-gcm
- aes-256-gcm
- chacha20-ietf-poly1305
- **Shadowsocks** (TCP, QUIC)
- Supported ciphers:
- aes-128-gcm
- aes-256-gcm
- chacha20-ietf-poly1305
- 2022-blake3-aes-128-gcm
- 2022-blake3-aes-256-gcm
- 2022-blake3-chacha20-ietf-poly1305
- **Trojan** (TCP, QUIC)
- Supported ciphers:
- aes-128-gcm
- aes-256-gcm
- chacha20-ietf-poly1305
- **Hysteria2** (QUIC)
- **TUIC v5** (QUIC)
### Proxy Protocols
- **HTTP/HTTPS**
- **SOCKS5**
- **VMess AEAD**
- **VLESS**
- **Shadowsocks**
- **Trojan**
- **Snell v3**
- **Hysteria2**
- **TUIC v5**
### Transport Protocols
All server protocols plus:
- **SagerNet UDP over TCP** (for Shadowsocks and SOCKS5)
- **ShadowTLS v3**
- **TLS**
- **WebSocket** (Shadowsocks SIP003)
- **XTLS Reality**
- **XTLS Vision** (for VLESS)
### Supported Ciphers
- **VMess**: `aes-128-gcm`, `chacha20-poly1305`, `none`
- **Shadowsocks**: `aes-128-gcm`, `aes-256-gcm`, `chacha20-ietf-poly1305`, `2022-blake3-aes-128-gcm`, `2022-blake3-aes-256-gcm`, `2022-blake3-chacha20-ietf-poly1305`
- **Snell v3**: `aes-128-gcm`, `aes-256-gcm`, `chacha20-ietf-poly1305`
## Features
All supported protocols can be combined with the following features:
- **Multi-transport**: TCP or QUIC for all protocols
- **TLS with SNI routing**: Route by Server Name Indication
- **Upstream proxy chaining**: Route through multiple proxies
- **Rule-based routing**: Route by IP/CIDR or hostname masks
- **Named PEM certificates**: Define once, reference everywhere
- **TLS fingerprint authentication**: Certificate pinning for TLS/QUIC
- **Hot reloading**: Apply config changes without restart
- **Unix socket support**: Bind to Unix domain sockets
- **TLS support** with SNI based forwarding
- **Websocket obfs** (Shadowsocks SIP003)
- **ShadowTLS v3**
- **Upstream proxy support**: route connections through other proxy servers
- **Forwarding rules**: Redirect or block connections based on target IP or hostname
- **Hot reloading**: Updated configs are automatically reloaded
- **Netmask and proxy groups**
For advanced access control of incoming connections (eg. IP allowlist/blocklists), check out [tobaru](https://github.com/cfal/tobaru).
## Examples
Here's an example of running a WSS vmess and shadowsocks server, with all requests routed through a SOCKS proxy:
```yaml
# Listen on all IPv4 interfaces, port 443 (HTTPS)
- address: 0.0.0.0:443
transport: tcp
# Use TLS as the first protocol layer
protocol:
type: tls
# Set a default target, for any (or no) SNI
default_target:
cert: cert.pem
key: key.pem
# ..which goes to a websocket server
protocol:
type: ws
# .. where we have different supported proxy protocols, based on HTTP request path and headers.
targets:
- matching_path: /vmess
matching_headers:
X-Secret-Key: "secret"
protocol:
type: vmess
# allow any cipher, which means: none, aes-128-gcm, or chacha20-poly1305.
cipher: any
user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4
- matching_path: /shadowsocks
protocol:
type: shadowsocks
cipher: 2022-blake3-aes-256-gcm
password: Hax8btYlNao5qcaN/l/NUl9JgbwapfqG5QyAtH+aKPg=
# Set a ShadowTLS v3 target by SNI
shadowtls_targets:
google.com:
# ShadowTLS password
password: 83a44859c0e7fbb589b
# Configure handshake server.
handshake:
address: google.com:443
# Use the local SOCKS server to connect to the handshake server.
client_proxies:
- address: 127.0.0.1:1080
protocol:
type: socks
username: socksuser
password: secretpass
rules:
# Allow clients to connect to all IPs
- mask: 0.0.0.0/0
action: allow
# Forward all requests through a local SOCKS server.
client_proxy:
address: 127.0.0.1:5000
protocol:
type: socks
username: socksuser
password: secretpass
```
For other YAML config examples, see the [examples](./examples) directory.
For advanced access control (IP allowlist/blocklists), see [tobaru](https://github.com/cfal/tobaru).
## Installation
Precompiled binaries for x86_64 and Apple aarch64 are available on [Github Releases](https://github.com/cfal/shoes/releases).
Else, if you have a fairly recent Rust and cargo installation on your system, shoes can be installed with `cargo`.
Or install with cargo:
```bash
cargo install shoes
@@ -126,34 +55,146 @@ cargo install shoes
## Usage
```
shoes [OPTIONS] <YAML CONFIG PATH> [YAML CONFIG PATH] [..]
shoes [OPTIONS] <config.yaml> [config.yaml...]
OPTIONS:
-t, --threads NUM Set the number of worker threads (default: CPU count)
-d, --dry-run Parse the config and exit
--no-reload Disable automatic config reloading on file changes
-t, --threads NUM
Set the number of worker threads. This usually defaults to the number of CPUs.
COMMANDS:
generate-reality-keypair Generate a new Reality X25519 keypair
generate-shadowsocks-password <cipher> Generate a Shadowsocks password
```
-d, --dry-run
Parse the config and exit.
### Examples
```bash
# Run with a single config file
shoes config.yaml
# Run with multiple config files
shoes server1.yaml server2.yaml rules.yaml
# Run with custom thread count
shoes --threads 8 config.yaml
# Validate configuration without starting
shoes --dry-run config.yaml
# Run without hot-reloading
shoes --no-reload config.yaml
# Generate Reality keypair
shoes generate-reality-keypair
# Generate Shadowsocks password
shoes generate-shadowsocks-password 2022-blake3-aes-256-gcm
```
## Configuration
See [CONFIG.md](./CONFIG.md) for the YAML config format. You can also refer to the [examples](./examples), or open an issue if you need help.
See [CONFIG.md](./CONFIG.md) for the complete YAML configuration reference.
## Roadmap
## Examples
- Proxy client chaining
- SOCKS and Shadowsocks UDP support
See the [examples](./examples) directory for all examples.
## Similar and related projects
### Basic VMess Server
```yaml
- address: 0.0.0.0:16823
protocol:
type: vmess
cipher: chacha20-poly1305
user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4
udp_enabled: true
```
- [shadowsocks/shadowsocks-rust](https://github.com/shadowsocks/shadowsocks-rust): A Rust port of shadowsocks
### VLESS with Vision over TLS
```yaml
- address: 0.0.0.0:443
protocol:
type: tls
tls_targets:
"vless.example.com":
cert: cert.pem
key: key.pem
vision: true
alpn_protocols: ["http/1.1"]
protocol:
type: vless
user_id: b85798ef-e9dc-46a4-9a87-8da4499d36d0
udp_enabled: true
```
- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core): A full-featured proxy platform written in Go
### Reality Server
```yaml
- address: 0.0.0.0:443
protocol:
type: tls
reality_targets:
"www.example.com":
private_key: "YOUR_BASE64URL_PRIVATE_KEY"
short_ids: ["0123456789abcdef", ""]
dest: "www.example.com:443"
protocol:
type: vless
user_id: b85798ef-e9dc-46a4-9a87-8da4499d36d0
udp_enabled: true
```
- [ihciah/shadow-tls](https://github.com/ihciah/shadow-tls): A proxy to expose real TLS handshake to the firewall
### Reality Client
```yaml
- address: 127.0.0.1:1080
protocol:
type: socks
rules:
- masks: "0.0.0.0/0"
action: allow
client_proxy:
address: "server.example.com:443"
protocol:
type: reality
public_key: "SERVER_PUBLIC_KEY"
short_id: "0123456789abcdef"
sni_hostname: "www.example.com"
protocol:
type: vless
user_id: b85798ef-e9dc-46a4-9a87-8da4499d36d0
```
- [apernet/hysteria](https://github.com/apernet/hysteria): Hysteria is a powerful, lightning fast and censorship resistant proxy
### Hysteria2 Server
```yaml
- address: 0.0.0.0:443
transport: quic
quic_settings:
cert: cert.pem
key: key.pem
alpn_protocols: ["h3"]
protocol:
type: hysteria2
password: supersecret
udp_enabled: true
```
- [tuic-protocol/tuic](https://github.com/tuic-protocol/tuic): Delicately-TUICed 0-RTT proxy protocol
### TUIC v5 Server
```yaml
- address: 0.0.0.0:443
transport: quic
quic_settings:
cert: cert.pem
key: key.pem
protocol:
type: tuic
uuid: d685aef3-b3c4-4932-9a9d-d0c2f6727dfa
password: supersecret
```
## Similar Projects
- [apernet/hysteria](https://github.com/apernet/hysteria)
- [ihciah/shadow-tls](https://github.com/ihciah/shadow-tls)
- [SagerNet/sing-box](https://github.com/SagerNet/sing-box)
- [shadowsocks/shadowsocks-rust](https://github.com/shadowsocks/shadowsocks-rust)
- [EAimTY/tuic](https://github.com/EAimTY/tuic)
- [v2fly/v2ray-core](https://github.com/v2fly/v2ray-core)
- [XTLS/Xray-core](https://github.com/XTLS/Xray-core)
+40 -1
View File
@@ -70,4 +70,43 @@
protocol:
type: shadowsocks
cipher: 2022-blake3-chacha20-ietf-poly1305
password: "N5tp51JibWA9kIPEUoDHzyyjfgkDYToOERh+u9L47fk="
password: "N5tp51JibWA9kIPEUoDHzyyjfgkDYToOERh+u9L47fk="
# Reality server (note: Reality uses X25519 keys, not certificates)
# Reality doesn't use named PEMs for its keys since they're not certificates
- address: "0.0.0.0:10443"
protocol:
type: tls
reality_targets:
"www.cloudflare.com":
private_key: "YOUR_REALITY_PRIVATE_KEY_HERE"
short_ids: ["0123456789abcdef", ""]
dest: "www.cloudflare.com:443"
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
udp_enabled: true
# Mixed configuration: Regular TLS and Reality on same server
- address: "0.0.0.0:11443"
protocol:
type: tls
# Regular TLS target using named certificates
tls_targets:
"tls.example.com":
cert: "main-server-cert"
key: "main-server-key"
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
# Reality target (uses its own X25519 keys)
reality_targets:
"www.google.com":
private_key: "YOUR_REALITY_PRIVATE_KEY_HERE"
short_ids: ["0123456789abcdef"]
dest: "www.google.com:443"
vision: true
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
udp_enabled: true
+39
View File
@@ -0,0 +1,39 @@
# Basic Reality server with VLESS
# Provides enhanced privacy through TLS 1.3 mimicry
#
# Reality uses X25519 key exchange and HMAC-signed certificates to provide
# enhanced privacy. Non-Reality traffic is forwarded to the fallback
# destination, making the server look like a real website to observers.
#
# Generate a Reality keypair with: an X25519 keypair generation tool
- address: "0.0.0.0:443"
protocol:
type: tls
reality_targets:
"www.cloudflare.com":
# X25519 private key (32 bytes, base64url encoded)
# Keep this secret! Clients need the corresponding public key.
private_key: "YOUR_PRIVATE_KEY_HERE"
# List of valid short IDs (hex strings, 0-16 chars)
# Clients must use one of these IDs to connect
# Empty string "" allows all clients (less secure but convenient)
short_ids:
- "0123456789abcdef" # Specific client ID
- "" # Allow all clients
# Fallback destination for non-Reality traffic
# This should be a real website - traffic that doesn't match Reality
# protocol will be forwarded here, making observers see a real site
dest: "www.cloudflare.com:443"
# Optional: Maximum timestamp difference in milliseconds (default: 60000)
# Prevents replay attacks by rejecting connections with old timestamps
max_time_diff: 60000 # 1 minute
# Inner protocol - the actual proxy protocol used after Reality handshake
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
udp_enabled: true # Enable XUDP for UDP multiplexing
+45
View File
@@ -0,0 +1,45 @@
# Reality client configuration
# Connects to a Reality server with censorship resistance
#
# This example shows how to configure a client to connect to a Reality server
# The client needs the server's public key and a valid short ID
# Local SOCKS proxy that uses Reality client
- address: "127.0.0.1:1080"
protocol:
type: socks
rules:
- masks: "0.0.0.0/0"
action: allow
client_proxy:
address: "your-server.example.com:443"
protocol:
type: reality
# Server's public key (from keypair generation)
public_key: "SERVER_PUBLIC_KEY_HERE"
# Your client ID (must match server's short_ids list)
short_id: "0123456789abcdef"
# Hostname to use (should match server's reality_targets key)
sni_hostname: "www.cloudflare.com"
# Inner protocol - must match what the server expects
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
# Alternative: HTTP proxy using Reality
- address: "127.0.0.1:8080"
protocol:
type: http
rules:
- masks: "0.0.0.0/0"
action: allow
client_proxy:
address: "your-server.example.com:443"
protocol:
type: reality
public_key: "SERVER_PUBLIC_KEY_HERE"
short_id: "0123456789abcdef"
sni_hostname: "www.cloudflare.com"
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
+56
View File
@@ -0,0 +1,56 @@
# Reality server with Trojan inner protocol
# Shows Reality works with protocols other than VLESS (when Vision is disabled)
#
# Reality provides censorship resistance regardless of the inner protocol
# However, Vision (TLS-in-TLS optimization) only works with VLESS
# This example uses Trojan without Vision
- address: "0.0.0.0:443"
protocol:
type: tls
reality_targets:
"www.cloudflare.com":
private_key: "YOUR_PRIVATE_KEY_HERE"
short_ids: ["0123456789abcdef"]
dest: "www.cloudflare.com:443"
# vision: false (default)
# Vision is not enabled because Trojan is the inner protocol
# Vision only works with VLESS
protocol:
type: trojan
password: "your-trojan-password"
# Multiple Reality targets with different inner protocols
- address: "0.0.0.0:8443"
protocol:
type: tls
reality_targets:
# VLESS target
"vless.example.com":
private_key: "YOUR_PRIVATE_KEY_1"
short_ids: ["0123456789abcdef"]
dest: "www.cloudflare.com:443"
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
# Trojan target
"trojan.example.com":
private_key: "YOUR_PRIVATE_KEY_2"
short_ids: ["1234567890abcdef"]
dest: "www.google.com:443"
protocol:
type: trojan
password: "trojan-password"
# Shadowsocks target
"ss.example.com":
private_key: "YOUR_PRIVATE_KEY_3"
short_ids: ["abcdef0123456789"]
dest: "www.microsoft.com:443"
protocol:
type: shadowsocks
cipher: 2022-blake3-chacha20-ietf-poly1305
password: "N5tp51JibWA9kIPEUoDHzyyjfgkDYToOERh+u9L47fk="
+31
View File
@@ -0,0 +1,31 @@
# Reality + Vision server
# Combines censorship resistance with TLS-in-TLS optimization
#
# Reality provides censorship resistance through TLS 1.3 mimicry
# Vision provides zero-copy performance when TLS-in-TLS is detected
# Together they offer both benefits
- address: "0.0.0.0:443"
protocol:
type: tls
reality_targets:
"www.google.com":
private_key: "YOUR_PRIVATE_KEY_HERE"
short_ids: ["0123456789abcdef"]
dest: "www.google.com:443"
# Enable XTLS-Vision for TLS-in-TLS optimization
# When enabled, Vision detects TLS-in-TLS scenarios and switches
# to Direct mode for zero-copy performance
# IMPORTANT: Vision requires VLESS as the inner protocol
vision: true
protocol:
type: vless # Vision REQUIRES VLESS
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
udp_enabled: true
# Benefits of Reality + Vision:
# - Reality: Censorship resistance, looks like real TLS traffic
# - Vision: Zero-copy performance, better anti-detection
# - Combined: Maximum security and performance
+31
View File
@@ -0,0 +1,31 @@
# Reality + Vision client configuration
# Maximum performance and censorship resistance
#
# Reality provides censorship resistance through TLS 1.3 mimicry
# Vision provides zero-copy performance for TLS-in-TLS scenarios
# Together they offer both benefits with VLESS as the inner protocol
# Local SOCKS proxy using Reality + Vision
- address: "127.0.0.1:1080"
protocol:
type: socks
rules:
- masks: "0.0.0.0/0"
action: allow
client_proxy:
address: "your-server.example.com:443"
protocol:
type: reality
public_key: "SERVER_PUBLIC_KEY_HERE"
short_id: "0123456789abcdef"
sni_hostname: "www.google.com"
vision: true # Enable Vision (must use VLESS)
protocol:
type: vless # Vision REQUIRES VLESS
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
# Benefits of Reality + Vision client:
# - Censorship resistance: Looks like normal TLS traffic to censors
# - Performance: Vision provides zero-copy for TLS-in-TLS
# - Anti-detection: Vision adds padding to mimic real TLS patterns
# - Compatibility: Works with CDNs and middleboxes
+44
View File
@@ -50,3 +50,47 @@
address: "proxy.example.com:1080"
protocol:
type: socks
# VLESS over Reality (censorship resistance)
- address: "0.0.0.0:8443"
protocol:
type: tls
reality_targets:
"www.cloudflare.com":
private_key: "YOUR_PRIVATE_KEY_HERE"
short_ids: ["0123456789abcdef", ""]
dest: "www.cloudflare.com:443"
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
udp_enabled: true
# VLESS over Reality with Vision (censorship + performance)
- address: "0.0.0.0:9443"
protocol:
type: tls
reality_targets:
"www.google.com":
private_key: "YOUR_PRIVATE_KEY_HERE"
short_ids: ["0123456789abcdef"]
dest: "www.google.com:443"
vision: true # Enable Vision
protocol:
type: vless # Vision requires VLESS
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
udp_enabled: true
# VLESS over TLS with Vision (no Reality)
- address: "0.0.0.0:10443"
protocol:
type: tls
tls_targets:
"vless.example.com":
cert: "cert.pem"
key: "key.pem"
vision: true # Enable Vision
alpn_protocols: ["http/1.1"]
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
udp_enabled: true
+60 -5
View File
@@ -472,14 +472,69 @@ impl NetLocationMask {
};
pub fn from(s: &str) -> std::io::Result<Self> {
let (address_mask_str, port) = match s.find(':') {
Some(i) => {
let port = s[i + 1..]
// Handle IPv6 with port: [::1/128]:80
if s.starts_with('[') {
if let Some(bracket_end) = s.find(']') {
let address_mask_str = &s[1..bracket_end];
let port = if s.len() > bracket_end + 1 && s.as_bytes()[bracket_end + 1] == b':' {
s[bracket_end + 2..]
.parse::<u16>()
.map_err(|e| std::io::Error::other(format!("Failed to parse port: {e}")))?
} else {
0
};
return Ok(Self {
address_mask: AddressMask::from(address_mask_str)?,
port,
});
}
}
// For addresses without brackets, we need to distinguish IPv6 from port notation.
// IPv6 addresses contain multiple colons. If there's a `/` (CIDR), the port must come
// after it. If no `/`, use rfind(':') and check if what follows looks like a port.
let (address_mask_str, port) = if let Some(slash_pos) = s.rfind('/') {
// Has CIDR notation. Port (if any) must come after the slash.
// Format: addr/bits:port or addr/bits
let after_slash = &s[slash_pos + 1..];
if let Some(colon_in_suffix) = after_slash.find(':') {
// There's a port after the CIDR
let bits_str = &after_slash[..colon_in_suffix];
let port_str = &after_slash[colon_in_suffix + 1..];
// Validate bits is a number
bits_str.parse::<u8>().map_err(|e| {
std::io::Error::other(format!("Failed to parse netmask bits: {e}"))
})?;
let port = port_str
.parse::<u16>()
.map_err(|e| std::io::Error::other(format!("Failed to parse port: {e}")))?;
(&s[0..i], port)
(&s[..slash_pos + 1 + colon_in_suffix], port)
} else {
// No port, just addr/bits
(s, 0)
}
} else {
// No CIDR notation. Could be:
// - hostname:port (example.com:80)
// - IPv4:port (1.2.3.4:80)
// - IPv6 (::1, fe80::1)
// - hostname (example.com)
// - IPv4 (1.2.3.4)
//
// IPv6 addresses have multiple colons, so if there's more than one colon,
// it's IPv6 and has no port. If there's exactly one colon, use it as port separator.
let colon_count = s.chars().filter(|&c| c == ':').count();
if colon_count == 1 {
// Single colon - treat as port separator
let colon_pos = s.find(':').unwrap();
let port = s[colon_pos + 1..]
.parse::<u16>()
.map_err(|e| std::io::Error::other(format!("Failed to parse port: {e}")))?;
(&s[..colon_pos], port)
} else {
// Zero or multiple colons - no port (IPv6 or plain hostname/IPv4)
(s, 0)
}
None => (s, 0),
};
Ok(Self {
+82 -32
View File
@@ -80,6 +80,28 @@ pub trait AsyncWriteSourcedMessage {
) -> Poll<std::io::Result<()>>;
}
/// Session-based message reading trait. Used by protocols like XUDP that have session IDs.
/// Returns (session_id, data, source_addr) tuples.
pub trait AsyncReadSessionMessage {
fn poll_read_session_message(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<(u16, SocketAddr)>>;
}
/// Session-based message writing trait. Used by protocols like XUDP that have session IDs.
/// Writes data for a specific session ID to a target address.
pub trait AsyncWriteSessionMessage {
fn poll_write_session_message(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
session_id: u16,
buf: &[u8],
target: &SocketAddr,
) -> Poll<std::io::Result<()>>;
}
impl AsyncReadMessage for UdpSocket {
fn poll_read_message(
self: Pin<&mut Self>,
@@ -158,6 +180,19 @@ pub trait AsyncSourcedMessageStream:
{
}
/// Session-based stream trait for protocols like XUDP that multiplex sessions over a single connection.
/// Reads return (session_id, data, source_addr) and writes target (session_id, data, target_addr).
pub trait AsyncSessionMessageStream:
AsyncReadSessionMessage
+ AsyncWriteSessionMessage
+ AsyncFlushMessage
+ AsyncShutdownMessage
+ AsyncPing
+ Unpin
+ Send
{
}
impl AsyncPing for TcpStream {
fn supports_ping(&self) -> bool {
false
@@ -196,38 +231,6 @@ impl AsyncPing for UdpSocket {
impl AsyncMessageStream for UdpSocket {}
impl<AS> AsyncPing for tokio_rustls::client::TlsStream<AS>
where
AS: AsyncPing + Unpin,
{
fn supports_ping(&self) -> bool {
self.get_ref().0.supports_ping()
}
fn poll_write_ping(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
let this = self.get_mut();
Pin::new(this.get_mut().0).poll_write_ping(cx)
}
}
impl<AS> AsyncStream for tokio_rustls::client::TlsStream<AS> where AS: AsyncStream {}
impl<AS> AsyncPing for tokio_rustls::server::TlsStream<AS>
where
AS: AsyncPing + Unpin,
{
fn supports_ping(&self) -> bool {
self.get_ref().0.supports_ping()
}
fn poll_write_ping(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
let this = self.get_mut();
Pin::new(this.get_mut().0).poll_write_ping(cx)
}
}
impl<AS> AsyncStream for tokio_rustls::server::TlsStream<AS> where AS: AsyncStream {}
// pattern copied from deref_async_read macro: https://docs.rs/tokio/latest/src/tokio/io/async_read.rs.html#60
impl<T: ?Sized + AsyncPing + Unpin> AsyncPing for Box<T> {
fn supports_ping(&self) -> bool {
@@ -426,3 +429,50 @@ impl<T: ?Sized + AsyncTargetedMessageStream + Unpin> AsyncTargetedMessageStream
impl<T: ?Sized + AsyncSourcedMessageStream + Unpin> AsyncSourcedMessageStream for Box<T> {}
impl<T: ?Sized + AsyncSourcedMessageStream + Unpin> AsyncSourcedMessageStream for &mut T {}
impl<T: ?Sized + AsyncReadSessionMessage + Unpin> AsyncReadSessionMessage for Box<T> {
fn poll_read_session_message(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<(u16, SocketAddr)>> {
Pin::new(&mut **self).poll_read_session_message(cx, buf)
}
}
impl<T: ?Sized + AsyncReadSessionMessage + Unpin> AsyncReadSessionMessage for &mut T {
fn poll_read_session_message(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<(u16, SocketAddr)>> {
Pin::new(&mut **self).poll_read_session_message(cx, buf)
}
}
impl<T: ?Sized + AsyncWriteSessionMessage + Unpin> AsyncWriteSessionMessage for Box<T> {
fn poll_write_session_message(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
session_id: u16,
buf: &[u8],
target: &SocketAddr,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut **self).poll_write_session_message(cx, session_id, buf, target)
}
}
impl<T: ?Sized + AsyncWriteSessionMessage + Unpin> AsyncWriteSessionMessage for &mut T {
fn poll_write_session_message(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
session_id: u16,
buf: &[u8],
target: &SocketAddr,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut **self).poll_write_session_message(cx, session_id, buf, target)
}
}
impl<T: ?Sized + AsyncSessionMessageStream + Unpin> AsyncSessionMessageStream for Box<T> {}
impl<T: ?Sized + AsyncSessionMessageStream + Unpin> AsyncSessionMessageStream for &mut T {}
File diff suppressed because it is too large Load Diff
+366 -37
View File
@@ -3,7 +3,6 @@ mod types;
pub use types::*;
use std::collections::HashMap;
use std::path::Path;
use log::debug;
@@ -100,7 +99,11 @@ pub async fn convert_cert_paths(all_configs: Vec<Config>) -> std::io::Result<(Ve
// we do this as a separate step because we can't load the data in an async manner
// right away due to recursion.
for config in server_configs.iter_mut() {
gather_pem_file_paths_from_server_config(config, &path_pem_configs, &mut unknown_pem_paths);
gather_pem_file_paths_from_server_config(
config,
&path_pem_configs,
&mut unknown_pem_paths,
)?;
}
for config in client_group_configs.iter_mut() {
@@ -227,7 +230,7 @@ fn gather_pem_file_paths_from_server_config(
server_config: &mut ServerConfig,
known_pem_paths: &HashMap<String, NamedPem>,
unknown_pem_paths: &mut HashMap<String, String>,
) {
) -> std::io::Result<()> {
// Check QUIC settings
if let Some(ref mut quic_settings) = server_config.quic_settings {
process_pem_path(&mut quic_settings.cert, known_pem_paths, unknown_pem_paths);
@@ -242,12 +245,13 @@ fn gather_pem_file_paths_from_server_config(
&mut server_config.protocol,
known_pem_paths,
unknown_pem_paths,
);
)?;
// Check rules
for rule in server_config.rules.iter_mut() {
gather_pem_file_paths_from_rule(rule, known_pem_paths, unknown_pem_paths);
}
Ok(())
}
fn gather_pem_file_paths_from_client_config_group(
@@ -260,31 +264,126 @@ fn gather_pem_file_paths_from_client_config_group(
}
}
/// Validates that Vision is only enabled when the inner protocol is VLESS.
///
/// Vision (XTLS-RPRX-Vision) is a TLS-in-TLS optimization protocol that requires
/// VLESS as the inner protocol to function correctly.
fn validate_vision_protocol(
vision_enabled: bool,
protocol: &ServerProxyConfig,
config_type: &str,
) -> std::io::Result<()> {
if !vision_enabled {
// Vision not enabled, no validation needed
return Ok(());
}
// Vision is enabled - verify inner protocol is VLESS
match protocol {
ServerProxyConfig::Vless { .. } => {
// Valid: Vision + VLESS
Ok(())
}
other_protocol => {
// Invalid: Vision requires VLESS
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"{} config has vision=true but inner protocol is {} (not VLESS). \
Vision (XTLS-RPRX-Vision) requires VLESS as the inner protocol. \
Either set vision=false or change the inner protocol to VLESS.",
config_type, other_protocol
),
))
}
}
}
/// Validates that Vision is only enabled when the inner protocol is VLESS (client-side)
fn validate_client_vision_protocol(
vision_enabled: bool,
protocol: &ClientProxyConfig,
config_type: &str,
) -> std::io::Result<()> {
if !vision_enabled {
return Ok(());
}
match protocol {
ClientProxyConfig::Vless { .. } => Ok(()),
other_protocol => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"{} client config has vision=true but inner protocol is {} (not VLESS). \
Vision (XTLS-RPRX-Vision) requires VLESS as the inner protocol. \
Either set vision=false or change the inner protocol to VLESS.",
config_type,
other_protocol.protocol_name()
),
)),
}
}
/// Recursive validation of client proxy config structure (Vision rules, etc.)
fn validate_client_proxy_structure(config: &ClientProxyConfig) -> std::io::Result<()> {
match config {
ClientProxyConfig::Tls(tls_config) => {
validate_client_vision_protocol(tls_config.vision, &tls_config.protocol, "TLS")?;
validate_client_proxy_structure(&tls_config.protocol)
}
ClientProxyConfig::Reality {
vision, protocol, ..
} => {
validate_client_vision_protocol(*vision, protocol, "Reality")?;
validate_client_proxy_structure(protocol)
}
ClientProxyConfig::ShadowTls { protocol, .. } => validate_client_proxy_structure(protocol),
ClientProxyConfig::Websocket(ws_config) => {
validate_client_proxy_structure(&ws_config.protocol)
}
// Leaf protocols
_ => Ok(()),
}
}
fn gather_pem_file_paths_from_server_proxy(
server_proxy: &mut ServerProxyConfig,
known_pem_paths: &HashMap<String, NamedPem>,
unknown_pem_paths: &mut HashMap<String, String>,
) {
) -> std::io::Result<()> {
match server_proxy {
ServerProxyConfig::Tls {
tls_targets,
default_tls_target,
shadowtls_targets,
reality_targets,
..
} => {
// Process TLS targets
for (_, tls_config) in tls_targets.iter_mut() {
for (sni, tls_config) in tls_targets.iter_mut() {
process_pem_path(&mut tls_config.cert, known_pem_paths, unknown_pem_paths);
process_pem_path(&mut tls_config.key, known_pem_paths, unknown_pem_paths);
for cert in tls_config.client_ca_certs.iter_mut() {
process_pem_path(cert, known_pem_paths, unknown_pem_paths);
}
// Validate Vision configuration
validate_vision_protocol(
tls_config.vision,
&tls_config.protocol,
&format!("TLS target '{}'", sni),
)?;
// Recurse into inner protocol
gather_pem_file_paths_from_server_proxy(
&mut tls_config.protocol,
known_pem_paths,
unknown_pem_paths,
);
)?;
// Check override rules
for rule in tls_config.override_rules.iter_mut() {
gather_pem_file_paths_from_rule(rule, known_pem_paths, unknown_pem_paths);
@@ -298,12 +397,20 @@ fn gather_pem_file_paths_from_server_proxy(
for cert in tls_config.client_ca_certs.iter_mut() {
process_pem_path(cert, known_pem_paths, unknown_pem_paths);
}
// Validate Vision configuration
validate_vision_protocol(
tls_config.vision,
&tls_config.protocol,
"default TLS target",
)?;
// Recurse into inner protocol
gather_pem_file_paths_from_server_proxy(
&mut tls_config.protocol,
known_pem_paths,
unknown_pem_paths,
);
)?;
// Check override rules
for rule in tls_config.override_rules.iter_mut() {
gather_pem_file_paths_from_rule(rule, known_pem_paths, unknown_pem_paths);
@@ -323,12 +430,33 @@ fn gather_pem_file_paths_from_server_proxy(
&mut shadowtls_config.protocol,
known_pem_paths,
unknown_pem_paths,
);
)?;
// Check override rules
for rule in shadowtls_config.override_rules.iter_mut() {
gather_pem_file_paths_from_rule(rule, known_pem_paths, unknown_pem_paths);
}
}
// Process Reality targets
for (sni, reality_config) in reality_targets.iter_mut() {
// Validate Vision configuration
validate_vision_protocol(
reality_config.vision,
&reality_config.protocol,
&format!("Reality target '{}'", sni),
)?;
// Recurse into inner protocol
gather_pem_file_paths_from_server_proxy(
&mut reality_config.protocol,
known_pem_paths,
unknown_pem_paths,
)?;
// Check override rules
for rule in reality_config.override_rules.iter_mut() {
gather_pem_file_paths_from_rule(rule, known_pem_paths, unknown_pem_paths);
}
}
}
ServerProxyConfig::Websocket { targets } => {
for websocket_config in targets.iter_mut() {
@@ -337,15 +465,17 @@ fn gather_pem_file_paths_from_server_proxy(
&mut websocket_config.protocol,
known_pem_paths,
unknown_pem_paths,
);
)?;
// Check override rules
for rule in websocket_config.override_rules.iter_mut() {
gather_pem_file_paths_from_rule(rule, known_pem_paths, unknown_pem_paths);
}
}
}
ServerProxyConfig::Vless { .. } => {}
_ => {}
}
Ok(())
}
fn gather_pem_file_paths_from_rule(
@@ -499,7 +629,135 @@ fn validate_client_fingerprints(
if client_fingerprints.iter().any(|fp| fp == "any") {
let _ = std::mem::replace(client_fingerprints, NoneOrSome::Unspecified);
} else {
let _ = crate::rustls_util::process_fingerprints(&client_fingerprints.clone().into_vec())?;
let _ = crate::rustls_config_util::process_fingerprints(
&client_fingerprints.clone().into_vec(),
)?;
}
Ok(())
}
/// Validates Reality short_ids to ensure they are valid hexadecimal strings.
///
/// Each short_id must be:
/// - Valid hexadecimal (0-9, a-f, A-F)
/// - Even length (to decode to whole bytes)
/// - Maximum 16 characters (8 bytes when decoded)
///
/// Validates Reality private_key to ensure it's a valid base64url-encoded X25519 key.
///
/// The private key must:
/// - Be valid base64url encoding
/// - Decode to exactly 32 bytes (X25519 key size)
fn validate_reality_private_key(private_key: &str, target_name: &str) -> std::io::Result<()> {
use crate::reality::decode_private_key;
decode_private_key(private_key).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"REALITY target '{}': invalid private_key: {}",
target_name, e
),
)
})?;
Ok(())
}
/// Validates Reality client short_id to ensure it's a valid hexadecimal string
fn validate_reality_client_short_id(short_id: &str) -> std::io::Result<()> {
use crate::reality::decode_short_id;
// Check length
if short_id.len() > 16 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Reality client short_id is too long: '{}' ({} chars, max 16)",
short_id,
short_id.len()
),
));
}
// Check for valid hexadecimal characters
if !short_id.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Reality client short_id contains non-hexadecimal characters: '{}'. \
Only 0-9, a-f, and A-F are allowed.",
short_id
),
));
}
// Try to decode to validate format
decode_short_id(short_id).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Reality client short_id decode failed: {}", e),
)
})?;
Ok(())
}
fn validate_reality_server_short_ids(
short_ids: &crate::option_util::OneOrSome<String>,
target_name: &str,
) -> std::io::Result<()> {
// OneOrSome ensures at least one short_id is always present (default is all zeros)
// Check if it's exactly one short_id with the default all-zeros value
let is_default = match short_ids {
crate::option_util::OneOrSome::One(id) => {
id == crate::config::types::DEFAULT_REALITY_SHORT_ID
}
crate::option_util::OneOrSome::Some(ids) => {
ids.len() == 1 && ids[0] == crate::config::types::DEFAULT_REALITY_SHORT_ID
}
};
if is_default {
log::warn!(
"Reality server '{}' using default short_ids (all zeros). \
For better security in production, configure explicit short_ids.",
target_name
);
}
for (i, short_id) in short_ids.iter().enumerate() {
// Check length
if short_id.len() > 16 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"REALITY target '{}': short_ids[{}] is too long: '{}' ({} chars, max 16)",
target_name,
i,
short_id,
short_id.len()
),
));
}
// Check for valid hexadecimal characters
if !short_id.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"REALITY target '{}': short_ids[{}] contains non-hexadecimal characters: '{}'. \
Only 0-9, a-f, and A-F are allowed.",
target_name, i, short_id
),
));
}
// Empty string is technically valid (will be left-padded to 16 zeros)
// but odd-length strings are also valid (will be left-padded)
// The decode_short_id function handles padding internally
}
Ok(())
@@ -566,7 +824,9 @@ fn validate_server_fingerprints(
if server_fingerprints.iter().any(|fp| fp == "any") {
let _ = std::mem::replace(server_fingerprints, NoneOrSome::Unspecified);
} else {
let _ = crate::rustls_util::process_fingerprints(&server_fingerprints.clone().into_vec())?;
let _ = crate::rustls_config_util::process_fingerprints(
&server_fingerprints.clone().into_vec(),
)?;
}
Ok(())
@@ -576,21 +836,45 @@ fn validate_client_proxy_config(
client_proxy_config: &mut ClientProxyConfig,
named_pems: &HashMap<String, String>,
) -> std::io::Result<()> {
if let ClientProxyConfig::Tls(ref mut tls_config) = client_proxy_config {
// Embed certificates before validation
embed_optional_pem_from_map(&mut tls_config.cert, named_pems);
embed_optional_pem_from_map(&mut tls_config.key, named_pems);
// First validate the structure (Vision rules, etc.)
validate_client_proxy_structure(client_proxy_config)?;
if tls_config.cert.is_none() != tls_config.key.is_none() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Both client cert and key have to be specified, or both have to be omitted",
));
match client_proxy_config {
ClientProxyConfig::Reality {
short_id, protocol, ..
} => {
// Validate short_id format
validate_reality_client_short_id(short_id)?;
// Warn if using default all-zeros short_id
if short_id == crate::config::types::DEFAULT_REALITY_SHORT_ID {
log::warn!(
"Reality client using default short_id (all zeros). \
For better security in production, configure an explicit short_id that matches your server."
);
}
// Recurse into inner protocol
validate_client_proxy_config(protocol, named_pems)?;
}
validate_server_fingerprints(&mut tls_config.server_fingerprints)?;
// Recursively validate inner protocol
validate_client_proxy_config(&mut tls_config.protocol, named_pems)?;
ClientProxyConfig::Tls(ref mut tls_config) => {
// Embed certificates before validation
embed_optional_pem_from_map(&mut tls_config.cert, named_pems);
embed_optional_pem_from_map(&mut tls_config.key, named_pems);
if tls_config.cert.is_none() != tls_config.key.is_none() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Both client cert and key have to be specified, or both have to be omitted",
));
}
validate_server_fingerprints(&mut tls_config.server_fingerprints)?;
// Recursively validate inner protocol
validate_client_proxy_config(&mut tls_config.protocol, named_pems)?;
}
_ => {}
}
Ok(())
}
@@ -612,8 +896,19 @@ fn validate_server_proxy_config(
tls_targets,
default_tls_target,
shadowtls_targets,
reality_targets,
tls_buffer_size,
} => {
if tls_targets.is_empty()
&& default_tls_target.is_none()
&& shadowtls_targets.is_empty()
&& reality_targets.is_empty()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"TLS server has no entries",
));
}
for (_, tls_server_config) in tls_targets.iter_mut() {
// Embed certificates
embed_pem_from_map(&mut tls_server_config.cert, named_pems);
@@ -704,6 +999,53 @@ fn validate_server_proxy_config(
}
}
for (sni_hostname, reality_config) in reality_targets.iter_mut() {
if tls_targets.contains_key(sni_hostname) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"duplicated SNI hostname between TLS and REALITY targets: {sni_hostname}"
),
));
}
if shadowtls_targets.contains_key(sni_hostname) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"duplicated SNI hostname between ShadowTLS and REALITY targets: {sni_hostname}"
),
));
}
// Validate Reality private_key (must be valid base64url-encoded X25519 key)
validate_reality_private_key(&reality_config.private_key, sni_hostname)?;
// Validate Reality short_ids (must be valid hex strings)
validate_reality_server_short_ids(&reality_config.short_ids, sni_hostname)?;
// Validate inner protocol
validate_server_proxy_config(
&mut reality_config.protocol,
client_groups,
rule_groups,
named_pems,
)?;
// Validate override_rules
ConfigSelection::replace_none_or_some_groups(
&mut reality_config.override_rules,
rule_groups,
)?;
for rule_config_selection in reality_config.override_rules.iter_mut() {
validate_rule_config(
rule_config_selection.unwrap_config_mut(),
client_groups,
named_pems,
)?;
}
}
if let Some(size) = tls_buffer_size {
if *size < MIN_TLS_BUFFER_SIZE {
return Err(std::io::Error::new(
@@ -821,19 +1163,6 @@ fn embed_optional_pem_from_map(pem: &mut Option<String>, named_pems: &HashMap<St
}
}
#[allow(dead_code)]
pub async fn save_config(path: &Path, configs: &[Config]) -> std::io::Result<()> {
let yaml_str = serde_yaml::to_string(configs).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to serialize to YAML: {e}"),
)
})?;
tokio::fs::write(path, yaml_str).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
+336 -50
View File
@@ -6,6 +6,9 @@ use serde::{Deserialize, Serialize};
use crate::address::{NetLocation, NetLocationMask, NetLocationPortRange};
use crate::option_util::{NoneOrOne, NoneOrSome, OneOrSome};
/// Default Reality short_id: all zeros (16 hex chars = 8 bytes of zeros)
pub const DEFAULT_REALITY_SHORT_ID: &str = "0000000000000000";
fn default_true() -> bool {
true
}
@@ -14,6 +17,86 @@ fn default_snell_udp_num_sockets() -> usize {
1
}
fn default_reality_client_short_id() -> String {
DEFAULT_REALITY_SHORT_ID.to_string()
}
fn default_reality_server_short_ids() -> OneOrSome<String> {
OneOrSome::One(DEFAULT_REALITY_SHORT_ID.to_string())
}
/// Custom deserializer for ServerProxyConfig::Vmess that validates legacy force_aead field
fn deserialize_vmess_server<'de, D>(deserializer: D) -> Result<(String, String, bool), D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
struct VmessServerTemp {
cipher: String,
user_id: String,
#[serde(default = "default_true")]
udp_enabled: bool,
#[serde(default)]
force_aead: Option<bool>,
}
let temp = VmessServerTemp::deserialize(deserializer)?;
// Check if force_aead was explicitly set
if let Some(force_aead_value) = temp.force_aead {
if !force_aead_value {
return Err(Error::custom(
"Non-AEAD VMess mode (force_aead=false) is no longer supported. \
Please remove the force_aead field from your configuration, or set it to true.",
));
}
// Warn about deprecated field
log::warn!(
"The 'force_aead' field in VMess server configuration is deprecated and will be removed in a future version. \
AEAD mode is now always enabled. Please remove this field from your configuration."
);
}
Ok((temp.cipher, temp.user_id, temp.udp_enabled))
}
/// Custom deserializer for ClientProxyConfig::Vmess that validates legacy aead field
fn deserialize_vmess_client<'de, D>(deserializer: D) -> Result<(String, String), D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
struct VmessClientTemp {
cipher: String,
user_id: String,
#[serde(default)]
aead: Option<bool>,
}
let temp = VmessClientTemp::deserialize(deserializer)?;
// Check if aead was explicitly set
if let Some(aead_value) = temp.aead {
if !aead_value {
return Err(Error::custom(
"Non-AEAD VMess mode (aead=false) is no longer supported. \
Please remove the aead field from your configuration, or set it to true.",
));
}
// Warn about deprecated field
log::warn!(
"The 'aead' field in VMess client configuration is deprecated and will be removed in a future version. \
AEAD mode is now always enabled. Please remove this field from your configuration."
);
}
Ok((temp.cipher, temp.user_id))
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum BindLocation {
@@ -30,20 +113,15 @@ impl std::fmt::Display for BindLocation {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum Transport {
#[default]
Tcp,
Quic,
Udp,
}
impl Default for Transport {
fn default() -> Self {
Self::Tcp
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TcpConfig {
#[serde(default = "default_true")]
@@ -322,6 +400,51 @@ pub struct ShadowsocksConfig {
pub password: String,
}
// REALITY Protocol Configuration
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RealityServerConfig {
/// X25519 private key (32 bytes, base64url encoded)
pub private_key: String,
/// List of valid short IDs (hex strings, 0-16 chars each)
#[serde(alias = "short_id", default = "default_reality_server_short_ids")]
pub short_ids: OneOrSome<String>,
/// Fallback destination (e.g., "example.com:443")
pub dest: NetLocation,
/// Maximum timestamp difference in milliseconds (optional)
#[serde(default = "default_reality_time_diff")]
pub max_time_diff: Option<u64>,
/// Minimum client version [major, minor, patch] (optional)
#[serde(default)]
pub min_client_version: Option<[u8; 3]>,
/// Maximum client version [major, minor, patch] (optional)
#[serde(default)]
pub max_client_version: Option<[u8; 3]>,
/// Enable XTLS-Vision protocol for TLS-in-TLS optimization.
/// When enabled, the inner protocol MUST be VLESS.
/// Vision detects TLS-in-TLS scenarios and switches to Direct mode for zero-copy performance.
/// Reality provides censorship resistance while Vision provides performance optimization.
#[serde(default)]
pub vision: bool,
/// Inner protocol (VLESS, Trojan, etc.)
pub protocol: ServerProxyConfig,
/// Override rules
#[serde(alias = "override_rule", default)]
pub override_rules: NoneOrSome<ConfigSelection<RuleConfig>>,
}
fn default_reality_time_diff() -> Option<u64> {
// 1 minute
Some(1000 * 60)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TlsServerConfig {
pub cert: String,
@@ -352,6 +475,12 @@ pub struct TlsServerConfig {
#[serde(alias = "client_fingerprint", default)]
pub client_fingerprints: NoneOrSome<String>,
/// Enable XTLS-Vision protocol for TLS-in-TLS optimization.
/// When enabled, the inner protocol MUST be VLESS.
/// Vision detects TLS-in-TLS scenarios and switches to Direct mode for zero-copy performance.
/// Requires TLS 1.3.
#[serde(default)]
pub vision: bool,
pub protocol: ServerProxyConfig,
#[serde(alias = "override_rule", default)]
@@ -556,16 +685,17 @@ pub enum ServerProxyConfig {
default_tls_target: Option<Box<TlsServerConfig>>,
#[serde(default)]
shadowtls_targets: HashMap<String, ShadowTlsServerConfig>,
#[serde(default)]
reality_targets: HashMap<String, RealityServerConfig>,
#[serde(default)]
tls_buffer_size: Option<usize>,
},
#[serde(deserialize_with = "deserialize_vmess_server")]
Vmess {
cipher: String,
user_id: String,
#[serde(default = "default_true")]
force_aead: bool,
#[serde(default = "default_true")]
udp_enabled: bool,
},
#[serde(alias = "ws")]
@@ -589,39 +719,47 @@ pub enum ServerProxyConfig {
impl std::fmt::Display for ServerProxyConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Http { .. } => "HTTP",
Self::Socks { .. } => "SOCKS",
Self::Shadowsocks { .. } => "Shadowsocks",
Self::Snell { .. } => "Snell",
Self::Vless { .. } => "Vless",
Self::Trojan { .. } => "Trojan",
Self::Tls {
tls_targets,
default_tls_target,
shadowtls_targets,
..
} => {
let has_tls = !tls_targets.is_empty() || !default_tls_target.is_none();
let has_shadowtls = !shadowtls_targets.is_empty();
if has_tls && has_shadowtls {
"TLS+ShadowTLSv3"
} else if has_shadowtls {
"ShadowTLSv3"
} else {
"TLS"
}
match self {
Self::Http { .. } => write!(f, "HTTP"),
Self::Socks { .. } => write!(f, "SOCKS"),
Self::Shadowsocks { .. } => write!(f, "Shadowsocks"),
Self::Snell { .. } => write!(f, "Snell"),
Self::Vless { .. } => write!(f, "Vless"),
Self::Trojan { .. } => write!(f, "Trojan"),
Self::Tls {
tls_targets,
default_tls_target,
shadowtls_targets,
reality_targets,
..
} => {
let mut parts = vec![];
if !tls_targets.is_empty() {
parts.push("TLS");
}
Self::Vmess { .. } => "Vmess",
Self::Websocket { .. } => "Websocket",
Self::PortForward { .. } => "Portforward",
Self::Hysteria2 { .. } => "Hysteria2",
Self::TuicV5 { .. } => "TuicV5",
if !reality_targets.is_empty() {
parts.push("REALITY");
}
if !shadowtls_targets.is_empty() {
parts.push("ShadowTLSv3");
}
if tls_targets.values().any(|cfg| cfg.vision)
|| default_tls_target.as_ref().is_some_and(|cfg| cfg.vision)
|| reality_targets.values().any(|cfg| cfg.vision)
{
parts.push("Vision");
}
write!(f, "{}", parts.join("+"))
}
)
Self::Vmess { .. } => write!(f, "Vmess"),
Self::Websocket { .. } => write!(f, "Websocket"),
Self::PortForward { .. } => write!(f, "Portforward"),
Self::Hysteria2 { .. } => write!(f, "Hysteria2"),
Self::TuicV5 { .. } => write!(f, "TuicV5"),
}
}
}
@@ -860,24 +998,160 @@ pub enum ClientProxyConfig {
#[serde(default)]
shadowsocks: Option<ShadowsocksConfig>,
},
Reality {
public_key: String,
#[serde(default = "default_reality_client_short_id")]
short_id: String,
#[serde(default)]
sni_hostname: Option<String>,
/// Enable XTLS-Vision protocol for TLS-in-TLS optimization.
/// When enabled, the inner protocol MUST be VLESS.
#[serde(default)]
vision: bool,
protocol: Box<ClientProxyConfig>,
},
#[serde(alias = "shadowtls")]
ShadowTls {
/// ShadowTLS password for authentication
password: String,
/// Optional SNI hostname override
#[serde(default)]
sni_hostname: Option<String>,
/// Inner protocol (typically VLESS, Trojan, etc.)
protocol: Box<ClientProxyConfig>,
},
#[serde(deserialize_with = "deserialize_tls_variant")]
Tls(TlsClientConfig),
#[serde(deserialize_with = "deserialize_vmess_client")]
Vmess {
cipher: String,
user_id: String,
#[serde(default = "default_true")]
aead: bool,
},
#[serde(alias = "ws")]
Websocket(WebsocketClientConfig),
#[serde(alias = "noop")]
PortForward,
}
impl ClientProxyConfig {
pub fn is_direct(&self) -> bool {
matches!(self, ClientProxyConfig::Direct)
}
/// Returns the protocol name for display/error messages
pub fn protocol_name(&self) -> &str {
match self {
ClientProxyConfig::Direct => "Direct",
ClientProxyConfig::Http { .. } => "HTTP",
ClientProxyConfig::Socks { .. } => "SOCKS5",
ClientProxyConfig::Shadowsocks(..) => "Shadowsocks",
ClientProxyConfig::Snell(..) => "Snell",
ClientProxyConfig::Vless { .. } => "VLESS",
ClientProxyConfig::Trojan { .. } => "Trojan",
ClientProxyConfig::Reality { .. } => "Reality",
ClientProxyConfig::Tls(..) => "TLS",
ClientProxyConfig::ShadowTls { .. } => "ShadowTLS",
ClientProxyConfig::Vmess { .. } => "VMess",
ClientProxyConfig::Websocket(..) => "WebSocket",
ClientProxyConfig::PortForward => "PortForward",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
/// Custom deserializer for TlsClientConfig that handles deprecated shadowtls_password field
fn deserialize_tls_client_config<'de, D>(deserializer: D) -> Result<TlsClientConfig, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
struct TlsClientConfigTemp {
#[serde(default = "default_true")]
verify: bool,
#[serde(alias = "server_fingerprint", default)]
server_fingerprints: NoneOrSome<String>,
#[serde(default)]
sni_hostname: NoneOrOne<String>,
#[serde(alias = "alpn_protocol", default)]
alpn_protocols: NoneOrSome<String>,
#[serde(default)]
tls_buffer_size: Option<usize>,
#[serde(default)]
key: Option<String>,
#[serde(default)]
cert: Option<String>,
#[serde(default)]
shadowtls_password: Option<String>,
#[serde(default)]
vision: bool,
protocol: Box<ClientProxyConfig>,
}
let temp = TlsClientConfigTemp::deserialize(deserializer)?;
// Check for mutually exclusive fields
if temp.vision && temp.shadowtls_password.is_some() {
return Err(Error::custom(
"TLS client config cannot have both vision=true and shadowtls_password set. \
Vision and ShadowTLS are incompatible. \
Use either 'vision: true' with regular TLS, or 'type: shadowtls' for ShadowTLS.",
));
}
// Check if deprecated shadowtls_password was used
if let Some(password) = temp.shadowtls_password {
log::warn!(
"The 'shadowtls_password' field in TLS client configuration is deprecated. \
Please use 'type: shadowtls' with 'password' field instead. \
This field will be removed in a future version."
);
// Transform to ShadowTLS variant internally by wrapping protocol
return Ok(TlsClientConfig {
verify: temp.verify,
server_fingerprints: temp.server_fingerprints,
sni_hostname: temp.sni_hostname.clone(),
alpn_protocols: temp.alpn_protocols,
tls_buffer_size: temp.tls_buffer_size,
key: temp.key,
cert: temp.cert,
vision: false,
protocol: Box::new(ClientProxyConfig::ShadowTls {
password,
sni_hostname: temp.sni_hostname.into_option(),
protocol: temp.protocol,
}),
});
}
// Normal case - no shadowtls_password
Ok(TlsClientConfig {
verify: temp.verify,
server_fingerprints: temp.server_fingerprints,
sni_hostname: temp.sni_hostname,
alpn_protocols: temp.alpn_protocols,
tls_buffer_size: temp.tls_buffer_size,
key: temp.key,
cert: temp.cert,
vision: temp.vision,
protocol: temp.protocol,
})
}
/// Variant deserializer for Tls in ClientProxyConfig enum
fn deserialize_tls_variant<'de, D>(deserializer: D) -> Result<TlsClientConfig, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_tls_client_config(deserializer)
}
#[derive(Debug, Clone, Serialize)]
pub struct TlsClientConfig {
#[serde(default = "default_true")]
pub verify: bool,
@@ -893,8 +1167,13 @@ pub struct TlsClientConfig {
pub key: Option<String>,
#[serde(default)]
pub cert: Option<String>,
/// Enable XTLS-Vision protocol for TLS-in-TLS optimization.
/// When enabled, the inner protocol MUST be VLESS.
/// Requires TLS 1.3.
#[serde(default)]
pub shadowtls_password: Option<String>,
pub vision: bool,
pub protocol: Box<ClientProxyConfig>,
}
@@ -1037,7 +1316,7 @@ mod tests {
transport: Transport::Tcp,
tcp_settings: Some(TcpConfig { no_delay: true }),
quic_settings: None,
rules: NoneOrSome::One(ConfigSelection::GroupName("test-rules".to_string())),
rules: NoneOrSome::None,
}
}
@@ -1067,7 +1346,7 @@ mod tests {
transport: Transport::Tcp,
tcp_settings: None,
quic_settings: None,
rules: NoneOrSome::One(ConfigSelection::GroupName("test-rules".to_string())),
rules: NoneOrSome::None,
}
}
@@ -1124,6 +1403,7 @@ mod tests {
alpn_protocols: NoneOrSome::Some(vec!["h2".to_string(), "http/1.1".to_string()]),
client_ca_certs: NoneOrSome::One("ca.crt".to_string()),
client_fingerprints: NoneOrSome::One("abc123".to_string()),
vision: false,
protocol: ServerProxyConfig::Http {
username: None,
password: None,
@@ -1144,6 +1424,7 @@ mod tests {
alpn_protocols: NoneOrSome::None,
client_ca_certs: NoneOrSome::None,
client_fingerprints: NoneOrSome::None,
vision: false,
protocol: ServerProxyConfig::Http {
username: None,
password: None,
@@ -1151,6 +1432,7 @@ mod tests {
override_rules: NoneOrSome::None,
})),
shadowtls_targets: HashMap::new(),
reality_targets: HashMap::new(),
tls_buffer_size: Some(8192),
},
transport: Transport::Tcp,
@@ -1168,7 +1450,6 @@ mod tests {
protocol: ServerProxyConfig::Vmess {
cipher: "aes-128-gcm".to_string(),
user_id: "b831381d-6324-4d53-ad4f-8cda48b30811".to_string(),
force_aead: true,
udp_enabled: false,
},
transport: Transport::Tcp,
@@ -1628,6 +1909,7 @@ mod tests {
client_ca_certs: NoneOrSome::None,
client_fingerprints: NoneOrSome::Unspecified,
alpn_protocols: NoneOrSome::Unspecified,
vision: false,
protocol: ServerProxyConfig::Http {
username: None,
password: None,
@@ -1639,13 +1921,14 @@ mod tests {
.collect(),
default_tls_target: None,
shadowtls_targets: HashMap::new(),
reality_targets: HashMap::new(),
tls_buffer_size: None,
},
transport: Transport::Tcp,
tcp_settings: None,
quic_settings: None,
rules: NoneOrSome::One(ConfigSelection::GroupName("allow-all-direct".to_string())),
}),
}),
];
let result = validate_configs_test(configs).await;
@@ -2065,7 +2348,7 @@ mod tests {
num_endpoints: 1,
}),
rules: NoneOrSome::One(ConfigSelection::GroupName("allow-all-direct".to_string())),
}),
}),
];
let result = validate_configs_test(configs).await;
@@ -2125,13 +2408,14 @@ mod tests {
)]
.into_iter()
.collect(),
reality_targets: HashMap::new(),
tls_buffer_size: None,
},
transport: Transport::Tcp,
tcp_settings: None,
quic_settings: None,
rules: NoneOrSome::One(ConfigSelection::GroupName("allow-all-direct".to_string())),
}),
}),
];
let result = validate_configs_test(configs).await;
@@ -2218,6 +2502,7 @@ mod tests {
]),
client_fingerprints: NoneOrSome::Unspecified,
alpn_protocols: NoneOrSome::Unspecified,
vision: false,
protocol: ServerProxyConfig::Http {
username: None,
password: None,
@@ -2229,6 +2514,7 @@ mod tests {
.collect(),
default_tls_target: None,
shadowtls_targets: HashMap::new(),
reality_targets: HashMap::new(),
tls_buffer_size: None,
},
transport: Transport::Tcp,
+287
View File
@@ -0,0 +1,287 @@
use futures::ready;
use tokio::io::ReadBuf;
use std::future::Future;
use std::io;
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Instant;
use crate::async_stream::AsyncSessionMessageStream;
// Informed by https://stackoverflow.com/questions/14856639/udp-hole-punching-timeout
pub const DEFAULT_ASSOCIATION_TIMEOUT_SECS: u32 = 200;
/// Buffer for copying messages from one session stream to another
#[derive(Debug)]
struct CopySessionToSessionBuffer {
read_done: bool,
need_flush: bool,
need_write_ping: bool,
cache_length: usize,
buf: [u8; 65535],
session_id: u16,
source_addr: SocketAddr,
read_count: usize,
write_count: usize,
}
impl CopySessionToSessionBuffer {
pub fn new(need_flush: bool) -> Self {
Self {
read_done: false,
need_flush,
need_write_ping: false,
cache_length: 0,
buf: [0u8; 65535],
session_id: 0,
source_addr: SocketAddr::from(([0, 0, 0, 0], 0)),
read_count: 0,
write_count: 0,
}
}
pub fn poll_copy_session_to_session<R, W>(
&mut self,
cx: &mut Context<'_>,
mut reader: Pin<&mut R>,
mut writer: Pin<&mut W>,
) -> Poll<io::Result<()>>
where
R: AsyncSessionMessageStream + ?Sized,
W: AsyncSessionMessageStream + ?Sized,
{
loop {
let mut did_read = false;
let mut did_write = false;
let mut read_pending = false;
let mut write_pending = false;
// Read from session stream
if !self.read_done && self.cache_length == 0 {
let me = &mut *self;
let mut buf = ReadBuf::new(&mut me.buf);
match reader.as_mut().poll_read_session_message(cx, &mut buf) {
Poll::Ready(val) => {
let (session_id, source_addr) = val?;
let n = buf.filled().len();
if n == 0 {
self.read_done = true;
} else {
self.session_id = session_id;
self.source_addr = source_addr;
self.cache_length = n;
did_read = true;
self.read_count = self.read_count.wrapping_add(n);
}
}
Poll::Pending => {
read_pending = true;
}
}
}
// Write to session stream
if self.cache_length > 0 {
let me = &mut *self;
match writer.as_mut().poll_write_session_message(
cx,
me.session_id,
&me.buf[0..me.cache_length],
&me.source_addr,
) {
Poll::Ready(val) => {
val?;
self.write_count = self.write_count.wrapping_add(1);
self.cache_length = 0;
self.need_flush = true;
self.need_write_ping = false;
did_write = true;
}
Poll::Pending => {
write_pending = true;
}
}
}
if !write_pending && self.need_write_ping {
match writer.as_mut().poll_write_ping(cx) {
Poll::Ready(val) => {
let written = val?;
self.need_write_ping = false;
if written {
self.need_flush = true;
}
}
Poll::Pending => {
write_pending = true;
}
}
}
if did_read && did_write && !read_pending && !write_pending {
continue;
}
if self.need_flush {
ready!(writer.as_mut().poll_flush_message(cx))?;
self.need_flush = false;
continue;
}
if self.read_done && self.cache_length == 0 {
return Poll::Ready(Ok(()));
}
if read_pending || write_pending {
return Poll::Pending;
}
}
}
}
enum TransferState {
Running,
ShuttingDown,
Done,
}
struct CopySessionMessages<'a, A: ?Sized, B: ?Sized> {
a: &'a mut A,
b: &'a mut B,
a_buf: CopySessionToSessionBuffer,
b_buf: CopySessionToSessionBuffer,
a_to_b: TransferState,
b_to_a: TransferState,
sleep_future: Pin<Box<tokio::time::Sleep>>,
a_last_active: Instant,
b_last_active: Instant,
}
fn transfer_session_to_session<A, B>(
cx: &mut Context<'_>,
state: &mut TransferState,
buf: &mut CopySessionToSessionBuffer,
r: &mut A,
w: &mut B,
) -> Poll<io::Result<()>>
where
A: AsyncSessionMessageStream + ?Sized,
B: AsyncSessionMessageStream + ?Sized,
{
let mut r = Pin::new(r);
let mut w = Pin::new(w);
loop {
match state {
TransferState::Running => {
ready!(buf.poll_copy_session_to_session(cx, r.as_mut(), w.as_mut()))?;
*state = TransferState::ShuttingDown;
}
TransferState::ShuttingDown => {
ready!(w.as_mut().poll_shutdown_message(cx))?;
*state = TransferState::Done;
}
TransferState::Done => return Poll::Ready(Ok(())),
}
}
}
impl<A, B> Future for CopySessionMessages<'_, A, B>
where
A: AsyncSessionMessageStream + ?Sized,
B: AsyncSessionMessageStream + ?Sized,
{
type Output = io::Result<()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let CopySessionMessages {
a,
b,
a_buf,
b_buf,
a_to_b,
b_to_a,
sleep_future,
a_last_active,
b_last_active,
} = &mut *self;
let ping_fired = sleep_future.as_mut().poll(cx).is_ready();
if ping_fired {
a_buf.need_write_ping = b.supports_ping();
b_buf.need_write_ping = a.supports_ping();
sleep_future
.as_mut()
.reset(tokio::time::Instant::now() + std::time::Duration::from_secs(60));
}
let a_read_count = a_buf.read_count;
let a_write_count = a_buf.write_count;
let b_read_count = b_buf.read_count;
let b_write_count = b_buf.write_count;
let a_to_b = transfer_session_to_session(cx, a_to_b, &mut *a_buf, &mut *a, &mut *b);
let b_to_a = transfer_session_to_session(cx, b_to_a, &mut *b_buf, &mut *b, &mut *a);
if a_buf.read_count != a_read_count || a_buf.write_count != a_write_count {
*a_last_active = Instant::now();
} else if a_last_active.elapsed().as_secs() >= DEFAULT_ASSOCIATION_TIMEOUT_SECS.into() {
return Poll::Ready(Ok(()));
}
if b_buf.read_count != b_read_count || b_buf.write_count != b_write_count {
*b_last_active = Instant::now();
} else if b_last_active.elapsed().as_secs() >= DEFAULT_ASSOCIATION_TIMEOUT_SECS.into() {
return Poll::Ready(Ok(()));
}
if a_to_b.is_ready() {
return a_to_b;
} else if b_to_a.is_ready() {
return b_to_a;
}
Poll::Pending
}
}
/// Copies data in both directions between two session-based streams.
///
/// This is used for session-based protocols like XUDP where:
/// - `a` is the protocol stream (e.g., VlessXudpMessageStream) that reads/writes XUDP frames with session IDs
/// - `b` is the UDP manager (e.g., SessionUdpManager) that manages dedicated UDP sockets per session
///
/// The function returns a future that will:
/// - Read from `a` (XUDP frames with session IDs) and write to `b` (create/use UDP sockets for each session)
/// - Read from `b` (UDP socket responses with session IDs) and write to `a` (encode as XUDP frames)
///
/// This happens in both directions concurrently with timeout handling.
pub async fn copy_session_messages<A, B>(
a: &mut A,
b: &mut B,
a_initial_flush: bool,
b_initial_flush: bool,
) -> Result<(), std::io::Error>
where
A: AsyncSessionMessageStream + ?Sized,
B: AsyncSessionMessageStream + ?Sized,
{
let sleep_future = Box::pin(tokio::time::sleep(std::time::Duration::from_secs(60)));
CopySessionMessages {
a,
b,
// a_buf copies from a (XUDP reader) to b (UDP manager writer), so need_flush is for b
a_buf: CopySessionToSessionBuffer::new(b_initial_flush),
// b_buf copies from b (UDP manager reader) to a (XUDP writer), so need_flush is for a
b_buf: CopySessionToSessionBuffer::new(a_initial_flush),
a_to_b: TransferState::Running,
b_to_a: TransferState::Running,
sleep_future,
a_last_active: Instant::now(),
b_last_active: Instant::now(),
}
.await
}
+296
View File
@@ -0,0 +1,296 @@
// Unified connection enum for all cryptographic protocols
//
// This enum mirrors rustls::Connection, allowing different TLS-like
// protocols to be used interchangeably.
use std::io::{self, Read, Write};
use crate::reality::{
feed_reality_client_connection, feed_reality_server_connection, RealityClientConnection,
RealityServerConnection,
};
use crate::rustls_connection_util::{feed_rustls_client_connection, feed_rustls_server_connection};
use super::crypto_reader_writer::{CryptoReader, CryptoWriter};
/// Represents the I/O state after processing packets
#[derive(Debug, Clone, Copy)]
pub struct CryptoIoState {
/// Number of plaintext bytes available to read
plaintext_bytes_to_read: usize,
}
impl CryptoIoState {
/// Create a new IoState
pub fn new(plaintext_bytes_to_read: usize) -> Self {
CryptoIoState {
plaintext_bytes_to_read,
}
}
/// How many plaintext bytes could be obtained via Read without further I/O
pub fn plaintext_bytes_to_read(&self) -> usize {
self.plaintext_bytes_to_read
}
}
/// Unified connection type supporting multiple crypto protocols
///
/// This enum allows switching between rustls and REALITY implementations
/// while maintaining a consistent API that matches rustls.
pub enum CryptoConnection {
/// rustls server-side connection
RustlsServer(rustls::ServerConnection),
/// rustls client-side connection
RustlsClient(rustls::ClientConnection),
/// REALITY server-side connection
RealityServer(RealityServerConnection),
/// REALITY client-side connection
RealityClient(RealityClientConnection),
}
impl CryptoConnection {
/// Create a new rustls server connection
pub fn new_rustls_server(conn: rustls::ServerConnection) -> Self {
CryptoConnection::RustlsServer(conn)
}
/// Create a new rustls client connection
pub fn new_rustls_client(conn: rustls::ClientConnection) -> Self {
CryptoConnection::RustlsClient(conn)
}
/// Create a new REALITY server connection
pub fn new_reality_server(conn: RealityServerConnection) -> Self {
CryptoConnection::RealityServer(conn)
}
/// Create a new REALITY client connection
pub fn new_reality_client(conn: RealityClientConnection) -> Self {
CryptoConnection::RealityClient(conn)
}
/// Check if this is a server-side connection
pub fn is_server(&self) -> bool {
matches!(
self,
CryptoConnection::RustlsServer(_) | CryptoConnection::RealityServer(_)
)
}
/// Check if this is a client-side connection
pub fn is_client(&self) -> bool {
matches!(
self,
CryptoConnection::RustlsClient(_) | CryptoConnection::RealityClient(_)
)
}
/// Read TLS messages from `rd` into internal buffers
///
/// Returns the number of bytes read, or 0 if the connection is closed.
/// This does NOT decrypt data - call `process_new_packets()` for that.
pub fn read_tls(&mut self, rd: &mut dyn Read) -> io::Result<usize> {
match self {
CryptoConnection::RustlsServer(conn) => conn.read_tls(rd),
CryptoConnection::RustlsClient(conn) => conn.read_tls(rd),
CryptoConnection::RealityServer(conn) => conn.read_tls(rd),
CryptoConnection::RealityClient(conn) => conn.read_tls(rd),
}
}
/// Process any buffered TLS messages and update internal state
///
/// This decrypts data and advances the handshake state machine.
/// Returns the I/O state including how many plaintext bytes are available.
pub fn process_new_packets(&mut self) -> io::Result<CryptoIoState> {
match self {
CryptoConnection::RustlsServer(conn) => {
let io_state = conn.process_new_packets().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("rustls server error processing new packets: {:?}", e),
)
})?;
Ok(CryptoIoState::new(io_state.plaintext_bytes_to_read()))
}
CryptoConnection::RustlsClient(conn) => {
let io_state = conn.process_new_packets().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("rustls client error processing new packets: {:?}", e),
)
})?;
Ok(CryptoIoState::new(io_state.plaintext_bytes_to_read()))
}
CryptoConnection::RealityServer(conn) => {
let io_state = conn.process_new_packets().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("reality server error processing new packets: {:?}", e),
)
})?;
Ok(CryptoIoState::new(io_state.plaintext_bytes_to_read()))
}
CryptoConnection::RealityClient(conn) => {
let io_state = conn.process_new_packets().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("reality client error processing new packets: {:?}", e),
)
})?;
Ok(CryptoIoState::new(io_state.plaintext_bytes_to_read()))
}
}
}
/// Get a unified reader for reading decrypted plaintext
///
/// Works for both Rustls and REALITY connections.
/// Returns a CryptoReader enum that abstracts over rustls::Reader and REALITY's reader.
pub fn reader(&mut self) -> CryptoReader<'_> {
match self {
CryptoConnection::RustlsServer(conn) => CryptoReader::Rustls(conn.reader()),
CryptoConnection::RustlsClient(conn) => CryptoReader::Rustls(conn.reader()),
CryptoConnection::RealityServer(conn) => CryptoReader::Reality(conn.reader()),
CryptoConnection::RealityClient(conn) => CryptoReader::Reality(conn.reader()),
}
}
/// Get a unified writer for writing plaintext to be encrypted
///
/// Works for both Rustls and REALITY connections.
/// Returns a CryptoWriter enum that abstracts over rustls::Writer and REALITY's writer.
pub fn writer(&mut self) -> CryptoWriter<'_> {
match self {
CryptoConnection::RustlsServer(conn) => CryptoWriter::Rustls(conn.writer()),
CryptoConnection::RustlsClient(conn) => CryptoWriter::Rustls(conn.writer()),
CryptoConnection::RealityServer(conn) => CryptoWriter::Reality(conn.writer()),
CryptoConnection::RealityClient(conn) => CryptoWriter::Reality(conn.writer()),
}
}
/// Write any buffered TLS messages to `wr`
///
/// This encrypts any pending plaintext and writes the ciphertext.
/// Returns the number of bytes written.
pub fn write_tls(&mut self, wr: &mut dyn Write) -> io::Result<usize> {
match self {
CryptoConnection::RustlsServer(conn) => conn.write_tls(wr),
CryptoConnection::RustlsClient(conn) => conn.write_tls(wr),
CryptoConnection::RealityServer(conn) => conn.write_tls(wr),
CryptoConnection::RealityClient(conn) => conn.write_tls(wr),
}
}
/// Check if the connection wants to write data
///
/// If true, the application should call `write_tls()` as soon as possible.
pub fn wants_write(&self) -> bool {
match self {
CryptoConnection::RustlsServer(conn) => conn.wants_write(),
CryptoConnection::RustlsClient(conn) => conn.wants_write(),
CryptoConnection::RealityServer(conn) => conn.wants_write(),
CryptoConnection::RealityClient(conn) => conn.wants_write(),
}
}
/// Check whether the handshake is complete
///
/// For both client and server connections, this returns true when
/// the handshake has completed and application data can be exchanged.
pub fn is_handshaking(&self) -> bool {
match self {
CryptoConnection::RustlsServer(conn) => conn.is_handshaking(),
CryptoConnection::RustlsClient(conn) => conn.is_handshaking(),
CryptoConnection::RealityServer(conn) => conn.is_handshaking(),
CryptoConnection::RealityClient(conn) => conn.is_handshaking(),
}
}
/// Queue a close notification
///
/// This queues a close notification to be sent to the peer.
/// Call `write_tls()` to actually send it.
pub fn send_close_notify(&mut self) {
match self {
CryptoConnection::RustlsServer(conn) => conn.send_close_notify(),
CryptoConnection::RustlsClient(conn) => conn.send_close_notify(),
CryptoConnection::RealityServer(conn) => conn.send_close_notify(),
CryptoConnection::RealityClient(conn) => conn.send_close_notify(),
}
}
}
#[inline(always)]
pub fn feed_crypto_connection(
connection: &mut CryptoConnection,
data: &[u8],
) -> std::io::Result<()> {
match connection {
CryptoConnection::RustlsServer(conn) => feed_rustls_server_connection(conn, data),
CryptoConnection::RustlsClient(conn) => feed_rustls_client_connection(conn, data),
CryptoConnection::RealityServer(conn) => feed_reality_server_connection(conn, data),
CryptoConnection::RealityClient(conn) => feed_reality_client_connection(conn, data),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connection_type_queries() {
use crate::address::{Address, NetLocation};
use crate::reality::{
RealityClientConfig, RealityClientConnection, RealityServerConfig,
RealityServerConnection,
};
// Test with REALITY server connection
let server_config = RealityServerConfig {
private_key: [0u8; 32],
short_ids: vec![[0u8; 8]],
dest: NetLocation::new(Address::UNSPECIFIED, 443),
max_time_diff: None,
min_client_version: None,
max_client_version: None,
};
let reality_server = RealityServerConnection::new(server_config).unwrap();
let server_conn = CryptoConnection::new_reality_server(reality_server);
assert!(server_conn.is_server());
assert!(!server_conn.is_client());
assert!(matches!(server_conn, CryptoConnection::RealityServer(_)));
assert!(!matches!(
server_conn,
CryptoConnection::RustlsServer(_) | CryptoConnection::RustlsClient(_)
));
// Test with REALITY client connection
// Using a valid X25519 test public key (derived from private key [1u8; 32])
let client_config = RealityClientConfig {
public_key: [
0x25, 0x31, 0x51, 0x68, 0x12, 0x8f, 0xf3, 0x7b, 0x46, 0x5e, 0x0c, 0x0c, 0xd8, 0x28,
0x3d, 0xd5, 0x35, 0x86, 0x8d, 0x8d, 0x3e, 0x8c, 0x1c, 0x85, 0x1e, 0x0e, 0x86, 0x28,
0xcf, 0x48, 0x0e, 0x66,
],
short_id: [0u8; 8],
server_name: "test.example.com".to_string(),
};
let reality_client = RealityClientConnection::new(client_config).unwrap();
let client_conn = CryptoConnection::new_reality_client(reality_client);
assert!(!client_conn.is_server());
assert!(client_conn.is_client());
assert!(matches!(client_conn, CryptoConnection::RealityClient(_)));
assert!(!matches!(
client_conn,
CryptoConnection::RustlsServer(_) | CryptoConnection::RustlsClient(_)
));
}
}
+61
View File
@@ -0,0 +1,61 @@
// Reader and Writer types for crypto connections
//
// These types provide a consistent API for reading decrypted plaintext
// and writing plaintext to be encrypted, working with both rustls and REALITY.
use std::io::{self, BufRead, Read, Write};
use crate::reality::{RealityReader, RealityWriter};
/// Unified reader that works with both Rustls and REALITY connections
pub enum CryptoReader<'a> {
Rustls(rustls::Reader<'a>),
Reality(RealityReader<'a>),
}
impl<'a> Read for CryptoReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
CryptoReader::Rustls(reader) => reader.read(buf),
CryptoReader::Reality(reader) => reader.read(buf),
}
}
}
impl<'a> BufRead for CryptoReader<'a> {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
match self {
CryptoReader::Rustls(reader) => reader.fill_buf(),
CryptoReader::Reality(reader) => reader.fill_buf(),
}
}
fn consume(&mut self, amt: usize) {
match self {
CryptoReader::Rustls(reader) => reader.consume(amt),
CryptoReader::Reality(reader) => reader.consume(amt),
}
}
}
/// Unified writer that works with both Rustls and REALITY connections
pub enum CryptoWriter<'a> {
Rustls(rustls::Writer<'a>),
Reality(RealityWriter<'a>),
}
impl<'a> Write for CryptoWriter<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
CryptoWriter::Rustls(writer) => writer.write(buf),
CryptoWriter::Reality(writer) => writer.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
CryptoWriter::Rustls(writer) => writer.flush(),
CryptoWriter::Reality(writer) => writer.flush(),
}
}
}
+344
View File
@@ -0,0 +1,344 @@
// CryptoTlsStream: Async wrapper around Connection enum
//
// This provides AsyncRead + AsyncWrite for the Connection enum,
// allowing it to work with tokio-based async code.
//
// Based on VisionStream's TLS mode logic (non-padded), which itself
// is adapted from tokio-rustls.
use std::io::{self, BufRead, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use super::crypto_connection::CryptoConnection;
use crate::sync_adapter::{SyncReadAdapter, SyncWriteAdapter};
/// Async stream wrapper for Connection enum
///
/// This combines a Connection (rustls or REALITY) with an async I/O stream,
/// providing AsyncRead + AsyncWrite interface.
pub struct CryptoTlsStream<IO> {
/// The underlying async I/O stream (e.g., TcpStream)
io: IO,
/// The crypto connection (rustls or REALITY)
session: CryptoConnection,
/// Whether we've hit EOF when reading
is_read_eof: bool,
}
impl<IO> CryptoTlsStream<IO>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
/// Create a new CryptoTlsStream
pub fn new(io: IO, session: CryptoConnection) -> Self {
CryptoTlsStream {
io,
session,
is_read_eof: false,
}
}
/// Extract the underlying I/O stream and crypto session
///
/// This consumes the CryptoTlsStream and returns its components.
/// Useful for scenarios like Vision that need direct access to both.
pub fn into_inner(self) -> (IO, CryptoConnection) {
(self.io, self.session)
}
/// Write TLS data directly to the underlying stream
///
/// Returns Poll::Ready(Ok(n)) with bytes written, or Poll::Pending if would block.
fn write_tls_direct(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
let mut adapter = SyncWriteAdapter {
io: &mut self.io,
cx,
};
match self.session.write_tls(&mut adapter) {
Ok(n) => Poll::Ready(Ok(n)),
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending,
Err(e) => Poll::Ready(Err(e)),
}
}
/// Drain all pending TLS writes to the underlying stream
fn drain_all_writes(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
while self.session.wants_write() {
match self.write_tls_direct(cx) {
Poll::Ready(Ok(_)) => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
Poll::Ready(Ok(()))
}
/// Complete handshake if needed
fn complete_handshake_if_needed(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
while self.session.is_handshaking() {
// Write any pending TLS data
match self.drain_all_writes(cx) {
Poll::Ready(Ok(())) => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
// Read more data for handshake
let mut adapter = SyncReadAdapter {
io: &mut self.io,
cx,
};
match self.session.read_tls(&mut adapter) {
Ok(0) => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"EOF during handshake",
)));
}
Ok(_) => {
// Process the data
self.session.process_new_packets()?;
}
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
return Poll::Pending;
}
Err(e) => return Poll::Ready(Err(e)),
}
}
Poll::Ready(Ok(()))
}
}
impl<IO> AsyncRead for CryptoTlsStream<IO>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let this = self.get_mut();
// Check if session already has decrypted data available
{
let mut reader = this.session.reader();
match reader.fill_buf() {
Ok(available) if !available.is_empty() => {
// Copy directly from session buffer to user buffer
let len = buf.remaining().min(available.len());
buf.put_slice(&available[..len]);
reader.consume(len);
return Poll::Ready(Ok(()));
}
Ok(_) => {
// Empty buffer, need to read more
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// No data available yet
}
Err(e) => return Poll::Ready(Err(e)),
}
}
if this.is_read_eof {
return Poll::Ready(Ok(()));
}
// Complete handshake first if needed
if this.session.is_handshaking() {
match this.complete_handshake_if_needed(cx) {
Poll::Ready(Ok(())) => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
// No data in session buffer, read from TCP and decrypt
// Split borrows manually to avoid borrow checker issues
loop {
let bytes_read = {
let mut adapter = SyncReadAdapter {
io: &mut this.io,
cx,
};
match this.session.read_tls(&mut adapter) {
Ok(n) => n,
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
return Poll::Pending;
}
Err(e) => return Poll::Ready(Err(e)),
}
};
if bytes_read == 0 {
// EOF
this.is_read_eof = true;
return Poll::Ready(Ok(()));
}
// Process encrypted data - try to send alerts on error
let io_state = match this.session.process_new_packets() {
Ok(state) => state,
Err(e) => {
// Try last-gasp write to send any pending TLS alerts (tokio-rustls pattern)
// Make best-effort attempt to drain all pending writes without blocking.
// This increases the chance that the peer receives an alert explaining
// the protocol error. We ignore write failures to ensure the primary
// TLS protocol error (e) is always returned.
while this.session.wants_write() {
let mut adapter = SyncWriteAdapter {
io: &mut this.io,
cx,
};
match this.session.write_tls(&mut adapter) {
Ok(_) => {} // Continue draining
Err(ref write_err) if write_err.kind() == io::ErrorKind::WouldBlock => {
break
}
Err(_) => break, // Give up on write error
}
}
return Poll::Ready(Err(e));
}
};
if io_state.plaintext_bytes_to_read() == 0 {
// No plaintext yet, need more data
continue;
}
// Extract plaintext from session
let mut reader = this.session.reader();
match reader.fill_buf() {
Ok(available) => {
if available.is_empty() {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Read zero bytes when plaintext is available",
)));
}
let len = buf.remaining().min(available.len());
buf.put_slice(&available[..len]);
reader.consume(len);
return Poll::Ready(Ok(()));
}
Err(e) => {
return Poll::Ready(Err(e));
}
}
}
}
}
impl<IO> AsyncWrite for CryptoTlsStream<IO>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
// Complete handshake first if needed
if self.session.is_handshaking() {
match self.complete_handshake_if_needed(cx) {
Poll::Ready(Ok(())) => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
let mut pos = 0;
while pos < buf.len() {
let mut would_block = false;
// Write plaintext to session
match self.session.writer().write(&buf[pos..]) {
Ok(n) => pos += n,
Err(e) => return Poll::Ready(Err(e)),
};
// Drain TLS output to TCP stream
while self.session.wants_write() {
match self.write_tls_direct(cx) {
Poll::Ready(Ok(0)) | Poll::Pending => {
would_block = true;
break;
}
Poll::Ready(Ok(_)) => (),
Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
}
}
return match (pos, would_block) {
(0, true) => Poll::Pending,
(n, true) => Poll::Ready(Ok(n)), // Partial write
(_, false) => continue, // Keep writing
};
}
Poll::Ready(Ok(pos))
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
// Drain all pending TLS writes
match self.drain_all_writes(cx) {
Poll::Ready(Ok(())) => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
// Flush underlying stream
Pin::new(&mut self.io).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
// Send close notification
self.session.send_close_notify();
// Drain all pending writes
match self.drain_all_writes(cx) {
Poll::Ready(Ok(())) => {}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
// Shutdown underlying stream - ignore NotConnected errors (tokio-rustls pattern)
match Pin::new(&mut self.io).poll_shutdown(cx) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
Poll::Ready(Err(e)) if e.kind() == io::ErrorKind::NotConnected => {
// When trying to shutdown, not being connected is fine
Poll::Ready(Ok(()))
}
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Pending => Poll::Pending,
}
}
}
// Implement AsyncPing for CryptoTlsStream
impl<IO> crate::async_stream::AsyncPing for CryptoTlsStream<IO>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
fn supports_ping(&self) -> bool {
false // TLS doesn't have a built-in ping mechanism
}
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<bool>> {
Poll::Ready(Ok(false))
}
}
// Implement AsyncStream blanket trait
impl<IO> crate::async_stream::AsyncStream for CryptoTlsStream<IO> where
IO: AsyncRead + AsyncWrite + Unpin + Send
{
}
+13
View File
@@ -0,0 +1,13 @@
// Unified cryptographic connection abstraction
//
// This module provides a common interface for different TLS-like protocols,
// allowing rustls and REALITY to be used interchangeably throughout the codebase.
mod crypto_connection;
mod crypto_reader_writer;
mod crypto_tls_stream;
// Re-export core types
pub use crypto_connection::{feed_crypto_connection, CryptoConnection};
pub use crypto_tls_stream::CryptoTlsStream;
// CryptoReader, CryptoWriter, and IoState are used internally within this module
+88 -8
View File
@@ -6,6 +6,8 @@ mod config;
mod copy_bidirectional;
mod copy_bidirectional_message;
mod copy_multidirectional_message;
mod copy_session_messages;
mod crypto;
mod http_handler;
mod hysteria2_server;
mod noop_stream;
@@ -13,28 +15,35 @@ mod option_util;
mod port_forward_handler;
mod quic_server;
mod quic_stream;
mod reality;
mod reality_client_handler;
mod resolver;
mod rustls_util;
mod salt_checker;
mod rustls_config_util;
mod rustls_connection_util;
mod rustls_handshake;
mod shadow_tls;
mod shadowsocks;
mod slide_buffer;
mod snell;
mod socket_util;
mod socks_handler;
mod stream_reader;
mod sync_adapter;
mod tcp;
mod thread_util;
mod timed_salt_checker;
mod tls_handler;
mod tls_client_handler;
mod tls_server_handler;
mod trojan_handler;
mod tuic_server;
mod udp_message_stream;
mod udp_multi_message_stream;
mod udp_session_message_stream;
mod uot;
mod util;
mod vless_handler;
mod vless_message_stream;
mod vless;
mod vmess;
mod websocket;
mod xudp;
#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;
@@ -55,6 +64,8 @@ use tokio::task::JoinHandle;
use crate::config::{ServerConfig, Transport};
use crate::quic_server::start_quic_servers;
use crate::reality::generate_keypair;
use crate::shadowsocks::ShadowsocksCipher;
use crate::thread_util::set_num_threads;
use tcp::*;
@@ -191,6 +202,74 @@ fn main() {
}
}
if args.iter().any(|s| s == "generate-reality-keypair") {
let (private_key, public_key) = generate_keypair().unwrap();
println!(
"--------------------------------------------------------------------------------"
);
println!("REALITY private key: {}", private_key);
println!("REALITY public key: {}", public_key);
println!(
"--------------------------------------------------------------------------------"
);
return;
}
if let Some(pos) = args
.iter()
.position(|s| s == "generate-shadowsocks-password")
{
let cipher = args.get(pos + 1).map(|s| s.as_str());
match cipher {
Some(c) => {
// Strip 2022-blake3- prefix if present for cipher lookup
let base_cipher = c.strip_prefix("2022-blake3-").unwrap_or(c);
match ShadowsocksCipher::try_from(base_cipher) {
Ok(cipher) => {
use aws_lc_rs::rand::{SecureRandom, SystemRandom};
use base64::engine::{general_purpose::STANDARD, Engine as _};
let rng = SystemRandom::new();
let mut key_bytes = vec![0u8; cipher.key_len()];
rng.fill(&mut key_bytes).expect("RNG failed");
let password = STANDARD.encode(&key_bytes);
println!(
"--------------------------------------------------------------------------------"
);
println!("Cipher: {}", c);
println!("Password: {}", password);
println!(
"--------------------------------------------------------------------------------"
);
}
Err(_) => {
eprintln!("Unknown cipher: {}", c);
eprintln!("Supported ciphers:");
eprintln!(" aes-128-gcm");
eprintln!(" aes-256-gcm");
eprintln!(" chacha20-ietf-poly1305");
eprintln!(" 2022-blake3-aes-128-gcm");
eprintln!(" 2022-blake3-aes-256-gcm");
eprintln!(" 2022-blake3-chacha20-poly1305");
std::process::exit(1);
}
}
}
None => {
eprintln!("Usage: {} generate-shadowsocks-password <cipher>", arg0);
eprintln!("Supported ciphers:");
eprintln!(" aes-128-gcm");
eprintln!(" aes-256-gcm");
eprintln!(" chacha20-ietf-poly1305");
eprintln!(" 2022-blake3-aes-128-gcm");
eprintln!(" 2022-blake3-aes-256-gcm");
eprintln!(" 2022-blake3-chacha20-poly1305");
std::process::exit(1);
}
}
return;
}
if args.is_empty() {
println!("No config specified, assuming loading from file config.shoes.yaml");
args.push("config.shoes.yaml".to_string())
@@ -276,8 +355,6 @@ fn main() {
return;
}
println!("\nStarting {} server(s)..", configs.len());
let mut join_handles = vec![];
let server_configs = match config::create_server_configs(configs).await {
@@ -288,6 +365,9 @@ fn main() {
return;
}
};
println!("\nStarting {} server(s)..", server_configs.len());
for server_config in server_configs {
join_handles.extend(start_servers(server_config).await.unwrap());
}
+17 -1
View File
@@ -5,6 +5,7 @@ use async_trait::async_trait;
use crate::address::NetLocation;
use crate::async_stream::AsyncStream;
use crate::option_util::NoneOrOne;
use crate::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
#[derive(Debug)]
@@ -38,10 +39,25 @@ impl TcpServerHandler for PortForwardServerHandler {
Ok(TcpServerSetupResult::TcpForward {
remote_location: location.clone(),
stream: server_stream,
need_initial_flush: false,
need_initial_flush: true,
connection_success_response: None,
initial_remote_data: None,
override_proxy_provider: NoneOrOne::Unspecified,
})
}
}
#[derive(Debug)]
pub struct PortForwardClientHandler;
#[async_trait]
impl TcpClientHandler for PortForwardClientHandler {
async fn setup_client_stream(
&self,
_server_stream: &mut Box<dyn AsyncStream>,
client_stream: Box<dyn AsyncStream>,
_remote_location: NetLocation,
) -> std::io::Result<TcpClientSetupResult> {
Ok(TcpClientSetupResult { client_stream })
}
}
+42 -1
View File
@@ -16,9 +16,10 @@ use crate::config::{
use crate::copy_bidirectional::copy_bidirectional;
use crate::copy_bidirectional_message::copy_bidirectional_message;
use crate::copy_multidirectional_message::copy_multidirectional_message;
use crate::copy_session_messages::copy_session_messages;
use crate::quic_stream::QuicStream;
use crate::resolver::{resolve_single_address, NativeResolver, Resolver};
use crate::rustls_util::create_server_config;
use crate::rustls_config_util::create_server_config;
use crate::socket_util::new_socket2_udp_socket;
use crate::tcp_client_connector::TcpClientConnector;
use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
@@ -26,6 +27,7 @@ use crate::tcp_handler_util::{create_tcp_client_proxy_selector, create_tcp_serve
use crate::tcp_server::setup_client_stream;
use crate::udp_message_stream::UdpMessageStream;
use crate::udp_multi_message_stream::UdpMultiMessageStream;
use crate::udp_session_message_stream::{UdpSessionMessageStream, UdpSocketConfig};
use crate::util::parse_uuid;
async fn start_quic_server(
@@ -320,6 +322,45 @@ async fn process_streams(
}
}
}
TcpServerSetupResult::SessionBasedUdp {
stream: mut server_stream,
need_initial_flush: server_need_initial_flush,
override_proxy_provider,
} => {
let selected_proxy_provider = if override_proxy_provider.is_one() {
override_proxy_provider.unwrap()
} else {
client_proxy_selector
};
let action = selected_proxy_provider.default_decision();
match action {
ConnectDecision::Allow {
client_proxy,
remote_location: _,
} => {
// Create UdpSessionMessageStream with user's interface configuration
let config = UdpSocketConfig {
bind_interface: client_proxy.bind_interface().clone(),
};
let mut session_manager = UdpSessionMessageStream::new(config);
let copy_result = copy_session_messages(
&mut server_stream,
&mut session_manager,
server_need_initial_flush,
false,
)
.await;
copy_result?;
Ok(())
}
ConnectDecision::Block => {
warn!("Blocked session-based udp forward, because the default action is to block.");
Ok(())
}
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
// Common constants and helpers shared between REALITY client and server implementations
//
// This module provides:
// - TLS constants (content types, alert codes, version bytes, handshake types)
// - Close notify alert construction
use super::reality_aead::encrypt_tls13_record;
use std::io;
// TLS ContentType values
pub const CONTENT_TYPE_CHANGE_CIPHER_SPEC: u8 = 0x14;
pub const CONTENT_TYPE_ALERT: u8 = 0x15;
pub const CONTENT_TYPE_HANDSHAKE: u8 = 0x16;
pub const CONTENT_TYPE_APPLICATION_DATA: u8 = 0x17;
// TLS alert levels and descriptions
pub const ALERT_LEVEL_WARNING: u8 = 0x01;
pub const ALERT_DESC_CLOSE_NOTIFY: u8 = 0x00;
// TLS version bytes (used on wire for compatibility)
// TLS 1.2 version bytes: 0x03, 0x03
// Used in TLS 1.3 for compatibility (appears in record layer)
pub const VERSION_TLS_1_2_MAJOR: u8 = 0x03;
pub const VERSION_TLS_1_2_MINOR: u8 = 0x03;
// TLS 1.3 handshake message types
pub const HANDSHAKE_TYPE_SERVER_HELLO: u8 = 2;
pub const HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS: u8 = 8;
pub const HANDSHAKE_TYPE_CERTIFICATE: u8 = 11;
pub const HANDSHAKE_TYPE_CERTIFICATE_VERIFY: u8 = 15;
pub const HANDSHAKE_TYPE_FINISHED: u8 = 20;
// TLS 1.3 record size limits per RFC 8446
//
// The TLS record header's `length` field specifies the size of the ENCRYPTED payload.
// Per RFC 8446, the TLS 1.3 limit is stricter than TLS 1.2:
//
// - TLS 1.3: Plaintext limit = 16,384 bytes (2^14)
// Encryption overhead allowance = 256 bytes
// Ciphertext limit = 16,384 + 256 = 16,640 bytes
//
// - TLS 1.2: Plaintext limit = 16,384 bytes (2^14)
// Encryption overhead allowance = 2,048 bytes
// Ciphertext limit = 16,384 + 2,048 = 18,432 bytes
//
// REALITY uses TLS 1.3, so we MUST use the TLS 1.3 limit. Using the larger
// TLS 1.2 limit causes "record overflow" errors in libraries like utls.
/// Maximum TLS 1.3 ciphertext payload size (16,640 bytes)
pub const MAX_TLS_CIPHERTEXT_LEN: usize = 16384 + 256;
/// Maximum plaintext payload size for a single TLS 1.3 record
///
/// RFC 8446 Section 5.1: "The record layer fragments information blocks into
/// TLSPlaintext records carrying data in chunks of 2^14 bytes or less."
///
/// This is the hard limit enforced by TLS implementations.
/// The 256-byte allowance in MAX_TLS_CIPHERTEXT_LEN is for:
/// - AEAD tag (16 bytes for AES-GCM)
/// - Content type byte (1 byte)
/// - Optional padding (up to 239 bytes)
///
/// We MUST NOT exceed 16384 bytes of actual plaintext per record, or clients
/// will reject with "record overflow" error.
pub const MAX_TLS_PLAINTEXT_LEN: usize = 16384;
/// TLS record header size (ContentType + ProtocolVersion + Length)
pub const TLS_RECORD_HEADER_SIZE: usize = 5;
/// Maximum TLS record size (ciphertext + header)
pub const TLS_MAX_RECORD_SIZE: usize = MAX_TLS_CIPHERTEXT_LEN + TLS_RECORD_HEADER_SIZE;
/// Buffer capacity for ciphertext read (2x TLS max record for safety)
pub const CIPHERTEXT_READ_BUF_CAPACITY: usize = TLS_MAX_RECORD_SIZE * 2;
/// Buffer capacity for plaintext read
pub const PLAINTEXT_READ_BUF_CAPACITY: usize = TLS_MAX_RECORD_SIZE * 2;
/// Build an encrypted close_notify alert for TLS 1.3
///
/// In TLS 1.3, alerts must be encrypted like application data.
pub fn build_close_notify_alert(key: &[u8], iv: &[u8], seq_num: u64) -> io::Result<Vec<u8>> {
// Build alert message: level(1) + description(0) + ContentType
let alert_with_type = vec![
ALERT_LEVEL_WARNING,
ALERT_DESC_CLOSE_NOTIFY,
CONTENT_TYPE_ALERT, // ContentType byte for TLS 1.3
];
// Build TLS header with correct ciphertext length
let ciphertext_len = (alert_with_type.len() + 16) as u16; // plaintext + tag
let mut tls_header = [
CONTENT_TYPE_APPLICATION_DATA,
VERSION_TLS_1_2_MAJOR,
VERSION_TLS_1_2_MINOR,
0x00,
0x00, // Length will be set
];
tls_header[3..5].copy_from_slice(&ciphertext_len.to_be_bytes());
// Encrypt the alert
let ciphertext = encrypt_tls13_record(key, iv, seq_num, &alert_with_type, &tls_header)?;
// Build complete TLS record
let mut record = Vec::with_capacity(5 + ciphertext.len());
record.push(CONTENT_TYPE_APPLICATION_DATA);
record.push(VERSION_TLS_1_2_MAJOR);
record.push(VERSION_TLS_1_2_MINOR);
record.push(((ciphertext.len() >> 8) & 0xff) as u8);
record.push((ciphertext.len() & 0xff) as u8);
record.extend_from_slice(&ciphertext);
Ok(record)
}
+33
View File
@@ -0,0 +1,33 @@
// REALITY Protocol Implementation
//
// This module implements the REALITY obfuscation protocol for TLS connections.
// REALITY disguises proxy traffic as legitimate HTTPS connections using:
// - X25519 ECDH key exchange
// - HKDF-SHA256 key derivation
// - AES-256-GCM encryption
// - HMAC-SHA512 authentication
mod common;
mod reality_aead;
mod reality_auth;
mod reality_certificate;
mod reality_client_connection;
mod reality_client_verify;
mod reality_io_state;
mod reality_reader_writer;
mod reality_records;
mod reality_server_connection;
mod reality_tls13_keys;
mod reality_tls13_messages;
mod reality_util;
pub use reality_util::{decode_private_key, decode_public_key, decode_short_id, generate_keypair};
// Re-export connection types for crypto_connection module
pub use reality_client_connection::{
feed_reality_client_connection, RealityClientConfig, RealityClientConnection,
};
pub use reality_reader_writer::{RealityReader, RealityWriter};
pub use reality_server_connection::{
feed_reality_server_connection, RealityServerConfig, RealityServerConnection,
};
+365
View File
@@ -0,0 +1,365 @@
// TLS 1.3 Encryption/Decryption Helpers
//
// AES-GCM encryption for TLS 1.3 records using aws-lc-rs
use super::common::{
CONTENT_TYPE_ALERT, CONTENT_TYPE_APPLICATION_DATA, VERSION_TLS_1_2_MAJOR, VERSION_TLS_1_2_MINOR,
};
use aws_lc_rs::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_128_GCM};
use std::io::{Error, ErrorKind, Result};
/// Encrypt TLS 1.3 record using AES-128-GCM
///
/// # Arguments
/// * `key` - AES key (16 bytes for AES-128)
/// * `iv` - Base IV (12 bytes)
/// * `sequence_number` - TLS record sequence number
/// * `plaintext` - Plaintext data (including ContentType trailer)
/// * `additional_data` - TLS record header for AEAD
///
/// # Returns
/// Ciphertext with authentication tag appended
pub fn encrypt_tls13_record(
key: &[u8],
iv: &[u8],
sequence_number: u64,
plaintext: &[u8],
additional_data: &[u8],
) -> Result<Vec<u8>> {
if key.len() != 16 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Invalid key length: {} (expected 16)", key.len()),
));
}
if iv.len() != 12 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Invalid IV length: {} (expected 12)", iv.len()),
));
}
// Construct nonce: IV XOR sequence_number
let mut nonce_bytes = [0u8; 12];
nonce_bytes.copy_from_slice(iv);
// XOR the last 8 bytes with the sequence number
let seq_bytes = sequence_number.to_be_bytes();
for i in 0..8 {
nonce_bytes[4 + i] ^= seq_bytes[i];
}
// Create key and nonce for aws-lc-rs
let unbound_key = UnboundKey::new(&AES_128_GCM, key)
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid key: {:?}", e)))?;
let sealing_key = LessSafeKey::new(unbound_key);
let nonce = Nonce::try_assume_unique_for_key(&nonce_bytes)
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid nonce: {:?}", e)))?;
let aad = Aad::from(additional_data);
// Note: The caller is responsible for adding the ContentType byte to plaintext
// For handshake: plaintext = handshake_msg || 0x16
// For app data: plaintext = app_data || 0x17
let mut in_out = plaintext.to_vec();
sealing_key
.seal_in_place_append_tag(nonce, aad, &mut in_out)
.map_err(|e| {
Error::new(
ErrorKind::InvalidData,
format!("Encryption failed: {:?}", e),
)
})?;
Ok(in_out)
}
/// Decrypt TLS 1.3 record using AES-128-GCM
///
/// # Arguments
/// * `key` - AES key (16 bytes for AES-128)
/// * `iv` - Base IV (12 bytes)
/// * `sequence_number` - TLS record sequence number
/// * `ciphertext` - Ciphertext with authentication tag
/// * `additional_data` - TLS record header for AEAD
///
/// # Returns
/// Plaintext data (including ContentType trailer)
pub fn decrypt_tls13_record(
key: &[u8],
iv: &[u8],
sequence_number: u64,
ciphertext: &[u8],
additional_data: &[u8],
) -> Result<Vec<u8>> {
if key.len() != 16 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Invalid key length: {} (expected 16)", key.len()),
));
}
if iv.len() != 12 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Invalid IV length: {} (expected 12)", iv.len()),
));
}
// Construct nonce: IV XOR sequence_number
let mut nonce_bytes = [0u8; 12];
nonce_bytes.copy_from_slice(iv);
// XOR the last 8 bytes with the sequence number
let seq_bytes = sequence_number.to_be_bytes();
for i in 0..8 {
nonce_bytes[4 + i] ^= seq_bytes[i];
}
// Create key and nonce for aws-lc-rs
let unbound_key = UnboundKey::new(&AES_128_GCM, key)
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid key: {:?}", e)))?;
let opening_key = LessSafeKey::new(unbound_key);
let nonce = Nonce::try_assume_unique_for_key(&nonce_bytes)
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid nonce: {:?}", e)))?;
let aad = Aad::from(additional_data);
// aws-lc-rs requires in-place decryption
let mut in_out = ciphertext.to_vec();
let plaintext = opening_key
.open_in_place(nonce, aad, &mut in_out)
.map_err(|e| {
Error::new(
ErrorKind::InvalidData,
format!("Decryption failed: {:?}", e),
)
})?;
Ok(plaintext.to_vec())
}
/// Decrypt TLS 1.3 handshake message
///
/// Decrypts and extracts handshake message, removing ContentType trailer
pub fn decrypt_handshake_message(
key: &[u8],
iv: &[u8],
sequence_number: u64,
ciphertext: &[u8],
record_length: u16,
) -> Result<Vec<u8>> {
// Additional data for decryption
let mut additional_data = Vec::new();
additional_data.push(0x17); // ApplicationData
additional_data.extend_from_slice(&[VERSION_TLS_1_2_MAJOR, VERSION_TLS_1_2_MINOR]); // TLS 1.2
additional_data.extend_from_slice(&record_length.to_be_bytes());
let mut plaintext =
decrypt_tls13_record(key, iv, sequence_number, ciphertext, &additional_data)?;
// Remove ContentType trailer
if plaintext.is_empty() {
return Err(Error::new(ErrorKind::InvalidData, "Empty plaintext"));
}
// TLS 1.3 has format: content | type_byte | padding (zeros)
// We need to find the type byte by removing trailing zeros first
// Remove trailing zeros (padding)
while !plaintext.is_empty() && plaintext[plaintext.len() - 1] == 0 {
plaintext.pop();
}
if plaintext.is_empty() {
return Err(Error::new(ErrorKind::InvalidData, "Plaintext is all zeros"));
}
// Now the last byte should be the content type
let content_type = plaintext.pop().unwrap();
if content_type != 0x16
&& content_type != CONTENT_TYPE_APPLICATION_DATA
&& content_type != CONTENT_TYPE_ALERT
{
return Err(Error::new(
ErrorKind::InvalidData,
format!("Invalid content type: 0x{:02x}", content_type),
));
}
Ok(plaintext)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt_record() {
let key = vec![0x42u8; 16];
let iv = vec![0x99u8; 12];
let plaintext = b"Hello, TLS 1.3!";
let aad = b"additional data";
let ciphertext = encrypt_tls13_record(&key, &iv, 0, plaintext, aad).unwrap();
let decrypted = decrypt_tls13_record(&key, &iv, 0, &ciphertext, aad).unwrap();
assert_eq!(&decrypted[..], plaintext);
}
#[test]
fn test_roundtrip_handshake() {
let key = vec![0x11u8; 16];
let iv = vec![0x22u8; 12];
let handshake_msg = vec![0x33u8; 50];
// Manually encrypt like encrypt_handshake_to_records does
let mut plaintext = handshake_msg.clone();
plaintext.push(0x16); // ContentType: Handshake
let ciphertext_length = (plaintext.len() + 16) as u16;
let aad: [u8; 5] = [
0x17, // ApplicationData
0x03,
0x03, // TLS 1.2
(ciphertext_length >> 8) as u8,
(ciphertext_length & 0xff) as u8,
];
let ciphertext = encrypt_tls13_record(&key, &iv, 0, &plaintext, &aad).unwrap();
let decrypted =
decrypt_handshake_message(&key, &iv, 0, &ciphertext, ciphertext_length).unwrap();
assert_eq!(decrypted, handshake_msg);
}
#[test]
fn test_encrypt_with_sequence_number() {
let key = vec![0x42u8; 16];
let iv = vec![0x99u8; 12];
let plaintext = b"Test with sequence";
let aad = b"aad";
// Test that different sequence numbers produce different ciphertexts
let cipher1 = encrypt_tls13_record(&key, &iv, 1, plaintext, aad).unwrap();
let cipher2 = encrypt_tls13_record(&key, &iv, 2, plaintext, aad).unwrap();
let cipher3 = encrypt_tls13_record(&key, &iv, 100, plaintext, aad).unwrap();
// Ciphertexts should all be different
assert_ne!(cipher1, cipher2);
assert_ne!(cipher2, cipher3);
assert_ne!(cipher1, cipher3);
// But they should all decrypt correctly
let decrypt1 = decrypt_tls13_record(&key, &iv, 1, &cipher1, aad).unwrap();
let decrypt2 = decrypt_tls13_record(&key, &iv, 2, &cipher2, aad).unwrap();
let decrypt3 = decrypt_tls13_record(&key, &iv, 100, &cipher3, aad).unwrap();
assert_eq!(decrypt1, plaintext);
assert_eq!(decrypt2, plaintext);
assert_eq!(decrypt3, plaintext);
}
#[test]
fn test_decrypt_with_wrong_sequence_number() {
let key = vec![0x42u8; 16];
let iv = vec![0x99u8; 12];
let plaintext = b"Test sequence";
let aad = b"aad";
let ciphertext = encrypt_tls13_record(&key, &iv, 5, plaintext, aad).unwrap();
// Decrypting with wrong sequence number should fail
let result = decrypt_tls13_record(&key, &iv, 6, &ciphertext, aad);
assert!(result.is_err());
}
#[test]
fn test_decrypt_with_wrong_aad() {
let key = vec![0x42u8; 16];
let iv = vec![0x99u8; 12];
let plaintext = b"Test AAD";
let aad = b"correct aad";
let wrong_aad = b"wrong aad";
let ciphertext = encrypt_tls13_record(&key, &iv, 0, plaintext, aad).unwrap();
// Decrypting with wrong AAD should fail
let result = decrypt_tls13_record(&key, &iv, 0, &ciphertext, wrong_aad);
assert!(result.is_err());
}
#[test]
fn test_encrypt_invalid_key_length() {
let invalid_key = vec![0x42u8; 15]; // Wrong length (not 16)
let iv = vec![0x99u8; 12];
let plaintext = b"Test";
let aad = b"aad";
let result = encrypt_tls13_record(&invalid_key, &iv, 0, plaintext, aad);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidInput);
}
#[test]
fn test_encrypt_invalid_iv_length() {
let key = vec![0x42u8; 16];
let invalid_iv = vec![0x99u8; 11]; // Wrong length (not 12)
let plaintext = b"Test";
let aad = b"aad";
let result = encrypt_tls13_record(&key, &invalid_iv, 0, plaintext, aad);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidInput);
}
#[test]
fn test_decrypt_corrupted_ciphertext() {
let key = vec![0x42u8; 16];
let iv = vec![0x99u8; 12];
let plaintext = b"Test corruption";
let aad = b"aad";
let mut ciphertext = encrypt_tls13_record(&key, &iv, 0, plaintext, aad).unwrap();
// Corrupt the ciphertext
ciphertext[5] ^= 0xFF;
let result = decrypt_tls13_record(&key, &iv, 0, &ciphertext, aad);
assert!(result.is_err());
}
#[test]
fn test_encrypt_empty_plaintext() {
let key = vec![0x42u8; 16];
let iv = vec![0x99u8; 12];
let plaintext = b"";
let aad = b"aad";
let ciphertext = encrypt_tls13_record(&key, &iv, 0, plaintext, aad).unwrap();
// Should still produce a ciphertext with auth tag
assert!(ciphertext.len() >= 16); // At least the auth tag
let decrypted = decrypt_tls13_record(&key, &iv, 0, &ciphertext, aad).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_large_plaintext() {
let key = vec![0x42u8; 16];
let iv = vec![0x99u8; 12];
let plaintext = vec![0xAB; 16384]; // 16KB
let aad = b"aad";
let ciphertext = encrypt_tls13_record(&key, &iv, 42, &plaintext, aad).unwrap();
let decrypted = decrypt_tls13_record(&key, &iv, 42, &ciphertext, aad).unwrap();
assert_eq!(decrypted, plaintext);
}
}
+552
View File
@@ -0,0 +1,552 @@
use aws_lc_rs::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
use aws_lc_rs::agreement;
use aws_lc_rs::hkdf::{Salt, HKDF_SHA256};
#[cfg(test)]
use std::time::{SystemTime, UNIX_EPOCH};
/// Custom error type for REALITY cryptographic operations
#[derive(Debug)]
pub enum CryptoError {
InvalidKeyLength,
InvalidNonceLength,
InvalidCiphertextLength,
EncryptionFailed,
DecryptionFailed,
EcdhFailed,
HkdfFailed,
CertificateGenerationFailed(String),
}
impl std::fmt::Display for CryptoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CryptoError::InvalidKeyLength => write!(f, "Invalid key length"),
CryptoError::InvalidNonceLength => write!(f, "Invalid nonce length"),
CryptoError::InvalidCiphertextLength => write!(f, "Invalid ciphertext length"),
CryptoError::EncryptionFailed => write!(f, "Encryption failed"),
CryptoError::DecryptionFailed => write!(f, "Decryption failed"),
CryptoError::EcdhFailed => write!(f, "ECDH key exchange failed"),
CryptoError::HkdfFailed => write!(f, "HKDF derivation failed"),
CryptoError::CertificateGenerationFailed(e) => {
write!(f, "Certificate generation failed: {}", e)
}
}
}
}
impl std::error::Error for CryptoError {}
impl From<CryptoError> for std::io::Error {
fn from(err: CryptoError) -> Self {
std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string())
}
}
/// Performs X25519 ECDH key exchange
///
/// # Arguments
/// * `private_key` - 32-byte X25519 private key
/// * `public_key` - 32-byte X25519 public key
///
/// # Returns
/// 32-byte shared secret
pub fn perform_ecdh(
private_key: &[u8; 32],
public_key: &[u8; 32],
) -> Result<[u8; 32], CryptoError> {
// Create private key from raw bytes
let my_private_key = agreement::PrivateKey::from_private_key(&agreement::X25519, private_key)
.map_err(|_| CryptoError::EcdhFailed)?;
// Compute public key from private key (not used, but verifies key is valid)
let _my_public_key = my_private_key
.compute_public_key()
.map_err(|_| CryptoError::EcdhFailed)?;
// Create unparsed public key
let peer_public_key =
agreement::UnparsedPublicKey::new(&agreement::X25519, public_key.as_ref());
// Perform ECDH
let mut shared_secret = [0u8; 32];
agreement::agree(
&my_private_key,
peer_public_key,
CryptoError::EcdhFailed,
|key_material| {
shared_secret.copy_from_slice(key_material);
Ok(())
},
)?;
Ok(shared_secret)
}
/// Derives authentication key using HKDF-SHA256
///
/// # Arguments
/// * `shared_secret` - 32-byte shared secret from ECDH
/// * `salt` - Salt bytes (must be exactly 20 bytes, from ClientHello.Random[0..20])
/// * `info` - Context string (should be b"REALITY")
///
/// # Returns
/// 32-byte derived authentication key
///
/// # Panics
/// Panics if salt is not exactly 20 bytes.
pub fn derive_auth_key(
shared_secret: &[u8; 32],
salt: &[u8],
info: &[u8],
) -> Result<[u8; 32], CryptoError> {
debug_assert_eq!(salt.len(), 20, "salt must be exactly 20 bytes");
let salt = Salt::new(HKDF_SHA256, salt);
let prk = salt.extract(shared_secret);
let info_pieces = [info];
let okm = prk
.expand(&info_pieces, HKDF_SHA256)
.map_err(|_| CryptoError::HkdfFailed)?;
let mut auth_key = [0u8; 32];
okm.fill(&mut auth_key)
.map_err(|_| CryptoError::HkdfFailed)?;
Ok(auth_key)
}
/// Encrypts SessionId using AES-256-GCM
///
/// # Arguments
/// * `plaintext` - 16-byte plaintext (first 16 bytes of SessionId)
/// * `auth_key` - 32-byte authentication key
/// * `nonce` - 12-byte nonce (ClientHello.Random[20..32])
/// * `aad` - Additional authenticated data (entire ClientHello)
///
/// # Returns
/// 32-byte result (16 bytes ciphertext + 16 bytes GCM tag)
///
/// # Panics
/// Panics if nonce is not exactly 12 bytes.
pub fn encrypt_session_id(
plaintext: &[u8; 16],
auth_key: &[u8; 32],
nonce: &[u8],
aad: &[u8],
) -> Result<[u8; 32], CryptoError> {
debug_assert_eq!(nonce.len(), 12, "nonce must be exactly 12 bytes");
let unbound_key =
UnboundKey::new(&AES_256_GCM, auth_key).map_err(|_| CryptoError::EncryptionFailed)?;
let sealing_key = LessSafeKey::new(unbound_key);
let nonce_obj =
Nonce::try_assume_unique_for_key(nonce).map_err(|_| CryptoError::InvalidNonceLength)?;
let aad_obj = Aad::from(aad);
// aws-lc-rs requires in-place encryption
let mut in_out = plaintext.to_vec();
sealing_key
.seal_in_place_append_tag(nonce_obj, aad_obj, &mut in_out)
.map_err(|_| CryptoError::EncryptionFailed)?;
if in_out.len() != 32 {
return Err(CryptoError::EncryptionFailed);
}
let mut result = [0u8; 32];
result.copy_from_slice(&in_out);
Ok(result)
}
/// Decrypts SessionId using AES-256-GCM
///
/// # Arguments
/// * `ciphertext_and_tag` - 32-byte encrypted data (16 ciphertext + 16 tag)
/// * `auth_key` - 32-byte authentication key
/// * `nonce` - 12-byte nonce (ClientHello.Random[20..32])
/// * `aad` - Additional authenticated data (entire ClientHello)
///
/// # Returns
/// 16-byte decrypted plaintext
///
/// # Panics
/// Panics if nonce is not exactly 12 bytes.
pub fn decrypt_session_id(
ciphertext_and_tag: &[u8; 32],
auth_key: &[u8; 32],
nonce: &[u8],
aad: &[u8],
) -> Result<[u8; 16], CryptoError> {
debug_assert_eq!(nonce.len(), 12, "nonce must be exactly 12 bytes");
let unbound_key =
UnboundKey::new(&AES_256_GCM, auth_key).map_err(|_| CryptoError::DecryptionFailed)?;
let opening_key = LessSafeKey::new(unbound_key);
let nonce_obj =
Nonce::try_assume_unique_for_key(nonce).map_err(|_| CryptoError::InvalidNonceLength)?;
let aad_obj = Aad::from(aad);
// aws-lc-rs requires in-place decryption
let mut in_out = ciphertext_and_tag.to_vec();
let plaintext = opening_key
.open_in_place(nonce_obj, aad_obj, &mut in_out)
.map_err(|_| CryptoError::DecryptionFailed)?;
if plaintext.len() != 16 {
return Err(CryptoError::DecryptionFailed);
}
let mut result = [0u8; 16];
result.copy_from_slice(plaintext);
Ok(result)
}
#[cfg(test)]
/// Creates a REALITY SessionId (test helper)
fn create_session_id(version: [u8; 3], timestamp: u32, short_id: &[u8; 8]) -> [u8; 32] {
let mut session_id = [0u8; 32];
session_id[0] = version[0]; // Major version
session_id[1] = version[1]; // Minor version
session_id[2] = version[2]; // Patch version
// session_id[3] = 0 (reserved)
session_id[4..8].copy_from_slice(&timestamp.to_be_bytes());
session_id[8..16].copy_from_slice(short_id);
// session_id[16..32] remain zeros
session_id
}
#[cfg(test)]
/// Gets current Unix timestamp (test helper)
fn get_current_timestamp() -> u32 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ecdh() {
// Test vectors
let private_key_a = [1u8; 32];
let private_key_b = [2u8; 32];
// Compute public keys using aws-lc-rs
let priv_a =
agreement::PrivateKey::from_private_key(&agreement::X25519, &private_key_a).unwrap();
let pub_a = priv_a.compute_public_key().unwrap();
let priv_b =
agreement::PrivateKey::from_private_key(&agreement::X25519, &private_key_b).unwrap();
let pub_b = priv_b.compute_public_key().unwrap();
// Perform ECDH both ways
let mut shared_a_bytes = [0u8; 32];
agreement::agree(
&priv_a,
&agreement::UnparsedPublicKey::new(&agreement::X25519, pub_b.as_ref()),
(),
|key_material| {
shared_a_bytes.copy_from_slice(key_material);
Ok(())
},
)
.unwrap();
let mut shared_b_bytes = [0u8; 32];
agreement::agree(
&priv_b,
&agreement::UnparsedPublicKey::new(&agreement::X25519, pub_a.as_ref()),
(),
|key_material| {
shared_b_bytes.copy_from_slice(key_material);
Ok(())
},
)
.unwrap();
assert_eq!(shared_a_bytes, shared_b_bytes);
}
#[test]
fn test_session_id_structure() {
// Test that session ID has the correct structure
let version = [1, 8, 1]; // major, minor, patch
let timestamp = get_current_timestamp();
let short_id = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
let session_id = create_session_id(version, timestamp, &short_id);
// Verify structure
assert_eq!(session_id[0], 1); // version major
assert_eq!(session_id[1], 8); // version minor
assert_eq!(session_id[2], 1); // version patch
assert_eq!(session_id[3], 0); // reserved
// Verify timestamp
let extracted_timestamp =
u32::from_be_bytes([session_id[4], session_id[5], session_id[6], session_id[7]]);
assert_eq!(extracted_timestamp, timestamp);
// Verify short_id
assert_eq!(&session_id[8..16], &short_id[..]);
// Verify remaining bytes are zero
for i in 16..32 {
assert_eq!(session_id[i], 0);
}
}
#[test]
fn test_version_comparison() {
// Test version comparison logic (same as used in reality_server_handler.rs)
let v1_8_1 = [1u8, 8, 1];
let v1_8_0 = [1u8, 8, 0];
let v1_9_0 = [1u8, 9, 0];
let v2_0_0 = [2u8, 0, 0];
assert!(v1_8_0 < v1_8_1);
assert!(v1_8_1 < v1_9_0);
assert!(v1_9_0 < v2_0_0);
assert!(v1_8_1 > v1_8_0);
}
#[test]
fn test_timestamp_validation_logic() {
// Test the timestamp validation logic used in reality_server_handler.rs
let now = get_current_timestamp();
// Test within bounds (60 seconds = 60000 milliseconds)
let max_diff_ms = 60000u64;
let max_diff_secs = max_diff_ms / 1000;
// Current time - should pass
let diff = now.abs_diff(now);
assert!((diff as u64) <= max_diff_secs);
// 30 seconds ago - should pass
let past_timestamp = now - 30;
let diff = now.abs_diff(past_timestamp);
assert!((diff as u64) <= max_diff_secs);
// 30 seconds future - should pass
let future_timestamp = now + 30;
let diff = now.abs_diff(future_timestamp);
assert!((diff as u64) <= max_diff_secs);
// 2 minutes ago - should fail
let old_timestamp = now.saturating_sub(120);
let diff = now.abs_diff(old_timestamp);
assert!((diff as u64) > max_diff_secs);
// 2 minutes future - should fail
let future_timestamp = now + 120;
let diff = now.abs_diff(future_timestamp);
assert!((diff as u64) > max_diff_secs);
}
#[test]
fn test_session_id_encryption_preserves_structure() {
// Create session ID with known values
let version = [1, 8, 1];
let timestamp = 1234567890u32;
let short_id = [0xAB; 8];
let session_id = create_session_id(version, timestamp, &short_id);
// Perform ECDH
let client_private = [0x01; 32];
let server_public = [0x02; 32];
let shared_secret = perform_ecdh(&client_private, &server_public).unwrap();
// Derive auth key
let salt = [0x03; 20];
let auth_key = derive_auth_key(&shared_secret, &salt, b"REALITY").unwrap();
// Encrypt session ID (first 16 bytes)
let plaintext: [u8; 16] = session_id[0..16].try_into().unwrap();
let nonce = [0x04; 12];
let aad = b"test additional authenticated data";
let encrypted = encrypt_session_id(&plaintext, &auth_key, &nonce, aad).unwrap();
// Decrypt session ID
let decrypted = decrypt_session_id(&encrypted, &auth_key, &nonce, aad).unwrap();
// Verify we can recover the structure
assert_eq!(decrypted[0], version[0]);
assert_eq!(decrypted[1], version[1]);
assert_eq!(decrypted[2], version[2]);
assert_eq!(decrypted[3], 0); // reserved
let recovered_timestamp =
u32::from_be_bytes([decrypted[4], decrypted[5], decrypted[6], decrypted[7]]);
assert_eq!(recovered_timestamp, timestamp);
assert_eq!(&decrypted[8..16], &short_id[..]);
}
#[test]
fn test_hkdf() {
let shared_secret = [0x42u8; 32];
let salt = [0x43u8; 20];
let info = b"REALITY";
let auth_key = derive_auth_key(&shared_secret, &salt, info).unwrap();
// Verify length
assert_eq!(auth_key.len(), 32);
// Verify deterministic
let auth_key2 = derive_auth_key(&shared_secret, &salt, info).unwrap();
assert_eq!(auth_key, auth_key2);
// Verify different salt produces different key
let salt2 = [0x44u8; 20];
let auth_key3 = derive_auth_key(&shared_secret, &salt2, info).unwrap();
assert_ne!(auth_key, auth_key3);
}
#[test]
fn test_aes_gcm_roundtrip() {
let plaintext = [0x55u8; 16];
let auth_key = [0x66u8; 32];
let nonce = [0x77u8; 12];
let aad = b"additional authenticated data";
let encrypted = encrypt_session_id(&plaintext, &auth_key, &nonce, aad).unwrap();
assert_eq!(encrypted.len(), 32);
let decrypted = decrypt_session_id(&encrypted, &auth_key, &nonce, aad).unwrap();
assert_eq!(plaintext, decrypted);
}
#[test]
fn test_aes_gcm_wrong_key_fails() {
let plaintext = [0x55u8; 16];
let auth_key = [0x66u8; 32];
let wrong_key = [0x67u8; 32];
let nonce = [0x77u8; 12];
let aad = b"additional authenticated data";
let encrypted = encrypt_session_id(&plaintext, &auth_key, &nonce, aad).unwrap();
let result = decrypt_session_id(&encrypted, &wrong_key, &nonce, aad);
assert!(result.is_err());
}
#[test]
fn test_aes_gcm_wrong_aad_fails() {
let plaintext = [0x55u8; 16];
let auth_key = [0x66u8; 32];
let nonce = [0x77u8; 12];
let aad = b"additional authenticated data";
let wrong_aad = b"wrong additional authenticated data";
let encrypted = encrypt_session_id(&plaintext, &auth_key, &nonce, aad).unwrap();
let result = decrypt_session_id(&encrypted, &auth_key, &nonce, wrong_aad);
assert!(result.is_err());
}
#[test]
fn test_create_session_id() {
let version = [1, 8, 1];
let timestamp = 1234567890u32;
let short_id = [0xAB; 8];
let session_id = create_session_id(version, timestamp, &short_id);
assert_eq!(session_id[0], 1);
assert_eq!(session_id[1], 8);
assert_eq!(session_id[2], 1);
assert_eq!(session_id[3], 0);
let ts = u32::from_be_bytes([session_id[4], session_id[5], session_id[6], session_id[7]]);
assert_eq!(ts, timestamp);
assert_eq!(&session_id[8..16], &short_id[..]);
// Verify remaining bytes are zeros
for i in 16..32 {
assert_eq!(session_id[i], 0);
}
}
#[test]
fn test_validation_scenarios() {
println!("\n=== REALITY Validation Test Scenarios ===\n");
// Scenario 1: Version validation
println!("Scenario 1: Version Validation");
println!(" Client version: [1, 8, 1]");
println!(" Min version: [1, 8, 0]");
println!(" Max version: [1, 9, 0]");
println!(" Expected: PASS ✓");
let client_version = [1u8, 8, 1];
let min_version = [1u8, 8, 0];
let max_version = [1u8, 9, 0];
assert!(client_version >= min_version);
assert!(client_version <= max_version);
// Scenario 2: Version below minimum
println!("\nScenario 2: Version Below Minimum");
println!(" Client version: [1, 7, 5]");
println!(" Min version: [1, 8, 0]");
println!(" Expected: FAIL ✗");
let old_client_version = [1u8, 7, 5];
assert!(old_client_version < min_version);
// Scenario 3: Version above maximum
println!("\nScenario 3: Version Above Maximum");
println!(" Client version: [2, 0, 0]");
println!(" Max version: [1, 9, 0]");
println!(" Expected: FAIL ✗");
let new_client_version = [2u8, 0, 0];
assert!(new_client_version > max_version);
// Scenario 4: Timestamp validation (within bounds)
println!("\nScenario 4: Timestamp Within Bounds");
let now = get_current_timestamp();
let client_timestamp = now - 30; // 30 seconds ago
let max_diff_ms = 60000u64; // 60 seconds
let diff = now.abs_diff(client_timestamp);
println!(" Server time: {}", now);
println!(" Client time: {} (30 seconds ago)", client_timestamp);
println!(" Max allowed: {} seconds", max_diff_ms / 1000);
println!(" Actual diff: {} seconds", diff);
println!(" Expected: PASS ✓");
assert!((diff as u64) <= (max_diff_ms / 1000));
// Scenario 5: Timestamp validation (out of bounds)
println!("\nScenario 5: Timestamp Out of Bounds");
let old_timestamp = now - 120; // 2 minutes ago
let diff = now.abs_diff(old_timestamp);
println!(" Server time: {}", now);
println!(" Client time: {} (2 minutes ago)", old_timestamp);
println!(" Max allowed: {} seconds", max_diff_ms / 1000);
println!(" Actual diff: {} seconds", diff);
println!(" Expected: FAIL ✗");
assert!((diff as u64) > (max_diff_ms / 1000));
// Scenario 6: Future timestamp validation
println!("\nScenario 6: Future Timestamp Validation");
let future_timestamp = now + 45; // 45 seconds in future
let diff = now.abs_diff(future_timestamp);
println!(" Server time: {}", now);
println!(" Client time: {} (45 seconds in future)", future_timestamp);
println!(" Max allowed: {} seconds", max_diff_ms / 1000);
println!(" Actual diff: {} seconds", diff);
println!(" Expected: PASS ✓");
assert!((diff as u64) <= (max_diff_ms / 1000));
println!("\n=== All Validation Scenarios Passed ===\n");
}
}
+178
View File
@@ -0,0 +1,178 @@
use aws_lc_rs::hmac;
use aws_lc_rs::signature::Ed25519KeyPair;
use std::io::{Error, ErrorKind, Result};
/// Generate a template Ed25519 certificate for the given hostname
fn generate_template_cert(hostname: &str) -> (Vec<u8>, Ed25519KeyPair) {
// Generate Ed25519 keypair using rcgen
let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
.expect("Failed to generate Ed25519 key pair");
// Generate a minimal self-signed certificate using rcgen with Ed25519
let params = rcgen::CertificateParams::new(vec![hostname.to_string()])
.expect("Failed to create certificate params");
// Create self-signed certificate
let cert = params
.self_signed(&key_pair)
.expect("Failed to create self-signed certificate");
let cert_der = cert.der().to_vec();
// Create the Ed25519KeyPair from the PKCS#8 document
let signing_key = Ed25519KeyPair::from_pkcs8(key_pair.serialized_der())
.expect("Failed to parse generated key");
use aws_lc_rs::signature::KeyPair;
let public_key_bytes = signing_key.public_key().as_ref();
log::debug!(
"REALITY DEBUG: Generated template certificate ({} bytes) for {}",
cert_der.len(),
hostname
);
log::debug!(
"REALITY DEBUG: Using public key: {:?}",
&public_key_bytes[..16.min(public_key_bytes.len())]
);
// Debug: print last 70 bytes to see signature structure
if cert_der.len() > 70 {
log::debug!(
"REALITY DEBUG: Last 70 bytes: {:?}",
&cert_der[cert_der.len() - 70..]
);
}
(cert_der, signing_key)
}
/// Generate HMAC-signed Ed25519 certificate
///
/// This follows the uTLS approach:
/// 1. Generate a certificate for the destination hostname
/// 2. Replace the signature (last 64 bytes) with HMAC-SHA512(auth_key, ed25519_public_key)
///
/// This is NOT a cryptographically valid certificate!
pub fn generate_hmac_certificate(
auth_key: &[u8; 32],
hostname: &str,
) -> Result<(Vec<u8>, Ed25519KeyPair)> {
// Generate a new certificate for the destination hostname
let (cert_der, signing_key) = generate_template_cert(hostname);
log::debug!(
"REALITY DEBUG: Using template certificate ({} bytes)",
cert_der.len()
);
// Extract the Ed25519 public key from the signing key
use aws_lc_rs::signature::KeyPair;
let public_key_bytes = signing_key.public_key().as_ref();
// Replace signature with HMAC
let cert_with_hmac = replace_signature_with_hmac(cert_der, auth_key, public_key_bytes)?;
log::debug!("REALITY DEBUG: Replaced signature with HMAC");
// Return the HMAC-signed certificate with the signing key
Ok((cert_with_hmac, signing_key))
}
/// Replace the certificate signature with HMAC-SHA512(auth_key, ed25519_public_key)
fn replace_signature_with_hmac(
mut cert_der: Vec<u8>,
auth_key: &[u8; 32],
public_key_bytes: &[u8],
) -> Result<Vec<u8>> {
// The signature is the last BIT STRING in the certificate DER
// For Ed25519, it's always 64 bytes
// Find the signature BIT STRING tag
// It should be near the end of the certificate
// BIT STRING tag = 0x03
// Length = 0x41 (65 bytes: 1 byte unused bits + 64 bytes signature)
// Search backwards for BIT STRING tag
let mut sig_offset = None;
for i in (0..cert_der.len().saturating_sub(66)).rev() {
if cert_der[i] == 0x03 && cert_der[i + 1] == 0x41 && cert_der[i + 2] == 0x00 {
// Found it!
sig_offset = Some(i + 3); // Skip tag, length, unused bits
break;
}
}
let sig_offset = sig_offset.ok_or_else(|| {
Error::new(
ErrorKind::InvalidData,
"Could not find signature in certificate DER",
)
})?;
if sig_offset + 64 > cert_der.len() {
return Err(Error::new(
ErrorKind::InvalidData,
"Invalid signature offset",
));
}
// Compute HMAC-SHA512(auth_key, ed25519_public_key)
let key = hmac::Key::new(hmac::HMAC_SHA512, auth_key);
let tag = hmac::sign(&key, public_key_bytes);
let hmac_bytes = tag.as_ref();
// Replace signature bytes with HMAC
cert_der[sig_offset..sig_offset + 64].copy_from_slice(hmac_bytes);
log::debug!(
"REALITY DEBUG: HMAC signature (first 16 bytes): {:?}",
&hmac_bytes[..16]
);
Ok(cert_der)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_hmac_certificate() {
let auth_key = [42u8; 32];
let hostname = "test.example.com";
let result = generate_hmac_certificate(&auth_key, hostname);
assert!(result.is_ok());
let (cert_der, _signing_key) = result.unwrap();
// Certificate should be a reasonable size (few hundred bytes)
assert!(cert_der.len() > 100);
assert!(cert_der.len() < 1000);
// Should start with SEQUENCE tag
assert_eq!(cert_der[0], 0x30);
}
#[test]
fn test_hmac_signature_deterministic() {
let auth_key = [99u8; 32];
let hostname = "test.example.com";
let (cert1, key1) = generate_hmac_certificate(&auth_key, hostname).unwrap();
use aws_lc_rs::signature::KeyPair;
let public_key1 = key1.public_key().as_ref().to_vec();
// Generate another certificate with same auth_key
let (cert2, key2) = generate_hmac_certificate(&auth_key, hostname).unwrap();
let public_key2 = key2.public_key().as_ref().to_vec();
// Different Ed25519 keys (random generation)
assert_ne!(public_key1, public_key2);
// But certificates should have different signatures
// (because they're based on different public keys)
assert_ne!(cert1, cert2);
}
}
+865
View File
@@ -0,0 +1,865 @@
// REALITY client-side connection
//
// This implements a rustls-compatible API for REALITY protocol client connections,
// allowing REALITY to be used as a drop-in replacement for rustls.
use std::io::{self, Read, Write};
use crate::slide_buffer::SlideBuffer;
use aws_lc_rs::{
agreement, digest,
rand::{SecureRandom, SystemRandom},
};
use super::common::{
self, CIPHERTEXT_READ_BUF_CAPACITY, CONTENT_TYPE_ALERT, CONTENT_TYPE_APPLICATION_DATA,
CONTENT_TYPE_CHANGE_CIPHER_SPEC, CONTENT_TYPE_HANDSHAKE, HANDSHAKE_TYPE_CERTIFICATE,
HANDSHAKE_TYPE_CERTIFICATE_VERIFY, HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS,
HANDSHAKE_TYPE_FINISHED, PLAINTEXT_READ_BUF_CAPACITY, TLS_MAX_RECORD_SIZE,
TLS_RECORD_HEADER_SIZE,
};
use super::reality_aead::{decrypt_handshake_message, decrypt_tls13_record};
use super::reality_auth::{derive_auth_key, encrypt_session_id, perform_ecdh};
use super::reality_client_verify::{extract_certificate_der, verify_certificate_hmac};
use super::reality_io_state::RealityIoState;
use super::reality_reader_writer::{RealityReader, RealityWriter};
use super::reality_records::{encrypt_handshake_to_records, encrypt_plaintext_to_records};
use super::reality_tls13_keys::{
compute_finished_verify_data, derive_application_secrets, derive_handshake_keys,
derive_traffic_keys,
};
use super::reality_tls13_messages::*;
use super::reality_util::extract_server_public_key;
/// Configuration for REALITY client connections
#[derive(Clone)]
pub struct RealityClientConfig {
/// Server's X25519 public key (32 bytes)
pub public_key: [u8; 32],
/// Short ID for authentication (8 bytes)
pub short_id: [u8; 8],
/// Server name for SNI
pub server_name: String,
}
/// Handshake state machine for REALITY client
enum HandshakeState {
/// ClientHello sent, waiting for ServerHello
AwaitingServerHello {
client_hello_hash: [u8; 32],
client_hello_bytes: Vec<u8>, // Full ClientHello handshake message
client_private_key: [u8; 32],
auth_key: [u8; 32], // REALITY authentication key for HMAC verification
},
/// ServerHello received, processing encrypted handshake messages
ProcessingHandshake {
client_handshake_traffic_secret: Vec<u8>,
server_handshake_traffic_secret: Vec<u8>,
master_secret: Vec<u8>,
cipher_suite: u16,
handshake_transcript_bytes: Vec<u8>, // Accumulated transcript for hash computation
auth_key: [u8; 32], // REALITY authentication key for HMAC verification
},
/// Handshake complete, ready for application data
Complete,
}
/// REALITY client-side connection implementing rustls-compatible API
pub struct RealityClientConnection {
// Configuration
config: RealityClientConfig,
// Handshake state
handshake_state: HandshakeState,
// TLS 1.3 application traffic encryption (post-handshake)
app_read_key: Option<Vec<u8>>,
app_read_iv: Option<Vec<u8>>,
app_write_key: Option<Vec<u8>>,
app_write_iv: Option<Vec<u8>>,
read_seq: u64,
write_seq: u64,
cipher_suite: u16,
// Pre-allocated buffer for TLS read operations (reused across calls)
tls_read_buffer: Box<[u8; TLS_MAX_RECORD_SIZE]>,
// Buffers for I/O - using SlideBuffer for efficient zero-alloc operations
ciphertext_read_buf: SlideBuffer, // Incoming encrypted TLS records
ciphertext_write_buf: Vec<u8>, // Outgoing encrypted TLS records
plaintext_read_buf: SlideBuffer, // Decrypted application data
plaintext_write_buf: Vec<u8>, // Application data to encrypt
}
impl RealityClientConnection {
/// Create a new REALITY client connection and generate ClientHello
pub fn new(config: RealityClientConfig) -> io::Result<Self> {
let mut conn = RealityClientConnection {
config,
handshake_state: HandshakeState::AwaitingServerHello {
client_hello_hash: [0u8; 32],
client_hello_bytes: Vec::new(),
client_private_key: [0u8; 32],
auth_key: [0u8; 32],
},
app_read_key: None,
app_read_iv: None,
app_write_key: None,
app_write_iv: None,
read_seq: 0,
write_seq: 0,
cipher_suite: 0,
tls_read_buffer: Box::new([0u8; TLS_MAX_RECORD_SIZE]),
ciphertext_read_buf: SlideBuffer::new(CIPHERTEXT_READ_BUF_CAPACITY),
ciphertext_write_buf: Vec::with_capacity(CIPHERTEXT_READ_BUF_CAPACITY),
plaintext_read_buf: SlideBuffer::new(PLAINTEXT_READ_BUF_CAPACITY),
plaintext_write_buf: Vec::with_capacity(TLS_MAX_RECORD_SIZE),
};
// Generate ClientHello immediately
conn.generate_client_hello()?;
Ok(conn)
}
/// Generate and buffer ClientHello
fn generate_client_hello(&mut self) -> io::Result<()> {
let rng = SystemRandom::new();
// Generate our X25519 keypair
let mut our_private_bytes = [0u8; 32];
rng.fill(&mut our_private_bytes)
.map_err(|_| io::Error::other("RNG failed"))?;
let our_private_key =
agreement::PrivateKey::from_private_key(&agreement::X25519, &our_private_bytes)
.map_err(|_| io::Error::other("Failed to create X25519 key"))?;
let our_public_key_bytes = our_private_key
.compute_public_key()
.map_err(|_| io::Error::other("Failed to compute public key"))?;
// Generate client random
let mut client_random = [0u8; 32];
rng.fill(&mut client_random)
.map_err(|_| io::Error::other("RNG failed"))?;
// Perform ECDH with server's public key to derive auth key
let shared_secret = perform_ecdh(&our_private_bytes, &self.config.public_key)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
// Use slice directly from client_random to avoid copying
let auth_key = derive_auth_key(&shared_secret, &client_random[0..20], b"REALITY")
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
// Create session ID with REALITY metadata
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| io::Error::other("System time error"))?
.as_secs();
let mut session_id_plaintext = [0u8; 16];
session_id_plaintext[0] = 1; // Protocol version major
session_id_plaintext[1] = 8; // Protocol version minor
session_id_plaintext[2] = 0; // Protocol version patch
session_id_plaintext[3] = 0; // Padding byte
// Timestamp (4 bytes as uint32, in seconds)
session_id_plaintext[4..8].copy_from_slice(&(timestamp as u32).to_be_bytes());
// Short ID (8 bytes)
session_id_plaintext[8..16].copy_from_slice(&self.config.short_id);
// Create a 32-byte SessionId (16 bytes plaintext + 16 bytes zeros for padding)
let mut session_id_for_hello = [0u8; 32];
session_id_for_hello[0..16].copy_from_slice(&session_id_plaintext);
// Build ClientHello with plaintext SessionId first
let mut client_hello = construct_client_hello(
&client_random,
&session_id_for_hello,
our_public_key_bytes.as_ref(),
&self.config.server_name,
)?;
// Now encrypt the SessionId using the ClientHello with zeroed SessionId as AAD
// Use slice directly from client_random to avoid copying
let nonce = &client_random[20..32];
// Zero out the SessionId in ClientHello to create AAD (matches what server will use)
// SessionId is at offset 39 in ClientHello handshake
client_hello[39..71].fill(0);
log::debug!("REALITY CLIENT: Encrypting SessionId");
log::debug!(" auth_key={:02x?}", &auth_key);
log::debug!(" nonce={:02x?}", nonce);
log::debug!(" plaintext={:02x?}", &session_id_plaintext);
log::debug!(
" aad_len={} (ClientHello with zero SessionId)",
client_hello.len()
);
log::debug!(" aad[0..4]={:02x?}", &client_hello[0..4]);
let encrypted_session_id =
encrypt_session_id(&session_id_plaintext, &auth_key, nonce, &client_hello)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
log::debug!(
"REALITY CLIENT: Encrypted SessionId={:02x?}",
&encrypted_session_id
);
// Replace the zeros with the encrypted SessionId
client_hello[39..71].copy_from_slice(&encrypted_session_id);
// Compute ClientHello hash
let mut ch_transcript = digest::Context::new(&digest::SHA256);
ch_transcript.update(&client_hello);
let client_hello_hash = ch_transcript.finish();
let mut client_hello_hash_arr = [0u8; 32];
client_hello_hash_arr.copy_from_slice(client_hello_hash.as_ref());
// Wrap in TLS record
let mut record = write_record_header(CONTENT_TYPE_HANDSHAKE, client_hello.len() as u16);
record.extend_from_slice(&client_hello);
// Buffer for sending
self.ciphertext_write_buf.extend_from_slice(&record);
// Update state
self.handshake_state = HandshakeState::AwaitingServerHello {
client_hello_hash: client_hello_hash_arr,
client_hello_bytes: client_hello.clone(), // Save the actual ClientHello bytes
client_private_key: our_private_bytes,
auth_key, // Save auth_key for HMAC certificate verification
};
log::debug!(
"REALITY: ClientHello generated and buffered ({} bytes)",
record.len()
);
Ok(())
}
/// Read TLS messages from the provided reader into internal buffer
///
/// Uses pre-allocated buffer to avoid allocation on every call.
pub fn read_tls(&mut self, rd: &mut dyn Read) -> io::Result<usize> {
// Compact if remaining capacity is insufficient for a full TLS record
if self.ciphertext_read_buf.remaining_capacity() < TLS_MAX_RECORD_SIZE {
self.ciphertext_read_buf.compact();
}
// Read into pre-allocated buffer
let n = rd.read(&mut self.tls_read_buffer[..])?;
if n > 0 {
self.ciphertext_read_buf
.extend_from_slice(&self.tls_read_buffer[..n]);
}
Ok(n)
}
/// Process buffered packets and advance state machine
pub fn process_new_packets(&mut self) -> io::Result<RealityIoState> {
match &self.handshake_state {
HandshakeState::AwaitingServerHello { .. } => {
self.process_server_hello()?;
}
HandshakeState::ProcessingHandshake { .. } => {
self.process_encrypted_handshake()?;
}
HandshakeState::Complete => {
self.process_application_data()?;
}
}
Ok(RealityIoState::new(self.plaintext_read_buf.len()))
}
/// Process ServerHello
fn process_server_hello(&mut self) -> io::Result<()> {
// Extract state
let (client_hello_hash, client_private_key, auth_key) = match &self.handshake_state {
HandshakeState::AwaitingServerHello {
client_hello_hash,
client_hello_bytes: _,
client_private_key,
auth_key,
} => (*client_hello_hash, *client_private_key, *auth_key),
_ => return Ok(()), // Wrong state
};
// Check if we have enough data for a TLS record header
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE {
return Ok(()); // Need more data
}
// Parse TLS record length
let record_len = self
.ciphertext_read_buf
.get_u16_be(3)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
as usize;
// Check if we have the complete record
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
if self.ciphertext_read_buf.len() < total_record_len {
return Ok(()); // Need more data
}
// Copy ServerHello record to Vec for processing
let record: Vec<u8> = self.ciphertext_read_buf[..total_record_len].to_vec();
self.ciphertext_read_buf.consume(total_record_len);
let server_hello = &record[TLS_RECORD_HEADER_SIZE..]; // Skip TLS record header (includes handshake header)
log::debug!(
"REALITY CLIENT: ServerHello for transcript: len={}, bytes={:02x?}",
server_hello.len(),
server_hello
);
// Extract server public key from ServerHello
let server_public_key = extract_server_public_key(&record)?;
// Compute transcript hash including ClientHello and ServerHello
// NOTE: We need to update with the actual ClientHello bytes, not its hash!
// We need to preserve the ClientHello bytes to include them in the transcript
// For now, we'll need to get them from the state
// Get the actual ClientHello bytes from our saved state
let client_hello_bytes = match &self.handshake_state {
HandshakeState::AwaitingServerHello {
client_hello_bytes, ..
} => client_hello_bytes.clone(),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid handshake state",
))
}
};
let mut full_transcript = digest::Context::new(&digest::SHA256);
log::debug!(
"REALITY CLIENT: Transcript includes ClientHello ({} bytes), first bytes: {:02x?}",
client_hello_bytes.len(),
&client_hello_bytes[..client_hello_bytes.len().min(20)]
);
log::debug!(
"REALITY CLIENT: Transcript includes ServerHello ({} bytes), first bytes: {:02x?}",
server_hello.len(),
&server_hello[..server_hello.len().min(20)]
);
full_transcript.update(&client_hello_bytes); // Use actual ClientHello bytes, not hash!
full_transcript.update(server_hello); // ServerHello already includes handshake header
let server_hello_hash = full_transcript.finish();
let mut server_hello_hash_arr = [0u8; 32];
server_hello_hash_arr.copy_from_slice(server_hello_hash.as_ref());
// Perform ECDH for TLS 1.3 key derivation
let peer_public_key =
agreement::UnparsedPublicKey::new(&agreement::X25519, &server_public_key);
let my_private_key =
agreement::PrivateKey::from_private_key(&agreement::X25519, &client_private_key)
.map_err(|_| io::Error::other("Failed to create private key"))?;
let mut tls_shared_secret = [0u8; 32];
agreement::agree(
&my_private_key,
peer_public_key,
io::Error::other("ECDH failed"),
|key_material| {
tls_shared_secret.copy_from_slice(key_material);
Ok(())
},
)?;
// Derive handshake keys
// Removed verbose secret logging for security
let hs_keys = derive_handshake_keys(
&tls_shared_secret,
&client_hello_hash,
&server_hello_hash_arr,
)?;
// Use standard cipher suite
const CIPHER_SUITE: u16 = 0x1301; // TLS_AES_128_GCM_SHA256
log::debug!("REALITY: ServerHello processed, handshake keys derived");
// Initialize transcript with actual ClientHello and ServerHello bytes (not hashes!)
let mut transcript_bytes = Vec::new();
transcript_bytes.extend_from_slice(&client_hello_bytes); // Actual ClientHello bytes
transcript_bytes.extend_from_slice(server_hello); // Actual ServerHello bytes
// Update state
self.handshake_state = HandshakeState::ProcessingHandshake {
client_handshake_traffic_secret: hs_keys.client_handshake_traffic_secret.clone(),
server_handshake_traffic_secret: hs_keys.server_handshake_traffic_secret.clone(),
master_secret: hs_keys.master_secret.clone(),
cipher_suite: CIPHER_SUITE,
handshake_transcript_bytes: transcript_bytes,
auth_key, // Pass auth_key for certificate HMAC verification
};
Ok(())
}
/// Process encrypted handshake messages (EncryptedExtensions, Certificate, CertificateVerify, Finished)
fn process_encrypted_handshake(&mut self) -> io::Result<()> {
// Extract state - we need to preserve it across multiple calls
let (
client_hs_secret,
server_hs_secret,
master_secret,
cipher_suite,
transcript_bytes,
auth_key,
) = match &self.handshake_state {
HandshakeState::ProcessingHandshake {
client_handshake_traffic_secret,
server_handshake_traffic_secret,
master_secret,
cipher_suite,
handshake_transcript_bytes,
auth_key,
} => (
client_handshake_traffic_secret.clone(),
server_handshake_traffic_secret.clone(),
master_secret.clone(),
*cipher_suite,
handshake_transcript_bytes.clone(),
*auth_key,
),
_ => return Ok(()),
};
// Derive server handshake traffic keys for decryption
let (server_hs_key, server_hs_iv) = derive_traffic_keys(&server_hs_secret, cipher_suite)?;
log::debug!(
"REALITY CLIENT: Server HS key={:02x?}, iv={:02x?}",
&server_hs_key[..16],
&server_hs_iv
);
// Check if we have enough data for a TLS record header
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE {
return Ok(()); // Need more data
}
// Check record type
let record_type = self.ciphertext_read_buf[0];
let tls_version = self
.ciphertext_read_buf
.get_u16_be(1)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?;
let record_len = self
.ciphertext_read_buf
.get_u16_be(3)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
as usize;
log::debug!(
"REALITY CLIENT: TLS record header: type=0x{:02x}, version=0x{:04x}, len={}",
record_type,
tls_version,
record_len
);
// Check if we have the complete record
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
if self.ciphertext_read_buf.len() < total_record_len {
return Ok(()); // Need more data
}
// Skip ChangeCipherSpec records - these are dummy in TLS 1.3
if record_type == CONTENT_TYPE_CHANGE_CIPHER_SPEC {
log::debug!(
"REALITY CLIENT: Skipping ChangeCipherSpec record ({} bytes)",
record_len
);
self.ciphertext_read_buf.consume(total_record_len);
// Process next record recursively
return self.process_encrypted_handshake();
}
// If it's not Application Data, we have a problem
if record_type != CONTENT_TYPE_APPLICATION_DATA {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Expected Application Data record, got 0x{:02x}",
record_type
),
));
}
// Copy and extract the combined handshake record
let ciphertext: Vec<u8> =
self.ciphertext_read_buf[TLS_RECORD_HEADER_SIZE..total_record_len].to_vec();
self.ciphertext_read_buf.consume(total_record_len);
log::debug!("REALITY CLIENT: Decrypting combined handshake record - record_type=0x{:02x}, record_len={}, seq=0",
record_type, record_len);
// Try different sequence numbers - sing-box might count differently
let mut combined_plaintext = None;
for seq in 0..3 {
match decrypt_handshake_message(
&server_hs_key,
&server_hs_iv,
seq,
&ciphertext,
record_len as u16,
) {
Ok(plaintext) => {
combined_plaintext = Some(plaintext);
break;
}
Err(_) if seq < 2 => {
log::debug!(
"REALITY CLIENT: Decryption failed with seq={}, trying next",
seq
);
continue;
}
Err(e) => {
log::error!("REALITY CLIENT: Failed to decrypt combined handshake with all sequence numbers: {}", e);
return Err(e);
}
}
}
let combined_plaintext = combined_plaintext.unwrap();
log::info!(
"REALITY CLIENT: Successfully decrypted combined handshake record ({} bytes plaintext)",
combined_plaintext.len()
);
// Now parse the individual handshake messages from the combined plaintext
let mut offset = 0;
let mut messages_found = 0;
let mut certificate_verified = false;
while offset < combined_plaintext.len() && messages_found < 4 {
// Each handshake message has: type (1 byte) + length (3 bytes) + data
if offset + 4 > combined_plaintext.len() {
break;
}
let msg_type = combined_plaintext[offset];
let msg_len = u32::from_be_bytes([
0,
combined_plaintext[offset + 1],
combined_plaintext[offset + 2],
combined_plaintext[offset + 3],
]) as usize;
if offset + 4 + msg_len > combined_plaintext.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid handshake message length: {} at offset {}",
msg_len, offset
),
));
}
let msg_name = match msg_type {
HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS => "EncryptedExtensions",
HANDSHAKE_TYPE_CERTIFICATE => "Certificate",
HANDSHAKE_TYPE_CERTIFICATE_VERIFY => "CertificateVerify",
HANDSHAKE_TYPE_FINISHED => "Finished",
_ => "Unknown",
};
log::info!(
"REALITY CLIENT: Found {} message (type={}, len={})",
msg_name,
msg_type,
msg_len
);
// Verify HMAC signature when we encounter the Certificate message
if msg_type == HANDSHAKE_TYPE_CERTIFICATE {
let cert_der =
extract_certificate_der(&combined_plaintext[offset..offset + 4 + msg_len])?;
verify_certificate_hmac(cert_der, &auth_key)?;
certificate_verified = true;
}
messages_found += 1;
offset += 4 + msg_len;
}
// Check if we got all 4 messages AND the certificate was verified
if messages_found != 4 {
// Not all messages received yet - this shouldn't happen with combined record
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Expected 4 handshake messages, found {}", messages_found),
));
}
// Ensure the Certificate message was present and HMAC verified
if !certificate_verified {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"REALITY handshake failed: Certificate message not received or not verified",
));
}
// Build handshake transcript
// We need the actual handshake message bytes, not their hashes!
// The transcript_bytes already contains the actual ClientHello and ServerHello bytes
let mut handshake_transcript = digest::Context::new(&digest::SHA256);
handshake_transcript.update(&transcript_bytes); // Contains actual ClientHello + ServerHello bytes
handshake_transcript.update(&combined_plaintext); // EncryptedExtensions + Certificate + CertificateVerify + Finished
let handshake_hash = handshake_transcript.finish();
let mut handshake_hash_arr = [0u8; 32];
handshake_hash_arr.copy_from_slice(handshake_hash.as_ref());
log::info!(
"REALITY CLIENT: Handshake hash for client Finished: {:02x?}",
handshake_hash_arr
);
log::info!(
"REALITY CLIENT: Transcript bytes len={}, combined_plaintext len={}",
transcript_bytes.len(),
combined_plaintext.len()
);
// Generate client Finished message
let client_verify_data =
compute_finished_verify_data(&client_hs_secret, &handshake_hash_arr)?;
log::info!(
"REALITY CLIENT: Client verify data: {:02x?}",
client_verify_data
);
let client_finished = construct_finished(&client_verify_data)?;
// Derive client handshake traffic keys for encryption
let (client_hs_key, client_hs_iv) = derive_traffic_keys(&client_hs_secret, cipher_suite)?;
// Encrypt Finished message
let mut client_hs_seq = 0u64;
let buf_len_before = self.ciphertext_write_buf.len();
encrypt_handshake_to_records(
&client_finished,
&client_hs_key,
&client_hs_iv,
&mut client_hs_seq,
&mut self.ciphertext_write_buf,
)?;
log::info!(
"REALITY CLIENT: Client Finished message generated and buffered ({} bytes)",
self.ciphertext_write_buf.len() - buf_len_before
);
// Derive application secrets
let (client_app_secret, server_app_secret) =
derive_application_secrets(&master_secret, &handshake_hash_arr)?;
// Derive application traffic keys
let (client_app_key, client_app_iv) =
derive_traffic_keys(&client_app_secret, cipher_suite)?;
let (server_app_key, server_app_iv) =
derive_traffic_keys(&server_app_secret, cipher_suite)?;
// Store application keys
self.app_read_key = Some(server_app_key);
self.app_read_iv = Some(server_app_iv);
self.app_write_key = Some(client_app_key);
self.app_write_iv = Some(client_app_iv);
self.read_seq = 0;
self.write_seq = 0;
self.cipher_suite = cipher_suite;
// Mark handshake complete
self.handshake_state = HandshakeState::Complete;
log::info!("REALITY CLIENT: Handshake complete, application keys derived");
Ok(())
}
/// Decrypt application data using TLS 1.3 keys
fn process_application_data(&mut self) -> io::Result<()> {
let (app_read_key, app_read_iv) = match (&self.app_read_key, &self.app_read_iv) {
(Some(key), Some(iv)) => (key, iv),
_ => return Ok(()),
};
while self.ciphertext_read_buf.len() >= TLS_RECORD_HEADER_SIZE {
let record_len = self
.ciphertext_read_buf
.get_u16_be(3)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
as usize;
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
if self.ciphertext_read_buf.len() < total_record_len {
break;
}
// Copy record header for decryption AAD
let tls_header: [u8; TLS_RECORD_HEADER_SIZE] = [
self.ciphertext_read_buf[0],
self.ciphertext_read_buf[1],
self.ciphertext_read_buf[2],
self.ciphertext_read_buf[3],
self.ciphertext_read_buf[4],
];
let ciphertext: Vec<u8> =
self.ciphertext_read_buf[TLS_RECORD_HEADER_SIZE..total_record_len].to_vec();
self.ciphertext_read_buf.consume(total_record_len);
let mut plaintext = decrypt_tls13_record(
app_read_key,
app_read_iv,
self.read_seq,
&ciphertext,
&tls_header,
)?;
self.read_seq += 1;
// TLS 1.3: Remove ContentType trailer byte
// The last byte of the plaintext is the actual ContentType
if !plaintext.is_empty() {
let content_type = plaintext.pop().unwrap();
// For now, we only handle ApplicationData and Alert
// Alerts should be handled separately, but for now just pass through
if content_type != CONTENT_TYPE_APPLICATION_DATA
&& content_type != CONTENT_TYPE_ALERT
{
log::warn!(
"REALITY CLIENT: Unexpected ContentType: 0x{:02x}",
content_type
);
}
}
// Compact plaintext buffer if needed before extending
self.plaintext_read_buf.maybe_compact(4096);
self.plaintext_read_buf.extend_from_slice(&plaintext);
}
Ok(())
}
/// Get a reader for accessing decrypted plaintext
pub fn reader(&mut self) -> RealityReader<'_> {
// SlideBuffer handles compaction internally via maybe_compact()
// Compact before returning reader if we've consumed significant data
self.plaintext_read_buf.maybe_compact(4096);
RealityReader::new(&mut self.plaintext_read_buf)
}
/// Get a writer for buffering plaintext to be encrypted
pub fn writer(&mut self) -> RealityWriter<'_> {
RealityWriter::new(&mut self.plaintext_write_buf)
}
/// Write buffered TLS messages to the provided writer
///
/// Large plaintext is automatically fragmented into multiple TLS records
/// to comply with the TLS 1.3 record size limit.
pub fn write_tls(&mut self, wr: &mut dyn Write) -> io::Result<usize> {
// If handshake not complete, just write buffered handshake data
if !matches!(self.handshake_state, HandshakeState::Complete) {
let n = wr.write(&self.ciphertext_write_buf)?;
self.ciphertext_write_buf.drain(..n);
return Ok(n);
}
// Encrypt any pending plaintext (with automatic fragmentation for large data)
if !self.plaintext_write_buf.is_empty() {
let (app_write_key, app_write_iv) = match (&self.app_write_key, &self.app_write_iv) {
(Some(key), Some(iv)) => (key, iv),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Application keys not available",
))
}
};
encrypt_plaintext_to_records(
&mut self.plaintext_write_buf,
app_write_key,
app_write_iv,
&mut self.write_seq,
&mut self.ciphertext_write_buf,
)?;
}
let n = wr.write(&self.ciphertext_write_buf)?;
self.ciphertext_write_buf.drain(..n);
Ok(n)
}
/// Check if the connection wants to write data
pub fn wants_write(&self) -> bool {
!self.ciphertext_write_buf.is_empty() || !self.plaintext_write_buf.is_empty()
}
/// Check if handshake is still in progress
pub fn is_handshaking(&self) -> bool {
!matches!(self.handshake_state, HandshakeState::Complete)
}
/// Queue a close notification alert
pub fn send_close_notify(&mut self) {
// In TLS 1.3, alerts must be encrypted like application data
if !matches!(self.handshake_state, HandshakeState::Complete) {
log::debug!("REALITY CLIENT: Cannot send close_notify - handshake not complete");
return;
}
// Get application keys
let (app_write_key, app_write_iv) = match (&self.app_write_key, &self.app_write_iv) {
(Some(key), Some(iv)) => (key, iv),
_ => {
log::debug!(
"REALITY CLIENT: Cannot send close_notify - application keys not available"
);
return;
}
};
// Use common helper to build encrypted close_notify alert
match common::build_close_notify_alert(app_write_key, app_write_iv, self.write_seq) {
Ok(record) => {
self.write_seq += 1;
self.ciphertext_write_buf.extend_from_slice(&record);
log::debug!("REALITY CLIENT: Encrypted close_notify alert queued");
}
Err(e) => {
log::error!("REALITY CLIENT: Failed to encrypt close_notify: {}", e);
}
}
}
}
#[inline(always)]
pub fn feed_reality_client_connection(
client_connection: &mut RealityClientConnection,
data: &[u8],
) -> std::io::Result<()> {
let mut cursor = std::io::Cursor::new(data);
let mut i = 0;
while i < data.len() {
let n = client_connection.read_tls(&mut cursor).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to feed TLS connection: {e}"),
)
})?;
i += n;
}
Ok(())
}
+314
View File
@@ -0,0 +1,314 @@
// REALITY client certificate verification
//
// This module handles HMAC verification of REALITY server certificates.
// In REALITY protocol, the server embeds HMAC-SHA512(auth_key, ed25519_public_key)
// in the signature field of the certificate.
use std::io;
use aws_lc_rs::hmac;
use subtle::ConstantTimeEq;
use x509_parser::certificate::X509Certificate;
use x509_parser::prelude::FromDer;
/// Extract the DER-encoded certificate from a TLS 1.3 Certificate message
///
/// Certificate message structure:
/// - certificate_request_context (1 byte length + data)
/// - certificate_list length (3 bytes)
/// - For each certificate entry:
/// - cert_data length (3 bytes)
/// - cert_data (DER-encoded X.509 certificate)
/// - extensions length (2 bytes)
/// - extensions data
///
#[inline]
pub fn extract_certificate_der(certificate_message: &[u8]) -> io::Result<&[u8]> {
// Skip handshake header (type + 3-byte length)
if certificate_message.len() < 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Certificate message too short",
));
}
let mut pos = 4; // Skip handshake header
// certificate_request_context length (1 byte)
if pos >= certificate_message.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Certificate message truncated at context length",
));
}
let context_len = certificate_message[pos] as usize;
pos += 1 + context_len;
// certificate_list length (3 bytes)
if pos + 3 > certificate_message.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Certificate message truncated at list length",
));
}
let _list_len = u32::from_be_bytes([
0,
certificate_message[pos],
certificate_message[pos + 1],
certificate_message[pos + 2],
]) as usize;
pos += 3;
// First certificate entry: cert_data length (3 bytes)
if pos + 3 > certificate_message.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Certificate message truncated at cert length",
));
}
let cert_len = u32::from_be_bytes([
0,
certificate_message[pos],
certificate_message[pos + 1],
certificate_message[pos + 2],
]) as usize;
pos += 3;
// Extract the DER-encoded certificate
if pos + cert_len > certificate_message.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Certificate message truncated at cert data",
));
}
Ok(&certificate_message[pos..pos + cert_len])
}
/// Verify the HMAC signature embedded in the REALITY certificate
///
/// In REALITY protocol, the server embeds HMAC-SHA512(auth_key, ed25519_public_key)
/// in the signature field of the certificate. We compare the first 32 bytes.
///
/// Uses proper X.509 parsing via x509-parser crate for robust extraction.
#[inline]
pub fn verify_certificate_hmac(cert_der: &[u8], auth_key: &[u8; 32]) -> io::Result<()> {
// Parse the X.509 certificate properly
let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse X.509 certificate: {}", e),
)
})?;
// Extract the public key from SubjectPublicKeyInfo
let spki = cert.public_key();
let pubkey_data: &[u8] = &spki.subject_public_key.data;
let signature: &[u8] = &cert.signature_value.data;
log::debug!(
"REALITY CLIENT: Parsed certificate - pubkey len={}, sig len={}",
pubkey_data.len(),
signature.len()
);
// Verify this is an Ed25519 public key (32 bytes)
if pubkey_data.len() != 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Expected Ed25519 public key (32 bytes), got {} bytes",
pubkey_data.len()
),
));
}
// Verify signature is long enough (should be 64 bytes for Ed25519, containing HMAC-SHA512)
if signature.len() < 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Signature too short for HMAC verification: {} bytes",
signature.len()
),
));
}
// Compute expected HMAC: HMAC-SHA512(auth_key, ed25519_public_key)
let hmac_key = hmac::Key::new(hmac::HMAC_SHA512, auth_key);
let hmac_tag = hmac::sign(&hmac_key, pubkey_data);
let expected_signature = &hmac_tag.as_ref()[..32]; // First 32 bytes
log::debug!(
"REALITY CLIENT: HMAC verification - ed25519_pubkey={:02x?}",
pubkey_data
);
log::debug!(
"REALITY CLIENT: HMAC verification - expected_sig={:02x?}",
expected_signature
);
log::debug!(
"REALITY CLIENT: HMAC verification - actual_sig={:02x?}",
&signature[..32]
);
// Compare first 32 bytes of signature with expected HMAC using constant-time comparison
// to prevent timing attacks that could leak information about the expected signature
if expected_signature.ct_eq(&signature[..32]).unwrap_u8() == 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Certificate HMAC verification failed - signature mismatch",
));
}
log::info!("REALITY CLIENT: Certificate HMAC verified successfully");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use aws_lc_rs::signature::KeyPair;
/// Test certificate extraction with a properly formatted TLS 1.3 Certificate message
#[test]
fn test_extract_certificate_der_valid() {
// Build a minimal TLS 1.3 Certificate message:
// Handshake header: type (0x0b) + 3-byte length
// certificate_request_context: 1 byte length (0) + no data
// certificate_list: 3 byte length + entries
// First entry: 3 byte cert length + cert data + 2 byte extensions length (0)
let cert_data = b"fake_certificate_der_data";
let cert_len = cert_data.len();
// Build certificate entry: cert_len (3 bytes) + cert + extensions_len (2 bytes)
let entry_len = 3 + cert_len + 2;
// Build certificate_list: list_len (3 bytes) + entry
let list_len = entry_len;
// Build message body: context_len (1) + list_len (3) + entry
let body_len = 1 + 3 + entry_len;
let mut message = Vec::new();
// Handshake type (Certificate = 0x0b)
message.push(0x0b);
// Handshake length (3 bytes, big-endian)
message.push(((body_len >> 16) & 0xff) as u8);
message.push(((body_len >> 8) & 0xff) as u8);
message.push((body_len & 0xff) as u8);
// certificate_request_context length = 0
message.push(0x00);
// certificate_list length (3 bytes)
message.push(((list_len >> 16) & 0xff) as u8);
message.push(((list_len >> 8) & 0xff) as u8);
message.push((list_len & 0xff) as u8);
// First certificate: cert_data length (3 bytes)
message.push(((cert_len >> 16) & 0xff) as u8);
message.push(((cert_len >> 8) & 0xff) as u8);
message.push((cert_len & 0xff) as u8);
// cert_data
message.extend_from_slice(cert_data);
// extensions length = 0
message.push(0x00);
message.push(0x00);
let result = extract_certificate_der(&message);
assert!(result.is_ok());
assert_eq!(result.unwrap(), cert_data);
}
/// Test certificate extraction with message too short
#[test]
fn test_extract_certificate_der_too_short() {
let message = vec![0x0b, 0x00, 0x00]; // Only 3 bytes, need at least 4
let result = extract_certificate_der(&message);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
}
/// Test certificate extraction with truncated context
#[test]
fn test_extract_certificate_der_truncated_context() {
// Handshake header but no body
let message = vec![0x0b, 0x00, 0x00, 0x10]; // Claims 16 bytes but none present
let result = extract_certificate_der(&message);
assert!(result.is_err());
}
/// Test HMAC verification with a real Ed25519 certificate
#[test]
fn test_verify_certificate_hmac_with_real_cert() {
// Generate a real Ed25519 certificate using rcgen
let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
.expect("Failed to generate Ed25519 key pair");
let params = rcgen::CertificateParams::new(vec!["test.example.com".to_string()])
.expect("Failed to create certificate params");
let cert = params
.self_signed(&key_pair)
.expect("Failed to create self-signed certificate");
let mut cert_der = cert.der().to_vec();
// Get the public key
let signing_key =
aws_lc_rs::signature::Ed25519KeyPair::from_pkcs8(key_pair.serialized_der())
.expect("Failed to parse key");
let public_key_bytes = signing_key.public_key().as_ref();
// Parse the certificate to find where the signature actually is
let (_, parsed_cert) =
X509Certificate::from_der(&cert_der).expect("Failed to parse certificate");
let sig_offset =
parsed_cert.signature_value.data.as_ptr() as usize - cert_der.as_ptr() as usize;
// Create auth_key and compute HMAC
let auth_key = [0x42u8; 32];
let hmac_key = hmac::Key::new(hmac::HMAC_SHA512, &auth_key);
let hmac_tag = hmac::sign(&hmac_key, public_key_bytes);
// Replace the signature bytes at the correct offset
cert_der[sig_offset..sig_offset + 64].copy_from_slice(hmac_tag.as_ref());
// Now verify
let result = verify_certificate_hmac(&cert_der, &auth_key);
assert!(result.is_ok(), "HMAC verification should succeed");
}
/// Test HMAC verification with invalid signature
#[test]
fn test_verify_certificate_hmac_invalid_signature() {
// Generate a real Ed25519 certificate but don't modify the signature
let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
.expect("Failed to generate Ed25519 key pair");
let params = rcgen::CertificateParams::new(vec!["test.example.com".to_string()])
.expect("Failed to create certificate params");
let cert = params
.self_signed(&key_pair)
.expect("Failed to create self-signed certificate");
let cert_der = cert.der().to_vec();
let auth_key = [0x42u8; 32];
// This should fail because the signature is a real Ed25519 signature, not HMAC
let result = verify_certificate_hmac(&cert_der, &auth_key);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::PermissionDenied);
}
/// Test HMAC verification with invalid certificate DER
#[test]
fn test_verify_certificate_hmac_invalid_der() {
let auth_key = [0x11u8; 32];
let invalid_der = vec![0x30, 0x00]; // Empty SEQUENCE, not a valid certificate
let result = verify_certificate_hmac(&invalid_der, &auth_key);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
}
}
+20
View File
@@ -0,0 +1,20 @@
/// Represents the I/O state after processing packets
#[derive(Debug, Clone, Copy)]
pub struct RealityIoState {
/// Number of plaintext bytes available to read
plaintext_bytes_to_read: usize,
}
impl RealityIoState {
/// Create a new RealityIoState
pub fn new(plaintext_bytes_to_read: usize) -> Self {
Self {
plaintext_bytes_to_read,
}
}
/// How many plaintext bytes could be obtained via Read without further I/O
pub fn plaintext_bytes_to_read(&self) -> usize {
self.plaintext_bytes_to_read
}
}
+117
View File
@@ -0,0 +1,117 @@
use std::io::{BufRead, Read, Write};
use crate::slide_buffer::SlideBuffer;
/// Reader for accessing decrypted plaintext from REALITY connections
///
/// This reader provides a view over a SlideBuffer and consumes data from it
/// as it is read. The SlideBuffer handles efficient memory management.
pub struct RealityReader<'a> {
buffer: &'a mut SlideBuffer,
}
impl<'a> RealityReader<'a> {
pub fn new(buffer: &'a mut SlideBuffer) -> Self {
RealityReader { buffer }
}
}
impl<'a> Read for RealityReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let available = self.buffer.len();
let to_read = buf.len().min(available);
if to_read > 0 {
buf[..to_read].copy_from_slice(&self.buffer[..to_read]);
self.buffer.consume(to_read);
}
Ok(to_read)
}
}
impl<'a> BufRead for RealityReader<'a> {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
Ok(self.buffer.as_slice())
}
fn consume(&mut self, amt: usize) {
let actual = amt.min(self.buffer.len());
self.buffer.consume(actual);
}
}
/// Writer for buffering plaintext to be encrypted in REALITY connections
pub struct RealityWriter<'a> {
buffer: &'a mut Vec<u8>,
}
impl<'a> RealityWriter<'a> {
pub fn new(buffer: &'a mut Vec<u8>) -> Self {
RealityWriter { buffer }
}
}
impl<'a> Write for RealityWriter<'a> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buffer.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reality_crypto_reader() {
let mut buffer = SlideBuffer::new(1024);
buffer.extend_from_slice(b"hello world");
{
let mut reader = RealityReader::new(&mut buffer);
let mut buf = [0u8; 5];
assert_eq!(reader.read(&mut buf).unwrap(), 5);
assert_eq!(&buf, b"hello");
}
// Buffer should have consumed 5 bytes
assert_eq!(buffer.len(), 6); // " world" remaining
{
let mut reader = RealityReader::new(&mut buffer);
let mut buf = [0u8; 5];
assert_eq!(reader.read(&mut buf).unwrap(), 5);
assert_eq!(&buf[..5], b" worl");
}
assert_eq!(buffer.len(), 1); // "d" remaining
}
#[test]
fn test_reality_crypto_reader_bufread() {
let mut buffer = SlideBuffer::new(1024);
buffer.extend_from_slice(b"test data");
let mut reader = RealityReader::new(&mut buffer);
let buf = reader.fill_buf().unwrap();
assert_eq!(buf, b"test data");
reader.consume(5);
let buf = reader.fill_buf().unwrap();
assert_eq!(buf, b"data");
}
#[test]
fn test_reality_crypto_writer() {
let mut buffer = Vec::new();
let mut writer = RealityWriter::new(&mut buffer);
assert_eq!(writer.write(b"hello").unwrap(), 5);
assert_eq!(writer.write(b" world").unwrap(), 6);
assert_eq!(buffer.as_slice(), b"hello world");
}
}
File diff suppressed because it is too large Load Diff
+901
View File
@@ -0,0 +1,901 @@
// REALITY server-side connection
//
// This implements a rustls-compatible API for REALITY protocol server connections,
// allowing REALITY to be used as a drop-in replacement for rustls.
use std::io::{self, Read, Write};
use crate::address::{Address, NetLocation};
use crate::slide_buffer::SlideBuffer;
use super::common::{
self, CIPHERTEXT_READ_BUF_CAPACITY, CONTENT_TYPE_ALERT, CONTENT_TYPE_APPLICATION_DATA,
CONTENT_TYPE_CHANGE_CIPHER_SPEC, CONTENT_TYPE_HANDSHAKE, HANDSHAKE_TYPE_FINISHED,
PLAINTEXT_READ_BUF_CAPACITY, TLS_MAX_RECORD_SIZE, TLS_RECORD_HEADER_SIZE,
};
use super::reality_aead::{decrypt_handshake_message, decrypt_tls13_record};
use super::reality_auth::{decrypt_session_id, derive_auth_key, perform_ecdh};
use super::reality_certificate::generate_hmac_certificate;
use super::reality_io_state::RealityIoState;
use super::reality_reader_writer::{RealityReader, RealityWriter};
use super::reality_records::{encrypt_handshake_to_records, encrypt_plaintext_to_records};
use super::reality_tls13_keys::{
compute_finished_verify_data, derive_application_secrets, derive_handshake_keys,
derive_traffic_keys,
};
use super::reality_tls13_messages::*;
use super::reality_util::{
extract_client_public_key, extract_client_random, extract_session_id_slice,
};
use aws_lc_rs::{
agreement, digest,
rand::{SecureRandom, SystemRandom},
};
use subtle::ConstantTimeEq;
/// Configuration for REALITY server connections
#[derive(Clone)]
pub struct RealityServerConfig {
/// Server's X25519 private key (32 bytes)
pub private_key: [u8; 32],
/// List of valid short IDs for authentication (8 bytes each)
pub short_ids: Vec<[u8; 8]>,
/// Destination server for certificate generation
pub dest: NetLocation,
/// Maximum allowed time difference in milliseconds (None = no check)
pub max_time_diff: Option<u64>,
/// Minimum accepted client version (3 bytes: major.minor.patch)
pub min_client_version: Option<[u8; 3]>,
/// Maximum accepted client version (3 bytes: major.minor.patch)
pub max_client_version: Option<[u8; 3]>,
}
/// Handshake state machine for REALITY server
enum HandshakeState {
/// Initial state, waiting for ClientHello
Initial,
/// ServerHello and encrypted handshake messages sent, waiting for client Finished
ServerHelloSent {
handshake_hash: [u8; 32], // Hash before server Finished
handshake_hash_with_server_finished: [u8; 32], // Hash including server Finished (for verifying client Finished)
client_handshake_traffic_secret: Vec<u8>,
master_secret: Vec<u8>,
cipher_suite: u16,
},
/// Handshake complete, ready for application data
Complete,
}
/// REALITY server-side connection implementing rustls-compatible API
pub struct RealityServerConnection {
// Configuration
config: RealityServerConfig,
// Handshake state
handshake_state: HandshakeState,
// TLS 1.3 application traffic encryption (post-handshake)
app_read_key: Option<Vec<u8>>,
app_read_iv: Option<Vec<u8>>,
app_write_key: Option<Vec<u8>>,
app_write_iv: Option<Vec<u8>>,
read_seq: u64,
write_seq: u64,
cipher_suite: u16,
// Pre-allocated buffer for TLS read operations (reused across calls)
tls_read_buffer: Box<[u8; TLS_MAX_RECORD_SIZE]>,
// Buffers for I/O - using SlideBuffer for efficient zero-alloc operations
ciphertext_read_buf: SlideBuffer, // Incoming encrypted TLS records
ciphertext_write_buf: Vec<u8>, // Outgoing encrypted TLS records
plaintext_read_buf: SlideBuffer, // Decrypted application data
plaintext_write_buf: Vec<u8>, // Application data to encrypt
}
impl RealityServerConnection {
/// Create a new REALITY server connection
pub fn new(config: RealityServerConfig) -> io::Result<Self> {
Ok(RealityServerConnection {
config,
handshake_state: HandshakeState::Initial,
app_read_key: None,
app_read_iv: None,
app_write_key: None,
app_write_iv: None,
read_seq: 0,
write_seq: 0,
cipher_suite: 0,
tls_read_buffer: Box::new([0u8; TLS_MAX_RECORD_SIZE]),
ciphertext_read_buf: SlideBuffer::new(CIPHERTEXT_READ_BUF_CAPACITY),
ciphertext_write_buf: Vec::with_capacity(CIPHERTEXT_READ_BUF_CAPACITY),
plaintext_read_buf: SlideBuffer::new(PLAINTEXT_READ_BUF_CAPACITY),
plaintext_write_buf: Vec::with_capacity(TLS_MAX_RECORD_SIZE),
})
}
/// Read TLS messages from the provided reader into internal buffer
///
/// This does NOT decrypt - call process_new_packets() for that.
/// Uses pre-allocated buffer to avoid allocation on every call.
pub fn read_tls(&mut self, rd: &mut dyn Read) -> io::Result<usize> {
// Compact if remaining capacity is insufficient for a full TLS record
if self.ciphertext_read_buf.remaining_capacity() < TLS_MAX_RECORD_SIZE {
self.ciphertext_read_buf.compact();
}
// Read into pre-allocated buffer
let n = rd.read(&mut self.tls_read_buffer[..])?;
if n > 0 {
self.ciphertext_read_buf
.extend_from_slice(&self.tls_read_buffer[..n]);
}
Ok(n)
}
/// Process buffered TLS messages and advance handshake/decrypt data
///
/// Returns I/O state with available plaintext bytes and write status.
pub fn process_new_packets(&mut self) -> io::Result<RealityIoState> {
match &self.handshake_state {
HandshakeState::Initial => {
self.process_client_hello()?;
}
HandshakeState::ServerHelloSent { .. } => {
self.process_client_finished()?;
}
HandshakeState::Complete => {
self.process_application_data()?;
}
}
Ok(RealityIoState::new(self.plaintext_read_buf.len()))
}
/// Process ClientHello message and send ServerHello
fn process_client_hello(&mut self) -> io::Result<()> {
// Need at least TLS record header (5 bytes)
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE {
return Ok(()); // Need more data
}
// Parse TLS record length
let record_len = self
.ciphertext_read_buf
.get_u16_be(3)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
as usize;
// Check if we have the complete record
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
if self.ciphertext_read_buf.len() < total_record_len {
return Ok(()); // Need more data
}
// Copy the ClientHello record to a Vec for processing
// (We need to keep it around for transcript hashing and AAD modification)
let client_hello: Vec<u8> = self.ciphertext_read_buf[..total_record_len].to_vec();
self.ciphertext_read_buf.consume(total_record_len);
// Step 1: Extract fields from ClientHello (using slice for session_id to avoid allocation)
let client_random = extract_client_random(&client_hello)?;
let session_id = extract_session_id_slice(&client_hello)?;
let client_public_key = extract_client_public_key(&client_hello)?;
log::debug!(
"REALITY: ClientHello received, client_random: {:?}",
&client_random[..8]
);
// Step 2: Perform ECDH to derive auth key
let shared_secret = perform_ecdh(&self.config.private_key, &client_public_key)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
// Use slices directly from client_random to avoid copying
let salt = &client_random[0..20];
let auth_key = derive_auth_key(&shared_secret, salt, b"REALITY")
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
// Step 3: Validate session ID (contains encrypted metadata)
if session_id.len() != 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid session ID length",
));
}
let nonce = &client_random[20..32];
let mut encrypted_session_id_arr = [0u8; 32];
encrypted_session_id_arr.copy_from_slice(session_id);
// CRITICAL: Reconstruct AAD with zeros at SessionId location
// The AAD during encryption had zeros where the session ID would go
// SessionId is at offset 39 in ClientHello handshake (after type(1) + length(3) + version(2) + random(32) + sessionid_length(1))
let client_hello_handshake = &client_hello[TLS_RECORD_HEADER_SIZE..];
let mut aad_for_decryption = client_hello_handshake.to_vec();
if aad_for_decryption.len() >= 39 + 32 {
// Replace encrypted SessionId with zeros
aad_for_decryption[39..39 + 32].fill(0);
}
let decrypted_session_id = decrypt_session_id(
&encrypted_session_id_arr,
&auth_key,
nonce,
&aad_for_decryption,
)
.map_err(|e| {
io::Error::new(
io::ErrorKind::PermissionDenied,
format!("Session ID decrypt failed: {:?}", e),
)
})?;
// Validate session ID contents
// Bytes 0-2: Version
// Byte 3: Padding
// Bytes 4-7: Timestamp (uint32)
// Bytes 8-15: ShortId
let client_version = &decrypted_session_id[0..3];
let client_timestamp = u32::from_be_bytes([
decrypted_session_id[4],
decrypted_session_id[5],
decrypted_session_id[6],
decrypted_session_id[7],
]) as u64;
let client_short_id = &decrypted_session_id[8..16];
log::debug!("REALITY: Client version: {:?}", client_version);
log::debug!("REALITY: Client timestamp: {}", client_timestamp);
log::debug!("REALITY: Client short_id: {:02x?}", client_short_id);
// Validate short ID - check if client's short_id is in the configured list
let mut client_short_id_arr = [0u8; 8];
client_short_id_arr.copy_from_slice(client_short_id);
let short_id_ok = self.config.short_ids.contains(&client_short_id_arr);
if !short_id_ok {
log::warn!(
"REALITY: Client short_id {:02x?} not in configured list",
client_short_id
);
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("Invalid short_id: {:02x?}", client_short_id),
));
}
// Validate timestamp if max_time_diff is configured
if let Some(max_diff_ms) = self.config.max_time_diff {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| io::Error::other("System time error"))?
.as_secs();
let time_diff_secs = now.abs_diff(client_timestamp);
let max_diff_secs = max_diff_ms / 1000;
if time_diff_secs > max_diff_secs {
log::warn!(
"REALITY: Client timestamp {} differs from server {} by {} seconds (max: {} seconds)",
client_timestamp, now, time_diff_secs, max_diff_secs
);
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"Timestamp difference {} seconds exceeds maximum {} seconds",
time_diff_secs, max_diff_secs
),
));
}
}
// Validate client version (min)
if let Some(min_ver) = &self.config.min_client_version {
if client_version < &min_ver[..] {
log::warn!(
"REALITY: Client version {:?} is below minimum {:?}",
client_version,
min_ver
);
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"Client version {:?} is below minimum {:?}",
client_version, min_ver
),
));
}
}
// Validate client version (max)
if let Some(max_ver) = &self.config.max_client_version {
if client_version > &max_ver[..] {
log::warn!(
"REALITY: Client version {:?} is above maximum {:?}",
client_version,
max_ver
);
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"Client version {:?} is above maximum {:?}",
client_version, max_ver
),
));
}
}
log::info!("REALITY: Client authentication successful - short_id: {:02x?}, version: {:?}, timestamp: {}",
client_short_id, client_version, client_timestamp);
// Step 4: Generate our server X25519 keypair
let rng = SystemRandom::new();
let mut our_private_bytes = [0u8; 32];
rng.fill(&mut our_private_bytes)
.map_err(|_| io::Error::other("RNG failed"))?;
let our_private_key =
agreement::PrivateKey::from_private_key(&agreement::X25519, &our_private_bytes)
.map_err(|_| io::Error::other("Failed to create X25519 key"))?;
let our_public_key_bytes = our_private_key
.compute_public_key()
.map_err(|_| io::Error::other("Failed to compute public key"))?;
// Step 5: Generate server random
let mut server_random = [0u8; 32];
rng.fill(&mut server_random)
.map_err(|_| io::Error::other("RNG failed"))?;
// Step 6: Use standard cipher suite (TLS_AES_128_GCM_SHA256)
const CIPHER_SUITE: u16 = 0x1301;
// Step 7: Build ServerHello
let server_hello = construct_server_hello(
&server_random,
session_id,
CIPHER_SUITE,
our_public_key_bytes.as_ref(),
)?;
// Step 8: Compute transcript hashes
let client_hello_handshake = &client_hello[TLS_RECORD_HEADER_SIZE..]; // Skip TLS record header
let mut ch_transcript = digest::Context::new(&digest::SHA256);
ch_transcript.update(client_hello_handshake);
let client_hello_hash = ch_transcript.finish();
let mut ch_sh_transcript = digest::Context::new(&digest::SHA256);
ch_sh_transcript.update(client_hello_handshake);
ch_sh_transcript.update(&server_hello);
// Clone before finalizing
let mut handshake_transcript = ch_sh_transcript.clone();
let server_hello_hash = ch_sh_transcript.finish();
// Step 9: Perform ECDH for TLS 1.3 key derivation
let peer_public_key =
agreement::UnparsedPublicKey::new(&agreement::X25519, &client_public_key);
let mut tls_shared_secret = [0u8; 32];
agreement::agree(
&our_private_key,
peer_public_key,
io::Error::other("ECDH failed"),
|key_material| {
tls_shared_secret.copy_from_slice(key_material);
Ok(())
},
)?;
// Step 10: Derive TLS 1.3 keys
let mut client_hello_hash_arr = [0u8; 32];
client_hello_hash_arr.copy_from_slice(client_hello_hash.as_ref());
let mut server_hello_hash_arr = [0u8; 32];
server_hello_hash_arr.copy_from_slice(server_hello_hash.as_ref());
let hs_keys = derive_handshake_keys(
&tls_shared_secret,
&client_hello_hash_arr,
&server_hello_hash_arr,
)?;
// Get destination hostname for certificate
let dest_hostname = match self.config.dest.address() {
Address::Hostname(h) => h.as_str(),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"REALITY requires a hostname destination",
))
}
};
// Step 11: Generate HMAC-signed certificate
let (cert_der, signing_key) = generate_hmac_certificate(&auth_key, dest_hostname)?;
// Step 12: Build encrypted handshake messages
let encrypted_extensions = construct_encrypted_extensions()?;
handshake_transcript.update(&encrypted_extensions);
let certificate = construct_certificate(&cert_der)?;
handshake_transcript.update(&certificate);
let cert_verify_hash = handshake_transcript.clone().finish();
let mut cert_verify_hash_arr = [0u8; 32];
cert_verify_hash_arr.copy_from_slice(cert_verify_hash.as_ref());
let certificate_verify = construct_certificate_verify(&signing_key, &cert_verify_hash_arr)?;
handshake_transcript.update(&certificate_verify);
let handshake_hash_before_finished = handshake_transcript.clone().finish();
let mut handshake_hash_arr = [0u8; 32];
handshake_hash_arr.copy_from_slice(handshake_hash_before_finished.as_ref());
// Step 13: Derive server handshake traffic keys for encryption
let (server_hs_key, server_hs_iv) =
derive_traffic_keys(&hs_keys.server_handshake_traffic_secret, CIPHER_SUITE)?;
// Step 14: Build server Finished message first (before encryption)
let server_verify_data = compute_finished_verify_data(
&hs_keys.server_handshake_traffic_secret,
&handshake_hash_arr,
)?;
let server_finished = construct_finished(&server_verify_data)?;
// Step 15: Combine all 4 handshake messages into one plaintext buffer
// This is required by REALITY protocol - all 4 messages in one encrypted record
let mut combined_plaintext = Vec::new();
combined_plaintext.extend_from_slice(&encrypted_extensions);
combined_plaintext.extend_from_slice(&certificate);
combined_plaintext.extend_from_slice(&certificate_verify);
combined_plaintext.extend_from_slice(&server_finished);
log::debug!("REALITY SERVER: Combining 4 handshake messages: EE={}, Cert={}, CV={}, Fin={}, Total={}",
encrypted_extensions.len(), certificate.len(), certificate_verify.len(),
server_finished.len(), combined_plaintext.len());
// Step 16: Encrypt the combined handshake messages, fragmenting into multiple records if needed
// TLS 1.3 limits records to 16,640 bytes ciphertext (16,623 bytes plaintext).
// Large certificates can exceed this, so we fragment like uTLS does.
let mut handshake_ciphertext = Vec::new();
let mut handshake_seq = 0u64;
encrypt_handshake_to_records(
&combined_plaintext,
&server_hs_key,
&server_hs_iv,
&mut handshake_seq,
&mut handshake_ciphertext,
)?;
// Update transcript with server Finished (needed for client Finished verification)
handshake_transcript.update(&server_finished);
let handshake_hash_with_server_finished = handshake_transcript.finish();
let mut handshake_hash_with_finished_arr = [0u8; 32];
handshake_hash_with_finished_arr
.copy_from_slice(handshake_hash_with_server_finished.as_ref());
// Step 16: Buffer all handshake messages to write buffer
// ServerHello (plaintext)
self.ciphertext_write_buf
.extend_from_slice(&write_record_header(
CONTENT_TYPE_HANDSHAKE,
server_hello.len() as u16,
));
self.ciphertext_write_buf.extend_from_slice(&server_hello);
// ChangeCipherSpec (for compatibility)
self.ciphertext_write_buf
.extend_from_slice(&write_record_header(CONTENT_TYPE_CHANGE_CIPHER_SPEC, 1));
self.ciphertext_write_buf.push(0x01);
// Encrypted handshake record(s) - may be fragmented into multiple records
self.ciphertext_write_buf
.extend_from_slice(&handshake_ciphertext);
log::info!(
"REALITY: ServerHello and encrypted handshake messages buffered ({} bytes)",
self.ciphertext_write_buf.len()
);
// Step 17: Update handshake state
self.handshake_state = HandshakeState::ServerHelloSent {
handshake_hash: handshake_hash_arr,
handshake_hash_with_server_finished: handshake_hash_with_finished_arr,
client_handshake_traffic_secret: hs_keys.client_handshake_traffic_secret.clone(),
master_secret: hs_keys.master_secret,
cipher_suite: CIPHER_SUITE,
};
Ok(())
}
/// Process client's Finished message and complete handshake
fn process_client_finished(&mut self) -> io::Result<()> {
// Check if we have enough data for a TLS record header BEFORE extracting state
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE {
return Ok(()); // Need more data
}
// Check for ChangeCipherSpec (TLS 1.3 compatibility message)
if self.ciphertext_read_buf[0] == CONTENT_TYPE_CHANGE_CIPHER_SPEC {
// ChangeCipherSpec record
let ccs_len = self
.ciphertext_read_buf
.get_u16_be(3)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
as usize;
// Need complete ChangeCipherSpec record
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE + ccs_len {
return Ok(()); // Need more data
}
// Skip ChangeCipherSpec (compatibility message)
log::debug!("REALITY: Skipping ChangeCipherSpec (compatibility message)");
self.ciphertext_read_buf
.consume(TLS_RECORD_HEADER_SIZE + ccs_len);
// Check if we have the next record header
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE {
return Ok(()); // Need more data
}
}
// Parse TLS record length
let record_len = self
.ciphertext_read_buf
.get_u16_be(3)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
as usize;
// Check if we have the complete record
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
if self.ciphertext_read_buf.len() < total_record_len {
return Ok(()); // Need more data
}
// Verify it's ApplicationData (encrypted Finished)
if self.ciphertext_read_buf[0] != CONTENT_TYPE_APPLICATION_DATA {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Expected ApplicationData (0x17), got 0x{:02x}",
self.ciphertext_read_buf[0]
),
));
}
// NOW we're committed to processing - take ownership of handshake state
// This avoids cloning Vec<u8> fields
let old_state = std::mem::replace(&mut self.handshake_state, HandshakeState::Complete);
let (
client_handshake_traffic_secret,
master_secret,
cipher_suite,
_handshake_hash,
handshake_hash_with_server_finished,
) = match old_state {
HandshakeState::ServerHelloSent {
client_handshake_traffic_secret,
master_secret,
cipher_suite,
handshake_hash,
handshake_hash_with_server_finished,
} => (
client_handshake_traffic_secret, // moved, not cloned
master_secret, // moved, not cloned
cipher_suite,
handshake_hash,
handshake_hash_with_server_finished,
),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid state for process_client_finished",
))
}
};
// Extract the encrypted Finished record (copy to Vec for decryption)
let record: Vec<u8> = self.ciphertext_read_buf[..total_record_len].to_vec();
self.ciphertext_read_buf.consume(total_record_len);
let ciphertext = &record[TLS_RECORD_HEADER_SIZE..]; // Skip TLS record header
// Derive client handshake traffic keys for decryption
let (client_hs_key, client_hs_iv) =
derive_traffic_keys(&client_handshake_traffic_secret, cipher_suite)?;
// Decrypt the Finished message (sequence number = 0 for client's first encrypted record)
let plaintext = decrypt_handshake_message(
&client_hs_key,
&client_hs_iv,
0, // Client's first encrypted record
ciphertext,
record_len as u16,
)?;
// Verify it's a Finished message (type 0x14)
if plaintext.is_empty() || plaintext[0] != HANDSHAKE_TYPE_FINISHED {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Expected Finished message",
));
}
// Extract verify_data (skip type(1) + length(3) = 4 bytes)
if plaintext.len() < 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Finished message too short",
));
}
let client_verify_data = &plaintext[4..];
// Compute expected client Finished verify_data
// IMPORTANT: Use hash that includes server Finished (per TLS 1.3 RFC 8446)
let expected_verify_data = compute_finished_verify_data(
&client_handshake_traffic_secret,
&handshake_hash_with_server_finished,
)?;
// Verify it matches using constant-time comparison to prevent timing attacks
if client_verify_data
.ct_eq(expected_verify_data.as_slice())
.unwrap_u8()
== 0
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Client Finished verify_data mismatch",
));
}
log::debug!("REALITY: Client Finished verified successfully");
// Derive application secrets
// IMPORTANT: Use hash that includes server Finished (per TLS 1.3 RFC 8446)
let (client_app_secret, server_app_secret) =
derive_application_secrets(&master_secret, &handshake_hash_with_server_finished)?;
// Derive application traffic keys
let (client_app_key, client_app_iv) =
derive_traffic_keys(&client_app_secret, cipher_suite)?;
let (server_app_key, server_app_iv) =
derive_traffic_keys(&server_app_secret, cipher_suite)?;
// Store application traffic keys
self.app_read_key = Some(client_app_key);
self.app_read_iv = Some(client_app_iv);
self.app_write_key = Some(server_app_key);
self.app_write_iv = Some(server_app_iv);
self.read_seq = 0;
self.write_seq = 0;
self.cipher_suite = cipher_suite;
// Handshake state already set to Complete above
log::debug!("REALITY: Handshake complete, application keys derived");
Ok(())
}
/// Decrypt application data using TLS 1.3 keys
fn process_application_data(&mut self) -> io::Result<()> {
// Check if we have application keys
let (app_read_key, app_read_iv) = match (&self.app_read_key, &self.app_read_iv) {
(Some(key), Some(iv)) => (key, iv),
_ => return Ok(()), // Keys not ready yet
};
// Process all complete TLS records in the buffer
while self.ciphertext_read_buf.len() >= 5 {
// Parse TLS record header
let record_len = self
.ciphertext_read_buf
.get_u16_be(3)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
as usize;
// Check if we have the complete record
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
if self.ciphertext_read_buf.len() < total_record_len {
break; // Need more data
}
// Copy record header for decryption AAD
let tls_header: [u8; TLS_RECORD_HEADER_SIZE] = [
self.ciphertext_read_buf[0],
self.ciphertext_read_buf[1],
self.ciphertext_read_buf[2],
self.ciphertext_read_buf[3],
self.ciphertext_read_buf[4],
];
let ciphertext: Vec<u8> =
self.ciphertext_read_buf[TLS_RECORD_HEADER_SIZE..total_record_len].to_vec();
self.ciphertext_read_buf.consume(total_record_len);
// Decrypt the application data
let mut plaintext = decrypt_tls13_record(
app_read_key,
app_read_iv,
self.read_seq,
&ciphertext,
&tls_header,
)?;
self.read_seq += 1;
// TLS 1.3: Remove ContentType trailer byte
// The last byte of the plaintext is the actual ContentType
if !plaintext.is_empty() {
let content_type = plaintext.pop().unwrap();
// For now, we only handle ApplicationData (0x17) and Alert (0x15)
// Alerts should be handled separately, but for now just pass through
if content_type != CONTENT_TYPE_APPLICATION_DATA
&& content_type != CONTENT_TYPE_ALERT
{
log::warn!("REALITY: Unexpected ContentType: 0x{:02x}", content_type);
}
}
// Compact plaintext buffer if needed before extending
self.plaintext_read_buf.maybe_compact(4096);
// Append to plaintext buffer (without ContentType)
self.plaintext_read_buf.extend_from_slice(&plaintext);
}
Ok(())
}
/// Get a reader for accessing decrypted plaintext
pub fn reader(&mut self) -> RealityReader<'_> {
// SlideBuffer handles compaction internally via maybe_compact()
// Compact before returning reader if we've consumed significant data
self.plaintext_read_buf.maybe_compact(4096);
RealityReader::new(&mut self.plaintext_read_buf)
}
/// Get a writer for buffering plaintext to be encrypted
pub fn writer(&mut self) -> RealityWriter<'_> {
RealityWriter::new(&mut self.plaintext_write_buf)
}
/// Write buffered TLS messages to the provided writer
///
/// This encrypts any pending plaintext and writes ciphertext.
/// Large plaintext is automatically fragmented into multiple TLS records
/// to comply with the TLS 1.3 record size limit.
pub fn write_tls(&mut self, wr: &mut dyn Write) -> io::Result<usize> {
// If handshake not complete, just write buffered handshake data
if !matches!(self.handshake_state, HandshakeState::Complete) {
let n = wr.write(&self.ciphertext_write_buf)?;
self.ciphertext_write_buf.drain(..n);
return Ok(n);
}
// Encrypt any pending plaintext (with automatic fragmentation for large data)
if !self.plaintext_write_buf.is_empty() {
let (app_write_key, app_write_iv) = match (&self.app_write_key, &self.app_write_iv) {
(Some(key), Some(iv)) => (key, iv),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Application keys not available",
))
}
};
encrypt_plaintext_to_records(
&mut self.plaintext_write_buf,
app_write_key,
app_write_iv,
&mut self.write_seq,
&mut self.ciphertext_write_buf,
)?;
}
// Write buffered ciphertext
let n = wr.write(&self.ciphertext_write_buf)?;
self.ciphertext_write_buf.drain(..n);
Ok(n)
}
/// Check if the connection wants to write data
pub fn wants_write(&self) -> bool {
!self.ciphertext_write_buf.is_empty() || !self.plaintext_write_buf.is_empty()
}
/// Check if handshake is still in progress
pub fn is_handshaking(&self) -> bool {
!matches!(self.handshake_state, HandshakeState::Complete)
}
/// Queue a close notification alert
pub fn send_close_notify(&mut self) {
// In TLS 1.3, alerts must be encrypted like application data
if !matches!(self.handshake_state, HandshakeState::Complete) {
log::debug!("REALITY: Cannot send close_notify - handshake not complete");
return;
}
// Get application keys
let (app_write_key, app_write_iv) = match (&self.app_write_key, &self.app_write_iv) {
(Some(key), Some(iv)) => (key, iv),
_ => {
log::debug!("REALITY: Cannot send close_notify - application keys not available");
return;
}
};
// Use common helper to build encrypted close_notify alert
match common::build_close_notify_alert(app_write_key, app_write_iv, self.write_seq) {
Ok(record) => {
self.write_seq += 1;
self.ciphertext_write_buf.extend_from_slice(&record);
log::debug!("REALITY: Encrypted close_notify alert queued");
}
Err(e) => {
log::error!("REALITY: Failed to encrypt close_notify: {}", e);
}
}
}
}
#[inline(always)]
pub fn feed_reality_server_connection(
server_connection: &mut RealityServerConnection,
data: &[u8],
) -> std::io::Result<()> {
let mut cursor = std::io::Cursor::new(data);
let mut i = 0;
while i < data.len() {
let n = server_connection.read_tls(&mut cursor).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to feed TLS connection: {e}"),
)
})?;
i += n;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reality_server_connection_creation() {
let config = RealityServerConfig {
private_key: [0u8; 32],
short_ids: vec![[0u8; 8]],
dest: NetLocation::new(Address::UNSPECIFIED, 443),
max_time_diff: Some(60000),
min_client_version: None,
max_client_version: None,
};
let conn = RealityServerConnection::new(config).unwrap();
assert!(conn.is_handshaking());
assert!(!conn.wants_write());
}
#[test]
fn test_io_state() {
let config = RealityServerConfig {
private_key: [0u8; 32],
short_ids: vec![[0u8; 8]],
dest: NetLocation::new(Address::UNSPECIFIED, 443),
max_time_diff: None,
min_client_version: None,
max_client_version: None,
};
let mut conn = RealityServerConnection::new(config).unwrap();
let state = conn.process_new_packets().unwrap();
assert_eq!(state.plaintext_bytes_to_read(), 0);
assert!(!conn.wants_write());
}
}
+429
View File
@@ -0,0 +1,429 @@
// TLS 1.3 Key Schedule Implementation
//
// Implements RFC 8446 key derivation for TLS 1.3
// Using HKDF-SHA256 for all key derivation operations
use aws_lc_rs::digest;
use std::io::{Error, ErrorKind, Result};
/// Intermediate TLS 1.3 keys (handshake secrets + master secret)
/// Used for two-phase key derivation where application secrets
/// must be derived after server Finished message
#[derive(Debug, Clone)]
pub struct Tls13HandshakeKeys {
/// Client handshake traffic secret
pub client_handshake_traffic_secret: Vec<u8>,
/// Server handshake traffic secret
pub server_handshake_traffic_secret: Vec<u8>,
/// Master secret (for deriving application secrets later)
pub master_secret: Vec<u8>,
}
/// HKDF-Expand implementation using HMAC-SHA256 directly
/// This follows RFC 5869 Section 2.3
pub fn hkdf_expand_sha256(prk: &[u8], info: &[u8], length: usize) -> Result<Vec<u8>> {
use aws_lc_rs::hmac;
const HASH_LEN: usize = 32; // SHA256 output length
let n = length.div_ceil(HASH_LEN); // Number of iterations
if n > 255 {
return Err(Error::new(ErrorKind::InvalidData, "HKDF output too long"));
}
let mut output = Vec::new();
let mut prev = Vec::new();
for i in 1..=n {
let key = hmac::Key::new(hmac::HMAC_SHA256, prk);
let mut ctx = hmac::Context::with_key(&key);
log::debug!(
"HKDF iteration {}: prev_len={}, info_len={}",
i,
prev.len(),
info.len()
);
ctx.update(&prev);
ctx.update(info);
ctx.update(&[i as u8]);
let tag = ctx.sign();
log::debug!(
"HKDF iteration {}: output={:02x?}",
i,
&tag.as_ref()[..tag.as_ref().len().min(16)]
);
prev = tag.as_ref().to_vec();
output.extend_from_slice(tag.as_ref());
}
output.truncate(length);
Ok(output)
}
/// HKDF-Expand-Label as defined in RFC 8446 Section 7.1
fn hkdf_expand_label(
secret: &[u8],
label: &[u8],
context: &[u8],
length: usize,
) -> Result<Vec<u8>> {
log::debug!(
"DEBUG hkdf_expand_label: secret len={}, label={:?}, context len={}, length={}",
secret.len(),
std::str::from_utf8(label).unwrap_or("<binary>"),
context.len(),
length
);
// HkdfLabel structure:
// struct {
// uint16 length = Length;
// opaque label<7..255> = "tls13 " + Label;
// opaque context<0..255> = Context;
// } HkdfLabel;
let mut hkdf_label = Vec::new();
// Length (2 bytes, big-endian)
hkdf_label.extend_from_slice(&(length as u16).to_be_bytes());
// Label length and content
let full_label = format!("tls13 {}", std::str::from_utf8(label).unwrap());
hkdf_label.push(full_label.len() as u8);
hkdf_label.extend_from_slice(full_label.as_bytes());
// Context length and content
hkdf_label.push(context.len() as u8);
hkdf_label.extend_from_slice(context);
log::debug!("HKDF_LABEL_BYTES: {:02x?}", hkdf_label);
// Use our helper function
hkdf_expand_sha256(secret, &hkdf_label, length)
}
/// Derive-Secret as defined in RFC 8446 Section 7.1
fn derive_secret(secret: &[u8], label: &[u8], messages_hash: &[u8]) -> Result<Vec<u8>> {
hkdf_expand_label(secret, label, messages_hash, 32)
}
/// HKDF-Extract operation
fn hkdf_extract(salt: &[u8], ikm: &[u8]) -> Vec<u8> {
// Use hmac directly for extract operation since aws-lc-rs doesn't expose PRK bytes
use aws_lc_rs::hmac;
let key = hmac::Key::new(hmac::HMAC_SHA256, salt);
let tag = hmac::sign(&key, ikm);
tag.as_ref().to_vec()
}
/// Derive TLS 1.3 handshake keys and master secret (Phase 1)
///
/// This function derives handshake traffic secrets and the master secret,
/// but NOT the application traffic secrets. Application secrets must be
/// derived separately after the server Finished message is sent (Phase 2).
///
/// # Arguments
/// * `shared_secret` - ECDH shared secret (32 bytes for X25519)
/// * `client_hello_hash` - SHA256 hash of ClientHello (32 bytes)
/// * `server_hello_hash` - SHA256 hash of ClientHello...ServerHello (32 bytes)
///
/// # Returns
/// Handshake traffic secrets and master secret
pub fn derive_handshake_keys(
shared_secret: &[u8],
client_hello_hash: &[u8],
server_hello_hash: &[u8],
) -> Result<Tls13HandshakeKeys> {
// Validate input lengths
if shared_secret.len() != 32 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!(
"Invalid shared_secret length: {} (expected 32)",
shared_secret.len()
),
));
}
if client_hello_hash.len() != 32 || server_hello_hash.len() != 32 {
return Err(Error::new(
ErrorKind::InvalidInput,
"All hashes must be 32 bytes (SHA256)",
));
}
log::debug!("TLS13 DEBUG: Deriving handshake keys (Phase 1)...");
// 1. Early Secret = HKDF-Extract(salt=0, IKM=0)
let zero_salt = vec![0u8; 32];
let early_secret = hkdf_extract(&zero_salt, &zero_salt);
// 2. Derive-Secret(., "derived", "")
let mut empty_ctx = digest::Context::new(&digest::SHA256);
empty_ctx.update(b"");
let empty_hash = empty_ctx.finish();
let derived_secret = derive_secret(&early_secret, b"derived", empty_hash.as_ref())?;
// 3. Handshake Secret = HKDF-Extract(salt=derived_secret, IKM=shared_secret)
let handshake_secret = hkdf_extract(&derived_secret, shared_secret);
// 4. Client Handshake Traffic Secret
let client_handshake_traffic_secret =
derive_secret(&handshake_secret, b"c hs traffic", server_hello_hash)?;
// 5. Server Handshake Traffic Secret
let server_handshake_traffic_secret =
derive_secret(&handshake_secret, b"s hs traffic", server_hello_hash)?;
// 6. Derive-Secret(., "derived", "")
let mut empty_ctx_2 = digest::Context::new(&digest::SHA256);
empty_ctx_2.update(b"");
let empty_hash_2 = empty_ctx_2.finish();
let derived_secret_2 = derive_secret(&handshake_secret, b"derived", empty_hash_2.as_ref())?;
// 7. Master Secret = HKDF-Extract(salt=derived_secret, IKM=0)
let master_secret = hkdf_extract(&derived_secret_2, &zero_salt);
log::debug!(" master_secret: {:?}", &master_secret[..8]);
Ok(Tls13HandshakeKeys {
client_handshake_traffic_secret,
server_handshake_traffic_secret,
master_secret,
})
}
/// Derive TLS 1.3 application traffic secrets (Phase 2)
///
/// This function must be called AFTER the server Finished message is sent,
/// with a transcript hash that includes the Finished message.
///
/// # Arguments
/// * `master_secret` - Master secret from Phase 1
/// * `handshake_hash` - SHA256 hash including server Finished (32 bytes)
///
/// # Returns
/// (client_application_traffic_secret, server_application_traffic_secret)
pub fn derive_application_secrets(
master_secret: &[u8],
handshake_hash: &[u8],
) -> Result<(Vec<u8>, Vec<u8>)> {
if master_secret.len() != 32 || handshake_hash.len() != 32 {
return Err(Error::new(
ErrorKind::InvalidInput,
"Master secret and handshake hash must be 32 bytes",
));
}
log::debug!("TLS13 DEBUG: Deriving application secrets (Phase 2)...");
log::info!(
" handshake_hash (with Finished): {:?}",
&handshake_hash[..8]
);
// Client Application Traffic Secret
let client_application_traffic_secret =
derive_secret(master_secret, b"c ap traffic", handshake_hash)?;
log::debug!(
" client_app_traffic: {:?}",
&client_application_traffic_secret[..8]
);
log::info!(
"DERIVE_APP_SECRETS: ClientAppSecret(full)={:02x?}",
client_application_traffic_secret
);
// Server Application Traffic Secret
let server_application_traffic_secret =
derive_secret(master_secret, b"s ap traffic", handshake_hash)?;
log::debug!(
" server_app_traffic: {:?}",
&server_application_traffic_secret[..8]
);
log::info!(
"DERIVE_APP_SECRETS: ServerAppSecret(full)={:02x?}",
server_application_traffic_secret
);
Ok((
client_application_traffic_secret,
server_application_traffic_secret,
))
}
/// Derive traffic keys and IV from traffic secret
///
/// # Arguments
/// * `traffic_secret` - Traffic secret (32 bytes)
/// * `cipher_suite` - TLS cipher suite (e.g., 0x1301 for TLS_AES_128_GCM_SHA256)
///
/// # Returns
/// (key, iv) tuple for AES-GCM
pub fn derive_traffic_keys(traffic_secret: &[u8], cipher_suite: u16) -> Result<(Vec<u8>, Vec<u8>)> {
// For TLS_AES_128_GCM_SHA256 (0x1301):
// - key_length = 16 bytes
// - iv_length = 12 bytes
let (key_length, iv_length) = match cipher_suite {
0x1301 => (16, 12), // TLS_AES_128_GCM_SHA256
0x1302 => (32, 12), // TLS_AES_256_GCM_SHA384
0x1303 => (32, 12), // TLS_CHACHA20_POLY1305_SHA256
_ => {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Unsupported cipher suite: 0x{:04x}", cipher_suite),
))
}
};
log::debug!(
"TRAFFIC_KEY_DERIVE: cipher_suite=0x{:04x}, key_len={}, iv_len={}",
cipher_suite,
key_length,
iv_length
);
log::debug!("TRAFFIC_KEY_DERIVE: traffic_secret={:02x?}", traffic_secret);
// key = HKDF-Expand-Label(Secret, "key", "", key_length)
let key = hkdf_expand_label(traffic_secret, b"key", b"", key_length)?;
// iv = HKDF-Expand-Label(Secret, "iv", "", iv_length)
let iv = hkdf_expand_label(traffic_secret, b"iv", b"", iv_length)?;
log::info!("TRAFFIC_KEY_DERIVE: key={:02x?}", key);
log::info!("TRAFFIC_KEY_DERIVE: iv={:02x?}", iv);
Ok((key, iv))
}
/// Compute "Finished" verify data
///
/// finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)
/// verify_data = HMAC(finished_key, Transcript-Hash(Handshake Context))
pub fn compute_finished_verify_data(base_key: &[u8], handshake_hash: &[u8]) -> Result<Vec<u8>> {
use aws_lc_rs::hmac;
// finished_key = HKDF-Expand-Label(BaseKey, "finished", "", 32)
let finished_key = hkdf_expand_label(base_key, b"finished", b"", 32)?;
// verify_data = HMAC(finished_key, handshake_hash)
let key = hmac::Key::new(hmac::HMAC_SHA256, &finished_key);
let tag = hmac::sign(&key, handshake_hash);
let verify_data = tag.as_ref().to_vec();
Ok(verify_data)
}
#[cfg(test)]
mod tests {
use super::*;
// Test vectors from RFC 5869 Appendix A
#[test]
fn test_hkdf_expand_sha256_rfc_vector() {
// Test Case 1 from RFC 5869 (simplified - using extracted PRK directly)
let prk = [
0x07, 0x77, 0x09, 0x36, 0x2c, 0x2e, 0x32, 0xdf, 0x0d, 0xdc, 0x3f, 0x0d, 0xc4, 0x7b,
0xba, 0x63, 0x90, 0xb6, 0xc7, 0x3b, 0xb5, 0x0f, 0x9c, 0x31, 0x22, 0xec, 0x84, 0x4a,
0xd7, 0xc2, 0xb3, 0xe5,
];
let info = [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9];
let result = hkdf_expand_sha256(&prk, &info, 42).unwrap();
assert_eq!(result.len(), 42);
// Check first few bytes match expected pattern
assert_eq!(result[0], 0x3c);
assert_eq!(result[1], 0xb2);
assert_eq!(result[2], 0x5f);
}
#[test]
fn test_hkdf_expand_sha256_empty_info() {
let prk = vec![0x42u8; 32];
let result = hkdf_expand_sha256(&prk, &[], 16);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 16);
}
#[test]
fn test_hkdf_expand_sha256_max_length() {
let prk = vec![0x42u8; 32];
let info = b"test info";
// Maximum output length is 255 * hash_len (32 for SHA256) = 8160 bytes
let result = hkdf_expand_sha256(&prk, info, 8160);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 8160);
// Should fail for length > 8160
let result = hkdf_expand_sha256(&prk, info, 8161);
assert!(result.is_err());
}
#[test]
fn test_hkdf_expand_label() {
let secret = vec![0x42u8; 32];
let result = hkdf_expand_label(&secret, b"test", b"", 16);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 16);
}
#[test]
fn test_hkdf_expand_label_with_context() {
let secret = vec![0x42u8; 32];
let context = vec![0x11u8; 32];
let result = hkdf_expand_label(&secret, b"finished", &context, 32);
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.len(), 32);
// Result should be deterministic
let result2 = hkdf_expand_label(&secret, b"finished", &context, 32).unwrap();
assert_eq!(output, result2);
}
#[test]
fn test_hkdf_extract() {
let salt = vec![0x11u8; 32];
let ikm = vec![0x22u8; 32];
let result1 = hkdf_extract(&salt, &ikm);
assert_eq!(result1.len(), 32); // SHA256 output length
// Should be deterministic
let result2 = hkdf_extract(&salt, &ikm);
assert_eq!(result1, result2);
// Different input should give different output
let ikm2 = vec![0x33u8; 32];
let result3 = hkdf_extract(&salt, &ikm2);
assert_ne!(result1, result3);
}
#[test]
fn test_derive_traffic_keys() {
let traffic_secret = vec![0x99u8; 32];
// Test TLS_AES_128_GCM_SHA256
let result = derive_traffic_keys(&traffic_secret, 0x1301);
assert!(result.is_ok());
let (key, iv) = result.unwrap();
assert_eq!(key.len(), 16);
assert_eq!(iv.len(), 12);
}
#[test]
fn test_compute_finished_verify_data() {
let base_key = vec![0xAAu8; 32];
let handshake_hash = vec![0xBBu8; 32];
let result = compute_finished_verify_data(&base_key, &handshake_hash);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 32);
}
}
+431
View File
@@ -0,0 +1,431 @@
// TLS 1.3 Message Construction
//
// Construct TLS 1.3 handshake messages for REALITY protocol
use super::common::{
HANDSHAKE_TYPE_CERTIFICATE, HANDSHAKE_TYPE_CERTIFICATE_VERIFY,
HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, HANDSHAKE_TYPE_FINISHED, HANDSHAKE_TYPE_SERVER_HELLO,
VERSION_TLS_1_2_MAJOR, VERSION_TLS_1_2_MINOR,
};
use aws_lc_rs::signature::Ed25519KeyPair;
use std::io::Result;
/// Construct ServerHello message
///
/// # Arguments
/// * `server_random` - 32 bytes of server random
/// * `session_id` - Session ID from ClientHello (for compatibility)
/// * `cipher_suite` - Selected cipher suite (e.g., 0x1301)
/// * `key_share_data` - Server's X25519 public key (32 bytes)
pub fn construct_server_hello(
server_random: &[u8; 32],
session_id: &[u8],
cipher_suite: u16,
key_share_data: &[u8],
) -> Result<Vec<u8>> {
let mut server_hello = Vec::new();
// ServerHello structure:
// - handshake_type (1 byte) = 2
// - length (3 bytes)
// - version (2 bytes) = 0x0303 (TLS 1.2 for compatibility)
// - random (32 bytes)
// - session_id_length (1 byte)
// - session_id (variable)
// - cipher_suite (2 bytes)
// - compression_method (1 byte) = 0
// - extensions_length (2 bytes)
// - extensions (variable)
let mut payload = Vec::new();
// Version: 0x0303 (TLS 1.2 for compatibility)
payload.extend_from_slice(&[VERSION_TLS_1_2_MAJOR, VERSION_TLS_1_2_MINOR]);
// Random (32 bytes)
payload.extend_from_slice(server_random);
// Session ID
payload.push(session_id.len() as u8);
payload.extend_from_slice(session_id);
// Cipher suite
payload.extend_from_slice(&cipher_suite.to_be_bytes());
// Compression method = 0
payload.push(0x00);
// Extensions
let mut extensions = Vec::new();
// supported_versions extension (type=43)
extensions.extend_from_slice(&[0x00, 0x2b]); // type = 43
extensions.extend_from_slice(&[0x00, 0x02]); // length = 2
extensions.extend_from_slice(&[0x03, 0x04]); // TLS 1.3
// key_share extension (type=51)
let key_share_length = 2 + 2 + key_share_data.len(); // group + length + data
extensions.extend_from_slice(&[0x00, 0x33]); // type = 51
extensions.extend_from_slice(&(key_share_length as u16).to_be_bytes());
extensions.extend_from_slice(&[0x00, 0x1d]); // group = X25519 (0x001d)
extensions.extend_from_slice(&(key_share_data.len() as u16).to_be_bytes());
extensions.extend_from_slice(key_share_data);
// Extensions length
payload.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
payload.extend_from_slice(&extensions);
// Handshake header
server_hello.push(HANDSHAKE_TYPE_SERVER_HELLO);
// Payload length (3 bytes, big-endian)
let length_bytes = [
((payload.len() >> 16) & 0xff) as u8,
((payload.len() >> 8) & 0xff) as u8,
(payload.len() & 0xff) as u8,
];
server_hello.extend_from_slice(&length_bytes);
server_hello.extend_from_slice(&payload);
Ok(server_hello)
}
/// Construct EncryptedExtensions message
pub fn construct_encrypted_extensions() -> Result<Vec<u8>> {
let mut encrypted_extensions = Vec::new();
// EncryptedExtensions structure:
// - handshake_type (1 byte) = 8
// - length (3 bytes)
// - extensions_length (2 bytes)
// - extensions (variable, usually empty for minimal setup)
encrypted_extensions.push(HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS);
// Empty extensions for minimal setup
let extensions_length: u16 = 0;
let payload_length = 2; // Just the extensions_length field
// Payload length (3 bytes)
encrypted_extensions.extend_from_slice(&[0x00, 0x00, payload_length as u8]);
// Extensions length (2 bytes)
encrypted_extensions.extend_from_slice(&extensions_length.to_be_bytes());
Ok(encrypted_extensions)
}
/// Construct Certificate message with HMAC-signed Ed25519 certificate
///
/// # Arguments
/// * `cert_der` - DER-encoded certificate (with HMAC signature)
pub fn construct_certificate(cert_der: &[u8]) -> Result<Vec<u8>> {
let mut certificate = Vec::new();
// Certificate structure:
// - handshake_type (1 byte) = 11
// - length (3 bytes)
// - certificate_request_context (1 byte length + data, usually empty)
// - certificate_list (3 bytes length + entries)
// - certificate_entry:
// - cert_data (3 bytes length + DER)
// - extensions (2 bytes length, usually empty)
let mut payload = Vec::new();
// Certificate request context (empty for server certificates)
payload.push(0x00);
// Certificate list
let mut cert_list = Vec::new();
// Certificate entry
// Cert data length (3 bytes)
cert_list.extend_from_slice(&[
((cert_der.len() >> 16) & 0xff) as u8,
((cert_der.len() >> 8) & 0xff) as u8,
(cert_der.len() & 0xff) as u8,
]);
cert_list.extend_from_slice(cert_der);
// Extensions (empty)
cert_list.extend_from_slice(&[0x00, 0x00]);
// Certificate list length (3 bytes)
payload.extend_from_slice(&[
((cert_list.len() >> 16) & 0xff) as u8,
((cert_list.len() >> 8) & 0xff) as u8,
(cert_list.len() & 0xff) as u8,
]);
payload.extend_from_slice(&cert_list);
// Handshake header
certificate.push(HANDSHAKE_TYPE_CERTIFICATE);
// Payload length (3 bytes)
certificate.extend_from_slice(&[
((payload.len() >> 16) & 0xff) as u8,
((payload.len() >> 8) & 0xff) as u8,
(payload.len() & 0xff) as u8,
]);
certificate.extend_from_slice(&payload);
Ok(certificate)
}
/// Construct CertificateVerify message
///
/// # Arguments
/// * `signing_key` - Ed25519 signing key
/// * `handshake_hash` - SHA256 hash of all handshake messages up to this point
pub fn construct_certificate_verify(
signing_key: &Ed25519KeyPair,
handshake_hash: &[u8],
) -> Result<Vec<u8>> {
let mut certificate_verify = Vec::new();
// CertificateVerify structure:
// - handshake_type (1 byte) = 15
// - length (3 bytes)
// - signature_algorithm (2 bytes) = 0x0807 for Ed25519
// - signature (2 bytes length + data)
// Construct the signed content
// TLS 1.3 uses a specific prefix for CertificateVerify:
// " " * 64 + "TLS 1.3, server CertificateVerify" + 0x00 + handshake_hash
let mut signed_content = Vec::new();
signed_content.extend_from_slice(&[0x20u8; 64]); // 64 spaces
signed_content.extend_from_slice(b"TLS 1.3, server CertificateVerify");
signed_content.push(0x00);
signed_content.extend_from_slice(handshake_hash);
// Sign the content
let signature = signing_key.sign(&signed_content);
let signature_bytes = signature.as_ref();
let mut payload = Vec::new();
// Signature algorithm: Ed25519 (0x0807)
payload.extend_from_slice(&[0x08, 0x07]);
// Signature length and data
payload.extend_from_slice(&(signature_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(signature_bytes);
// Handshake header
certificate_verify.push(HANDSHAKE_TYPE_CERTIFICATE_VERIFY);
// Payload length (3 bytes)
certificate_verify.extend_from_slice(&[
((payload.len() >> 16) & 0xff) as u8,
((payload.len() >> 8) & 0xff) as u8,
(payload.len() & 0xff) as u8,
]);
certificate_verify.extend_from_slice(&payload);
Ok(certificate_verify)
}
/// Construct Finished message
///
/// # Arguments
/// * `verify_data` - HMAC of handshake transcript (32 bytes for SHA256)
pub fn construct_finished(verify_data: &[u8]) -> Result<Vec<u8>> {
let mut finished = Vec::new();
// Finished structure:
// - handshake_type (1 byte) = 20
// - length (3 bytes)
// - verify_data (variable, 32 bytes for SHA256)
finished.push(HANDSHAKE_TYPE_FINISHED);
// Payload length (3 bytes)
finished.extend_from_slice(&[
((verify_data.len() >> 16) & 0xff) as u8,
((verify_data.len() >> 8) & 0xff) as u8,
(verify_data.len() & 0xff) as u8,
]);
finished.extend_from_slice(verify_data);
Ok(finished)
}
/// Write TLS record header
///
/// Construct TLS 1.3 ClientHello message
///
/// Returns handshake message bytes (without record header)
pub fn construct_client_hello(
client_random: &[u8; 32],
session_id: &[u8; 32],
client_public_key: &[u8],
server_name: &str,
) -> Result<Vec<u8>> {
let mut hello = Vec::with_capacity(512);
// Handshake message type: ClientHello (0x01)
hello.push(0x01);
// Placeholder for handshake message length (3 bytes)
let length_offset = hello.len();
hello.extend_from_slice(&[0u8; 3]);
// TLS version: 3.3 (TLS 1.2 for compatibility)
hello.extend_from_slice(&[VERSION_TLS_1_2_MAJOR, VERSION_TLS_1_2_MINOR]);
// Client random (32 bytes)
hello.extend_from_slice(client_random);
// Session ID length (1 byte) + Session ID (32 bytes)
hello.push(32);
hello.extend_from_slice(session_id);
// Cipher suites
// Support only TLS_AES_128_GCM_SHA256 (0x1301)
hello.extend_from_slice(&[0x00, 0x02]); // Cipher suites length: 2 bytes
hello.extend_from_slice(&[0x13, 0x01]); // TLS_AES_128_GCM_SHA256
// Compression methods (1 method: null)
hello.extend_from_slice(&[0x01, 0x00]);
// Extensions
let extensions_offset = hello.len();
hello.extend_from_slice(&[0u8; 2]); // Placeholder for extensions length
let mut extensions = Vec::new();
// 1. server_name extension (type 0)
{
let server_name_bytes = server_name.as_bytes();
let server_name_len = server_name_bytes.len();
extensions.extend_from_slice(&[0x00, 0x00]); // Extension type: server_name
let ext_len = 5 + server_name_len;
extensions.extend_from_slice(&(ext_len as u16).to_be_bytes()); // Extension length
extensions.extend_from_slice(&((server_name_len + 3) as u16).to_be_bytes()); // Server name list length
extensions.push(0x00); // Name type: host_name
extensions.extend_from_slice(&(server_name_len as u16).to_be_bytes()); // Name length
extensions.extend_from_slice(server_name_bytes); // Server name
}
// 2. supported_versions extension (type 43)
{
extensions.extend_from_slice(&[0x00, 0x2b]); // Extension type: supported_versions
extensions.extend_from_slice(&[0x00, 0x03]); // Extension length: 3
extensions.push(0x02); // Supported versions length: 2
extensions.extend_from_slice(&[0x03, 0x04]); // TLS 1.3
}
// 3. supported_groups extension (type 10)
{
extensions.extend_from_slice(&[0x00, 0x0a]); // Extension type: supported_groups
extensions.extend_from_slice(&[0x00, 0x04]); // Extension length: 4
extensions.extend_from_slice(&[0x00, 0x02]); // Supported groups length: 2
extensions.extend_from_slice(&[0x00, 0x1d]); // x25519
}
// 4. key_share extension (type 51)
{
extensions.extend_from_slice(&[0x00, 0x33]); // Extension type: key_share
let key_share_len = 2 + 4 + client_public_key.len();
extensions.extend_from_slice(&(key_share_len as u16).to_be_bytes()); // Extension length
let key_share_list_len = 4 + client_public_key.len();
extensions.extend_from_slice(&(key_share_list_len as u16).to_be_bytes()); // Key share list length
extensions.extend_from_slice(&[0x00, 0x1d]); // Group: x25519
extensions.extend_from_slice(&(client_public_key.len() as u16).to_be_bytes()); // Key length
extensions.extend_from_slice(client_public_key); // Public key
}
// 5. signature_algorithms extension (type 13)
{
extensions.extend_from_slice(&[0x00, 0x0d]); // Extension type: signature_algorithms
extensions.extend_from_slice(&[0x00, 0x04]); // Extension length: 4
extensions.extend_from_slice(&[0x00, 0x02]); // Signature algorithms length: 2
extensions.extend_from_slice(&[0x08, 0x07]); // ed25519
}
// Write extensions length
let extensions_length = extensions.len();
hello[extensions_offset..extensions_offset + 2]
.copy_from_slice(&(extensions_length as u16).to_be_bytes());
// Append extensions
hello.extend_from_slice(&extensions);
// Write handshake message length
let message_length = hello.len() - 4; // Exclude type (1) and length (3)
hello[length_offset..length_offset + 3]
.copy_from_slice(&(message_length as u32).to_be_bytes()[1..]);
Ok(hello)
}
/// # Arguments
/// * `record_type` - TLS record type (0x16 for Handshake, 0x17 for ApplicationData)
/// * `length` - Length of record payload
pub fn write_record_header(record_type: u8, length: u16) -> Vec<u8> {
let mut header = Vec::new();
header.push(record_type);
header.extend_from_slice(&[VERSION_TLS_1_2_MAJOR, VERSION_TLS_1_2_MINOR]); // Version: TLS 1.2
header.extend_from_slice(&length.to_be_bytes());
header
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reality::common::CONTENT_TYPE_HANDSHAKE;
#[test]
fn test_construct_server_hello() {
let server_random = [0x42u8; 32];
let session_id = vec![0x99u8; 32];
let cipher_suite = 0x1301; // TLS_AES_128_GCM_SHA256
let key_share = vec![0xAAu8; 32];
let result = construct_server_hello(&server_random, &session_id, cipher_suite, &key_share);
assert!(result.is_ok());
let msg = result.unwrap();
assert_eq!(msg[0], HANDSHAKE_TYPE_SERVER_HELLO);
}
#[test]
fn test_construct_encrypted_extensions() {
let result = construct_encrypted_extensions();
assert!(result.is_ok());
let msg = result.unwrap();
assert_eq!(msg[0], HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS);
}
#[test]
fn test_construct_certificate() {
let cert_der = vec![0xBBu8; 100];
let result = construct_certificate(&cert_der);
assert!(result.is_ok());
let msg = result.unwrap();
assert_eq!(msg[0], HANDSHAKE_TYPE_CERTIFICATE);
}
#[test]
fn test_construct_finished() {
let verify_data = vec![0xCCu8; 32];
let result = construct_finished(&verify_data);
assert!(result.is_ok());
let msg = result.unwrap();
assert_eq!(msg[0], HANDSHAKE_TYPE_FINISHED);
assert_eq!(msg.len(), 1 + 3 + 32); // type + length + verify_data
}
#[test]
fn test_write_record_header() {
let header = write_record_header(CONTENT_TYPE_HANDSHAKE, 100);
assert_eq!(header.len(), 5);
assert_eq!(header[0], 0x16); // Handshake
assert_eq!(header[1], 0x03); // TLS 1.2
assert_eq!(header[2], 0x03);
assert_eq!(u16::from_be_bytes([header[3], header[4]]), 100);
}
}
+458
View File
@@ -0,0 +1,458 @@
use aws_lc_rs::{
agreement,
rand::{SecureRandom, SystemRandom},
};
use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine as _};
use crate::buf_reader::BufReader;
/// Decodes a base64url-encoded public key
pub fn decode_public_key(encoded: &str) -> Result<[u8; 32], std::io::Error> {
let decoded = URL_SAFE_NO_PAD.decode(encoded).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid base64: {}", e),
)
})?;
if decoded.len() != 32 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid public key length: {} (expected 32)", decoded.len()),
));
}
let mut key = [0u8; 32];
key.copy_from_slice(&decoded);
Ok(key)
}
/// Decodes a base64url-encoded private key
pub fn decode_private_key(encoded: &str) -> Result<[u8; 32], std::io::Error> {
let decoded = URL_SAFE_NO_PAD.decode(encoded).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid base64: {}", e),
)
})?;
if decoded.len() != 32 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid private key length: {} (expected 32)",
decoded.len()
),
));
}
let mut key = [0u8; 32];
key.copy_from_slice(&decoded);
Ok(key)
}
/// Decodes a hex-encoded short ID with zero-padding
///
/// Short IDs can be 0-16 hex characters (0-8 bytes).
/// If shorter than 16 characters, they are left-padded with zeros.
pub fn decode_short_id(hex: &str) -> Result<[u8; 8], std::io::Error> {
if hex.len() > 16 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Short ID too long: {} (max 16 hex chars)", hex.len()),
));
}
// Left-pad with zeros to make 16 chars
let padded = format!("{:0>16}", hex);
let mut short_id = [0u8; 8];
decode_hex_to_slice(&padded, &mut short_id).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid hex: {}", e),
)
})?;
Ok(short_id)
}
/// Decode hex string to byte slice
fn decode_hex_to_slice(hex: &str, output: &mut [u8]) -> Result<(), &'static str> {
if hex.len() != output.len() * 2 {
return Err("Invalid hex length");
}
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let high = hex_char_to_value(chunk[0])?;
let low = hex_char_to_value(chunk[1])?;
output[i] = (high << 4) | low;
}
Ok(())
}
/// Convert hex character to its numeric value
fn hex_char_to_value(c: u8) -> Result<u8, &'static str> {
match c {
b'0'..=b'9' => Ok(c - b'0'),
b'a'..=b'f' => Ok(c - b'a' + 10),
b'A'..=b'F' => Ok(c - b'A' + 10),
_ => Err("Invalid hex character"),
}
}
/// Extracts ClientRandom from ClientHello
///
/// ClientHello structure (simplified):
/// - TLS Header: 5 bytes
/// - Handshake Type: 1 byte
/// - Length: 3 bytes
/// - Protocol Version: 2 bytes
/// - ClientRandom: 32 bytes (starts at offset 11)
pub fn extract_client_random(client_hello: &[u8]) -> Result<[u8; 32], std::io::Error> {
const TLS_HEADER_LEN: usize = 5;
const HANDSHAKE_HEADER_LEN: usize = 4; // type(1) + length(3)
const PROTOCOL_VERSION_LEN: usize = 2;
const RANDOM_OFFSET: usize = TLS_HEADER_LEN + HANDSHAKE_HEADER_LEN + PROTOCOL_VERSION_LEN;
if client_hello.len() < RANDOM_OFFSET + 32 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"ClientHello too short to extract random",
));
}
let mut random = [0u8; 32];
random.copy_from_slice(&client_hello[RANDOM_OFFSET..RANDOM_OFFSET + 32]);
Ok(random)
}
/// Extracts SessionId slice from ClientHello without allocation
///
/// SessionId comes after ClientRandom and has a 1-byte length prefix.
/// Returns a slice pointing into the original buffer.
pub fn extract_session_id_slice(client_hello: &[u8]) -> Result<&[u8], std::io::Error> {
const TLS_HEADER_LEN: usize = 5;
const HANDSHAKE_HEADER_LEN: usize = 4;
const PROTOCOL_VERSION_LEN: usize = 2;
const RANDOM_LEN: usize = 32;
const SESSION_ID_LEN_OFFSET: usize =
TLS_HEADER_LEN + HANDSHAKE_HEADER_LEN + PROTOCOL_VERSION_LEN + RANDOM_LEN;
if client_hello.len() < SESSION_ID_LEN_OFFSET + 1 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"ClientHello too short to extract session ID",
));
}
let session_id_len = client_hello[SESSION_ID_LEN_OFFSET] as usize;
if client_hello.len() < SESSION_ID_LEN_OFFSET + 1 + session_id_len {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"ClientHello session ID extends past end",
));
}
Ok(&client_hello[SESSION_ID_LEN_OFFSET + 1..SESSION_ID_LEN_OFFSET + 1 + session_id_len])
}
/// Extracts X25519 public key from ClientHello KeyShare extension
///
/// This parses the TLS 1.3 extensions to find the KeyShare extension
/// and extracts the X25519 public key (group 0x001d).
pub fn extract_client_public_key(client_hello: &[u8]) -> Result<[u8; 32], std::io::Error> {
const TLS_HEADER_LEN: usize = 5;
if client_hello.len() < TLS_HEADER_LEN {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"ClientHello too short",
));
}
let mut reader = BufReader::new(&client_hello[TLS_HEADER_LEN..]);
// Parse handshake header
let _handshake_type = reader.read_u8()?;
let _handshake_len = reader.read_u24_be()?;
let _protocol_version = reader.read_u16_be()?;
// Skip random
reader.skip(32)?;
// Read session ID
let session_id_len = reader.read_u8()? as usize;
reader.skip(session_id_len)?;
// Read cipher suites
let cipher_suites_len = reader.read_u16_be()? as usize;
reader.skip(cipher_suites_len)?;
// Read compression methods
let compression_len = reader.read_u8()? as usize;
reader.skip(compression_len)?;
// Parse extensions
let extensions_len = reader.read_u16_be()? as usize;
let extensions_start = reader.position();
let extensions_end = extensions_start + extensions_len;
while reader.position() < extensions_end {
let ext_type = reader.read_u16_be()?;
let ext_len = reader.read_u16_be()? as usize;
if ext_type == 51 {
// KeyShare extension (0x0033)
return parse_keyshare_extension(reader.read_slice(ext_len)?);
} else {
reader.skip(ext_len)?;
}
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"KeyShare extension not found in ClientHello",
))
}
/// Parses the KeyShare extension to extract X25519 public key
fn parse_keyshare_extension(data: &[u8]) -> Result<[u8; 32], std::io::Error> {
let mut reader = BufReader::new(data);
// KeyShare extension format (client):
// - Client Key Share Length: 2 bytes
// - Key Share Entries (repeated):
// - Group: 2 bytes
// - Key Exchange Length: 2 bytes
// - Key Exchange Data: variable
let _client_shares_len = reader.read_u16_be()?;
// Find X25519 key share (group 0x001d = 29)
loop {
// Check if we have at least 4 bytes for group and length
if reader.position() + 4 > data.len() {
break;
}
let group = reader.read_u16_be()?;
let key_len = reader.read_u16_be()? as usize;
if reader.position() + key_len > data.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"KeyShare entry extends past end",
));
}
if group == 0x001d {
// X25519
if key_len != 32 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid X25519 key length: {}", key_len),
));
}
let key_bytes = reader.read_slice(32)?;
let mut key = [0u8; 32];
key.copy_from_slice(key_bytes);
return Ok(key);
} else {
reader.skip(key_len)?;
}
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"X25519 key share not found in KeyShare extension",
))
}
/// Extract server's X25519 public key from ServerHello message
///
/// This parses the TLS 1.3 ServerHello to find the KeyShare extension
/// and extracts the X25519 public key (group 0x001d).
pub fn extract_server_public_key(server_hello: &[u8]) -> Result<[u8; 32], std::io::Error> {
const TLS_HEADER_LEN: usize = 5;
if server_hello.len() < TLS_HEADER_LEN {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"ServerHello too short",
));
}
let mut reader = BufReader::new(&server_hello[TLS_HEADER_LEN..]);
// Parse handshake header
let _handshake_type = reader.read_u8()?;
let _handshake_len = reader.read_u24_be()?;
let _protocol_version = reader.read_u16_be()?;
// Skip random
reader.skip(32)?;
// Read session ID (ServerHello can echo it back)
let session_id_len = reader.read_u8()? as usize;
reader.skip(session_id_len)?;
// Read cipher suite (single 2-byte value in ServerHello)
reader.skip(2)?;
// Read compression method (single byte in ServerHello)
reader.skip(1)?;
// Parse extensions
let extensions_len = reader.read_u16_be()? as usize;
let extensions_start = reader.position();
let extensions_end = extensions_start + extensions_len;
while reader.position() < extensions_end {
let ext_type = reader.read_u16_be()?;
let ext_len = reader.read_u16_be()? as usize;
if ext_type == 51 {
// KeyShare extension (0x0033)
return parse_server_keyshare_extension(reader.read_slice(ext_len)?);
} else {
reader.skip(ext_len)?;
}
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"KeyShare extension not found in ServerHello",
))
}
/// Parses the ServerHello KeyShare extension to extract X25519 public key
fn parse_server_keyshare_extension(data: &[u8]) -> Result<[u8; 32], std::io::Error> {
let mut reader = BufReader::new(data);
// ServerHello KeyShare extension format:
// - Group: 2 bytes
// - Key Exchange Length: 2 bytes
// - Key Exchange Data: variable
let group = reader.read_u16_be()?;
let key_len = reader.read_u16_be()? as usize;
if group == 29 {
// X25519
if key_len != 32 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid X25519 key length: {}", key_len),
));
}
let key_bytes = reader.read_slice(32)?;
let mut key = [0u8; 32];
key.copy_from_slice(key_bytes);
return Ok(key);
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"X25519 key share not found in ServerHello KeyShare extension",
))
}
pub fn generate_keypair() -> std::io::Result<(String, String)> {
// Step 1: Generate 32 random bytes for private key
let rng = SystemRandom::new();
let mut private_key_bytes = [0u8; 32];
rng.fill(&mut private_key_bytes)
.map_err(|_| std::io::Error::other("RNG failed"))?;
// Step 2: Create X25519 private key from the random bytes
let private_key =
agreement::PrivateKey::from_private_key(&agreement::X25519, &private_key_bytes)
.map_err(|_| std::io::Error::other("Failed to create X25519 key"))?;
// Step 3: Derive public key from private key
let public_key_bytes = private_key
.compute_public_key()
.map_err(|_| std::io::Error::other("Failed to compute public key"))?;
// Step 4: Encode both keys as base64url (no padding)
let private_key_b64 = URL_SAFE_NO_PAD.encode(private_key_bytes);
let public_key_b64 = URL_SAFE_NO_PAD.encode(public_key_bytes.as_ref());
Ok((private_key_b64, public_key_b64))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_short_id() {
// Full 16-char hex
let short_id = decode_short_id("0123456789abcdef").unwrap();
assert_eq!(short_id, [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
// Partial hex (should be zero-padded on left)
let short_id2 = decode_short_id("abcdef").unwrap();
assert_eq!(short_id2, [0x00, 0x00, 0x00, 0x00, 0x00, 0xab, 0xcd, 0xef]);
// Empty (all zeros)
let short_id3 = decode_short_id("").unwrap();
assert_eq!(short_id3, [0; 8]);
// Too long should error
let result = decode_short_id("0123456789abcdef0");
assert!(result.is_err());
}
#[test]
fn test_extract_client_random() {
// Create a minimal ClientHello with random
let mut client_hello = vec![0u8; 100];
// TLS Header
client_hello[0] = 0x16; // Handshake
client_hello[1] = 0x03; // Version major
client_hello[2] = 0x03; // Version minor (TLS 1.2)
// Handshake header
client_hello[5] = 0x01; // ClientHello type
// Protocol version in handshake
client_hello[9] = 0x03; // Major
client_hello[10] = 0x03; // Minor
// ClientRandom starts at offset 11
for i in 0..32 {
client_hello[11 + i] = (i + 1) as u8;
}
let random = extract_client_random(&client_hello).unwrap();
for i in 0..32 {
assert_eq!(random[i], (i + 1) as u8);
}
}
#[test]
fn test_decode_public_key() {
use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine as _};
// Valid 32-byte key
let key_bytes = [0x42u8; 32];
let encoded = URL_SAFE_NO_PAD.encode(&key_bytes);
let decoded = decode_public_key(&encoded).unwrap();
assert_eq!(decoded, key_bytes);
// Invalid length
let short_key = [0x42u8; 16];
let encoded_short = URL_SAFE_NO_PAD.encode(&short_key);
assert!(decode_public_key(&encoded_short).is_err());
// Invalid base64
assert!(decode_public_key("not-valid-base64!!!").is_err());
}
}
+209
View File
@@ -0,0 +1,209 @@
// REALITY client handler
//
// This file lives at src/ (not in reality/) for two reasons:
// 1. Symmetry with tls_server_handler.rs which handles both TLS and REALITY server-side
// 2. Avoids a dependency cycle: crypto → reality (core types), but this handler → crypto
use async_trait::async_trait;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::address::NetLocation;
use crate::async_stream::AsyncStream;
use crate::crypto::{CryptoConnection, CryptoTlsStream};
use crate::reality::{RealityClientConfig, RealityClientConnection};
use crate::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
/// REALITY client handler using buffered Connection API
///
/// This handler establishes REALITY-obfuscated TLS connections using
/// the buffered sans-I/O pattern with RealityClientConnection.
#[derive(Debug)]
pub struct RealityClientHandler {
public_key: [u8; 32],
short_id: [u8; 8],
server_name: rustls::pki_types::ServerName<'static>,
handler: RealityInnerClientHandler,
}
#[derive(Debug)]
pub enum RealityInnerClientHandler {
Default(Box<dyn TcpClientHandler>),
VisionVless { uuid: Box<[u8]> },
}
impl RealityClientHandler {
pub fn new(
public_key: [u8; 32],
short_id: [u8; 8],
server_name: rustls::pki_types::ServerName<'static>,
handler: Box<dyn TcpClientHandler>,
) -> Self {
Self {
public_key,
short_id,
server_name,
handler: RealityInnerClientHandler::Default(handler),
}
}
pub fn new_vision_vless(
public_key: [u8; 32],
short_id: [u8; 8],
server_name: rustls::pki_types::ServerName<'static>,
user_id: Box<[u8]>,
) -> Self {
Self {
public_key,
short_id,
server_name,
handler: RealityInnerClientHandler::VisionVless { uuid: user_id },
}
}
}
#[async_trait]
impl TcpClientHandler for RealityClientHandler {
async fn setup_client_stream(
&self,
server_stream: &mut Box<dyn AsyncStream>,
mut client_stream: Box<dyn AsyncStream>,
remote_location: NetLocation,
) -> std::io::Result<TcpClientSetupResult> {
// Extract server name as string
let server_name_str = match &self.server_name {
rustls::pki_types::ServerName::DnsName(name) => name.as_ref(),
rustls::pki_types::ServerName::IpAddress(ip) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("REALITY requires DNS name, got IP address: {:?}", ip),
))
}
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"REALITY requires DNS name",
))
}
};
// Step 1: Create buffered REALITY client connection
log::debug!("REALITY CLIENT: Creating buffered RealityClientConnection");
let reality_config = RealityClientConfig {
public_key: self.public_key,
short_id: self.short_id,
server_name: server_name_str.to_string(),
};
let mut reality_conn = RealityClientConnection::new(reality_config)?;
// Step 2: Write ClientHello to server
log::debug!("REALITY CLIENT: Writing ClientHello");
{
let mut write_buf = Vec::new();
while reality_conn.wants_write() {
reality_conn.write_tls(&mut write_buf)?;
}
if !write_buf.is_empty() {
client_stream.write_all(&write_buf).await?;
client_stream.flush().await?;
}
}
// Step 3: Read server's handshake messages
log::debug!("REALITY CLIENT: Reading server handshake");
{
let mut buf = vec![0u8; 16384]; // Large enough for server handshake
let n = client_stream.read(&mut buf).await?;
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF while waiting for server handshake",
));
}
let mut cursor = std::io::Cursor::new(&buf[..n]);
reality_conn.read_tls(&mut cursor)?;
}
// Step 4: Process server handshake
log::debug!("REALITY CLIENT: Processing server handshake");
reality_conn.process_new_packets()?;
// Continue processing packets until handshake is complete
while reality_conn.is_handshaking() {
log::debug!("REALITY CLIENT: Handshake still in progress, checking for more work");
// Check if we need to write (e.g., client Finished)
if reality_conn.wants_write() {
log::debug!("REALITY CLIENT: Writing buffered handshake data");
let mut write_buf = Vec::new();
while reality_conn.wants_write() {
reality_conn.write_tls(&mut write_buf)?;
}
if !write_buf.is_empty() {
client_stream.write_all(&write_buf).await?;
client_stream.flush().await?;
log::debug!("REALITY CLIENT: Sent {} bytes to server", write_buf.len());
}
}
// Try to process more packets
let prev_state = reality_conn.is_handshaking();
reality_conn.process_new_packets()?;
// If we're still handshaking and state didn't change, read more data
if reality_conn.is_handshaking() && prev_state == reality_conn.is_handshaking() {
log::debug!("REALITY CLIENT: Reading more server handshake data");
let mut buf = vec![0u8; 16384];
let n = client_stream.read(&mut buf).await?;
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF while reading server handshake",
));
}
log::debug!("REALITY CLIENT: Read {} bytes from server", n);
let mut cursor = std::io::Cursor::new(&buf[..n]);
reality_conn.read_tls(&mut cursor)?;
// Process the new data
reality_conn.process_new_packets()?;
}
}
// Step 5: Final check for any remaining writes
if reality_conn.wants_write() {
log::debug!("REALITY CLIENT: Writing final handshake data");
let mut write_buf = Vec::new();
while reality_conn.wants_write() {
reality_conn.write_tls(&mut write_buf)?;
}
if !write_buf.is_empty() {
client_stream.write_all(&write_buf).await?;
client_stream.flush().await?;
}
}
// Step 6: Wrap in Connection enum
log::debug!("REALITY CLIENT: Creating Connection");
let connection = CryptoConnection::new_reality_client(reality_conn);
log::debug!("REALITY CLIENT: Handshake completed successfully");
let tls_stream = CryptoTlsStream::new(client_stream, connection);
match self.handler {
RealityInnerClientHandler::Default(ref handler) => {
handler
.setup_client_stream(server_stream, Box::new(tls_stream), remote_location)
.await
}
RealityInnerClientHandler::VisionVless { ref uuid } => {
crate::vless::vless_client_handler::setup_custom_tls_vision_vless_client_stream(
tls_stream,
uuid,
&remote_location,
)
.await
}
}
}
}
+77
View File
@@ -0,0 +1,77 @@
use std::io::Cursor;
use rustls::Connection;
/// Feed all data to a rustls connection, looping until all bytes are consumed.
///
/// rustls's read_tls() may not consume all bytes in one call, so we must loop
/// until all data has been fed. This is critical to avoid losing bytes.
#[inline(always)]
pub fn feed_rustls_connection(connection: &mut Connection, data: &[u8]) -> std::io::Result<()> {
match connection {
Connection::Client(client_connection) => {
feed_rustls_client_connection(client_connection, data)
}
Connection::Server(server_connection) => {
feed_rustls_server_connection(server_connection, data)
}
}
}
#[inline(always)]
pub fn feed_rustls_server_connection(
connection: &mut rustls::ServerConnection,
data: &[u8],
) -> std::io::Result<()> {
let mut cursor = Cursor::new(data);
let mut i = 0;
while i < data.len() {
let n = connection.read_tls(&mut cursor).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to feed rustls server connection: {e}"),
)
})?;
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"rustls server connection did not consume all bytes: fed {}/{} bytes",
i,
data.len()
),
));
}
i += n;
}
Ok(())
}
#[inline(always)]
pub fn feed_rustls_client_connection(
connection: &mut rustls::ClientConnection,
data: &[u8],
) -> std::io::Result<()> {
let mut cursor = Cursor::new(data);
let mut i = 0;
while i < data.len() {
let n = connection.read_tls(&mut cursor).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to feed rustls client connection: {e}"),
)
})?;
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"rustls client connection did not consume all bytes: fed {}/{} bytes",
i,
data.len()
),
));
}
i += n;
}
Ok(())
}
+263
View File
@@ -0,0 +1,263 @@
//! Generic rustls handshake helper that works for both client and server connections.
//!
//! This module provides a unified handshake implementation that works correctly
//! with both TLS 1.2 and TLS 1.3, for both client and server connections.
//!
//! The key insight from tokio-rustls is to use rustls's `wants_read()` and
//! `wants_write()` methods to let the TLS state machine guide the handshake,
//! rather than assuming packet boundaries or TLS version-specific message patterns.
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::async_stream::AsyncStream;
use crate::rustls_connection_util::feed_rustls_connection;
/// Perform a complete TLS handshake using the provided rustls connection and stream.
///
/// This function works for both client and server connections (via `rustls::Connection`),
/// and handles both TLS 1.2 and TLS 1.3 correctly by following rustls's state machine
/// guidance via `wants_read()` and `wants_write()`.
///
/// # Arguments
///
/// * `connection` - The rustls::Connection (either Client or Server variant)
/// * `stream` - The underlying async stream to read/write TLS data
/// * `buffer_size` - Size of the read buffer (typically 16384)
///
/// # Returns
///
/// Returns `Ok(())` if the handshake completes successfully.
///
/// # Errors
///
/// Returns an error if:
/// - EOF is encountered during handshake
/// - TLS processing fails (certificate validation, protocol error, etc.)
/// - I/O error occurs
/// - Handshake stalls (neither wants_read nor wants_write but still handshaking)
///
/// # Example
///
/// ```ignore
/// use rustls::Connection;
///
/// let client_conn = rustls::ClientConnection::new(config, server_name)?;
/// let mut connection = Connection::Client(client_conn);
/// perform_handshake(&mut connection, &mut tcp_stream, 16384).await?;
/// // Handshake complete, connection ready for application data
/// ```
pub async fn perform_handshake(
connection: &mut rustls::Connection,
stream: &mut Box<dyn AsyncStream>,
buffer_size: usize,
) -> std::io::Result<()> {
log::debug!("TLS handshake starting");
let mut iteration = 0;
let mut eof = false;
// Infinite loop - we explicitly break when done
// This matches tokio-rustls's structure: loop { ... return match ... }
loop {
iteration += 1;
let mut write_would_block = false;
let mut read_would_block = false;
log::debug!(
"TLS handshake iteration {}: wants_read={}, wants_write={}",
iteration,
connection.wants_read(),
connection.wants_write()
);
// Phase 1: Drain all pending writes
while connection.wants_write() {
match write_pending_data(connection, stream).await {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
write_would_block = true;
break;
}
Err(e) => return Err(e),
}
}
// Phase 2: Flush the stream to ensure data is sent
match stream.flush().await {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
write_would_block = true;
}
Err(e) => return Err(e),
}
// Phase 3: Read ONCE if rustls wants more data
// CRITICAL: Unlike tokio-rustls which uses non-blocking I/O with Poll::Pending,
// we use blocking async I/O. Therefore we can NOT loop on wants_read() because
// each read() blocks. We only read ONCE per main loop iteration.
if !eof && connection.wants_read() {
match read_and_process_data(connection, stream, buffer_size).await {
Ok(0) => {
log::debug!("TLS handshake: EOF from peer");
eof = true;
}
Ok(n) => {
log::debug!("TLS handshake: read and processed {} bytes", n);
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
read_would_block = true;
}
Err(e) => return Err(e),
}
}
// Phase 4: Check handshake state and decide what to do next
// This EXACTLY matches tokio-rustls's logic at lines 173-187
log::debug!(
"TLS handshake iteration {} complete: eof={}, is_handshaking={}, write_would_block={}, read_would_block={}",
iteration, eof, connection.is_handshaking(), write_would_block, read_would_block
);
match (eof, connection.is_handshaking()) {
// EOF + still handshaking = error
(true, true) => {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF during TLS handshake",
));
}
// Handshake complete! (regardless of EOF state)
(_, false) => {
log::debug!("TLS handshake complete after {} iterations", iteration);
break; // Exit the loop
}
// Still handshaking + would block on I/O
(_, true) if write_would_block || read_would_block => {
// In blocking mode, this shouldn't happen, but if it does we're stuck
log::error!("TLS handshake would block at iteration {}", iteration);
return Err(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"TLS handshake would block (should not happen in blocking mode)",
));
}
// Still handshaking, no I/O blocking - loop again
(..) => {
// Safety check to prevent infinite loops
if iteration > 100 {
log::error!("TLS handshake exceeded 100 iterations");
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"TLS handshake exceeded maximum iterations",
));
}
log::debug!("TLS handshake continuing to iteration {}", iteration + 1);
continue;
}
}
}
// Final write if there's any pending data after handshake completes
while connection.wants_write() {
log::debug!("TLS handshake: final write");
write_pending_data(connection, stream).await?;
}
Ok(())
}
/// Write all pending TLS data from the connection to the stream.
///
/// This drains rustls's write buffer and sends all data to the peer,
/// ensuring it's flushed to the network.
async fn write_pending_data(
connection: &mut rustls::Connection,
stream: &mut Box<dyn AsyncStream>,
) -> std::io::Result<()> {
let mut write_buf = Vec::new();
// Drain all pending writes into our buffer
while connection.wants_write() {
connection.write_tls(&mut write_buf)?;
}
// Send to peer if we have data
if !write_buf.is_empty() {
stream.write_all(&write_buf).await?;
stream.flush().await?;
}
Ok(())
}
/// Read data from the stream and process it through the TLS connection.
///
/// This reads available data from the network, feeds ALL of it to rustls via the
/// `feed_rustls_connection` helper (which loops until all bytes are consumed),
/// and processes it via `process_new_packets()` to advance the handshake state machine.
///
/// Returns the number of bytes read (0 indicates EOF).
async fn read_and_process_data(
connection: &mut rustls::Connection,
stream: &mut Box<dyn AsyncStream>,
buffer_size: usize,
) -> std::io::Result<usize> {
let mut buf = vec![0u8; buffer_size];
let n = stream.read(&mut buf).await?;
log::debug!("TLS: read {} bytes from peer", n);
if n == 0 {
// EOF - return 0 to let caller handle it
return Ok(0);
}
// Feed ALL the data to rustls using the helper that loops until all bytes are consumed
// This is CRITICAL - rustls may not consume all bytes in one read_tls() call
feed_rustls_connection(connection, &buf[..n])?;
// Process the new data to advance the state machine
// This is called after every feed, just like tokio-rustls does
connection.process_new_packets().map_err(|e| {
log::error!("TLS error processing packets: {}", e);
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("TLS error: {}", e))
})?;
log::debug!("TLS: processed packets successfully");
Ok(n)
}
#[cfg(test)]
mod tests {
#[test]
fn test_rustls_connection_enum_exists() {
// Compile-time test that rustls::Connection exists
// This will fail to compile if rustls::Connection doesn't exist
fn _assert_connection_type(_: &rustls::Connection) {}
}
#[test]
fn test_connection_has_required_methods() {
// This test ensures rustls::Connection has the methods we need
// It won't compile if the methods don't exist
use std::sync::Arc;
let config = Arc::new(
rustls::ClientConfig::builder()
.with_root_certificates(rustls::RootCertStore::empty())
.with_no_client_auth(),
);
let server_name = rustls::pki_types::ServerName::try_from("example.com")
.unwrap()
.to_owned();
let client_conn = rustls::ClientConnection::new(config, server_name).unwrap();
let connection = rustls::Connection::Client(client_conn);
// These method calls will fail to compile if the methods don't exist
let _handshaking = connection.is_handshaking();
let _wants_read = connection.wants_read();
let _wants_write = connection.wants_write();
}
}
+2 -2
View File
@@ -4,8 +4,8 @@ mod shadow_tls_server_handler;
mod shadow_tls_stream;
pub use shadow_tls_server_handler::{
feed_server_connection, read_client_hello, setup_shadowtls_server_stream, ParsedClientHello,
ShadowTlsServerTarget, ShadowTlsServerTargetHandshake,
read_client_hello, setup_shadowtls_server_stream, ParsedClientHello, ShadowTlsServerTarget,
ShadowTlsServerTargetHandshake,
};
pub use shadow_tls_client_handler::ShadowTlsClientHandler;
+3 -21
View File
@@ -8,6 +8,7 @@ use tokio::io::AsyncWriteExt; // For write_all // For random bytes
use crate::address::NetLocation;
use crate::async_stream::AsyncStream;
use crate::buf_reader::BufReader;
use crate::rustls_connection_util::feed_rustls_client_connection;
use crate::shadow_tls::shadow_tls_hmac::ShadowTlsHmac;
use crate::shadow_tls::shadow_tls_stream::ShadowTlsStream;
use crate::stream_reader::StreamReader;
@@ -94,7 +95,7 @@ impl TcpClientHandler for ShadowTlsClientHandler {
let parsed_server_hello = parse_server_hello(server_hello_frame)?;
feed_client_connection(&mut client_conn, server_hello_frame)?;
feed_rustls_client_connection(&mut client_conn, server_hello_frame)?;
client_conn.process_new_packets().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
@@ -169,7 +170,7 @@ impl TcpClientHandler for ShadowTlsClientHandler {
));
}
feed_client_connection(&mut client_conn, server_frame)?;
feed_rustls_client_connection(&mut client_conn, server_frame)?;
client_conn.process_new_packets().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
@@ -345,22 +346,3 @@ async fn read_tls_frame<'a>(
Ok(&read_buf[..TLS_HEADER_LEN + payload_len])
}
#[inline]
fn feed_client_connection(
client_connection: &mut rustls::ClientConnection,
data: &[u8],
) -> std::io::Result<()> {
let mut cursor = Cursor::new(data);
let mut i = 0;
while i < data.len() {
let n = client_connection.read_tls(&mut cursor).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to feed client connection: {e}"),
)
})?;
i += n;
}
Ok(())
}
+11 -23
View File
@@ -20,6 +20,7 @@ use crate::client_proxy_selector::ClientProxySelector;
use crate::noop_stream::NoopStream;
use crate::option_util::NoneOrOne;
use crate::resolver::Resolver;
use crate::rustls_connection_util::feed_rustls_server_connection;
use crate::stream_reader::StreamReader;
use crate::tcp_client_connector::TcpClientConnector;
use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
@@ -841,7 +842,7 @@ async fn setup_local_handshake(
)
})?;
feed_server_connection(&mut server_connection, &client_hello_frame)?;
feed_rustls_server_connection(&mut server_connection, &client_hello_frame)?;
server_connection.process_new_packets().map_err(|e| {
std::io::Error::new(
@@ -1041,7 +1042,7 @@ async fn setup_local_handshake(
hmac_client_data.update(initial_client_data);
hmac_client_data.update(&hmac_client_data.digest());
let shadow_tls_stream = ShadowTlsStream::new(
let mut shadow_tls_stream = ShadowTlsStream::new(
server_stream,
initial_client_data,
hmac_client_data,
@@ -1049,12 +1050,18 @@ async fn setup_local_handshake(
None,
)?;
// Feed any leftover data from the reader to the stream
let leftover = client_reader.unparsed_data();
if !leftover.is_empty() {
shadow_tls_stream.feed_initial_read_data(leftover)?;
}
return Ok(shadow_tls_stream);
}
}
feed_server_connection(&mut server_connection, &client_header_bytes)?;
feed_server_connection(&mut server_connection, client_payload_bytes)?;
feed_rustls_server_connection(&mut server_connection, &client_header_bytes)?;
feed_rustls_server_connection(&mut server_connection, client_payload_bytes)?;
server_connection.process_new_packets().map_err(|e| {
std::io::Error::new(
@@ -1065,25 +1072,6 @@ async fn setup_local_handshake(
}
}
#[inline]
pub fn feed_server_connection(
server_connection: &mut rustls::ServerConnection,
data: &[u8],
) -> std::io::Result<()> {
let mut cursor = Cursor::new(data);
let mut i = 0;
while i < data.len() {
let n = server_connection.read_tls(&mut cursor).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to feed server connection: {e}"),
)
})?;
i += n;
}
Ok(())
}
#[inline]
fn read_server_connection(
server_connection: &mut rustls::ServerConnection,
+2
View File
@@ -1,11 +1,13 @@
mod aead_util;
mod blake3_key;
mod default_key;
mod salt_checker;
mod shadowsocks_cipher;
mod shadowsocks_key;
mod shadowsocks_stream;
mod shadowsocks_stream_type;
mod shadowsocks_tcp_handler;
mod timed_salt_checker;
pub use default_key::DefaultKey;
pub use shadowsocks_cipher::ShadowsocksCipher;
+12 -8
View File
@@ -39,19 +39,23 @@ impl ShadowsocksCipher {
pub fn salt_len(&self) -> usize {
self.salt_len
}
pub fn key_len(&self) -> usize {
self.algorithm.key_len()
}
}
impl From<&str> for ShadowsocksCipher {
fn from(name: &str) -> Self {
impl TryFrom<&str> for ShadowsocksCipher {
type Error = std::io::Error;
fn try_from(name: &str) -> Result<Self, Self::Error> {
match name {
"chacha20-ietf-poly1305" | "chacha20-poly1305" => {
ShadowsocksCipher::chacha20_ietf_poly1305()
}
"aes-256-gcm" => ShadowsocksCipher::aes_256_gcm(),
"aes-128-gcm" => ShadowsocksCipher::aes_128_gcm(),
_ => {
panic!("Unknown cipher: {name}");
Ok(ShadowsocksCipher::chacha20_ietf_poly1305())
}
"aes-256-gcm" => Ok(ShadowsocksCipher::aes_256_gcm()),
"aes-128-gcm" => Ok(ShadowsocksCipher::aes_128_gcm()),
_ => Err(std::io::Error::other(format!("Unknown cipher: {name}"))),
}
}
}
+8 -8
View File
@@ -10,16 +10,17 @@ use aws_lc_rs::error::Unspecified;
use futures::ready;
use parking_lot::Mutex;
use rand::RngCore;
use subtle::ConstantTimeEq;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use super::aead_util::TAG_LEN;
use super::salt_checker::SaltChecker;
use super::shadowsocks_key::ShadowsocksKey;
use super::shadowsocks_stream_type::ShadowsocksStreamType;
use crate::async_stream::{
AsyncFlushMessage, AsyncMessageStream, AsyncPing, AsyncReadMessage, AsyncShutdownMessage,
AsyncStream, AsyncWriteMessage,
};
use crate::salt_checker::SaltChecker;
use crate::util::allocate_vec;
fn generate_iv(buf: &mut [u8]) {
@@ -534,13 +535,12 @@ impl ShadowsocksStream {
let request_salt =
&self.unprocessed_buf[self.salt_len + 9..self.salt_len + 9 + self.salt_len];
for (a, b) in request_salt.iter().zip(self.encrypt_iv.iter()) {
if a != b {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"server returned request salt does not match",
));
}
// Use constant-time comparison to prevent timing attacks
if request_salt.ct_eq(&self.encrypt_iv[..]).unwrap_u8() == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"server returned request salt does not match",
));
}
let first_chunk_len =
+80 -5
View File
@@ -5,16 +5,17 @@ use parking_lot::Mutex;
use rand::{Rng, RngCore};
use tokio::io::AsyncWriteExt;
use crate::address::NetLocation;
use super::salt_checker::SaltChecker;
use super::timed_salt_checker::TimedSaltChecker;
use crate::address::{Address, NetLocation};
use crate::async_stream::AsyncStream;
use crate::option_util::NoneOrOne;
use crate::salt_checker::SaltChecker;
use crate::socks_handler::{read_location, write_location_to_vec};
use crate::stream_reader::StreamReader;
use crate::tcp_handler::{
TcpClientHandler, TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult,
};
use crate::timed_salt_checker::TimedSaltChecker;
use crate::uot::{UotV1Stream, UotV2Stream, UOT_V1_MAGIC_ADDRESS, UOT_V2_MAGIC_ADDRESS};
use crate::util::write_all;
use super::blake3_key::Blake3Key;
@@ -34,7 +35,7 @@ pub struct ShadowsocksTcpHandler {
impl ShadowsocksTcpHandler {
pub fn new(cipher_name: &str, password: &str) -> Self {
let cipher: ShadowsocksCipher = cipher_name.into();
let cipher: ShadowsocksCipher = cipher_name.try_into().unwrap();
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(DefaultKey::new(
password,
cipher.algorithm().key_len(),
@@ -48,7 +49,7 @@ impl ShadowsocksTcpHandler {
}
pub fn new_aead2022(cipher_name: &str, key_bytes: &[u8]) -> Self {
let cipher: ShadowsocksCipher = cipher_name.into();
let cipher: ShadowsocksCipher = cipher_name.try_into().unwrap();
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(Blake3Key::new(
key_bytes.to_vec().into_boxed_slice(),
cipher.algorithm().key_len(),
@@ -105,6 +106,80 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
}
}
// Check for UDP-over-TCP (UoT) magic addresses
if let Address::Hostname(ref host) = remote_location.address() {
if host == UOT_V1_MAGIC_ADDRESS {
// UoT V1: Multi-destination UDP
// Each packet has: ATYP + address + port + length + data
let mut uot_stream = UotV1Stream::new(Box::new(server_stream));
// Feed any unparsed data (first UoT packet might be in same TCP segment)
let unparsed_data = stream_reader.unparsed_data();
if !unparsed_data.is_empty() {
log::debug!(
"Shadowsocks UoT V1: feeding {} bytes of initial data",
unparsed_data.len()
);
uot_stream.feed_initial_data(unparsed_data);
}
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
stream: Box::new(uot_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
num_sockets: 4, // TODO: make configurable
});
} else if host == UOT_V2_MAGIC_ADDRESS {
// UoT V2: Read request header first
// Request: isConnect(u8) + ATYP + address + port
// Note: V2 uses SOCKS address format (0x01=IPv4, 0x03=Domain, 0x04=IPv6),
// NOT UoT address format!
let is_connect = stream_reader.read_u8(&mut server_stream).await?;
log::debug!("Shadowsocks UoT V2: is_connect = {}", is_connect);
// Read destination address using SOCKS address format
let destination = read_location(&mut server_stream, &mut stream_reader).await?;
log::debug!("Shadowsocks UoT V2: destination = {:?}", destination);
if is_connect == 1 {
// V2 Connect mode: Single destination, length-prefixed packets only
// Reuse UotV2Stream which has identical format: length(u16be) + data
let unparsed_data = stream_reader.unparsed_data();
let mut uot_v2_stream = UotV2Stream::new(Box::new(server_stream));
if !unparsed_data.is_empty() {
uot_v2_stream.feed_initial_read_data(unparsed_data)?;
}
return Ok(TcpServerSetupResult::BidirectionalUdp {
remote_location: destination,
stream: Box::new(uot_v2_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
});
} else {
// V2 Non-connect mode: Same as V1 (multi-destination)
let mut uot_stream = UotV1Stream::new(Box::new(server_stream));
// Feed any unparsed data
let unparsed_data = stream_reader.unparsed_data();
if !unparsed_data.is_empty() {
log::debug!(
"Shadowsocks UoT V2 non-connect: feeding {} bytes of initial data",
unparsed_data.len()
);
uot_stream.feed_initial_data(unparsed_data);
}
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
stream: Box::new(uot_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
num_sockets: 4,
});
}
}
}
Ok(TcpServerSetupResult::TcpForward {
remote_location,
stream: Box::new(server_stream),
@@ -1,7 +1,7 @@
use std::collections::{HashSet, VecDeque};
use std::time::Instant;
use crate::salt_checker::SaltChecker;
use super::salt_checker::SaltChecker;
#[derive(Debug)]
struct TimeEntry {
+417
View File
@@ -0,0 +1,417 @@
//! Zero-allocation sliding buffer for streaming data
//!
//! This module provides a fixed-capacity sliding buffer optimized for
//! streaming protocols where data is written at one end and read from another.
//! Unlike a true ring buffer, this uses a linear layout with lazy compaction
//! via `copy_within()`, which is optimal for use cases requiring contiguous
//! slices (like TLS record processing).
//!
//! # Design Rationale
//!
//! A true ring buffer wraps around, but that creates non-contiguous data
//! regions which require either two-slice APIs or copies for consumers
//! expecting `&[u8]`. Since TLS encryption/decryption functions need
//! contiguous slices, this linear design with lazy compaction is more
//! efficient for our use case.
use std::io::{BufRead, Read};
/// A fixed-capacity sliding buffer with zero-allocation read/write operations.
///
/// This buffer maintains start and end offsets into a pre-allocated storage,
/// allowing efficient append and consume operations without allocating.
/// When the write end approaches capacity, `compact()` or `maybe_compact()`
/// can be used to move remaining data to the front using `copy_within()`.
///
/// # Example
/// ```ignore
/// let mut buf = SlideBuffer::new(1024);
/// buf.extend_from_slice(b"hello");
/// assert_eq!(buf.as_slice(), b"hello");
/// buf.consume(2);
/// assert_eq!(buf.as_slice(), b"llo");
/// ```
pub struct SlideBuffer {
/// Pre-allocated buffer storage
data: Box<[u8]>,
/// Start offset of valid data (inclusive)
start: usize,
/// End offset of valid data (exclusive)
end: usize,
}
impl SlideBuffer {
/// Create a new slide buffer with the specified capacity.
///
/// The buffer is allocated once and reused for all operations.
#[inline]
pub fn new(capacity: usize) -> Self {
Self {
data: vec![0u8; capacity].into_boxed_slice(),
start: 0,
end: 0,
}
}
/// Returns the number of bytes currently stored in the buffer.
#[inline]
pub fn len(&self) -> usize {
self.end - self.start
}
/// Returns the remaining space available for writing.
///
/// This is the space at the end of the buffer. To reclaim space
/// consumed from the front, call `compact()`.
#[inline]
pub fn remaining_capacity(&self) -> usize {
self.data.len() - self.end
}
/// Get a slice of the readable data.
#[inline]
pub fn as_slice(&self) -> &[u8] {
&self.data[self.start..self.end]
}
/// Get a mutable slice for writing new data at the end.
///
/// Returns the writable portion at the end of the buffer.
/// After writing, call `advance_write(n)` to mark bytes as written.
///
/// # Example
/// ```ignore
/// let mut buf = SlideBuffer::new(1024);
/// let write_buf = buf.write_slice();
/// write_buf[..5].copy_from_slice(b"hello");
/// buf.advance_write(5);
/// ```
#[inline]
pub fn write_slice(&mut self) -> &mut [u8] {
&mut self.data[self.end..]
}
/// Extend the buffer with data from a slice.
///
/// # Panics
/// Panics in debug mode if there isn't enough capacity.
#[inline]
pub fn extend_from_slice(&mut self, data: &[u8]) {
debug_assert!(
self.remaining_capacity() >= data.len(),
"SlideBuffer overflow: need {} bytes, have {}",
data.len(),
self.remaining_capacity()
);
let end = self.end;
self.data[end..end + data.len()].copy_from_slice(data);
self.end += data.len();
}
/// Mark n bytes as written (after writing to `write_slice()`).
#[inline]
pub fn advance_write(&mut self, n: usize) {
debug_assert!(
self.end + n <= self.data.len(),
"SlideBuffer advance_write overflow: end={}, n={}, capacity={}",
self.end,
n,
self.data.len()
);
self.end += n;
}
/// Consume n bytes from the front of the buffer.
///
/// # Panics
/// Panics in debug mode if n exceeds the available data.
#[inline]
pub fn consume(&mut self, n: usize) {
debug_assert!(
n <= self.len(),
"SlideBuffer consume underflow: n={}, len={}",
n,
self.len()
);
self.start += n;
// Reset offsets if buffer is now empty
if self.start >= self.end {
self.start = 0;
self.end = 0;
}
}
/// Compact the buffer by moving data to the front.
///
/// This reclaims the space that was consumed from the front,
/// making it available for writing at the end.
/// Uses `copy_within()` which is optimized by the compiler.
#[inline]
pub fn compact(&mut self) {
if self.start > 0 && self.start < self.end {
self.data.copy_within(self.start..self.end, 0);
self.end -= self.start;
self.start = 0;
} else if self.start >= self.end {
self.start = 0;
self.end = 0;
}
}
/// Compact only if we've consumed more than the threshold.
///
/// This amortizes the cost of compaction over many operations,
/// avoiding unnecessary copies when little space would be reclaimed.
#[inline]
pub fn maybe_compact(&mut self, threshold: usize) {
if self.start > threshold {
self.compact();
}
}
/// Returns a two-byte value at the given offset as big-endian u16.
#[inline]
pub fn get_u16_be(&self, offset: usize) -> Option<u16> {
if offset + 2 <= self.len() {
let idx = self.start + offset;
Some(u16::from_be_bytes([self.data[idx], self.data[idx + 1]]))
} else {
None
}
}
}
impl Read for SlideBuffer {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let available = self.len();
if available == 0 {
return Ok(0);
}
let to_read = buf.len().min(available);
buf[..to_read].copy_from_slice(&self.data[self.start..self.start + to_read]);
self.consume(to_read);
Ok(to_read)
}
}
impl BufRead for SlideBuffer {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
Ok(self.as_slice())
}
fn consume(&mut self, amt: usize) {
SlideBuffer::consume(self, amt);
}
}
// Implement indexing for convenient access
impl std::ops::Index<usize> for SlideBuffer {
type Output = u8;
#[inline]
fn index(&self, index: usize) -> &Self::Output {
&self.data[self.start + index]
}
}
impl std::ops::Index<std::ops::Range<usize>> for SlideBuffer {
type Output = [u8];
#[inline]
fn index(&self, range: std::ops::Range<usize>) -> &Self::Output {
&self.data[self.start + range.start..self.start + range.end]
}
}
impl std::ops::Index<std::ops::RangeFrom<usize>> for SlideBuffer {
type Output = [u8];
#[inline]
fn index(&self, range: std::ops::RangeFrom<usize>) -> &Self::Output {
&self.data[self.start + range.start..self.end]
}
}
impl std::ops::Index<std::ops::RangeTo<usize>> for SlideBuffer {
type Output = [u8];
#[inline]
fn index(&self, range: std::ops::RangeTo<usize>) -> &Self::Output {
&self.data[self.start..self.start + range.end]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_buffer() {
let buf = SlideBuffer::new(1024);
assert_eq!(buf.len(), 0);
assert_eq!(buf.remaining_capacity(), 1024);
}
#[test]
fn test_extend_from_slice() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"hello");
assert_eq!(buf.len(), 5);
assert_eq!(buf.as_slice(), b"hello");
assert_eq!(buf.remaining_capacity(), 1024 - 5);
}
#[test]
fn test_consume() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"hello world");
buf.consume(6);
assert_eq!(buf.as_slice(), b"world");
assert_eq!(buf.len(), 5);
}
#[test]
fn test_consume_all_resets() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"hello");
buf.consume(5);
assert_eq!(buf.len(), 0);
assert_eq!(buf.remaining_capacity(), 1024);
}
#[test]
fn test_compact() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"hello world");
buf.consume(6);
assert_eq!(buf.remaining_capacity(), 1024 - 11);
buf.compact();
assert_eq!(buf.as_slice(), b"world");
assert_eq!(buf.remaining_capacity(), 1024 - 5);
}
#[test]
fn test_maybe_compact() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"0123456789"); // 10 bytes
buf.consume(5);
// Threshold not met
buf.maybe_compact(10);
assert_eq!(buf.remaining_capacity(), 1024 - 10);
// Threshold met
buf.maybe_compact(4);
assert_eq!(buf.remaining_capacity(), 1024 - 5);
}
#[test]
fn test_write_slice() {
let mut buf = SlideBuffer::new(1024);
let write_buf = buf.write_slice();
write_buf[..5].copy_from_slice(b"hello");
buf.advance_write(5);
assert_eq!(buf.as_slice(), b"hello");
assert_eq!(buf.len(), 5);
}
#[test]
fn test_read_trait() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"hello world");
let mut output = [0u8; 5];
let n = buf.read(&mut output).unwrap();
assert_eq!(n, 5);
assert_eq!(&output, b"hello");
let n = buf.read(&mut output).unwrap();
assert_eq!(n, 5);
assert_eq!(&output, b" worl");
let n = buf.read(&mut output).unwrap();
assert_eq!(n, 1);
assert_eq!(&output[..1], b"d");
}
#[test]
fn test_bufread_trait() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"hello world");
{
let slice = buf.fill_buf().unwrap();
assert_eq!(slice, b"hello world");
}
buf.consume(6);
{
let slice = buf.fill_buf().unwrap();
assert_eq!(slice, b"world");
}
}
#[test]
fn test_indexing() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"hello world");
assert_eq!(buf[0], b'h');
assert_eq!(buf[6], b'w');
assert_eq!(&buf[0..5], b"hello");
assert_eq!(&buf[6..], b"world");
assert_eq!(&buf[..5], b"hello");
}
#[test]
fn test_indexing_after_consume() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(b"hello world");
buf.consume(6);
assert_eq!(buf[0], b'w');
assert_eq!(&buf[0..5], b"world");
}
#[test]
fn test_get_u16_be() {
let mut buf = SlideBuffer::new(1024);
buf.extend_from_slice(&[0x12, 0x34, 0x56, 0x78]);
assert_eq!(buf.get_u16_be(0), Some(0x1234));
assert_eq!(buf.get_u16_be(2), Some(0x5678));
assert_eq!(buf.get_u16_be(3), None);
}
#[test]
fn test_multiple_extend_consume_cycles() {
let mut buf = SlideBuffer::new(100);
for i in 0..10 {
buf.extend_from_slice(b"0123456789");
assert_eq!(buf.len(), 10);
buf.consume(10);
assert_eq!(buf.len(), 0);
assert_eq!(buf.remaining_capacity(), 100, "iteration {} failed", i);
}
}
#[test]
fn test_partial_consume_and_extend() {
let mut buf = SlideBuffer::new(100);
buf.extend_from_slice(b"hello world"); // 11 bytes
buf.consume(6); // consume "hello "
buf.extend_from_slice(b"!!!"); // add "!!!"
assert_eq!(buf.as_slice(), b"world!!!");
buf.compact();
assert_eq!(buf.as_slice(), b"world!!!");
assert_eq!(buf.remaining_capacity(), 100 - 8);
}
}
+2 -2
View File
@@ -71,7 +71,7 @@ impl SnellServerHandler {
udp_enabled: bool,
udp_num_sockets: usize,
) -> Self {
let cipher: ShadowsocksCipher = cipher_name.into();
let cipher: ShadowsocksCipher = cipher_name.try_into().unwrap();
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(SnellKey::new(
password,
cipher.algorithm().key_len(),
@@ -206,7 +206,7 @@ pub struct SnellClientHandler {
impl SnellClientHandler {
pub fn new(cipher_name: &str, password: &str) -> Self {
let cipher: ShadowsocksCipher = cipher_name.into();
let cipher: ShadowsocksCipher = cipher_name.try_into().unwrap();
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(SnellKey::new(
password,
cipher.algorithm().key_len(),
+79
View File
@@ -11,6 +11,7 @@ use crate::stream_reader::StreamReader;
use crate::tcp_handler::{
TcpClientHandler, TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult,
};
use crate::uot::{UotV1Stream, UotV2Stream, UOT_V1_MAGIC_ADDRESS, UOT_V2_MAGIC_ADDRESS};
use crate::util::write_all;
pub const VER_SOCKS5: u8 = 0x05;
@@ -199,6 +200,84 @@ impl TcpServerHandler for SocksTcpServerHandler {
let location = read_location(&mut server_stream, &mut stream_reader).await?;
// Check for UDP-over-TCP (UoT) magic addresses
if let Address::Hostname(ref host) = location.address() {
if host == UOT_V1_MAGIC_ADDRESS {
// UoT V1: Multi-destination UDP
// Each packet has: ATYP + address + port + length + data
// Send success response first
write_all(&mut server_stream, connection_success_response).await?;
server_stream.flush().await?;
let mut uot_stream = UotV1Stream::new(Box::new(server_stream));
// Feed any unparsed data (first UoT packet might be in same TCP segment)
let unparsed_data = stream_reader.unparsed_data();
if !unparsed_data.is_empty() {
log::debug!(
"SOCKS UoT V1: feeding {} bytes of initial data",
unparsed_data.len()
);
uot_stream.feed_initial_data(unparsed_data);
}
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
stream: Box::new(uot_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
num_sockets: 4,
});
} else if host == UOT_V2_MAGIC_ADDRESS {
// UoT V2: Read request header first
// Request: isConnect(u8) + ATYP + address + port
// Send success response first
write_all(&mut server_stream, connection_success_response).await?;
server_stream.flush().await?;
let is_connect = stream_reader.read_u8(&mut server_stream).await?;
log::debug!("SOCKS UoT V2: is_connect = {}", is_connect);
// Read destination address using SOCKS address format
let destination = read_location(&mut server_stream, &mut stream_reader).await?;
log::debug!("SOCKS UoT V2: destination = {:?}", destination);
if is_connect == 1 {
// V2 Connect mode: Single destination, length-prefixed packets only
let unparsed_data = stream_reader.unparsed_data();
let mut uot_v2_stream = UotV2Stream::new(Box::new(server_stream));
if !unparsed_data.is_empty() {
uot_v2_stream.feed_initial_read_data(unparsed_data)?;
}
return Ok(TcpServerSetupResult::BidirectionalUdp {
remote_location: destination,
stream: Box::new(uot_v2_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
});
} else {
// V2 Non-connect mode: Same as V1 (multi-destination)
let mut uot_stream = UotV1Stream::new(Box::new(server_stream));
let unparsed_data = stream_reader.unparsed_data();
if !unparsed_data.is_empty() {
log::debug!(
"SOCKS UoT V2 non-connect: feeding {} bytes of initial data",
unparsed_data.len()
);
uot_stream.feed_initial_data(unparsed_data);
}
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
stream: Box::new(uot_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
num_sockets: 4,
});
}
}
}
Ok(TcpServerSetupResult::TcpForward {
remote_location: location,
stream: server_stream,
+56
View File
@@ -0,0 +1,56 @@
/// Sync adapters for bridging async I/O with rustls's synchronous API
///
/// These adapters allow rustls's synchronous `read_tls()` and `write_tls()` methods
/// to work with Tokio's async I/O primitives.
///
/// Adapted from tokio-rustls:
/// https://github.com/rustls/tokio-rustls/blob/ba767aeb51611107e7cb6aa756f10a2f49e70926/src/common/mod.rs#L403
use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
/// Adapter to bridge async read to synchronous read for rustls
///
/// This allows rustls's `read_tls()` method to read from async TCP sockets.
/// When the async socket would block (Poll::Pending), this returns WouldBlock error.
pub struct SyncReadAdapter<'a, 'b, T> {
pub io: &'a mut T,
pub cx: &'a mut Context<'b>,
}
impl<T: AsyncRead + Unpin> std::io::Read for SyncReadAdapter<'_, '_, T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut read_buf = ReadBuf::new(buf);
match Pin::new(&mut self.io).poll_read(self.cx, &mut read_buf) {
Poll::Ready(Ok(())) => Ok(read_buf.filled().len()),
Poll::Ready(Err(e)) => Err(e),
Poll::Pending => Err(io::ErrorKind::WouldBlock.into()),
}
}
}
/// Adapter to bridge async write to synchronous write for rustls
///
/// This allows rustls's `write_tls()` method to write to async TCP sockets.
/// When the async socket would block (Poll::Pending), this returns WouldBlock error.
pub struct SyncWriteAdapter<'a, 'b, T> {
pub io: &'a mut T,
pub cx: &'a mut Context<'b>,
}
impl<T: AsyncWrite + Unpin> Write for SyncWriteAdapter<'_, '_, T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match Pin::new(&mut self.io).poll_write(self.cx, buf) {
Poll::Ready(result) => result,
Poll::Pending => Err(io::ErrorKind::WouldBlock.into()),
}
}
fn flush(&mut self) -> io::Result<()> {
match Pin::new(&mut self.io).poll_flush(self.cx) {
Poll::Ready(result) => result,
Poll::Pending => Err(io::ErrorKind::WouldBlock.into()),
}
}
}
+5 -1
View File
@@ -8,7 +8,7 @@ use crate::async_stream::AsyncStream;
use crate::config::{ClientConfig, ClientQuicConfig, TcpConfig, Transport};
use crate::quic_stream::QuicStream;
use crate::resolver::{resolve_single_address, Resolver};
use crate::rustls_util::create_client_config;
use crate::rustls_config_util::create_client_config;
use crate::socket_util::{
new_reuse_udp_sockets, new_tcp_socket, new_udp_socket, set_tcp_keepalive,
};
@@ -177,6 +177,10 @@ impl TcpClientConnector {
})
}
pub fn bind_interface(&self) -> &Option<String> {
&self.bind_interface
}
pub fn configure_udp_socket(&self, is_ipv6: bool) -> std::io::Result<tokio::net::UdpSocket> {
let udp_socket = new_udp_socket(is_ipv6, self.bind_interface.clone())?;
Ok(udp_socket)
+17
View File
@@ -34,6 +34,11 @@ pub enum TcpServerSetupResult {
override_proxy_provider: NoneOrOne<Arc<ClientProxySelector<TcpClientConnector>>>,
num_sockets: usize,
},
SessionBasedUdp {
need_initial_flush: bool,
stream: Box<dyn crate::async_stream::AsyncSessionMessageStream>,
override_proxy_provider: NoneOrOne<Arc<ClientProxySelector<TcpClientConnector>>>,
},
}
impl TcpServerSetupResult {
@@ -50,6 +55,10 @@ impl TcpServerSetupResult {
| TcpServerSetupResult::MultiDirectionalUdp {
need_initial_flush: ref mut flush,
..
}
| TcpServerSetupResult::SessionBasedUdp {
need_initial_flush: ref mut flush,
..
} => {
*flush = need_initial_flush;
}
@@ -68,6 +77,10 @@ impl TcpServerSetupResult {
| TcpServerSetupResult::MultiDirectionalUdp {
override_proxy_provider,
..
}
| TcpServerSetupResult::SessionBasedUdp {
override_proxy_provider,
..
} => override_proxy_provider.is_unspecified(),
}
}
@@ -88,6 +101,10 @@ impl TcpServerSetupResult {
| TcpServerSetupResult::MultiDirectionalUdp {
override_proxy_provider: ref mut provider,
..
}
| TcpServerSetupResult::SessionBasedUdp {
override_proxy_provider: ref mut provider,
..
} => {
*provider = override_proxy_provider;
}
+242 -36
View File
@@ -6,14 +6,15 @@ use rustc_hash::FxHashMap;
use crate::client_proxy_selector::{ClientProxySelector, ConnectAction, ConnectRule};
use crate::config::{
ClientConfig, ClientProxyConfig, ConfigSelection, RuleActionConfig, RuleConfig,
ServerProxyConfig, ShadowTlsServerConfig, ShadowTlsServerHandshakeConfig, ShadowsocksConfig,
TlsClientConfig, TlsServerConfig, WebsocketClientConfig, WebsocketServerConfig,
ClientConfig, ClientProxyConfig, ConfigSelection, RealityServerConfig, RuleActionConfig,
RuleConfig, ServerProxyConfig, ShadowTlsServerConfig, ShadowTlsServerHandshakeConfig,
ShadowsocksConfig, TlsClientConfig, TlsServerConfig, WebsocketClientConfig,
WebsocketServerConfig,
};
use crate::http_handler::{HttpTcpClientHandler, HttpTcpServerHandler};
use crate::option_util::NoneOrOne;
use crate::port_forward_handler::PortForwardServerHandler;
use crate::rustls_util::{create_client_config, create_server_config};
use crate::port_forward_handler::{PortForwardClientHandler, PortForwardServerHandler};
use crate::rustls_config_util::{create_client_config, create_server_config};
use crate::shadow_tls::{
ShadowTlsClientHandler, ShadowTlsServerTarget, ShadowTlsServerTargetHandshake,
};
@@ -22,9 +23,13 @@ use crate::snell::snell_handler::{SnellClientHandler, SnellServerHandler};
use crate::socks_handler::{SocksTcpClientHandler, SocksTcpServerHandler};
use crate::tcp_client_connector::TcpClientConnector;
use crate::tcp_handler::{TcpClientHandler, TcpServerHandler};
use crate::tls_handler::{TlsClientHandler, TlsServerHandler, TlsServerTarget};
use crate::tls_client_handler::TlsClientHandler;
use crate::tls_server_handler::{
RealityServerTarget, TlsServerHandler, TlsServerTarget, VisionConfig,
};
use crate::trojan_handler::TrojanTcpHandler;
use crate::vless_handler::{VlessTcpClientHandler, VlessTcpServerHandler};
use crate::vless::vless_client_handler::VlessTcpClientHandler;
use crate::vless::vless_server_handler::VlessTcpServerHandler;
use crate::vmess::{VmessTcpClientHandler, VmessTcpServerHandler};
use crate::websocket::{
WebsocketServerTarget, WebsocketTcpClientHandler, WebsocketTcpServerHandler,
@@ -84,6 +89,7 @@ pub fn create_tcp_server_handler(
tls_targets,
default_tls_target,
shadowtls_targets,
reality_targets,
tls_buffer_size,
} => {
let mut all_targets = tls_targets
@@ -97,6 +103,11 @@ pub fn create_tcp_server_handler(
.map(|(sni, config)| (sni, create_shadow_tls_server_target(config, rules_stack)))
.collect::<FxHashMap<String, TlsServerTarget>>();
all_targets.extend(shadowtls_targets);
let reality_server_targets = reality_targets
.into_iter()
.map(|(sni, config)| (sni, create_reality_server_target(config, rules_stack)))
.collect::<FxHashMap<String, TlsServerTarget>>();
all_targets.extend(reality_server_targets);
Box::new(TlsServerHandler::new(
all_targets,
default_tls_target,
@@ -106,14 +117,8 @@ pub fn create_tcp_server_handler(
ServerProxyConfig::Vmess {
cipher,
user_id,
force_aead,
udp_enabled,
} => Box::new(VmessTcpServerHandler::new(
&cipher,
&user_id,
force_aead,
udp_enabled,
)),
} => Box::new(VmessTcpServerHandler::new(&cipher, &user_id, udp_enabled)),
ServerProxyConfig::Websocket { targets } => {
let server_targets: Vec<WebsocketServerTarget> = targets
.into_vec()
@@ -142,6 +147,7 @@ fn create_tls_server_target(
alpn_protocols,
client_ca_certs,
client_fingerprints,
vision,
protocol,
override_rules,
} = tls_server_config;
@@ -173,6 +179,28 @@ fn create_tls_server_target(
);
}
// Create vision_config if vision is enabled
let vision_config = if vision {
// Vision requires VLESS protocol (validated in config/mod.rs)
if let ServerProxyConfig::Vless {
user_id,
udp_enabled,
} = &protocol
{
let user_id_bytes = crate::util::parse_uuid(user_id)
.expect("Invalid user_id UUID")
.into_boxed_slice();
Some(VisionConfig {
user_id: user_id_bytes,
udp_enabled: *udp_enabled,
})
} else {
unreachable!("Vision requires VLESS (should be validated during config load)")
}
} else {
None
};
let handler = create_tcp_server_handler(protocol, rules_stack);
let override_proxy_provider = if override_rules.is_empty() {
@@ -190,6 +218,7 @@ fn create_tls_server_target(
server_config,
handler,
override_proxy_provider,
vision_config,
}
}
@@ -272,6 +301,90 @@ fn create_shadow_tls_server_target(
))
}
fn create_reality_server_target(
reality_server_config: RealityServerConfig,
rules_stack: &mut Vec<Vec<RuleConfig>>,
) -> TlsServerTarget {
let RealityServerConfig {
private_key,
short_ids,
dest,
max_time_diff,
min_client_version,
max_client_version,
vision,
protocol,
override_rules,
} = reality_server_config;
// Decode private key from base64url (validated during config load)
let private_key_bytes = crate::reality::decode_private_key(&private_key)
.expect("Invalid REALITY private key (should be validated during config load)");
// Decode short IDs from hex strings (validated during config load)
// OneOrSome ensures at least one short_id is always present (default is all zeros)
let short_id_bytes: Vec<[u8; 8]> = short_ids
.into_vec()
.into_iter()
.map(|s| {
crate::reality::decode_short_id(&s)
.expect("Invalid REALITY short_id (should be validated during config load)")
})
.collect();
// Create vision_config if vision is enabled
let vision_config = if vision {
// Vision requires VLESS protocol (validated in config/mod.rs)
if let ServerProxyConfig::Vless {
user_id,
udp_enabled,
} = &protocol
{
let user_id_bytes = crate::util::parse_uuid(user_id)
.expect("Invalid user_id UUID")
.into_boxed_slice();
Some(VisionConfig {
user_id: user_id_bytes,
udp_enabled: *udp_enabled,
})
} else {
unreachable!("Vision requires VLESS (should be validated during config load)")
}
} else {
None
};
// Create inner handler
let handler = create_tcp_server_handler(protocol, rules_stack);
let override_proxy_provider = if override_rules.is_empty() {
NoneOrOne::None
} else {
rules_stack.push(
override_rules
.clone()
.map(ConfigSelection::unwrap_config)
.into_vec(),
);
let rules = rules_stack.last().unwrap().clone();
let provider = NoneOrOne::One(Arc::new(create_tcp_client_proxy_selector(rules)));
rules_stack.pop().unwrap();
provider
};
TlsServerTarget::Reality(RealityServerTarget {
private_key: private_key_bytes,
short_ids: short_id_bytes,
dest,
max_time_diff,
min_client_version,
max_client_version,
handler,
override_proxy_provider,
vision_config,
})
}
fn create_websocket_server_target(
websocket_server_config: WebsocketServerConfig,
rules_stack: &mut Vec<Vec<RuleConfig>>,
@@ -352,7 +465,10 @@ pub fn create_tcp_client_handler(
ClientProxyConfig::Snell(ShadowsocksConfig { cipher, password }) => {
Box::new(SnellClientHandler::new(&cipher, &password))
}
ClientProxyConfig::Vless { user_id } => Box::new(VlessTcpClientHandler::new(&user_id)),
ClientProxyConfig::Vless { user_id } => {
// Plain VLESS without TLS
Box::new(VlessTcpClientHandler::new(&user_id))
}
ClientProxyConfig::Trojan {
password,
shadowsocks,
@@ -367,7 +483,7 @@ pub fn create_tcp_client_handler(
protocol,
key,
cert,
shadowtls_password,
vision,
} = tls_client_config;
let sni_hostname = if sni_hostname.is_unspecified() {
@@ -405,33 +521,122 @@ pub fn create_tcp_client_handler(
None => "example.com".try_into().unwrap(),
};
let handler = create_tcp_client_handler(*protocol, None);
if vision {
let ClientProxyConfig::Vless { user_id } = protocol.as_ref() else {
// Validated when loading config
unreachable!();
};
let user_id_bytes = crate::util::parse_uuid(user_id)
.expect("Invalid user_id UUID")
.into_boxed_slice();
Box::new(TlsClientHandler::new_vision_vless(
client_config,
tls_buffer_size,
server_name,
user_id_bytes,
))
} else {
let handler = create_tcp_client_handler(*protocol, None);
match shadowtls_password {
None => Box::new(TlsClientHandler::new(
Box::new(TlsClientHandler::new(
client_config,
tls_buffer_size,
server_name,
handler,
)),
Some(password) => {
// TODO: Ensure client_config is suitable for TLS 1.3.
// Rustls ClientConfig by default supports TLS 1.3 if server does.
// The server handler enforces TLS 1.3 from client and for negotiation.
Box::new(ShadowTlsClientHandler::new(
password,
client_config,
server_name,
handler,
))
}
))
}
}
ClientProxyConfig::Vmess {
cipher,
user_id,
aead,
} => Box::new(VmessTcpClientHandler::new(&cipher, &user_id, aead)),
ClientProxyConfig::Reality {
public_key,
short_id,
sni_hostname,
vision,
protocol,
} => {
eprintln!("========== CREATING REALITY CLIENT HANDLER ==========");
// Decode public key from base64url
let public_key_bytes =
crate::reality::decode_public_key(&public_key).expect("Invalid REALITY public key");
// Decode short ID from hex string
let short_id_bytes =
crate::reality::decode_short_id(&short_id).expect("Invalid REALITY short_id");
// Determine SNI hostname
let sni_hostname = sni_hostname.or(default_sni_hostname.clone());
let server_name = match sni_hostname {
Some(s) => rustls::pki_types::ServerName::try_from(s)
.unwrap()
.to_owned(),
None => {
panic!("REALITY client requires sni_hostname to be specified");
}
};
if vision {
let ClientProxyConfig::Vless { user_id } = protocol.as_ref() else {
unreachable!("Vision requires VLESS (should be validated during config load)")
};
let user_id_bytes = crate::util::parse_uuid(user_id)
.expect("Invalid user_id UUID")
.into_boxed_slice();
Box::new(
crate::reality_client_handler::RealityClientHandler::new_vision_vless(
public_key_bytes,
short_id_bytes,
server_name,
user_id_bytes,
),
)
} else {
let inner_handler = create_tcp_client_handler(*protocol, None);
Box::new(crate::reality_client_handler::RealityClientHandler::new(
public_key_bytes,
short_id_bytes,
server_name,
inner_handler,
))
}
}
ClientProxyConfig::ShadowTls {
password,
sni_hostname,
protocol,
} => {
// ShadowTLS client handler
let sni_hostname = sni_hostname.or(default_sni_hostname);
let enable_sni = sni_hostname.is_some();
let server_name = match sni_hostname {
Some(s) => rustls::pki_types::ServerName::try_from(s).unwrap(),
None => "example.com".try_into().unwrap(), // Fallback
};
// Create TLS config for ShadowTLS
// TODO: Ensure client_config is suitable for TLS 1.3.
// Rustls ClientConfig by default supports TLS 1.3 if server does.
// The server handler enforces TLS 1.3 from client and for negotiation.
let client_config = Arc::new(create_client_config(
false, // No WebPKI verification needed for ShadowTLS
Vec::new(), // No fingerprints
Vec::new(), // No ALPN
enable_sni, // Enable SNI if hostname provided
None, // No client cert
));
let handler = create_tcp_client_handler(*protocol, None);
Box::new(ShadowTlsClientHandler::new(
password,
client_config,
server_name,
handler,
))
}
ClientProxyConfig::Vmess { cipher, user_id } => {
Box::new(VmessTcpClientHandler::new(&cipher, &user_id))
}
ClientProxyConfig::Websocket(websocket_client_config) => {
let WebsocketClientConfig {
matching_path,
@@ -449,6 +654,7 @@ pub fn create_tcp_client_handler(
handler,
))
}
ClientProxyConfig::PortForward => Box::new(PortForwardClientHandler),
}
}
+52
View File
@@ -15,6 +15,7 @@ use crate::config::{BindLocation, ConfigSelection, ServerConfig, TcpConfig};
use crate::copy_bidirectional::copy_bidirectional;
use crate::copy_bidirectional_message::copy_bidirectional_message;
use crate::copy_multidirectional_message::copy_multidirectional_message;
use crate::copy_session_messages::copy_session_messages;
use crate::resolver::{resolve_single_address, NativeResolver, Resolver};
use crate::socket_util::{new_tcp_listener, set_tcp_keepalive};
use crate::tcp_client_connector::TcpClientConnector;
@@ -22,6 +23,7 @@ use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
use crate::tcp_handler_util::{create_tcp_client_proxy_selector, create_tcp_server_handler};
use crate::udp_message_stream::UdpMessageStream;
use crate::udp_multi_message_stream::UdpMultiMessageStream;
use crate::udp_session_message_stream::{UdpSessionMessageStream, UdpSocketConfig};
use crate::util::write_all;
async fn run_tcp_server(
@@ -332,6 +334,56 @@ where
}
}
}
TcpServerSetupResult::SessionBasedUdp {
stream: mut server_stream,
need_initial_flush: server_need_initial_flush,
override_proxy_provider,
} => {
let selected_proxy_provider = if override_proxy_provider.is_one() {
override_proxy_provider.unwrap()
} else {
client_proxy_selector
};
let action = selected_proxy_provider.default_decision();
match action {
ConnectDecision::Allow {
client_proxy,
remote_location: _,
} => {
// Create UdpSessionMessageStream with user's interface configuration
let config = UdpSocketConfig {
bind_interface: client_proxy.bind_interface().clone(),
};
let mut session_manager = UdpSessionMessageStream::new(config);
// IMPORTANT: We do NOT create any sessions upfront
// Sessions will be created on-demand as XUDP frames arrive
// This is handled by the copy_session_messages function
// which will call poll_write_session_message, which in turn
// will create sessions via SessionUdpManager::create_session
let copy_result = copy_session_messages(
&mut server_stream,
&mut session_manager,
server_need_initial_flush,
false,
)
.await;
// TODO: add async trait ext and make this work
//let (_, _) = futures::join!(server_stream.shutdown_message(), client_stream.shutdown_message());
copy_result?;
Ok(())
}
ConnectDecision::Block => {
warn!("Blocked session-based udp forward, because the default action is to block.");
// TODO: add async trait ext and make this work
// let _ = server_stream.shutdown_message().await;
Ok(())
}
}
}
}
}
+112
View File
@@ -0,0 +1,112 @@
use std::sync::Arc;
use async_trait::async_trait;
use crate::address::NetLocation;
use crate::async_stream::AsyncStream;
use crate::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
#[derive(Debug)]
pub struct TlsClientHandler {
pub client_config: Arc<rustls::ClientConfig>,
pub tls_buffer_size: Option<usize>,
pub server_name: rustls::pki_types::ServerName<'static>,
pub handler: TlsInnerClientHandler,
}
#[derive(Debug)]
pub enum TlsInnerClientHandler {
Default(Box<dyn TcpClientHandler>),
VisionVless { uuid: Box<[u8]> },
}
impl TlsClientHandler {
pub fn new(
client_config: Arc<rustls::ClientConfig>,
tls_buffer_size: Option<usize>,
server_name: rustls::pki_types::ServerName<'static>,
handler: Box<dyn TcpClientHandler>,
) -> Self {
Self {
client_config,
tls_buffer_size,
server_name,
handler: TlsInnerClientHandler::Default(handler),
}
}
pub fn new_vision_vless(
client_config: Arc<rustls::ClientConfig>,
tls_buffer_size: Option<usize>,
server_name: rustls::pki_types::ServerName<'static>,
uuid: Box<[u8]>,
) -> Self {
Self {
client_config,
tls_buffer_size,
server_name,
handler: TlsInnerClientHandler::VisionVless { uuid },
}
}
}
#[async_trait]
impl TcpClientHandler for TlsClientHandler {
async fn setup_client_stream(
&self,
server_stream: &mut Box<dyn AsyncStream>,
mut client_stream: Box<dyn AsyncStream>,
remote_location: NetLocation,
) -> std::io::Result<TcpClientSetupResult> {
use crate::crypto::{CryptoConnection, CryptoTlsStream};
use crate::rustls_handshake::perform_handshake;
// Create rustls ClientConnection
let client_conn =
rustls::ClientConnection::new(self.client_config.clone(), self.server_name.clone())
.map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Failed to create client connection: {e}"),
)
})?;
// Wrap in rustls::Connection enum for handshake
let mut rustls_connection = rustls::Connection::Client(client_conn);
// Set buffer limits if configured
if let Some(size) = self.tls_buffer_size {
rustls_connection.set_buffer_limit(Some(size));
}
// Perform the TLS handshake using the generic helper
// This works for both TLS 1.2 and TLS 1.3
perform_handshake(&mut rustls_connection, &mut client_stream, 16384).await?;
// Extract the ClientConnection back from the enum
let client_conn = match rustls_connection {
rustls::Connection::Client(conn) => conn,
_ => unreachable!("We created a Client variant"),
};
// Wrap in CryptoTlsStream
let connection = CryptoConnection::new_rustls_client(client_conn);
let tls_stream = CryptoTlsStream::new(client_stream, connection);
match self.handler {
TlsInnerClientHandler::Default(ref handler) => {
handler
.setup_client_stream(server_stream, Box::new(tls_stream), remote_location)
.await
}
TlsInnerClientHandler::VisionVless { ref uuid } => {
crate::vless::vless_client_handler::setup_custom_tls_vision_vless_client_stream(
tls_stream,
uuid,
&remote_location,
)
.await
}
}
}
}
-210
View File
@@ -1,210 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use rustc_hash::FxHashMap;
use tokio_rustls::TlsAcceptor;
use crate::address::NetLocation;
use crate::async_stream::AsyncStream;
use crate::client_proxy_selector::ClientProxySelector;
use crate::option_util::NoneOrOne;
use crate::resolver::{NativeResolver, Resolver};
use crate::shadow_tls::{
feed_server_connection, read_client_hello, setup_shadowtls_server_stream, ParsedClientHello,
ShadowTlsServerTarget,
};
use crate::tcp_client_connector::TcpClientConnector;
use crate::tcp_handler::{
TcpClientHandler, TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult,
};
#[derive(Debug)]
pub struct TlsServerHandler {
sni_targets: FxHashMap<String, TlsServerTarget>,
default_target: Option<TlsServerTarget>,
// used to resolve handshake server hostnames
shadowtls_resolver: Arc<dyn Resolver>,
tls_buffer_size: Option<usize>,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum TlsServerTarget {
Tls {
server_config: Arc<rustls::ServerConfig>,
handler: Box<dyn TcpServerHandler>,
override_proxy_provider: NoneOrOne<Arc<ClientProxySelector<TcpClientConnector>>>,
},
ShadowTls(ShadowTlsServerTarget),
}
impl TlsServerHandler {
pub fn new(
sni_targets: FxHashMap<String, TlsServerTarget>,
default_target: Option<TlsServerTarget>,
tls_buffer_size: Option<usize>,
) -> Self {
Self {
sni_targets,
default_target,
shadowtls_resolver: Arc::new(NativeResolver::new()),
tls_buffer_size,
}
}
}
#[async_trait]
impl TcpServerHandler for TlsServerHandler {
async fn setup_server_stream(
&self,
mut server_stream: Box<dyn AsyncStream>,
) -> std::io::Result<TcpServerSetupResult> {
let parsed_client_hello = read_client_hello(&mut server_stream).await?;
let target = match parsed_client_hello.requested_server_name.as_ref() {
None => match self.default_target {
Some(ref t) => t,
None => {
return Err(std::io::Error::other(
"No default target for unspecified SNI",
));
}
},
Some(hostname) => match self.sni_targets.get(hostname) {
Some(t) => t,
None => match self.default_target {
Some(ref t) => t,
None => {
return Err(std::io::Error::other(format!(
"No default target for unknown SNI: {hostname}"
)));
}
},
},
};
match target {
TlsServerTarget::Tls {
ref server_config,
ref handler,
ref override_proxy_provider,
} => {
let ParsedClientHello {
client_hello_frame,
client_reader,
..
} = parsed_client_hello;
let tls_acceptor = TlsAcceptor::from(server_config.clone());
let accept_future = {
let mut accept_error: Option<std::io::Error> = None;
let accept_future = tls_acceptor.accept_with(server_stream, |server_conn| {
if let Some(size) = self.tls_buffer_size {
server_conn.set_buffer_limit(Some(size));
}
if let Err(e) = feed_server_connection(server_conn, &client_hello_frame) {
let _ = accept_error.insert(std::io::Error::other(format!(
"Failed to feed initial frame to server connection: {e}"
)));
return;
}
let unparsed_data = client_reader.unparsed_data();
if !unparsed_data.is_empty() {
if let Err(e) = feed_server_connection(server_conn, unparsed_data) {
let _ = accept_error.insert(std::io::Error::other(format!(
"Failed to feed unparsed data to server connection: {e}"
)));
return;
}
}
if let Err(e) = server_conn.process_new_packets() {
let _ = accept_error.insert(std::io::Error::other(format!(
"Failed to process new packets: {e}"
)));
}
});
if let Some(e) = accept_error {
return Err(e);
}
accept_future
};
let tls_stream: Box<dyn AsyncStream> = Box::new(accept_future.await?);
let mut target_setup_result = handler.setup_server_stream(tls_stream).await;
if let Ok(ref mut setup_result) = target_setup_result {
setup_result.set_need_initial_flush(true);
if setup_result.override_proxy_provider_unspecified()
&& !override_proxy_provider.is_unspecified()
{
setup_result.set_override_proxy_provider(override_proxy_provider.clone());
}
}
target_setup_result
}
TlsServerTarget::ShadowTls(ref target) => {
setup_shadowtls_server_stream(
server_stream,
target,
parsed_client_hello,
&self.shadowtls_resolver,
)
.await
}
}
}
}
#[derive(Debug)]
pub struct TlsClientHandler {
pub client_config: Arc<rustls::ClientConfig>,
pub tls_buffer_size: Option<usize>,
pub server_name: rustls::pki_types::ServerName<'static>,
pub handler: Box<dyn TcpClientHandler>,
}
impl TlsClientHandler {
pub fn new(
client_config: Arc<rustls::ClientConfig>,
tls_buffer_size: Option<usize>,
server_name: rustls::pki_types::ServerName<'static>,
handler: Box<dyn TcpClientHandler>,
) -> Self {
Self {
client_config,
tls_buffer_size,
server_name,
handler,
}
}
}
#[async_trait]
impl TcpClientHandler for TlsClientHandler {
async fn setup_client_stream(
&self,
server_stream: &mut Box<dyn AsyncStream>,
client_stream: Box<dyn AsyncStream>,
remote_location: NetLocation,
) -> std::io::Result<TcpClientSetupResult> {
let connector: tokio_rustls::TlsConnector = self.client_config.clone().into();
let tls_stream = Box::new(
connector
.connect_with(self.server_name.clone(), client_stream, |client_conn| {
if let Some(size) = self.tls_buffer_size {
client_conn.set_buffer_limit(Some(size));
}
})
.await?,
);
self.handler
.setup_client_stream(server_stream, tls_stream, remote_location)
.await
}
}
+397
View File
@@ -0,0 +1,397 @@
use std::sync::{Arc, LazyLock};
use async_trait::async_trait;
use rustc_hash::FxHashMap;
use crate::address::NetLocation;
use crate::async_stream::AsyncStream;
use crate::client_proxy_selector::ClientProxySelector;
use crate::option_util::NoneOrOne;
use crate::resolver::{NativeResolver, Resolver};
use crate::shadow_tls::{
read_client_hello, setup_shadowtls_server_stream, ParsedClientHello, ShadowTlsServerTarget,
};
use crate::tcp_client_connector::TcpClientConnector;
use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
#[derive(Debug, Clone)]
pub struct VisionConfig {
pub user_id: Box<[u8]>,
pub udp_enabled: bool,
}
#[derive(Debug)]
pub struct RealityServerTarget {
pub private_key: [u8; 32],
pub short_ids: Vec<[u8; 8]>,
pub dest: NetLocation,
pub max_time_diff: Option<u64>, // in milliseconds
pub min_client_version: Option<[u8; 3]>,
pub max_client_version: Option<[u8; 3]>,
pub handler: Box<dyn TcpServerHandler>,
pub override_proxy_provider: NoneOrOne<Arc<ClientProxySelector<TcpClientConnector>>>,
pub vision_config: Option<VisionConfig>,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum TlsServerTarget {
Tls {
server_config: Arc<rustls::ServerConfig>,
handler: Box<dyn TcpServerHandler>,
override_proxy_provider: NoneOrOne<Arc<ClientProxySelector<TcpClientConnector>>>,
vision_config: Option<VisionConfig>,
},
ShadowTls(ShadowTlsServerTarget),
Reality(RealityServerTarget),
}
#[derive(Debug)]
pub struct TlsServerHandler {
sni_targets: FxHashMap<String, TlsServerTarget>,
default_target: Option<TlsServerTarget>,
// used to resolve handshake server hostnames
shadowtls_resolver: Arc<dyn Resolver>,
tls_buffer_size: Option<usize>,
}
impl TlsServerHandler {
pub fn new(
sni_targets: FxHashMap<String, TlsServerTarget>,
default_target: Option<TlsServerTarget>,
tls_buffer_size: Option<usize>,
) -> Self {
Self {
sni_targets,
default_target,
shadowtls_resolver: Arc::new(NativeResolver::new()),
tls_buffer_size,
}
}
}
#[async_trait]
impl TcpServerHandler for TlsServerHandler {
async fn setup_server_stream(
&self,
mut server_stream: Box<dyn AsyncStream>,
) -> std::io::Result<TcpServerSetupResult> {
let parsed_client_hello = read_client_hello(&mut server_stream).await?;
let target = match parsed_client_hello.requested_server_name.as_ref() {
None => match self.default_target {
Some(ref t) => t,
None => {
return Err(std::io::Error::other(
"No default target for unspecified SNI",
));
}
},
Some(hostname) => match self.sni_targets.get(hostname) {
Some(t) => t,
None => match self.default_target {
Some(ref t) => t,
None => {
return Err(std::io::Error::other(format!(
"No default target for unknown SNI: {hostname}"
)));
}
},
},
};
match target {
TlsServerTarget::Tls {
ref server_config,
ref handler,
ref override_proxy_provider,
ref vision_config,
} => {
use crate::crypto::{CryptoConnection, CryptoTlsStream};
use crate::rustls_handshake::perform_handshake;
let ParsedClientHello {
client_hello_frame,
client_reader,
..
} = parsed_client_hello;
// Create rustls ServerConnection
let server_conn =
rustls::ServerConnection::new(server_config.clone()).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Failed to create server connection: {e}"),
)
})?;
// Wrap in rustls::Connection enum for handshake
let mut rustls_connection = rustls::Connection::Server(server_conn);
// Set buffer limits if configured
if let Some(size) = self.tls_buffer_size {
rustls_connection.set_buffer_limit(Some(size));
}
// Feed the ClientHello we already parsed
{
use std::io::Cursor;
let mut cursor = Cursor::new(&client_hello_frame);
rustls_connection.read_tls(&mut cursor)?;
let unparsed_data = client_reader.unparsed_data();
if !unparsed_data.is_empty() {
let mut cursor = Cursor::new(unparsed_data);
rustls_connection.read_tls(&mut cursor)?;
}
rustls_connection.process_new_packets().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to process ClientHello: {e}"),
)
})?;
}
// Perform the TLS handshake using the generic helper
// This works for both TLS 1.2 and TLS 1.3
perform_handshake(&mut rustls_connection, &mut server_stream, 16384).await?;
// Extract the ServerConnection back from the enum
let server_conn = match rustls_connection {
rustls::Connection::Server(conn) => conn,
_ => unreachable!("We created a Server variant"),
};
// Wrap in CryptoTlsStream
let connection = CryptoConnection::new_rustls_server(server_conn);
let tls_stream = CryptoTlsStream::new(server_stream, connection);
let mut target_setup_result = if let Some(vision_cfg) = vision_config {
// Vision is enabled - call setup_custom_tls_vision_vless_server_stream
crate::vless::vless_server_handler::setup_custom_tls_vision_vless_server_stream(
tls_stream,
&vision_cfg.user_id,
vision_cfg.udp_enabled,
)
.await
} else {
// Regular TLS - call inner handler
handler.setup_server_stream(Box::new(tls_stream)).await
};
if let Ok(ref mut setup_result) = target_setup_result {
setup_result.set_need_initial_flush(true);
if setup_result.override_proxy_provider_unspecified()
&& !override_proxy_provider.is_unspecified()
{
setup_result.set_override_proxy_provider(override_proxy_provider.clone());
}
}
target_setup_result
}
TlsServerTarget::ShadowTls(ref target) => {
setup_shadowtls_server_stream(
server_stream,
target,
parsed_client_hello,
&self.shadowtls_resolver,
)
.await
}
TlsServerTarget::Reality(ref target) => {
use crate::crypto::{CryptoConnection, CryptoTlsStream};
use crate::reality::{RealityServerConfig, RealityServerConnection};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// Note: Vision over Reality would require updating setup_vision_vless_server_stream
// to accept CryptoTlsStream instead of tokio_rustls::server::TlsStream.
// For now, vision_config should always be None for Reality targets.
// This will be enforced in tcp_handler_util.rs
log::debug!("REALITY DEBUG: setup_reality_server_stream called");
let client_hello_frame = &parsed_client_hello.client_hello_frame;
log::debug!(
"REALITY DEBUG: ClientHello frame length: {}",
client_hello_frame.len()
);
// Create buffered REALITY connection
log::debug!("REALITY DEBUG: Creating buffered RealityServerConnection");
let reality_config = RealityServerConfig {
private_key: target.private_key,
short_ids: target.short_ids.clone(),
dest: target.dest.clone(),
max_time_diff: target.max_time_diff,
min_client_version: target.min_client_version,
max_client_version: target.max_client_version,
};
let mut reality_conn = RealityServerConnection::new(reality_config)?;
// Feed the ClientHello to the connection
log::debug!("REALITY DEBUG: Feeding ClientHello to connection via read_tls");
{
let mut cursor = std::io::Cursor::new(client_hello_frame);
reality_conn.read_tls(&mut cursor)?;
}
// Process the ClientHello to advance handshake (this validates everything)
log::debug!("REALITY DEBUG: Processing ClientHello via process_new_packets");
if let Err(e) = reality_conn.process_new_packets() {
// Check if this is an authentication failure
if e.kind() == std::io::ErrorKind::PermissionDenied {
log::warn!(
"REALITY: Authentication failed, falling back to dest: {} - reason: {}",
target.dest,
e
);
// Implement fallback mechanism
return reality_fallback_to_dest(
server_stream,
client_hello_frame,
&target.dest,
)
.await;
} else {
// Other errors should propagate normally
return Err(e);
}
}
// Write the server's handshake response to the stream
log::debug!("REALITY DEBUG: Writing server handshake response");
{
let mut write_buf = Vec::new();
while reality_conn.wants_write() {
reality_conn.write_tls(&mut write_buf)?;
}
if !write_buf.is_empty() {
server_stream.write_all(&write_buf).await?;
server_stream.flush().await?;
}
}
// Read client's Finished message
log::debug!("REALITY DEBUG: Reading client Finished");
{
let mut buf = vec![0u8; 4096];
let n = server_stream.read(&mut buf).await?;
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF while waiting for client Finished",
));
}
let mut cursor = std::io::Cursor::new(&buf[..n]);
reality_conn.read_tls(&mut cursor)?;
}
// Process client Finished to complete handshake
log::debug!("REALITY DEBUG: Processing client Finished");
let io_state = reality_conn.process_new_packets()?;
log::debug!(
"REALITY DEBUG: After processing, plaintext_bytes_to_read={}",
io_state.plaintext_bytes_to_read()
);
// Wrap in Connection enum and CryptoTlsStream
log::debug!("REALITY DEBUG: Wrapping in CryptoTlsStream");
let connection = CryptoConnection::new_reality_server(reality_conn);
let tls_stream = CryptoTlsStream::new(server_stream, connection);
log::debug!("REALITY DEBUG: TLS 1.3 handshake completed successfully");
// Check if Vision is enabled
let mut target_setup_result = if let Some(vision_cfg) = &target.vision_config {
log::info!("REALITY+Vision: Setting up VLESS+VISION stream");
// Vision is enabled - call setup_vision_vless_server_stream_from_custom_tls
crate::vless::vless_server_handler::setup_custom_tls_vision_vless_server_stream(
tls_stream,
&vision_cfg.user_id,
vision_cfg.udp_enabled,
)
.await
} else {
// Regular REALITY (no Vision) - call inner handler
target
.handler
.setup_server_stream(Box::new(tls_stream))
.await
};
if let Ok(ref mut setup_result) = target_setup_result {
setup_result.set_need_initial_flush(true);
if setup_result.override_proxy_provider_unspecified()
&& !target.override_proxy_provider.is_unspecified()
{
setup_result
.set_override_proxy_provider(target.override_proxy_provider.clone());
}
}
target_setup_result
}
}
}
}
/// Fallback mechanism for Reality authentication failures
///
/// When a client fails Reality authentication (invalid short_id, timestamp, or version),
/// instead of dropping the connection, we transparently forward it to the configured
/// destination server. This makes the server indistinguishable from a legitimate
/// reverse proxy or CDN, defeating active probing attacks.
async fn reality_fallback_to_dest(
mut client_stream: Box<dyn AsyncStream>,
client_hello_bytes: &[u8],
dest: &NetLocation,
) -> std::io::Result<TcpServerSetupResult> {
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
log::info!("REALITY FALLBACK: Connecting to dest server: {}", dest);
/// Shared resolver instance for Reality fallback connections
/// Using LazyLock to avoid creating a new resolver for each fallback connection
static REALITY_FALLBACK_RESOLVER: LazyLock<Arc<dyn Resolver>> =
LazyLock::new(|| Arc::new(NativeResolver::new()));
// Resolve and connect to the fallback destination using shared resolver
let dest_addr =
crate::resolver::resolve_single_address(&REALITY_FALLBACK_RESOLVER, dest).await?;
log::debug!("REALITY FALLBACK: Resolved {} to {}", dest, dest_addr);
let mut dest_stream: Box<dyn AsyncStream> = Box::new(TcpStream::connect(dest_addr).await?);
log::info!(
"REALITY FALLBACK: Connected to dest, forwarding ClientHello ({} bytes)",
client_hello_bytes.len()
);
// Forward the raw ClientHello that we received from the client
dest_stream.write_all(client_hello_bytes).await?;
dest_stream.flush().await?;
log::info!("REALITY FALLBACK: ClientHello forwarded, entering bidirectional mode");
// Enter bidirectional forwarding mode
// This will copy all traffic between client and dest transparently
crate::copy_bidirectional::copy_bidirectional(
&mut *client_stream,
&mut *dest_stream,
false, // client doesn't need initial flush
false, // dest doesn't need initial flush
)
.await?;
log::info!("REALITY FALLBACK: Bidirectional forwarding completed");
// Connection handled, no further setup needed
// Return an error to signal that this connection has been fully handled
Err(std::io::Error::other(
"Connection handled by Reality fallback mechanism",
))
}
+5 -5
View File
@@ -2,6 +2,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use aws_lc_rs::digest::SHA224;
use subtle::ConstantTimeEq;
use tokio::io::AsyncWriteExt;
use crate::address::NetLocation;
@@ -38,7 +39,7 @@ impl TrojanTcpHandler {
cipher,
password: shadowsocks_password,
} = config;
let cipher: ShadowsocksCipher = cipher.as_str().into();
let cipher: ShadowsocksCipher = cipher.as_str().try_into().unwrap();
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(DefaultKey::new(
shadowsocks_password,
cipher.algorithm().key_len(),
@@ -88,10 +89,9 @@ impl TcpServerHandler for TrojanTcpHandler {
)));
}
for (b1, b2) in self.password_hash.iter().zip(received_hash.iter()) {
if b1 != b2 {
return Err(std::io::Error::other("Invalid password hash"));
}
// Use constant-time comparison to prevent timing attacks
if self.password_hash.ct_eq(received_hash).unwrap_u8() == 0 {
return Err(std::io::Error::other("Invalid password hash"));
}
let command_type = stream_reader.read_u8(&mut server_stream).await?;
+266
View File
@@ -0,0 +1,266 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::io::ReadBuf;
use tokio::sync::mpsc;
use crate::async_stream::{
AsyncFlushMessage, AsyncPing, AsyncReadSessionMessage, AsyncSessionMessageStream,
AsyncShutdownMessage, AsyncWriteSessionMessage,
};
use crate::socket_util::new_udp_socket;
/// Configuration for creating UDP sockets for each session.
/// Preserves user's interface binding configuration.
#[derive(Clone, Debug)]
pub struct UdpSocketConfig {
/// Optional network interface to bind sockets to (e.g., "tun0" for VPN)
pub bind_interface: Option<String>,
}
/// Message from a session socket reader task containing data received from a UDP peer.
struct SessionMessage {
session_id: u16,
data: Vec<u8>,
source_addr: SocketAddr,
}
/// Session-based UDP message stream that manages dedicated UDP sockets for each session.
/// Used for protocols like XUDP that require socket-per-session architecture.
/// This eliminates the need for destination-based session mapping by using socket identity
/// as session identity.
pub struct UdpSessionMessageStream {
/// Receives messages from all session socket readers
receiver: mpsc::UnboundedReceiver<SessionMessage>,
/// Used to send to session socket readers (cloned for each new session)
sender: mpsc::UnboundedSender<SessionMessage>,
/// Maps session ID to the sender for that session's writer task
session_writers: HashMap<u16, mpsc::UnboundedSender<(Vec<u8>, SocketAddr)>>,
/// Configuration for creating new UDP sockets
config: UdpSocketConfig,
/// Counter for session IDs (not used yet, reserved for future session creation API)
_next_session_id: u16,
}
impl UdpSessionMessageStream {
/// Creates a new UdpSessionMessageStream with the specified configuration.
pub fn new(config: UdpSocketConfig) -> Self {
// Use unbounded channel for maximum performance (no backpressure)
let (sender, receiver) = mpsc::unbounded_channel();
Self {
receiver,
sender,
session_writers: HashMap::new(),
config,
_next_session_id: 0,
}
}
/// Creates a new session with a dedicated UDP socket.
/// Returns the session ID that was created.
///
/// This spawns two tasks:
/// - Reader task: reads from UDP socket and sends to manager's channel
/// - Writer task: receives write requests and sends to UDP socket
pub fn create_session(&mut self, session_id: u16, is_ipv6: bool) -> std::io::Result<()> {
// Create dedicated UDP socket for this session
let socket = new_udp_socket(is_ipv6, self.config.bind_interface.clone())?;
let socket = Arc::new(socket);
// Create channel for write requests to this session
let (write_tx, mut write_rx) = mpsc::unbounded_channel::<(Vec<u8>, SocketAddr)>();
// Spawn reader task
let reader_socket = socket.clone();
let reader_sender = self.sender.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 65536];
loop {
match reader_socket.recv_from(&mut buf).await {
Ok((len, source_addr)) => {
let data = buf[..len].to_vec();
let msg = SessionMessage {
session_id,
data,
source_addr,
};
// For unbounded channels, send() doesn't need to be awaited
if reader_sender.send(msg).is_err() {
// Manager dropped, exit reader task
log::debug!(
"[Session {}] Manager dropped, stopping reader",
session_id
);
break;
}
}
Err(e) => {
log::error!("[Session {}] UDP recv error: {}", session_id, e);
break;
}
}
}
});
// Spawn writer task
let writer_socket = socket.clone();
tokio::spawn(async move {
while let Some((data, target_addr)) = write_rx.recv().await {
match writer_socket.send_to(&data, target_addr).await {
Ok(sent) => {
log::debug!(
"[Session {}] Sent {} bytes to {}",
session_id,
sent,
target_addr
);
}
Err(e) => {
log::error!("[Session {}] UDP send error: {}", session_id, e);
break;
}
}
}
log::debug!("[Session {}] Writer task exiting", session_id);
});
// Store the writer channel
self.session_writers.insert(session_id, write_tx);
log::info!(
"[Session {}] Created dedicated UDP socket (IPv6: {})",
session_id,
is_ipv6
);
Ok(())
}
/// Removes a session and its associated socket.
/// The socket will be closed when the writer task exits.
/// TODO: why isn't this used? add session cleanup
#[allow(dead_code)]
pub fn remove_session(&mut self, session_id: u16) {
if self.session_writers.remove(&session_id).is_some() {
log::info!("[Session {}] Removed session", session_id);
}
}
}
impl AsyncReadSessionMessage for UdpSessionMessageStream {
fn poll_read_session_message(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<(u16, SocketAddr)>> {
// Poll the receiver for messages from any session socket
match self.receiver.poll_recv(cx) {
Poll::Ready(Some(msg)) => {
// Write the data to the buffer
let len = msg.data.len();
if buf.remaining() < len {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Buffer too small: need {} bytes, have {}",
len,
buf.remaining()
),
)));
}
buf.put_slice(&msg.data);
Poll::Ready(Ok((msg.session_id, msg.source_addr)))
}
Poll::Ready(None) => {
// All session sockets dropped
Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::ConnectionAborted,
"All session sockets closed",
)))
}
Poll::Pending => Poll::Pending,
}
}
}
impl AsyncWriteSessionMessage for UdpSessionMessageStream {
fn poll_write_session_message(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
session_id: u16,
buf: &[u8],
target: &SocketAddr,
) -> Poll<std::io::Result<()>> {
// Auto-create session if it doesn't exist yet
if !self.session_writers.contains_key(&session_id) {
let is_ipv6 = target.is_ipv6();
if let Err(e) = self.create_session(session_id, is_ipv6) {
return Poll::Ready(Err(e));
}
}
// Get the writer channel for this session
match self.session_writers.get(&session_id) {
Some(writer) => {
let data = buf.to_vec();
let target_addr = *target;
// Send write request to the writer task
// For unbounded channels, send() doesn't need to be awaited
if writer.send((data, target_addr)).is_err() {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
format!("Session {} writer task closed", session_id),
)));
}
Poll::Ready(Ok(()))
}
None => {
// Should never happen because we just created it above
Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Session {} not found", session_id),
)))
}
}
}
}
impl AsyncFlushMessage for UdpSessionMessageStream {
fn poll_flush_message(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
// UDP is connectionless, no flushing needed
Poll::Ready(Ok(()))
}
}
impl AsyncShutdownMessage for UdpSessionMessageStream {
fn poll_shutdown_message(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
// Close all sessions
self.session_writers.clear();
Poll::Ready(Ok(()))
}
}
impl AsyncPing for UdpSessionMessageStream {
fn supports_ping(&self) -> bool {
false
}
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
Poll::Ready(Ok(false))
}
}
impl AsyncSessionMessageStream for UdpSessionMessageStream {}
+41
View File
@@ -0,0 +1,41 @@
//! SagerNet UDP-over-TCP (UoT) protocol implementation
//!
//! This module implements the sing-box UoT protocol for tunneling UDP over TCP.
//! It supports both V1 and V2 formats.
//!
//! ## Magic Addresses
//! - V1: `sp.udp-over-tcp.arpa` - Multi-destination mode, each packet has full address
//! - V2: `sp.v2.udp-over-tcp.arpa` - Optional connect mode for single destination
//!
//! ## V1 Packet Format
//! ```text
//! | ATYP | address | port | length | data |
//! | u8 | variable | u16be | u16be | variable |
//! ```
//!
//! ## V2 Request Format
//! ```text
//! | isConnect | ATYP | address | port |
//! | u8 | u8 | variable | u16be |
//! ```
//!
//! If isConnect=1, subsequent packets are length-prefixed only (V2 connect mode).
//! If isConnect=0, subsequent packets use V1 format (multi-destination).
//!
//! ## ATYP Values (different from SOCKS5!)
//! - 0x00: IPv4 Address (4 bytes)
//! - 0x01: IPv6 Address (16 bytes)
//! - 0x02: Domain Name (1 byte length + domain)
mod uot_v1_stream;
pub use uot_v1_stream::UotV1Stream;
/// UoT V2 connect mode stream - identical format to VlessMessageStream (length-prefixed u16be + data)
pub type UotV2Stream = crate::vless::VlessMessageStream;
/// Magic address used to signal UoT V1 mode (multi-destination)
pub const UOT_V1_MAGIC_ADDRESS: &str = "sp.udp-over-tcp.arpa";
/// Magic address used to signal UoT V2 mode (optional connect mode)
pub const UOT_V2_MAGIC_ADDRESS: &str = "sp.v2.udp-over-tcp.arpa";
+400
View File
@@ -0,0 +1,400 @@
//! UDP-over-TCP V1 stream implementation
//!
//! V1 Packet format (each packet has full address):
//! ```text
//! | ATYP | address | port | length | data |
//! | u8 | variable | u16be | u16be | variable |
//! ```
use futures::ready;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use crate::address::{Address, NetLocation};
use crate::async_stream::{
AsyncFlushMessage, AsyncPing, AsyncReadTargetedMessage, AsyncShutdownMessage, AsyncStream,
AsyncTargetedMessageStream, AsyncWriteSourcedMessage,
};
use crate::slide_buffer::SlideBuffer;
/// ATYP values for UoT (different from SOCKS5!)
const ATYP_IPV4: u8 = 0x00;
const ATYP_IPV6: u8 = 0x01;
const ATYP_DOMAIN: u8 = 0x02;
/// Buffer size for reading/writing UoT packets
const BUFFER_SIZE: usize = 65535;
/// UoT V1 stream for multi-destination UDP packets
///
/// Each packet includes the full destination address, making it suitable
/// for applications that send UDP packets to multiple destinations.
///
/// This stream wraps an `AsyncStream` (raw byte stream) and handles
/// UoT packet framing internally.
pub struct UotV1Stream {
stream: Box<dyn AsyncStream>,
/// Buffer for reading - accumulates bytes until we have a complete packet
read_buf: SlideBuffer,
/// Buffer for writing - holds a complete packet before sending
write_buf: [u8; BUFFER_SIZE],
write_buf_len: usize,
write_buf_sent: usize,
is_eof: bool,
}
impl UotV1Stream {
pub fn new(stream: Box<dyn AsyncStream>) -> Self {
Self {
stream,
read_buf: SlideBuffer::new(BUFFER_SIZE),
write_buf: [0u8; BUFFER_SIZE],
write_buf_len: 0,
write_buf_sent: 0,
is_eof: false,
}
}
/// Feed initial data that was read before the stream was created.
/// This is needed when the first UoT packet arrives in the same TCP segment
/// as the magic address header.
pub fn feed_initial_data(&mut self, data: &[u8]) {
if !data.is_empty() {
let len = data.len().min(self.read_buf.remaining_capacity());
self.read_buf.extend_from_slice(&data[..len]);
}
}
/// Try to parse a complete UoT packet from the read buffer.
/// Returns Some((location, payload_start, payload_len)) if a complete packet is available.
fn try_parse_packet(&self) -> Option<(NetLocation, usize, usize)> {
let data = self.read_buf.as_slice();
if data.is_empty() {
return None;
}
// Parse address to determine header length
let (location, addr_len) = parse_uot_address(data).ok()?;
// Need at least addr_len + 2 (length prefix) bytes
if data.len() < addr_len + 2 {
return None;
}
// Read length prefix
let payload_len = u16::from_be_bytes([data[addr_len], data[addr_len + 1]]) as usize;
let payload_start = addr_len + 2;
let total_len = payload_start + payload_len;
// Check if we have the complete packet
if data.len() < total_len {
return None;
}
Some((location, payload_start, payload_len))
}
}
/// Parse UoT address format (ATYP + address + port)
/// Returns (NetLocation, bytes consumed)
fn parse_uot_address(data: &[u8]) -> std::io::Result<(NetLocation, usize)> {
if data.is_empty() {
return Err(std::io::Error::other("empty UoT address"));
}
let atyp = data[0];
match atyp {
ATYP_IPV4 => {
// ATYP(1) + IPv4(4) + Port(2) = 7 bytes
if data.len() < 7 {
return Err(std::io::Error::other("UoT IPv4 address truncated"));
}
let ip = Ipv4Addr::new(data[1], data[2], data[3], data[4]);
let port = u16::from_be_bytes([data[5], data[6]]);
Ok((NetLocation::new(Address::Ipv4(ip), port), 7))
}
ATYP_IPV6 => {
// ATYP(1) + IPv6(16) + Port(2) = 19 bytes
if data.len() < 19 {
return Err(std::io::Error::other("UoT IPv6 address truncated"));
}
let ip_bytes: [u8; 16] = data[1..17].try_into().unwrap();
let ip = Ipv6Addr::from(ip_bytes);
let port = u16::from_be_bytes([data[17], data[18]]);
Ok((NetLocation::new(Address::Ipv6(ip), port), 19))
}
ATYP_DOMAIN => {
// ATYP(1) + DomainLen(1) + Domain(variable) + Port(2)
if data.len() < 2 {
return Err(std::io::Error::other("UoT domain length truncated"));
}
let domain_len = data[1] as usize;
let total_len = 1 + 1 + domain_len + 2; // ATYP + len + domain + port
if data.len() < total_len {
return Err(std::io::Error::other("UoT domain address truncated"));
}
let domain = std::str::from_utf8(&data[2..2 + domain_len])
.map_err(|e| std::io::Error::other(format!("invalid domain: {e}")))?;
let port = u16::from_be_bytes([data[2 + domain_len], data[3 + domain_len]]);
Ok((
NetLocation::new(Address::Hostname(domain.to_string()), port),
total_len,
))
}
_ => Err(std::io::Error::other(format!("unknown UoT ATYP: {atyp}"))),
}
}
/// Write UoT address format (ATYP + address + port)
/// Returns number of bytes written
fn write_uot_address(buf: &mut [u8], addr: &SocketAddr) -> usize {
match addr {
SocketAddr::V4(v4) => {
buf[0] = ATYP_IPV4;
buf[1..5].copy_from_slice(&v4.ip().octets());
buf[5..7].copy_from_slice(&v4.port().to_be_bytes());
7
}
SocketAddr::V6(v6) => {
buf[0] = ATYP_IPV6;
buf[1..17].copy_from_slice(&v6.ip().octets());
buf[17..19].copy_from_slice(&v6.port().to_be_bytes());
19
}
}
}
impl AsyncReadTargetedMessage for UotV1Stream {
fn poll_read_targeted_message(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<NetLocation>> {
let this = self.get_mut();
if this.is_eof {
return Poll::Ready(Ok(NetLocation::UNSPECIFIED));
}
loop {
// Try to parse a complete packet from buffered data
if let Some((location, payload_start, payload_len)) = this.try_parse_packet() {
let data = this.read_buf.as_slice();
buf.put_slice(&data[payload_start..payload_start + payload_len]);
// Consume the entire packet from the buffer
let total_consumed = payload_start + payload_len;
this.read_buf.consume(total_consumed);
return Poll::Ready(Ok(location));
}
// Need more data - compact buffer if needed
this.read_buf.maybe_compact(4096);
if this.read_buf.remaining_capacity() == 0 {
return Poll::Ready(Err(std::io::Error::other(
"UoT read buffer full but no complete packet",
)));
}
// Read more data from the underlying stream
let write_slice = this.read_buf.write_slice();
let mut read_buf = ReadBuf::new(write_slice);
match Pin::new(&mut this.stream).poll_read(cx, &mut read_buf) {
Poll::Ready(Ok(())) => {
let bytes_read = read_buf.filled().len();
if bytes_read == 0 {
this.is_eof = true;
return Poll::Ready(Ok(NetLocation::UNSPECIFIED));
}
this.read_buf.advance_write(bytes_read);
// Loop to try parsing again
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
}
}
impl AsyncWriteSourcedMessage for UotV1Stream {
fn poll_write_sourced_message(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
source: &SocketAddr,
) -> Poll<std::io::Result<()>> {
let this = self.get_mut();
// If there's pending data to send, flush it first
while this.write_buf_sent < this.write_buf_len {
let remaining = &this.write_buf[this.write_buf_sent..this.write_buf_len];
match Pin::new(&mut this.stream).poll_write(cx, remaining) {
Poll::Ready(Ok(n)) => {
this.write_buf_sent += n;
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
// Reset write buffer
this.write_buf_len = 0;
this.write_buf_sent = 0;
// Calculate required space: address + length(2) + payload
let addr_len = match source {
SocketAddr::V4(_) => 7, // ATYP + IPv4(4) + Port(2)
SocketAddr::V6(_) => 19, // ATYP + IPv6(16) + Port(2)
};
let total_len = addr_len + 2 + buf.len();
if total_len > this.write_buf.len() {
return Poll::Ready(Err(std::io::Error::other(format!(
"UoT packet too large: {total_len} > {}",
this.write_buf.len()
))));
}
// Write UoT address format
let offset = write_uot_address(&mut this.write_buf, source);
// Write length prefix (u16be)
let len_bytes = (buf.len() as u16).to_be_bytes();
this.write_buf[offset..offset + 2].copy_from_slice(&len_bytes);
let data_start = offset + 2;
// Write payload
this.write_buf[data_start..data_start + buf.len()].copy_from_slice(buf);
this.write_buf_len = data_start + buf.len();
Poll::Ready(Ok(()))
}
}
impl AsyncFlushMessage for UotV1Stream {
fn poll_flush_message(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
let this = self.get_mut();
// Flush any pending write data
while this.write_buf_sent < this.write_buf_len {
let remaining = &this.write_buf[this.write_buf_sent..this.write_buf_len];
match Pin::new(&mut this.stream).poll_write(cx, remaining) {
Poll::Ready(Ok(n)) => {
this.write_buf_sent += n;
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
this.write_buf_len = 0;
this.write_buf_sent = 0;
// Flush the underlying stream
Pin::new(&mut this.stream).poll_flush(cx)
}
}
impl AsyncShutdownMessage for UotV1Stream {
fn poll_shutdown_message(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
let mut this = self.get_mut();
ready!(Pin::new(&mut this).poll_flush_message(cx))?;
Pin::new(&mut this.stream).poll_shutdown(cx)
}
}
impl AsyncPing for UotV1Stream {
fn supports_ping(&self) -> bool {
false
}
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
Poll::Ready(Ok(false))
}
}
impl AsyncTargetedMessageStream for UotV1Stream {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_uot_ipv4_address() {
// ATYP=0x00, IP=192.168.1.1, Port=8080
let data = [0x00, 192, 168, 1, 1, 0x1F, 0x90];
let (location, len) = parse_uot_address(&data).unwrap();
assert_eq!(len, 7);
assert_eq!(location.port(), 8080);
match location.address() {
Address::Ipv4(ip) => assert_eq!(*ip, Ipv4Addr::new(192, 168, 1, 1)),
_ => panic!("expected IPv4"),
}
}
#[test]
fn test_parse_uot_ipv6_address() {
// ATYP=0x01, IP=::1, Port=443
let mut data = vec![ATYP_IPV6];
data.extend_from_slice(&Ipv6Addr::LOCALHOST.octets());
data.extend_from_slice(&443u16.to_be_bytes());
let (location, len) = parse_uot_address(&data).unwrap();
assert_eq!(len, 19);
assert_eq!(location.port(), 443);
match location.address() {
Address::Ipv6(ip) => assert_eq!(*ip, Ipv6Addr::LOCALHOST),
_ => panic!("expected IPv6"),
}
}
#[test]
fn test_parse_uot_domain_address() {
// ATYP=0x02, Domain="example.com", Port=53
let domain = b"example.com";
let mut data = vec![ATYP_DOMAIN, domain.len() as u8];
data.extend_from_slice(domain);
data.extend_from_slice(&53u16.to_be_bytes());
let (location, len) = parse_uot_address(&data).unwrap();
assert_eq!(len, 1 + 1 + domain.len() + 2);
assert_eq!(location.port(), 53);
match location.address() {
Address::Hostname(h) => assert_eq!(h, "example.com"),
_ => panic!("expected hostname"),
}
}
#[test]
fn test_write_uot_ipv4_address() {
let addr: SocketAddr = "192.168.1.1:8080".parse().unwrap();
let mut buf = [0u8; 32];
let len = write_uot_address(&mut buf, &addr);
assert_eq!(len, 7);
assert_eq!(buf[0], ATYP_IPV4);
assert_eq!(&buf[1..5], &[192, 168, 1, 1]);
assert_eq!(&buf[5..7], &0x1F90u16.to_be_bytes());
}
#[test]
fn test_write_uot_ipv6_address() {
let addr: SocketAddr = "[::1]:443".parse().unwrap();
let mut buf = [0u8; 32];
let len = write_uot_address(&mut buf, &addr);
assert_eq!(len, 19);
assert_eq!(buf[0], ATYP_IPV6);
assert_eq!(&buf[1..17], &Ipv6Addr::LOCALHOST.octets());
assert_eq!(&buf[17..19], &443u16.to_be_bytes());
}
}
+20
View File
@@ -0,0 +1,20 @@
// VLESS protocol implementation with VISION support
// Public API
pub mod vless_client_handler;
pub mod vless_server_handler;
// Internal implementation details
pub mod tls_deframer;
mod tls_fuzzy_deframer;
mod tls_handshake_util;
mod vision_filter;
mod vision_pad;
mod vision_stream;
mod vision_unpad;
mod vless_message_stream;
mod vless_response_stream;
mod vless_util;
// Re-export VlessMessageStream for use by other protocols (e.g., Shadowsocks UoT V2)
pub use vless_message_stream::VlessMessageStream;
+599
View File
@@ -0,0 +1,599 @@
use bytes::{Bytes, BytesMut};
use std::io;
/// TLS record header size (ContentType + ProtocolVersion + Length)
const TLS_RECORD_HEADER_SIZE: usize = 5;
/// Maximum TLS ciphertext payload size per RFC specifications
///
/// The TLS record header's `length` field specifies the size of the ENCRYPTED payload (ciphertext).
/// Per RFC 8446 (TLS 1.3) and RFC 5246 (TLS 1.2), the limits are:
///
/// - TLS 1.3: Plaintext limit = 16,384 bytes (2^14)
/// Encryption overhead allowance = 256 bytes
/// Ciphertext limit = 16,384 + 256 = 16,640 bytes
///
/// - TLS 1.2: Plaintext limit = 16,384 bytes (2^14)
/// Encryption overhead allowance = 2,048 bytes
/// Ciphertext limit = 16,384 + 2,048 = 18,432 bytes
///
/// We use the TLS 1.2 limit for maximum compatibility with both protocol versions.
/// See /shoes/VISION_TLS_RECORD_SIZE.md for detailed analysis and RFC references.
const MAX_TLS_CIPHERTEXT_LEN: usize = 16384 + 2048; // 18,432 bytes (TLS 1.2 limit)
/// Maximum TLS record size (ciphertext + header)
pub const TLS_MAX_RECORD_SIZE: usize = MAX_TLS_CIPHERTEXT_LEN + TLS_RECORD_HEADER_SIZE;
/// TLS protocol versions we expect (0x0303 = TLS 1.2 for compatibility)
const TLS_PROTOCOL_VERSION_MAJOR: u8 = 0x03;
const TLS_PROTOCOL_VERSION_MINOR_MIN: u8 = 0x01;
const TLS_PROTOCOL_VERSION_MINOR_MAX: u8 = 0x03;
/// Deframer that reassembles TLS records from partial reads/writes
///
/// ```ignore
/// let mut deframer = TlsDeframer::new();
///
/// // Feed data as it arrives (may be partial)
/// deframer.feed(&chunk1);
/// deframer.feed(&chunk2);
///
/// // Extract complete records
/// while let Some(record) = deframer.next_record()? {
/// process_tls_record(record);
/// }
/// ```
#[derive(Debug, PartialEq)]
pub struct TlsDeframer {
/// Buffer holding partial TLS record data
buffer: BytesMut,
/// Current parsing state
state: DeframerState,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum DeframerState {
/// Waiting for or reading the 5-byte TLS record header
ReadingHeader,
/// Reading payload of known length
/// Stores the expected payload length
ReadingPayload { payload_len: usize },
}
impl TlsDeframer {
pub fn new() -> Self {
Self {
buffer: BytesMut::with_capacity(TLS_MAX_RECORD_SIZE),
state: DeframerState::ReadingHeader,
}
}
/// Feed data into the deframer
///
/// This data may contain partial TLS records, complete records, or multiple records.
/// Call `next_record()` after feeding to extract complete records.
pub fn feed(&mut self, data: &[u8]) {
self.buffer.extend_from_slice(data);
}
/// Extract the next complete TLS record if available
///
/// This method automatically advances the internal buffer after extracting the record.
///
/// Returns:
/// - `Ok(Some(Bytes))` if a complete record is available (zero-copy slice)
/// - `Ok(None)` if more data is needed
/// - `Err(...)` if the data is malformed
pub fn next_record(&mut self) -> io::Result<Option<Bytes>> {
loop {
match self.state {
DeframerState::ReadingHeader => {
// Need at least 5 bytes for header
if self.buffer.len() < TLS_RECORD_HEADER_SIZE {
return Ok(None);
}
// Parse TLS record header
let content_type = self.buffer[0];
let version_major = self.buffer[1];
let version_minor = self.buffer[2];
let payload_len = u16::from_be_bytes([self.buffer[3], self.buffer[4]]) as usize;
// Validate protocol version (should be 0x03XX for TLS 1.x)
// Accept TLS 1.0 (0x0301), TLS 1.1 (0x0302), TLS 1.2 (0x0303), and TLS 1.3 (0x0303)
// TLS 1.3 uses 0x0303 in the record layer for backwards compatibility
if version_major != TLS_PROTOCOL_VERSION_MAJOR
|| !(TLS_PROTOCOL_VERSION_MINOR_MIN..=TLS_PROTOCOL_VERSION_MINOR_MAX)
.contains(&version_minor)
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid TLS protocol version: 0x{:02x}{:02x} (expected 0x0301 to 0x0303)",
version_major, version_minor
),
));
}
// Validate ciphertext payload length
// The `payload_len` from the TLS record header specifies encrypted data size.
// TLS 1.2 allows up to 18,432 bytes (16,384 plaintext + 2,048 overhead).
if payload_len > MAX_TLS_CIPHERTEXT_LEN {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"TLS record ciphertext length {} exceeds maximum {} (TLS 1.2 limit)",
payload_len, MAX_TLS_CIPHERTEXT_LEN
),
));
}
// Validate content type (basic sanity check)
// Valid types: 0x14-0x18 (ChangeCipherSpec, Alert, Handshake, Application Data, Heartbeat)
if !(0x14..=0x18).contains(&content_type) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid TLS content type: 0x{:02x}", content_type),
));
}
// Transition to reading payload
self.state = DeframerState::ReadingPayload { payload_len };
}
DeframerState::ReadingPayload { payload_len } => {
let total_len = TLS_RECORD_HEADER_SIZE + payload_len;
// Check if we have the complete record
if self.buffer.len() < total_len {
return Ok(None);
}
// We have a complete record! Extract it using split_to for zero-copy.
// BytesMut::split_to() returns a Bytes object that shares the underlying
// buffer without copying, and advances the BytesMut past the split point.
let record = self.buffer.split_to(total_len).freeze();
// Reset state for next record
self.state = DeframerState::ReadingHeader;
return Ok(Some(record));
}
}
}
}
/// Get the number of bytes currently buffered
pub fn pending_bytes(&self) -> usize {
self.buffer.len()
}
/// Extract all available complete TLS records
///
/// This is equivalent to calling `next_record()` repeatedly until it returns `Ok(None)`,
/// but more convenient when you want to process all available records at once.
///
/// Returns a vector of all complete records that can be extracted from the current buffer.
/// If no complete records are available, returns an empty vector.
///
/// Returns:
/// - `Ok(Vec<Bytes>)` containing all available complete records (may be empty)
/// - `Err(...)` if any record is malformed
pub fn next_records(&mut self) -> io::Result<Vec<Bytes>> {
let mut records = Vec::new();
while let Some(record) = self.next_record()? {
records.push(record);
}
Ok(records)
}
/// Consume the deframer and return all remaining buffered data
pub fn into_remaining_data(self) -> Bytes {
self.buffer.freeze()
}
pub fn remaining_data(&self) -> &[u8] {
&self.buffer
}
/// Clear all buffered data
pub fn clear(&mut self) {
self.buffer.clear();
self.state = DeframerState::ReadingHeader;
}
pub fn deallocate(&mut self) {
drop(std::mem::take(&mut self.buffer));
}
}
impl Default for TlsDeframer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Helper to create a TLS record with given content type and payload
fn make_tls_record(content_type: u8, payload: &[u8]) -> Vec<u8> {
let mut record = Vec::new();
record.push(content_type);
record.push(0x03); // Version major
record.push(0x03); // Version minor
let len = payload.len() as u16;
record.extend_from_slice(&len.to_be_bytes());
record.extend_from_slice(payload);
record
}
#[test]
fn test_single_complete_packet() {
let mut deframer = TlsDeframer::new();
let payload = b"Hello, TLS!";
let record = make_tls_record(0x17, payload); // Application Data
deframer.feed(&record);
let read_record = deframer.next_record().unwrap();
assert!(read_record.is_some());
let read_record = read_record.unwrap();
assert_eq!(read_record, &record[..]);
// Verify no more packets
assert!(deframer.next_record().unwrap().is_none());
}
#[test]
fn test_partial_packet_split_header() {
let mut deframer = TlsDeframer::new();
let payload = b"Partial header test";
let record = make_tls_record(0x16, payload); // Handshake
// Feed first 3 bytes (partial header)
deframer.feed(&record[..3]);
assert!(deframer.next_record().unwrap().is_none());
// Feed rest of header
deframer.feed(&record[3..5]);
assert!(deframer.next_record().unwrap().is_none());
// Feed payload
deframer.feed(&record[5..]);
let extracted_record = deframer.next_record().unwrap();
assert!(extracted_record.is_some());
assert_eq!(extracted_record.unwrap(), &record[..]);
}
#[test]
fn test_partial_packet_split_payload() {
let mut deframer = TlsDeframer::new();
let payload = vec![0x42; 100]; // 100 bytes of 0x42
let record = make_tls_record(0x17, &payload);
// Feed header + half payload
let split_point = 5 + 50;
deframer.feed(&record[..split_point]);
assert!(deframer.next_record().unwrap().is_none());
// Feed rest of payload
deframer.feed(&record[split_point..]);
let extracted_record = deframer.next_record().unwrap();
assert!(extracted_record.is_some());
assert_eq!(extracted_record.unwrap(), &record[..]);
}
#[test]
fn test_multiple_packets_at_once() {
let mut deframer = TlsDeframer::new();
let record1 = make_tls_record(0x16, b"First");
let record2 = make_tls_record(0x17, b"Second");
let record3 = make_tls_record(0x16, b"Third");
// Feed all three at once
let mut combined = Vec::new();
combined.extend_from_slice(&record1);
combined.extend_from_slice(&record2);
combined.extend_from_slice(&record3);
deframer.feed(&combined);
// Extract all three (automatically consumed)
let rec1 = deframer.next_record().unwrap().unwrap();
assert_eq!(rec1, &record1[..]);
let rec2 = deframer.next_record().unwrap().unwrap();
assert_eq!(rec2, &record2[..]);
let rec3 = deframer.next_record().unwrap().unwrap();
assert_eq!(rec3, &record3[..]);
assert!(deframer.next_record().unwrap().is_none());
}
#[test]
fn test_multiple_packets_partial_boundary() {
let mut deframer = TlsDeframer::new();
let record1 = make_tls_record(0x16, b"Record One");
let record2 = make_tls_record(0x17, b"Record Two");
let mut combined = Vec::new();
combined.extend_from_slice(&record1);
combined.extend_from_slice(&record2);
// Feed up to middle of second record
let split = record1.len() + 8;
deframer.feed(&combined[..split]);
// Should get first record (automatically consumed)
let rec1 = deframer.next_record().unwrap().unwrap();
assert_eq!(rec1, &record1[..]);
// Second record not complete yet
assert!(deframer.next_record().unwrap().is_none());
// Feed rest
deframer.feed(&combined[split..]);
let rec2 = deframer.next_record().unwrap().unwrap();
assert_eq!(rec2, &record2[..]);
}
#[test]
fn test_tls10_accepted() {
let mut deframer = TlsDeframer::new();
let mut record = vec![0x16, 0x03, 0x01, 0x00, 0x05]; // TLS 1.0
record.extend_from_slice(b"Hello");
deframer.feed(&record);
let result = deframer.next_record();
assert!(result.is_ok());
assert_eq!(result.unwrap().unwrap(), &record[..]);
}
#[test]
fn test_invalid_protocol_version() {
let mut deframer = TlsDeframer::new();
let mut record = vec![0x16, 0x02, 0x00, 0x00, 0x05]; // SSL 2.0 (invalid)
record.extend_from_slice(b"Hello");
deframer.feed(&record);
let result = deframer.next_record();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid TLS protocol version"));
}
#[test]
fn test_invalid_content_type() {
let mut deframer = TlsDeframer::new();
let mut record = vec![0xFF, 0x03, 0x03, 0x00, 0x05]; // Invalid content type
record.extend_from_slice(b"Hello");
deframer.feed(&record);
let result = deframer.next_record();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid TLS content type"));
}
#[test]
fn test_excessive_payload_length() {
let mut deframer = TlsDeframer::new();
// Payload length > 18432 (TLS 1.2 max ciphertext)
let record = vec![0x17, 0x03, 0x03, 0xFF, 0xFF]; // 65535 bytes
deframer.feed(&record);
let result = deframer.next_record();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
}
#[test]
fn test_byte_by_byte_feed() {
let mut deframer = TlsDeframer::new();
let record = make_tls_record(0x16, b"Byte by byte");
// Feed one byte at a time
for byte in &record[..record.len() - 1] {
deframer.feed(&[*byte]);
assert!(deframer.next_record().unwrap().is_none());
}
// Feed last byte
deframer.feed(&[record[record.len() - 1]]);
let extracted_record = deframer.next_record().unwrap().unwrap();
assert_eq!(extracted_record, &record[..]);
}
#[test]
fn test_clear() {
let mut deframer = TlsDeframer::new();
let record = make_tls_record(0x16, b"Test");
deframer.feed(&record[..3]); // Partial
assert_eq!(deframer.pending_bytes(), 3);
deframer.clear();
assert_eq!(deframer.pending_bytes(), 0);
assert!(deframer.next_record().unwrap().is_none());
}
#[test]
fn test_empty_payload() {
let mut deframer = TlsDeframer::new();
let record = make_tls_record(0x16, b""); // Empty payload
deframer.feed(&record);
let extracted_record = deframer.next_record().unwrap().unwrap();
assert_eq!(extracted_record.len(), 5); // Just the header
assert_eq!(extracted_record, &record[..]);
}
#[test]
fn test_max_size_payload() {
let mut deframer = TlsDeframer::new();
// Test with maximum TLS 1.2 ciphertext size (18,432 bytes)
let payload = vec![0xAA; 18432];
let record = make_tls_record(0x17, &payload);
deframer.feed(&record);
let extracted_record = deframer.next_record().unwrap().unwrap();
assert_eq!(extracted_record, &record[..]);
}
#[test]
fn test_reuse_after_consume() {
let mut deframer = TlsDeframer::new();
let record1 = make_tls_record(0x16, b"First");
let record2 = make_tls_record(0x17, b"Second");
// First packet (automatically consumed)
deframer.feed(&record1);
let rec1 = deframer.next_record().unwrap().unwrap();
assert_eq!(rec1, &record1[..]);
// Second packet (automatically consumed)
deframer.feed(&record2);
let rec2 = deframer.next_record().unwrap().unwrap();
assert_eq!(rec2, &record2[..]);
assert_eq!(deframer.pending_bytes(), 0);
}
#[test]
fn test_next_records_empty() {
let mut deframer = TlsDeframer::new();
let records = deframer.next_records().unwrap();
assert!(records.is_empty());
}
#[test]
fn test_next_records_single() {
let mut deframer = TlsDeframer::new();
let record = make_tls_record(0x16, b"Single");
deframer.feed(&record);
let records = deframer.next_records().unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0], &record[..]);
}
#[test]
fn test_next_records_multiple() {
let mut deframer = TlsDeframer::new();
let record1 = make_tls_record(0x16, b"First");
let record2 = make_tls_record(0x17, b"Second");
let record3 = make_tls_record(0x16, b"Third");
let mut combined = Vec::new();
combined.extend_from_slice(&record1);
combined.extend_from_slice(&record2);
combined.extend_from_slice(&record3);
deframer.feed(&combined);
let records = deframer.next_records().unwrap();
assert_eq!(records.len(), 3);
assert_eq!(records[0], &record1[..]);
assert_eq!(records[1], &record2[..]);
assert_eq!(records[2], &record3[..]);
assert_eq!(deframer.pending_bytes(), 0);
}
#[test]
fn test_next_records_partial_remaining() {
let mut deframer = TlsDeframer::new();
let record1 = make_tls_record(0x16, b"Complete");
let record2 = make_tls_record(0x17, b"Incomplete");
let mut combined = Vec::new();
combined.extend_from_slice(&record1);
combined.extend_from_slice(&record2[..10]); // Only partial second record
deframer.feed(&combined);
let records = deframer.next_records().unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0], &record1[..]);
// Should still have partial data
assert_eq!(deframer.pending_bytes(), 10);
}
#[test]
fn test_next_records_error_propagation() {
let mut deframer = TlsDeframer::new();
// Invalid record with bad content type
let mut bad_record = vec![0xFF, 0x03, 0x03, 0x00, 0x05];
bad_record.extend_from_slice(b"Hello");
deframer.feed(&bad_record);
let result = deframer.next_records();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid TLS content type"));
}
#[test]
fn test_next_records_mixed_complete_and_partial() {
let mut deframer = TlsDeframer::new();
let record1 = make_tls_record(0x16, b"First");
let record2 = make_tls_record(0x17, b"Second");
// Feed first complete record and partial second
deframer.feed(&record1);
deframer.feed(&record2[..8]);
let records = deframer.next_records().unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0], &record1[..]);
// Complete the second record
deframer.feed(&record2[8..]);
let records = deframer.next_records().unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0], &record2[..]);
}
}
+531
View File
@@ -0,0 +1,531 @@
/// Fuzzy TLS deframer that can handle unknown prefixes before TLS data
///
/// This deframer wraps TlsDeframer and adds the ability to:
/// 1. Search for TLS handshake patterns in buffers that may contain protocol headers
/// 2. Proactively return unknown prefix bytes that cannot be part of TLS patterns
/// 3. Minimize buffering by discarding non-TLS prefixes as soon as possible
///
/// The deframer operates in two states:
/// - **Searching**: Looking for the TLS handshake pattern [0x16, 0x03, 0x01-0x03, ?, ?, 0x01|0x02]
/// - Returns UnknownPrefix for bytes that cannot be part of the pattern
/// - Keeps minimal suffix that might complete into a pattern (max 5 bytes)
/// - **Deframing**: Found TLS pattern, now extracting complete TLS records
/// - Delegates to inner TlsDeframer for standard TLS record parsing
///
/// This is used in VLESS Vision protocol when chaining proxies, where the inner TLS data
/// may be prefixed with VLESS protocol headers from intermediate proxies.
use bytes::{Bytes, BytesMut};
use std::io;
use super::tls_deframer::TlsDeframer;
use super::tls_handshake_util::{
find_tls_handshake_start, get_partial_tls_pattern_suffix_len, MIN_TLS_HANDSHAKE_PATTERN_LEN,
};
/// Result of attempting to extract data from the fuzzy deframer
#[derive(Debug, PartialEq)]
pub enum DeframeResult {
/// Complete TLS record extracted
TlsRecord(Bytes),
/// Unknown prefix bytes that were skipped
/// Returned proactively as soon as we know bytes aren't part of TLS pattern
UnknownPrefix(Bytes),
/// Need more data (waiting for complete record or more pattern bytes)
NeedData,
}
/// Fuzzy TLS deframer that can skip unknown prefixes before TLS data
pub struct FuzzyTlsDeframer {
/// Underlying TLS deframer (only used in Deframing state)
inner: TlsDeframer,
/// Search buffer (only used in Searching state)
search_buffer: BytesMut,
/// Current state
state: FuzzyState,
/// Total prefix bytes discarded (for logging)
total_prefix_bytes: usize,
}
#[derive(Debug, PartialEq)]
enum FuzzyState {
/// Searching for TLS handshake pattern
Searching,
/// Found TLS pattern, now in normal deframing mode
Deframing,
}
impl FuzzyTlsDeframer {
pub fn new() -> Self {
Self {
inner: TlsDeframer::new(),
search_buffer: BytesMut::new(),
state: FuzzyState::Searching,
total_prefix_bytes: 0,
}
}
/// Feed data into the deframer
///
/// Data may contain protocol headers, partial TLS patterns, or complete TLS records.
/// Call `next_record()` after feeding to extract available data.
pub fn feed(&mut self, data: &[u8]) {
match self.state {
FuzzyState::Searching => {
// Just accumulate in search buffer
// Don't optimize here - let next_record() handle it for proper accounting
self.search_buffer.extend_from_slice(data);
}
FuzzyState::Deframing => {
// Normal mode, feed directly to inner deframer
self.inner.feed(data);
}
}
}
/// Extract the next available data
///
/// Returns:
/// - `TlsRecord`: A complete TLS record (in Deframing state)
/// - `UnknownPrefix`: Bytes that cannot be part of TLS pattern (in Searching state)
/// - `IncompleteRecord`: Need more data
///
/// This method should be called in a loop until it returns `IncompleteRecord`.
pub fn next_record(&mut self) -> io::Result<DeframeResult> {
loop {
match self.state {
FuzzyState::Searching => {
if self.search_buffer.is_empty() {
return Ok(DeframeResult::NeedData);
}
// Try to find complete TLS pattern (need at least 6 bytes)
if self.search_buffer.len() >= MIN_TLS_HANDSHAKE_PATTERN_LEN {
if let Some(offset) = find_tls_handshake_start(&self.search_buffer) {
// Found complete pattern!
if offset > 0 {
// There's a prefix before the TLS pattern
// Extract it and return as UnknownPrefix
let prefix = self.search_buffer.split_to(offset).freeze();
self.total_prefix_bytes += prefix.len();
// Feed remaining data (TLS) to inner deframer
self.inner.feed(&self.search_buffer);
self.search_buffer.clear();
// Transition to Deframing state
self.state = FuzzyState::Deframing;
log::debug!(
"FuzzyTlsDeframer: Found TLS pattern after {} byte prefix (total {} bytes discarded), transitioned to Deframing",
prefix.len(),
self.total_prefix_bytes
);
return Ok(DeframeResult::UnknownPrefix(prefix));
} else {
// offset == 0, pattern at start, no prefix
self.inner.feed(&self.search_buffer);
self.search_buffer.clear();
self.state = FuzzyState::Deframing;
log::debug!(
"FuzzyTlsDeframer: Found TLS pattern at offset 0 (total {} bytes discarded), transitioned to Deframing",
self.total_prefix_bytes
);
// Continue loop to try extracting a record from inner
continue;
}
} else {
// No complete pattern found, but we have enough bytes to check
// Check if we can discard some prefix bytes
let suffix_len =
get_partial_tls_pattern_suffix_len(&self.search_buffer);
// Only discard if suffix is shorter than buffer
// This means we have definite non-pattern bytes at the start
if suffix_len < self.search_buffer.len() {
let prefix_len = self.search_buffer.len() - suffix_len;
let prefix = self.search_buffer.split_to(prefix_len).freeze();
self.total_prefix_bytes += prefix.len();
log::debug!(
"FuzzyTlsDeframer: Returning {} byte UnknownPrefix (total {} bytes discarded, kept {} suffix bytes)",
prefix.len(),
self.total_prefix_bytes,
suffix_len
);
return Ok(DeframeResult::UnknownPrefix(prefix));
}
}
}
// Either:
// - Buffer too small (< 6 bytes) - need more data to check for complete pattern
// - Buffer >= 6, no complete pattern, entire buffer might be partial pattern
// Need more data in all cases
return Ok(DeframeResult::NeedData);
}
FuzzyState::Deframing => {
// Already in deframing mode, delegate to inner
match self.inner.next_record()? {
Some(record) => return Ok(DeframeResult::TlsRecord(record)),
None => return Ok(DeframeResult::NeedData),
}
}
}
}
}
/// Get the number of bytes currently buffered
pub fn pending_bytes(&self) -> usize {
match self.state {
FuzzyState::Searching => self.search_buffer.len(),
FuzzyState::Deframing => self.inner.pending_bytes(),
}
}
/// Get a reference to remaining buffered data
pub fn remaining_data(&self) -> &[u8] {
match self.state {
FuzzyState::Searching => &self.search_buffer,
FuzzyState::Deframing => self.inner.remaining_data(),
}
}
/// Clear all buffered data and reset to searching state
pub fn clear(&mut self) {
self.search_buffer.clear();
self.inner.clear();
self.state = FuzzyState::Searching;
self.total_prefix_bytes = 0;
}
/// Deallocate buffers
pub fn deallocate(&mut self) {
self.search_buffer = BytesMut::new();
self.inner.deallocate();
}
}
impl Default for FuzzyTlsDeframer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Helper to create a TLS record with given content type and payload
/// For Handshake records (0x16), prepends handshake type to payload
fn make_tls_record(content_type: u8, payload: &[u8]) -> Vec<u8> {
let mut record = Vec::new();
record.push(content_type);
record.push(0x03); // Version major
record.push(0x03); // Version minor
// For Handshake records, prepend handshake type (0x01 = ClientHello)
if content_type == 0x16 {
let total_len = 1 + payload.len(); // handshake type byte + payload
record.extend_from_slice(&(total_len as u16).to_be_bytes());
record.push(0x01); // ClientHello handshake type
record.extend_from_slice(payload);
} else {
let len = payload.len() as u16;
record.extend_from_slice(&len.to_be_bytes());
record.extend_from_slice(payload);
}
record
}
#[test]
fn test_no_prefix_complete_record() {
let mut deframer = FuzzyTlsDeframer::new();
// TLS record with no prefix
let record = make_tls_record(0x16, b"Hello");
deframer.feed(&record);
// Should immediately transition to Deframing and return the record
match deframer.next_record().unwrap() {
DeframeResult::TlsRecord(r) => assert_eq!(r, &record[..]),
other => panic!("Expected TlsRecord, got {:?}", other),
}
}
#[test]
fn test_prefix_then_complete_record() {
let mut deframer = FuzzyTlsDeframer::new();
// VLESS-like prefix (26 bytes of zeros) + TLS record
let mut data = vec![0x00; 26];
let record = make_tls_record(0x16, b"Hello");
data.extend_from_slice(&record);
deframer.feed(&data);
// First call should return the prefix
match deframer.next_record().unwrap() {
DeframeResult::UnknownPrefix(prefix) => assert_eq!(prefix.len(), 26),
other => panic!("Expected UnknownPrefix, got {:?}", other),
}
// Second call should return the TLS record
match deframer.next_record().unwrap() {
DeframeResult::TlsRecord(r) => assert_eq!(r, &record[..]),
other => panic!("Expected TlsRecord, got {:?}", other),
}
// Third call should return incomplete
match deframer.next_record().unwrap() {
DeframeResult::NeedData => {}
other => panic!("Expected IncompleteRecord, got {:?}", other),
}
}
#[test]
fn test_small_prefix_not_returned_immediately() {
let mut deframer = FuzzyTlsDeframer::new();
// Small prefix (3 bytes) that doesn't match any partial pattern
deframer.feed(&[0x00, 0x01, 0x02]);
// Should wait for more data (buffer <= MAX_PARTIAL_SUFFIX_LEN)
match deframer.next_record().unwrap() {
DeframeResult::NeedData => {}
other => panic!("Expected IncompleteRecord, got {:?}", other),
}
assert_eq!(deframer.pending_bytes(), 3);
}
#[test]
fn test_large_prefix_returned_proactively() {
let mut deframer = FuzzyTlsDeframer::new();
// Large prefix (10 bytes) with no partial pattern at end
deframer.feed(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]);
// Should return prefix, keeping nothing (suffix_len = 0)
match deframer.next_record().unwrap() {
DeframeResult::UnknownPrefix(prefix) => assert_eq!(prefix.len(), 10),
other => panic!("Expected UnknownPrefix, got {:?}", other),
}
assert_eq!(deframer.pending_bytes(), 0);
}
#[test]
fn test_prefix_with_partial_pattern_suffix() {
let mut deframer = FuzzyTlsDeframer::new();
// 10 bytes of junk + partial TLS pattern [0x16, 0x03]
deframer.feed(&[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x16, 0x03,
]);
// Should return 10 bytes as prefix, keep [0x16, 0x03] as potential pattern
match deframer.next_record().unwrap() {
DeframeResult::UnknownPrefix(prefix) => assert_eq!(prefix.len(), 10),
other => panic!("Expected UnknownPrefix, got {:?}", other),
}
assert_eq!(deframer.pending_bytes(), 2);
// Now feed the rest of the pattern
deframer.feed(&[0x01, 0x00, 0x05, 0x01, b'H', b'e', b'l', b'l', b'o']);
// Should get the complete TLS record
match deframer.next_record().unwrap() {
DeframeResult::TlsRecord(r) => {
assert_eq!(r[0], 0x16); // Handshake
assert_eq!(r.len(), 5 + 5); // header + payload
}
other => panic!("Expected TlsRecord, got {:?}", other),
}
}
#[test]
fn test_partial_pattern_alone_not_discarded() {
let mut deframer = FuzzyTlsDeframer::new();
// Just a partial pattern [0x16, 0x03]
deframer.feed(&[0x16, 0x03]);
// Should wait for more data, not discard
match deframer.next_record().unwrap() {
DeframeResult::NeedData => {}
other => panic!("Expected IncompleteRecord, got {:?}", other),
}
assert_eq!(deframer.pending_bytes(), 2);
}
#[test]
fn test_multiple_records_after_prefix() {
let mut deframer = FuzzyTlsDeframer::new();
// Prefix + two TLS records
let mut data = vec![0x00; 26];
let record1 = make_tls_record(0x16, b"First");
let record2 = make_tls_record(0x17, b"Second");
data.extend_from_slice(&record1);
data.extend_from_slice(&record2);
deframer.feed(&data);
// Get prefix
match deframer.next_record().unwrap() {
DeframeResult::UnknownPrefix(prefix) => assert_eq!(prefix.len(), 26),
other => panic!("Expected UnknownPrefix, got {:?}", other),
}
// Get first record
match deframer.next_record().unwrap() {
DeframeResult::TlsRecord(r) => assert_eq!(r, &record1[..]),
other => panic!("Expected TlsRecord, got {:?}", other),
}
// Get second record
match deframer.next_record().unwrap() {
DeframeResult::TlsRecord(r) => assert_eq!(r, &record2[..]),
other => panic!("Expected TlsRecord, got {:?}", other),
}
// No more data
match deframer.next_record().unwrap() {
DeframeResult::NeedData => {}
other => panic!("Expected IncompleteRecord, got {:?}", other),
}
}
#[test]
fn test_split_feed_across_pattern() {
let mut deframer = FuzzyTlsDeframer::new();
// Feed prefix + partial pattern in first call
deframer.feed(&[0x00, 0x01, 0x02, 0x16, 0x03]);
// Buffer too small, wait for more
match deframer.next_record().unwrap() {
DeframeResult::NeedData => {}
other => panic!("Expected IncompleteRecord, got {:?}", other),
}
// Feed rest of pattern + record
deframer.feed(&[0x01, 0x00, 0x05, 0x01, b'H', b'e', b'l', b'l', b'o']);
// Now should get prefix
match deframer.next_record().unwrap() {
DeframeResult::UnknownPrefix(prefix) => assert_eq!(prefix.len(), 3),
other => panic!("Expected UnknownPrefix, got {:?}", other),
}
// Then the record
match deframer.next_record().unwrap() {
DeframeResult::TlsRecord(_) => {}
other => panic!("Expected TlsRecord, got {:?}", other),
}
}
#[test]
fn test_pending_bytes_accounting() {
let mut deframer = FuzzyTlsDeframer::new();
assert_eq!(deframer.pending_bytes(), 0);
// Feed some data
deframer.feed(&[0x00, 0x01, 0x02]);
assert_eq!(deframer.pending_bytes(), 3);
// Feed more to exceed threshold
deframer.feed(&[0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
assert_eq!(deframer.pending_bytes(), 9);
// Extract prefix
match deframer.next_record().unwrap() {
DeframeResult::UnknownPrefix(prefix) => assert_eq!(prefix.len(), 9),
_ => panic!("Expected UnknownPrefix"),
}
assert_eq!(deframer.pending_bytes(), 0);
}
#[test]
fn test_pending_bytes_during_state_transition() {
let mut deframer = FuzzyTlsDeframer::new();
// Feed prefix + complete TLS record
let mut data = vec![0x00; 26]; // VLESS prefix
let record = make_tls_record(0x16, b"Test");
data.extend_from_slice(&record);
deframer.feed(&data);
// Before extracting anything, pending_bytes should be full buffer
assert_eq!(deframer.pending_bytes(), 26 + record.len());
// Extract prefix (state transitions to Deframing, TLS data fed to inner)
match deframer.next_record().unwrap() {
DeframeResult::UnknownPrefix(prefix) => {
assert_eq!(prefix.len(), 26);
}
other => panic!("Expected UnknownPrefix, got {:?}", other),
}
// After extracting prefix, pending_bytes should show TLS record length
// (TLS data is now in inner deframer)
assert_eq!(deframer.pending_bytes(), record.len());
// Extract the TLS record
match deframer.next_record().unwrap() {
DeframeResult::TlsRecord(r) => assert_eq!(r.len(), record.len()),
other => panic!("Expected TlsRecord, got {:?}", other),
}
// After extracting record, buffer should be empty
assert_eq!(deframer.pending_bytes(), 0);
}
#[test]
fn test_clear() {
let mut deframer = FuzzyTlsDeframer::new();
deframer.feed(&[0x00, 0x01, 0x02]);
assert_eq!(deframer.pending_bytes(), 3);
deframer.clear();
assert_eq!(deframer.pending_bytes(), 0);
assert_eq!(deframer.state, FuzzyState::Searching);
}
#[test]
fn test_false_tls_pattern_continues_search() {
let mut deframer = FuzzyTlsDeframer::new();
// Data with [0x16, 0x03] but not a valid TLS pattern (wrong minor version)
// Pattern needs: [0x16, 0x03, 0x01-0x03, ?, ?, 0x01|0x02]
let data = vec![0x00, 0x01, 0x16, 0x03, 0x04, 0x00, 0x00, 0x01]; // 0x04 is invalid minor version
deframer.feed(&data);
// Should return as prefix since pattern search fails
match deframer.next_record().unwrap() {
DeframeResult::UnknownPrefix(prefix) => assert_eq!(prefix.len(), 8),
other => panic!("Expected UnknownPrefix, got {:?}", other),
}
assert_eq!(deframer.state, FuzzyState::Searching);
}
}
+241
View File
@@ -0,0 +1,241 @@
/// Utilities for detecting TLS handshake patterns in Vision protocol
///
/// These functions search for TLS handshake patterns to identify where the actual
/// TLS data begins in buffers that may contain protocol headers (e.g., VLESS headers).
/// Minimum bytes needed for a complete TLS handshake pattern
/// Pattern: [0x16, 0x03, 0x01-0x03, ?, ?, 0x01|0x02] = 6 bytes
pub const MIN_TLS_HANDSHAKE_PATTERN_LEN: usize = 6;
/// Search for TLS handshake pattern using SIMD-optimized memchr
///
/// Returns Some(offset) if pattern found, None otherwise
///
/// Pattern: [0x16, 0x03, 0x01-0x03, ?, ?, 0x01|0x02]
/// - Byte 0: 0x16 (Handshake record type)
/// - Byte 1: 0x03 (TLS version major)
/// - Byte 2: 0x01-0x03 (TLS version minor: 1.0, 1.1, 1.2, or 1.3 compat)
/// - Bytes 3-4: [any] (Record length)
/// - Byte 5: 0x01 or 0x02 (ClientHello or ServerHello)
use memchr::memmem;
pub fn find_tls_handshake_start(data: &[u8]) -> Option<usize> {
if data.len() < MIN_TLS_HANDSHAKE_PATTERN_LEN {
return None;
}
// Use SIMD-optimized search for the 2-byte prefix [0x16, 0x03]
// This is much faster than naive byte-by-byte scanning
let finder = memmem::Finder::new(&[0x16, 0x03]);
let mut pos = 0;
while let Some(offset) = finder.find(&data[pos..]) {
let absolute_offset = pos + offset;
// Check if we have enough bytes for full pattern
if absolute_offset + MIN_TLS_HANDSHAKE_PATTERN_LEN > data.len() {
return None;
}
// Verify remaining bytes manually
let candidate = &data[absolute_offset..];
if (candidate[2] >= 0x01 && candidate[2] <= 0x03) // TLS minor version
&& (candidate[5] == 0x01 || candidate[5] == 0x02)
// ClientHello or ServerHello
{
return Some(absolute_offset);
}
// Not a match, continue searching after this position
pos = absolute_offset + 1;
}
None
}
/// Optimized suffix matching using decision tree based on last byte
///
/// Returns the length of suffix to keep (0-5 bytes)
///
/// Uses a decision tree: checks the last byte first to narrow down possibilities,
/// then validates from longest to shortest match for that last byte.
///
/// This is used to minimize buffering when searching for TLS patterns - if the data
/// doesn't end with a potential pattern prefix, we can flush it immediately.
///
/// If the data is shorter than the maximum pattern length (5 bytes), returns the
/// full buffer length since we can't make assumptions about partial data.
pub fn get_partial_tls_pattern_suffix_len(data: &[u8]) -> usize {
if data.is_empty() {
return 0;
}
let len = data.len();
// If buffer is smaller than max pattern length, return full length
// We can't make assumptions about what might grow into a pattern
if len <= 5 {
return len;
}
let last = data[len - 1];
match last {
// Last byte is 0x16 - can only be position 0 of pattern
0x16 => {
// Pattern: [0x16, ...]
// Only need to check 1-byte suffix
1
}
// Last byte is 0x03 - could be position 1 (major version) or 2 (minor version)
0x03 => {
// Check longest first (3-byte), fall back to 2-byte
if len >= 3 && data[len - 3] == 0x16 && data[len - 2] == 0x03 {
3 // [0x16, 0x03, 0x03]
} else if len >= 2 && data[len - 2] == 0x16 {
2 // [0x16, 0x03]
} else {
0
}
}
// Last byte is 0x01 or 0x02 - could be position 2 (minor version if 0x01)
// or position 3/4 (length bytes - wildcard) or position 5 (handshake type)
0x01 | 0x02 => {
// Check from longest to shortest: 5-byte → 4-byte → 3-byte
if len >= 5
&& data[len - 5] == 0x16
&& data[len - 4] == 0x03
&& (data[len - 3] >= 0x01 && data[len - 3] <= 0x03)
{
5 // [0x16, 0x03, 0x01-0x03, ?, 0x01/0x02]
} else if len >= 4
&& data[len - 4] == 0x16
&& data[len - 3] == 0x03
&& (data[len - 2] >= 0x01 && data[len - 2] <= 0x03)
{
4 // [0x16, 0x03, 0x01-0x03, 0x01/0x02]
} else if len >= 3 && data[len - 3] == 0x16 && data[len - 2] == 0x03 && last == 0x01 {
// Only valid if last byte is 0x01 (valid minor version)
// 0x02 is ServerHello handshake type, not minor version
3 // [0x16, 0x03, 0x01]
} else {
0
}
}
// Any other byte - can only be in wildcard length field (position 3 or 4)
_ => {
// Check 5-byte, then 4-byte
if len >= 5
&& data[len - 5] == 0x16
&& data[len - 4] == 0x03
&& (data[len - 3] >= 0x01 && data[len - 3] <= 0x03)
{
5 // [0x16, 0x03, 0x01-0x03, ?, last]
} else if len >= 4
&& data[len - 4] == 0x16
&& data[len - 3] == 0x03
&& (data[len - 2] >= 0x01 && data[len - 2] <= 0x03)
{
4 // [0x16, 0x03, 0x01-0x03, last]
} else {
0
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_tls_handshake_clienthello() {
// TLS 1.3 ClientHello: [0x16, 0x03, 0x03, length_bytes, 0x01]
let data = vec![0x16, 0x03, 0x03, 0x00, 0x10, 0x01];
assert_eq!(find_tls_handshake_start(&data), Some(0));
}
#[test]
fn test_find_tls_handshake_serverhello() {
// TLS 1.3 ServerHello: [0x16, 0x03, 0x03, length_bytes, 0x02]
let data = vec![0x16, 0x03, 0x03, 0x00, 0x10, 0x02];
assert_eq!(find_tls_handshake_start(&data), Some(0));
}
#[test]
fn test_find_tls_handshake_with_prefix() {
// VLESS header (26 bytes) + TLS ClientHello
let mut data = vec![0x00; 26]; // VLESS header
data.extend_from_slice(&[0x16, 0x03, 0x01, 0x02, 0x00, 0x01]); // TLS 1.0 ClientHello
assert_eq!(find_tls_handshake_start(&data), Some(26));
}
#[test]
fn test_find_tls_handshake_not_found() {
let data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05];
assert_eq!(find_tls_handshake_start(&data), None);
}
#[test]
fn test_find_tls_handshake_too_short() {
let data = vec![0x16, 0x03, 0x03];
assert_eq!(find_tls_handshake_start(&data), None);
}
#[test]
fn test_suffix_len_no_pattern() {
// Small buffer (≤ 5 bytes) returns full length
let data = vec![0x00, 0x01, 0x02, 0x03];
assert_eq!(get_partial_tls_pattern_suffix_len(&data), 4);
}
#[test]
fn test_suffix_len_single_byte() {
// Small buffer (≤ 5 bytes) returns full length
let data = vec![0x00, 0x01, 0x16];
assert_eq!(get_partial_tls_pattern_suffix_len(&data), 3);
}
#[test]
fn test_suffix_len_two_bytes() {
// Small buffer (≤ 5 bytes) returns full length
let data = vec![0x00, 0x01, 0x16, 0x03];
assert_eq!(get_partial_tls_pattern_suffix_len(&data), 4);
}
#[test]
fn test_suffix_len_three_bytes() {
// Small buffer (≤ 5 bytes) returns full length
let data = vec![0x00, 0x16, 0x03, 0x01];
assert_eq!(get_partial_tls_pattern_suffix_len(&data), 4);
}
#[test]
fn test_suffix_len_five_bytes() {
// Exactly 5 bytes, returns full length
let data = vec![0x16, 0x03, 0x03, 0xAB, 0x01];
assert_eq!(get_partial_tls_pattern_suffix_len(&data), 5);
}
#[test]
fn test_suffix_len_false_positive() {
// Exactly 5 bytes, returns full length (even if not valid pattern)
let data = vec![0x16, 0x03, 0x04, 0x00, 0x01]; // 0x04 is not valid TLS minor version
assert_eq!(get_partial_tls_pattern_suffix_len(&data), 5);
}
#[test]
fn test_suffix_len_large_buffer_no_pattern() {
// Large buffer (> 5 bytes) with no pattern at end
let data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05];
assert_eq!(get_partial_tls_pattern_suffix_len(&data), 0);
}
#[test]
fn test_suffix_len_large_buffer_with_pattern() {
// Large buffer (> 5 bytes) ending with partial pattern
let data = vec![0x00, 0x01, 0x02, 0x03, 0x16, 0x03];
assert_eq!(get_partial_tls_pattern_suffix_len(&data), 2);
}
}
+512
View File
@@ -0,0 +1,512 @@
use crate::buf_reader::BufReader;
// TLS record types
const TLS_CONTENT_TYPE_HANDSHAKE: u8 = 0x16;
// TLS handshake types
const TLS_HANDSHAKE_TYPE_CLIENT_HELLO: u8 = 0x01;
const TLS_HANDSHAKE_TYPE_SERVER_HELLO: u8 = 0x02;
// TLS ciphers
const TLS13_CIPHER_AES_128_CCM_8_SHA256: u16 = 0x1305;
/// TLS extension type for supported_versions
const TLS_EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
/// VISION protocol filter state
///
/// This structure maintains only the state needed for TLS record filtering
/// and detection, tracking TLS patterns and remaining records to analyze.
#[derive(Debug)]
pub struct VisionFilter {
/// Number of records remaining to analyze for TLS patterns
record_filter_count: usize,
/// Any TLS version detected (ClientHello seen)
is_tls: bool,
/// TLS 1.2 or above detected (ServerHello seen)
is_tls12_or_above: bool,
/// Whether XTLS can be enabled (TLS 1.3 + supported cipher)
supports_xtls: bool,
}
impl VisionFilter {
/// Create a new VisionFilter with default record count
pub fn new() -> Self {
Self::new_with_record_count(8)
}
fn new_with_record_count(record_count: usize) -> Self {
Self {
record_filter_count: record_count,
supports_xtls: false,
is_tls: false,
is_tls12_or_above: false,
}
}
/// Check if filtering is complete
pub fn is_filtering(&self) -> bool {
self.record_filter_count > 0
}
pub fn decrement_filter_count(&mut self) {
self.record_filter_count = self.record_filter_count.saturating_sub(1);
}
pub fn stop_filtering(&mut self, reason: String) {
log::debug!("VISION: Stopping filtering - {}", reason);
self.record_filter_count = 0;
}
/// Returns true if any TLS version has been detected (ClientHello seen)
pub fn is_tls(&self) -> bool {
self.is_tls
}
/// Returns true if TLS 1.2 or above has been detected (ServerHello seen)
pub fn is_tls12_or_above(&self) -> bool {
self.is_tls12_or_above
}
/// Returns true if XTLS can be enabled (TLS 1.3 + supported cipher)
pub fn supports_xtls(&self) -> bool {
self.supports_xtls
}
/// Analyze a complete TLS record for patterns and update state accordingly
///
/// Caller must extract complete TLS records using TlsDeframer before calling this.
/// This method detects ClientHello, ServerHello, TLS version, cipher suite, and
/// ApplicationData to determine when to switch to direct copy mode.
///
/// This is a public method now - VisionStream will call it after recordizing.
pub fn filter_record(&mut self, data: &[u8]) {
if self.record_filter_count == 0 {
return;
}
self.record_filter_count = self.record_filter_count.saturating_sub(1);
if data.len() < 5 {
// invalid record length
self.stop_filtering(format!("invalid record length: {}", data.len()));
return;
}
// Detect ClientHello - minimal check for TLS handshake record
if !self.is_tls
&& data.len() >= 6
&& data[0] == TLS_CONTENT_TYPE_HANDSHAKE
&& data[1] == 0x03 // TLS version major
&& data[5] == TLS_HANDSHAKE_TYPE_CLIENT_HELLO
{
self.is_tls = true;
log::debug!("VISION: Detected TLS ClientHello");
}
// Detect and parse ServerHello
if !self.is_tls12_or_above
&& data.len() >= 6
&& data[0] == TLS_CONTENT_TYPE_HANDSHAKE
&& data[1] == 0x03 // TLS version major
&& data[2] == 0x03 // TLS version minor (1.2)
&& data[5] == TLS_HANDSHAKE_TYPE_SERVER_HELLO
{
self.is_tls12_or_above = true;
self.is_tls = true;
// Parse ServerHello properly - delegate to vision_tls_util
match parse_server_hello(data) {
Ok(parsed) => {
log::debug!(
"VISION: Detected TLS cipher suite: 0x{:04x}",
parsed.cipher_suite
);
if parsed.has_tls13_version {
log::debug!("VISION: Detected TLS 1.3 via supported_versions extension");
// Check if cipher is supported for XTLS
// Following Xray-core: all TLS 1.3 ciphers EXCEPT 0x1305 support XTLS
if parsed.cipher_suite == TLS13_CIPHER_AES_128_CCM_8_SHA256 {
log::warn!(
"VISION: TLS 1.3 detected but cipher 0x{:04x} (TLS_AES_128_CCM_8_SHA256) not supported for XTLS",
parsed.cipher_suite
);
} else {
// Assume all other TLS 1.3 ciphers support XTLS
self.supports_xtls = true;
log::debug!(
"VISION: TLS 1.3 with supported cipher 0x{:04x} - XTLS enabled",
parsed.cipher_suite
);
}
// Stop filtering early
self.stop_filtering("TLS 1.3 handshake detected".to_string());
}
}
Err(e) => {
self.stop_filtering(format!("invalid ServerHello: {}", e));
}
}
}
}
}
/// Result of parsing a ServerHello message
struct ParsedServerHello {
pub cipher_suite: u16,
pub has_tls13_version: bool,
}
/// Parse ServerHello structure following RFC 8446 format
///
/// ServerHello format:
/// - TLS Record Header (5 bytes):
/// - ContentType (1 byte): 0x16 (Handshake)
/// - ProtocolVersion (2 bytes): 0x0303 (TLS 1.2 for compatibility)
/// - Length (2 bytes): payload length
/// - Handshake Header (4 bytes):
/// - HandshakeType (1 byte): 0x02 (ServerHello)
/// - Length (3 bytes): message length
/// - ServerHello Content:
/// - LegacyVersion (2 bytes): 0x0303 (TLS 1.2)
/// - Random (32 bytes): server random
/// - LegacySessionIdEcho (1 + N bytes): session ID length + session ID
/// - CipherSuite (2 bytes): selected cipher
/// - LegacyCompressionMethod (1 byte): 0x00
/// - Extensions (2 + N bytes): extensions length + extensions
///
/// TODO: consolidate with ShadowTLS version
fn parse_server_hello(data: &[u8]) -> std::io::Result<ParsedServerHello> {
// Minimum size: 5 (record header) + 4 (handshake header) + 2 (version) + 32 (random) + 1 (session id len) + 2 (cipher) + 1 (compression) = 47
if data.len() < 47 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"ServerHello too short",
));
}
// Skip TLS record header (5 bytes) and handshake type (1 byte)
let mut reader = BufReader::new(&data[6..]);
// Read handshake message length (3 bytes)
let _message_len = reader.read_u24_be()?;
// Read legacy version (2 bytes) - should be 0x0303 for TLS 1.2
let legacy_version_major = reader.read_u8()?;
let legacy_version_minor = reader.read_u8()?;
if legacy_version_major != 3 || legacy_version_minor != 3 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"expected TLS 1.2 (major/minor 3.3), got major/minor {}.{}",
legacy_version_major, legacy_version_minor
),
));
}
// Skip server random (32 bytes)
reader.skip(32)?;
// Read session ID length and skip session ID
let session_id_len = reader.read_u8()?;
if session_id_len > 0 {
reader.skip(session_id_len as usize)?;
}
// Read cipher suite (2 bytes)
let cipher_suite = reader.read_u16_be()?;
// Skip compression method (1 byte)
reader.skip(1)?;
// Check if we have extensions
if reader.is_consumed() {
// No extensions, assume not TLS 1.3
return Ok(ParsedServerHello {
cipher_suite,
has_tls13_version: false,
});
}
// Read extensions length
let extensions_len = match reader.read_u16_be() {
Ok(len) => len as usize,
Err(_) => {
// No extensions
return Ok(ParsedServerHello {
cipher_suite,
has_tls13_version: false,
});
}
};
// Parse extensions to find supported_versions
let mut has_tls13_version = false;
let extensions_data = match reader.read_slice(extensions_len) {
Ok(data) => data,
Err(_) => {
// Invalid extensions length
return Ok(ParsedServerHello {
cipher_suite,
has_tls13_version: false,
});
}
};
let mut ext_reader = BufReader::new(extensions_data);
while !ext_reader.is_consumed() {
let extension_type = match ext_reader.read_u16_be() {
Ok(t) => t,
Err(_) => break,
};
let extension_len = match ext_reader.read_u16_be() {
Ok(l) => l as usize,
Err(_) => break,
};
if extension_type == TLS_EXT_SUPPORTED_VERSIONS {
// supported_versions extension found
// In ServerHello, this is 2 bytes: the selected version
if extension_len == 2 {
match ext_reader.read_slice(2) {
Ok(version_bytes) => {
// TLS 1.3 is 0x0304
if version_bytes[0] == 0x03 && version_bytes[1] == 0x04 {
has_tls13_version = true;
break;
}
}
Err(_) => break,
}
} else if ext_reader.skip(extension_len).is_err() {
break;
}
} else if ext_reader.skip(extension_len).is_err() {
// Skip unknown extension
break;
}
}
Ok(ParsedServerHello {
cipher_suite,
has_tls13_version,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vision_filter_initialization() {
let filter = VisionFilter::new();
assert!(filter.is_filtering());
assert!(!filter.supports_xtls());
assert!(!filter.is_tls());
assert!(!filter.is_tls12_or_above());
}
#[test]
fn test_vision_filter_with_custom_record_count() {
let filter = VisionFilter::new_with_record_count(5);
assert!(filter.is_filtering());
assert!(!filter.supports_xtls());
assert!(!filter.is_tls());
}
#[test]
fn test_stop_filtering() {
let mut filter = VisionFilter::new();
assert!(filter.is_filtering());
filter.stop_filtering("test".to_string());
assert!(!filter.is_filtering());
}
#[test]
fn test_detect_client_hello() {
let mut filter = VisionFilter::new();
// Mock ClientHello record
let client_hello = vec![
0x16, 0x03, 0x01, // TLS Handshake, version 3.1
0x00, 0x05, // Length: 5
0x01, // ClientHello type
0x00, 0x00, 0x00, // Rest of handshake
];
filter.filter_record(&client_hello);
assert!(filter.is_tls());
assert!(!filter.supports_xtls()); // Not enabled until TLS 1.3 ServerHello
}
#[test]
fn test_detect_server_hello_tls12() {
let mut filter = VisionFilter::new();
// Mock ServerHello record (TLS 1.2)
let server_hello = vec![
0x16, 0x03, 0x03, // TLS Handshake, version 3.3 (TLS 1.2)
0x00, 0x05, // Length: 5
0x02, // ServerHello type
0x00, 0x00, 0x00, // Rest of handshake
];
filter.filter_record(&server_hello);
assert!(filter.is_tls());
assert!(filter.is_tls12_or_above());
}
#[test]
fn test_detect_application_data() {
let mut filter = VisionFilter::new();
// First detect TLS
let client_hello = vec![
0x16, 0x03, 0x01, // TLS Handshake
0x00, 0x05, // Length: 5
0x01, // ClientHello type
0x00, 0x00, 0x00,
];
filter.filter_record(&client_hello);
// Then ApplicationData
let app_data = vec![
0x17, 0x03, 0x03, // ApplicationData, TLS 1.2
0x00, 0x05, // Length: 5
0x00, 0x00, 0x00, 0x00, 0x00, // Encrypted data
];
filter.filter_record(&app_data);
assert!(filter.is_tls());
assert!(filter.is_filtering());
}
#[test]
fn test_record_count_decrements() {
let mut filter = VisionFilter::new_with_record_count(3);
let dummy_record = vec![0x16, 0x03, 0x01, 0x00, 0x05, 0x00];
filter.filter_record(&dummy_record);
assert!(filter.is_filtering());
filter.filter_record(&dummy_record);
assert!(filter.is_filtering());
filter.filter_record(&dummy_record);
assert!(!filter.is_filtering());
// Further calls should not decrement
filter.filter_record(&dummy_record);
assert!(!filter.is_filtering());
}
#[test]
fn test_filter_record_no_processing_when_complete() {
let mut filter = VisionFilter::new();
filter.stop_filtering("test".to_string());
let data = vec![0x16, 0x03, 0x01, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00];
// Should return immediately without processing
filter.filter_record(&data);
assert!(!filter.is_tls()); // State shouldn't change
assert!(!filter.is_filtering());
}
#[test]
fn test_short_record_ignored() {
let mut filter = VisionFilter::new();
let short_record = vec![0x16, 0x03, 0x01]; // Less than 6 bytes
filter.filter_record(&short_record);
assert!(!filter.is_tls()); // Should not detect anything
assert!(!filter.is_filtering()); // Filtering stopped due to invalid record
}
#[test]
fn test_non_tls_record_ignored() {
let mut filter = VisionFilter::new();
let non_tls_record = vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
filter.filter_record(&non_tls_record);
assert!(!filter.is_tls());
assert!(!filter.supports_xtls());
assert!(filter.is_filtering());
}
#[test]
fn test_filter_record_client_hello() {
let mut filter = VisionFilter::new();
// Simulate ClientHello
let client_hello = vec![0x16, 0x03, 0x01, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00];
filter.filter_record(&client_hello);
assert!(filter.is_tls());
assert!(!filter.supports_xtls()); // Not optimistically enabled anymore
}
#[test]
fn test_filter_record_server_hello() {
let mut filter = VisionFilter::new();
// Simulate ServerHello
let server_hello = vec![0x16, 0x03, 0x03, 0x00, 0x05, 0x02, 0x00, 0x00, 0x00, 0x00];
filter.filter_record(&server_hello);
assert!(filter.is_tls());
assert!(filter.is_tls12_or_above());
}
#[test]
fn test_client_hello_only_detected_once() {
let mut filter = VisionFilter::new();
let client_hello = vec![0x16, 0x03, 0x01, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00];
filter.filter_record(&client_hello);
assert!(filter.is_tls());
assert!(filter.is_filtering());
// Send again - should not reset state
filter.filter_record(&client_hello);
assert!(filter.is_tls());
assert!(filter.is_filtering());
}
#[test]
fn test_server_hello_only_detected_once() {
let mut filter = VisionFilter::new();
let server_hello = vec![0x16, 0x03, 0x03, 0x00, 0x05, 0x02, 0x00, 0x00, 0x00, 0x00];
filter.filter_record(&server_hello);
assert!(filter.is_tls12_or_above());
// Note: This mock ServerHello is too short and will cause parse_server_hello to fail,
// which stops filtering. This is expected behavior - invalid records stop filtering.
assert!(!filter.is_filtering());
}
}
+67
View File
@@ -0,0 +1,67 @@
use bytes::{BufMut, Bytes, BytesMut};
use rand::Rng;
// Padding parameters
const LONG_PADDING_MIN: usize = 900;
const LONG_PADDING_RANDOM_MAX: usize = 500;
const SHORT_PADDING_RANDOM_MAX: usize = 256;
const MAX_PADDING_SIZE: usize = 8171; // buf.Size - 21
pub fn pad_with_uuid_and_command(data: &[u8], uuid: &[u8; 16], command: u8, is_tls: bool) -> Bytes {
pad(data, Some(uuid), command, is_tls)
}
//
pub fn pad_with_command(data: &[u8], command: u8, is_tls: bool) -> Bytes {
pad(data, None, command, is_tls)
}
fn pad(data: &[u8], uuid: Option<&[u8; 16]>, command: u8, is_tls: bool) -> Bytes {
let content_len = data.len() as u16;
let padding_len = calculate_padding_length(content_len as usize, is_tls);
let uuid_len = if uuid.is_some() { 16 } else { 0 };
let total_size = uuid_len + 1 + 2 + 2 + data.len() + padding_len;
let mut output = BytesMut::with_capacity(total_size);
if let Some(uuid) = uuid {
output.put_slice(uuid);
}
output.put_u8(command);
output.put_u16(content_len);
output.put_u16(padding_len as u16);
output.put_slice(data);
// Write random padding
if padding_len > 0 {
let padding_start = output.len();
output.resize(padding_start + padding_len, 0);
rand::rng().fill(&mut output[padding_start..]);
}
output.freeze()
}
/// Calculate padding length based on content size and TLS detection
fn calculate_padding_length(content_len: usize, is_tls: bool) -> usize {
let mut rng = rand::rng();
// Calculate maximum allowable padding (avoid overflow)
// Matches Xray-core logic: buf.Size - 21 - contentLen
// MAX_PADDING_SIZE is already (buf.Size - 21), so we only subtract content_len
let max_allowable = MAX_PADDING_SIZE.saturating_sub(content_len);
if is_tls && content_len < LONG_PADDING_MIN {
// Long padding for TLS handshake phase
let random_part = rng.random_range(0..LONG_PADDING_RANDOM_MAX);
let padding = LONG_PADDING_MIN
.saturating_sub(content_len)
.saturating_add(random_part);
std::cmp::min(padding, max_allowable)
} else {
// Short random padding
let padding = rng.random_range(0..SHORT_PADDING_RANDOM_MAX);
std::cmp::min(padding, max_allowable)
}
}
File diff suppressed because it is too large Load Diff
+547
View File
@@ -0,0 +1,547 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnpadCommand {
Continue = 0,
End = 1,
Direct = 2,
}
impl TryFrom<u8> for UnpadCommand {
type Error = std::io::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(UnpadCommand::Continue),
1 => Ok(UnpadCommand::End),
2 => Ok(UnpadCommand::Direct),
_ => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid padding command: {}", value),
)),
}
}
}
/// Result of a completed padding block
#[derive(Debug, Default)]
pub struct UnpadResult {
/// Content data extracted from the block
pub content: Vec<u8>,
/// The command from the block, or None if this is not XTLS data
pub command: Option<UnpadCommand>,
}
#[derive(Debug, Clone)]
enum UnpadState {
/// Initial state: expecting UUID (16 bytes) before first command
Initial { expected_uuid: [u8; 16] },
/// Reading the command byte (first of 5 header bytes)
ReadingCommand,
/// Reading content length (2 bytes)
ReadingContentLength {
command: UnpadCommand,
first_byte: Option<u8>,
},
/// Reading padding length (2 bytes)
ReadingPaddingLength {
command: UnpadCommand,
content_len: u16,
first_byte: Option<u8>,
},
/// Reading content data
ReadingContent {
command: UnpadCommand,
partial_content: Vec<u8>,
remaining_content_len: u16,
padding_len: u16,
},
/// Reading (skipping) padding data
ReadingPadding {
command: UnpadCommand,
content: Vec<u8>,
remaining_padding_len: u16,
},
/// Finished processing (End or Direct command completed)
Done,
}
#[derive(Debug, Clone)]
pub struct VisionUnpadder {
state: UnpadState,
first_block: bool,
/// Reusable buffer for accumulated content to avoid allocation on every unpad() call
accumulated_buffer: Vec<u8>,
}
impl VisionUnpadder {
pub fn new(expected_uuid: [u8; 16]) -> Self {
Self {
state: UnpadState::Initial { expected_uuid },
first_block: true,
accumulated_buffer: Vec::new(),
}
}
/// Process input data and extract padding blocks
/// Returns UnpadResult when a complete block is parsed, empty result if more data is needed
/// Accumulates content across Continue commands and returns on End/Direct or when needing more data
pub fn unpad(&mut self, mut data: &[u8]) -> std::io::Result<UnpadResult> {
// Reuse the accumulated buffer - clear but keep capacity
self.accumulated_buffer.clear();
log::debug!(
"UNPADDER: unpad() called with {} bytes, state before: {:?}, first_block: {}",
data.len(),
self.state,
self.first_block
);
loop {
match &mut self.state {
UnpadState::Initial { expected_uuid } => {
if data.len() < 16 {
return Ok(UnpadResult::default()); // Need more data
}
// Check if this is XTLS padding data
if &data[..16] != expected_uuid {
// Not XTLS padding data
return Ok(UnpadResult {
content: data.to_vec(),
command: None,
});
}
// Consume UUID
data = &data[16..];
self.state = UnpadState::ReadingCommand;
}
UnpadState::ReadingCommand => {
if data.is_empty() {
// Out of data
if !self.first_block {
// We've completed at least one block, return what we have
return Ok(UnpadResult {
content: std::mem::take(&mut self.accumulated_buffer),
command: Some(UnpadCommand::Continue),
});
}
debug_assert!(self.accumulated_buffer.is_empty(), "self.accumulated_buffer should be empty when returning default in ReadingCommand");
return Ok(UnpadResult::default());
}
let command_byte = data[0];
data = &data[1..];
let command = UnpadCommand::try_from(command_byte)?;
self.state = UnpadState::ReadingContentLength {
command,
first_byte: None,
};
}
UnpadState::ReadingContentLength {
command,
first_byte,
} => {
if data.is_empty() {
// Out of data - return accumulated content if any
if !self.first_block {
return Ok(UnpadResult {
content: std::mem::take(&mut self.accumulated_buffer),
command: Some(UnpadCommand::Continue),
});
}
debug_assert!(self.accumulated_buffer.is_empty(), "self.accumulated_buffer should be empty when returning default in ReadingContentLength");
return Ok(UnpadResult::default());
}
match first_byte {
None => {
*first_byte = Some(data[0]);
data = &data[1..];
}
Some(high_byte) => {
let low_byte = data[0];
data = &data[1..];
let content_len = ((*high_byte as u16) << 8) | (low_byte as u16);
self.state = UnpadState::ReadingPaddingLength {
command: *command,
content_len,
first_byte: None,
};
}
}
}
UnpadState::ReadingPaddingLength {
command,
content_len,
first_byte,
} => {
if data.is_empty() {
// Out of data - return accumulated content if any
if !self.first_block {
return Ok(UnpadResult {
content: std::mem::take(&mut self.accumulated_buffer),
command: Some(UnpadCommand::Continue),
});
}
debug_assert!(self.accumulated_buffer.is_empty(), "self.accumulated_buffer should be empty when returning default in ReadingPaddingLength");
return Ok(UnpadResult::default());
}
match first_byte {
None => {
*first_byte = Some(data[0]);
data = &data[1..];
}
Some(high_byte) => {
let low_byte = data[0];
data = &data[1..];
let padding_len = ((*high_byte as u16) << 8) | (low_byte as u16);
log::debug!("UNPADDER: Parsed header - command={:?}, content_len={}, padding_len={}", *command, *content_len, padding_len);
// Pre-allocate based on min of content_len and available data
// (we eagerly return when data runs out, so we may not need full content_len)
let prealloc_size = (*content_len as usize).min(data.len());
self.state = UnpadState::ReadingContent {
command: *command,
partial_content: Vec::with_capacity(prealloc_size),
remaining_content_len: *content_len,
padding_len,
};
}
}
}
UnpadState::ReadingContent {
command,
partial_content,
remaining_content_len,
padding_len,
} => {
if *remaining_content_len > 0 {
if data.is_empty() {
// Out of data - return accumulated content if any.
// We need to return everything we have including the partial content
// else some clients will not return - see below.
self.accumulated_buffer.append(partial_content);
if !self.first_block {
return Ok(UnpadResult {
content: std::mem::take(&mut self.accumulated_buffer),
command: Some(UnpadCommand::Continue),
});
} else {
return Ok(UnpadResult {
content: std::mem::take(&mut self.accumulated_buffer),
command: None,
});
}
}
let to_read = (*remaining_content_len as usize).min(data.len());
partial_content.extend_from_slice(&data[..to_read]);
data = &data[to_read..];
*remaining_content_len -= to_read as u16;
}
if *remaining_content_len == 0 {
log::debug!("UNPADDER: Content read complete ({} bytes), accumulated at {}, transitioning to padding",
partial_content.len(), self.accumulated_buffer.len());
let content = std::mem::take(partial_content);
self.state = UnpadState::ReadingPadding {
command: *command,
content,
remaining_padding_len: *padding_len,
};
}
}
UnpadState::ReadingPadding {
command,
content,
remaining_padding_len,
} => {
if *remaining_padding_len > 0 {
if data.is_empty() {
// We need to return _all_ the content including from this incomplete
// block because some clients will not write the remaining padding until we've
// generated a response to the current content. Waiting for the remaining
// padding before returning the current block's content caused a
// deadlock. This is also the behavior on the official client:
// https://github.com/XTLS/Xray-core/blob/9f5dcb15910aadc7ef450514747576827a389853/proxy/proxy.go#L518
//
// It's important that we do not return the command until the rest of
// the padding has been consumed so that unpad() consumes all of the
// padded data before we switch modes due to a Direct or End command.
//
// Note that append moves all of `content` and leaves it empty.
self.accumulated_buffer.append(content);
log::debug!("UNPADDER: Out of data in ReadingPadding with {} padding bytes remaining, returning {} bytes (first_block={})",
*remaining_padding_len, self.accumulated_buffer.len(), self.first_block);
if !self.first_block {
return Ok(UnpadResult {
content: std::mem::take(&mut self.accumulated_buffer),
command: Some(UnpadCommand::Continue),
});
} else {
return Ok(UnpadResult {
content: std::mem::take(&mut self.accumulated_buffer),
command: None,
});
}
}
let to_skip = (*remaining_padding_len as usize).min(data.len());
data = &data[to_skip..];
*remaining_padding_len -= to_skip as u16;
}
if *remaining_padding_len == 0 {
self.first_block = false;
// Block complete (all padding skipped)
self.accumulated_buffer.append(content);
log::debug!("UNPADDER: Padding complete for command {:?}, {} accumulated bytes total", *command, self.accumulated_buffer.len());
match *command {
UnpadCommand::Continue => {
// Continue to next block
log::debug!("UNPADDER: Continue command - transitioning to ReadingCommand for next block");
self.state = UnpadState::ReadingCommand;
}
end_or_direct_command => {
// Return all accumulated content with final command
// Any remaining data in the input slice is unconsumed (comes after the final padding)
// Append it to content so caller gets everything in one place
let remaining_len = data.len();
self.accumulated_buffer.extend_from_slice(data);
log::debug!("UNPADDER: {:?} command - transitioning to Done state, returning {} bytes content ({} from padding, {} remaining after)",
end_or_direct_command, self.accumulated_buffer.len(), self.accumulated_buffer.len() - remaining_len, remaining_len);
self.state = UnpadState::Done;
return Ok(UnpadResult {
content: std::mem::take(&mut self.accumulated_buffer),
command: Some(end_or_direct_command),
});
}
}
}
}
UnpadState::Done => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Already in UnpadState::Done",
));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_padding_state_basic() {
let user_uuid = [0u8; 16];
let mut state = VisionUnpadder::new(user_uuid);
let data = vec![
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // UUID
1, // command (End)
0, 5, // content length = 5
0, 3, // padding length = 3
1, 2, 3, 4, 5, // content
0, 0, 0, // padding
];
let result = state.unpad(&data[..]).unwrap();
assert_eq!(result.content, vec![1, 2, 3, 4, 5]);
assert_eq!(result.command, Some(UnpadCommand::End));
assert!(matches!(state.state, UnpadState::Done));
}
#[test]
fn test_padding_command_continue() {
let user_uuid = [1u8; 16];
let mut state = VisionUnpadder::new(user_uuid);
let mut data = vec![1u8; 16]; // UUID
data.extend_from_slice(&[
0, // command (Continue)
0, 3, // content length = 3
0, 2, // padding length = 2
10, 11, 12, // content
0, 0, // padding
0, // command (Continue)
0, 2, // content length = 2
0, 1, // padding length = 1
20, 21, // content
0, // padding
]);
// Now it processes both Continue blocks and accumulates content
let result = state.unpad(&data[..]).unwrap();
assert_eq!(result.content, vec![10, 11, 12, 20, 21]);
assert_eq!(result.command, Some(UnpadCommand::Continue));
assert!(!matches!(state.state, UnpadState::Done));
}
#[test]
fn test_non_xtls_data() {
let user_uuid = [0u8; 16];
let mut state = VisionUnpadder::new(user_uuid);
// Data that's less than 16 bytes (too short to check UUID)
let short_data = vec![1, 2, 3, 4, 5];
let result = state.unpad(&short_data[..]).unwrap();
assert!(result.content.is_empty()); // Need more data
assert!(result.command.is_none());
// Data that's exactly 16 bytes but doesn't match UUID
let non_xtls_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
let result = state.unpad(&non_xtls_data[..]).unwrap();
assert_eq!(result.content, non_xtls_data);
assert_eq!(result.command, None);
// State remains Initial (doesn't transition to Done for non-XTLS data)
assert!(!matches!(state.state, UnpadState::Done));
}
#[test]
fn test_direct_command() {
let user_uuid = [2u8; 16];
let mut state = VisionUnpadder::new(user_uuid);
let mut data = vec![2u8; 16]; // UUID
data.extend_from_slice(&[
2, // command (Direct)
0, 3, // content length = 3
0, 2, // padding length = 2
10, 11, 12, // content
0, 0, // padding
99, 98, 97, // remaining data after Direct
]);
// Process the padded block
let result = state.unpad(&data[..]).unwrap();
// Direct command appends remaining data (99, 98, 97) to content
assert_eq!(result.content, vec![10, 11, 12, 99, 98, 97]);
assert_eq!(result.command, Some(UnpadCommand::Direct));
assert!(matches!(state.state, UnpadState::Done))
}
#[test]
fn test_incremental_processing() {
let user_uuid = [3u8; 16];
let mut state = VisionUnpadder::new(user_uuid);
// First chunk: UUID + partial command
let chunk1 = {
let mut d = vec![3u8; 16];
d.extend_from_slice(&[1, 0, 3]); // command (End), content_len high byte, partial
d
};
let result1 = state.unpad(&chunk1[..]).unwrap();
assert!(result1.content.is_empty()); // Not enough data yet
assert!(result1.command.is_none());
assert!(!matches!(state.state, UnpadState::Done));
// Second chunk: rest of command + content + padding
let chunk2 = vec![
0, 2, // rest of content length (=3), padding length = 2
10, 11, 12, // content
0, 0, // padding
];
let result2 = state.unpad(&chunk2[..]).unwrap();
assert_eq!(result2.content, vec![10, 11, 12]);
assert_eq!(result2.command, Some(UnpadCommand::End));
assert!(matches!(state.state, UnpadState::Done));
}
#[test]
fn test_empty_content_block() {
let user_uuid = [4u8; 16];
let mut state = VisionUnpadder::new(user_uuid);
let mut data = vec![4u8; 16]; // UUID
data.extend_from_slice(&[
0, // command (Continue)
0, 0, // content length = 0
0, 5, // padding length = 5
0, 0, 0, 0, 0, // padding
]);
let result = state.unpad(&data[..]).unwrap();
assert_eq!(result.content, Vec::<u8>::new());
assert_eq!(result.command, Some(UnpadCommand::Continue));
assert!(!matches!(state.state, UnpadState::Done));
}
#[test]
fn test_multiple_continue_with_end() {
let user_uuid = [5u8; 16];
let mut state = VisionUnpadder::new(user_uuid);
let mut data = vec![5u8; 16]; // UUID
data.extend_from_slice(&[
0, // command (Continue)
0, 2, // content length = 2
0, 1, // padding length = 1
10, 11, // content
0, // padding
0, // command (Continue)
0, 2, // content length = 2
0, 1, // padding length = 1
20, 21, // content
0, // padding
1, // command (End)
0, 2, // content length = 2
0, 1, // padding length = 1
30, 31, // content
0, // padding
]);
// Process all blocks - should accumulate and return with End
let result = state.unpad(&data[..]).unwrap();
assert_eq!(result.content, vec![10, 11, 20, 21, 30, 31]);
assert_eq!(result.command, Some(UnpadCommand::End));
assert!(matches!(state.state, UnpadState::Done));
}
#[test]
fn test_incomplete_padding_returns_content_to_prevent_deadlock() {
let user_uuid = [6u8; 16];
let mut state = VisionUnpadder::new(user_uuid);
// First chunk: UUID + command + header + content + partial padding
let mut chunk1 = vec![6u8; 16]; // UUID
chunk1.extend_from_slice(&[
1, // command (End)
0, 3, // content length = 3
0, 5, // padding length = 5
10, 11, 12, // content
0, 0, // only 2 bytes of padding (need 5 total)
]);
// Should return content immediately to prevent deadlock
// (some clients don't send remaining padding until they get a response)
// However, command should be None because padding is incomplete
let result1 = state.unpad(&chunk1[..]).unwrap();
assert_eq!(result1.content, vec![10, 11, 12]);
assert_eq!(result1.command, None); // Command not returned until padding complete
assert!(matches!(state.state, UnpadState::ReadingPadding { .. }));
// Second chunk: remaining padding
let chunk2 = vec![
0, 0, 0, // remaining 3 bytes of padding
];
// Now padding is complete, should return End command (no more content since it was already returned)
let result2 = state.unpad(&chunk2[..]).unwrap();
assert!(result2.content.is_empty()); // Content already returned in result1
assert_eq!(result2.command, Some(UnpadCommand::End));
assert!(matches!(state.state, UnpadState::Done));
}
}
+151
View File
@@ -0,0 +1,151 @@
use async_trait::async_trait;
use tokio::io::AsyncWriteExt;
use crate::address::{Address, NetLocation};
use crate::async_stream::AsyncStream;
use crate::crypto::CryptoTlsStream;
use crate::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
use crate::util::{allocate_vec, parse_uuid, write_all};
use super::vision_stream::VisionStream;
use super::vless_response_stream::VlessResponseStream;
use super::vless_util::{vision_flow_addon_data, COMMAND_TCP};
pub struct VlessTcpClientHandler {
user_id: Box<[u8]>,
}
impl std::fmt::Debug for VlessTcpClientHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VlessTcpClientHandler")
.field("user_id", &self.user_id)
.finish()
}
}
impl VlessTcpClientHandler {
pub fn new(user_id: &str) -> Self {
Self {
user_id: parse_uuid(user_id).unwrap().into_boxed_slice(),
}
}
}
#[async_trait]
impl TcpClientHandler for VlessTcpClientHandler {
async fn setup_client_stream(
&self,
_server_stream: &mut Box<dyn AsyncStream>,
mut client_stream: Box<dyn AsyncStream>,
remote_location: NetLocation,
) -> std::io::Result<TcpClientSetupResult> {
write_vless_header(&mut client_stream, &self.user_id, &[], &remote_location).await?;
client_stream.flush().await?;
// Wrap stream to read VLESS response on first read
let client_stream = Box::new(VlessResponseStream::new(client_stream));
Ok(TcpClientSetupResult { client_stream })
}
}
// REMOVED: Old tokio-rustls vision function - now using setup_custom_tls_vision_vless_client_stream
pub async fn setup_custom_tls_vision_vless_client_stream<IO>(
mut tls_stream: CryptoTlsStream<IO>,
user_id: &[u8],
remote_location: &NetLocation,
) -> std::io::Result<TcpClientSetupResult>
where
IO: crate::async_stream::AsyncStream + 'static,
{
// Write VLESS request header through TLS
write_vless_header(
&mut tls_stream,
user_id,
vision_flow_addon_data(),
remote_location,
)
.await?;
tls_stream.flush().await?;
// Create stream with VLESS response reading support
// CryptoTlsStream already wraps a Connection, so we don't need to wrap again
let (io, connection) = tls_stream.into_inner();
// VisionStream will read VLESS response header on first read
let mut user_uuid = [0u8; 16];
user_uuid.copy_from_slice(user_id);
let vision_stream = VisionStream::new_client(io, connection, user_uuid);
Ok(TcpClientSetupResult {
client_stream: Box::new(vision_stream),
})
}
async fn write_vless_header<S: AsyncWriteExt + Unpin>(
stream: &mut S,
user_id: &[u8],
addon_data: &[u8],
remote_location: &NetLocation,
) -> std::io::Result<()> {
// VLESS header format:
// version (1 byte) + user_id (16 bytes) + addon_length (1 byte) + addon_data + command (1 byte) + port (2 bytes) + address_type (1 byte) + address
// Calculate base header size: version + user_id + addon_length + addon_data + command + port + address_type
let base_header_size = 1 + 16 + 1 + addon_data.len() + 1 + 2 + 1;
let mut header_bytes = allocate_vec(base_header_size);
// version 0, we need to write since it's uninitialized
header_bytes[0] = 0;
// Copy user_id
header_bytes[1..17].copy_from_slice(user_id);
// addon length
header_bytes[17] = addon_data.len() as u8;
// Copy addon data if present
if !addon_data.is_empty() {
header_bytes[18..18 + addon_data.len()].copy_from_slice(addon_data);
}
let addon_end = 18 + addon_data.len();
// command (1 = tcp)
header_bytes[addon_end] = COMMAND_TCP;
// port (2 bytes, big-endian)
let remote_port = remote_location.port();
header_bytes[addon_end + 1] = (remote_port >> 8) as u8;
header_bytes[addon_end + 2] = (remote_port & 0xff) as u8;
// address_type
let address_type_offset = addon_end + 3;
match remote_location.address() {
Address::Ipv4(v4addr) => {
header_bytes[address_type_offset] = 1;
header_bytes.extend_from_slice(&v4addr.octets());
}
Address::Ipv6(v6addr) => {
header_bytes[address_type_offset] = 3;
header_bytes.extend_from_slice(&v6addr.octets());
}
Address::Hostname(hostname) => {
if hostname.len() > 255 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Hostname is too long: {hostname}"),
));
}
header_bytes[address_type_offset] = 2;
header_bytes.push(hostname.len() as u8);
header_bytes.extend_from_slice(hostname.as_bytes());
}
}
write_all(stream, &header_bytes).await?;
Ok(())
}
+137
View File
@@ -0,0 +1,137 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use log::info;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use crate::async_stream::AsyncStream;
/// A wrapper stream that reads VLESS response header on first read, similar
/// to vmess and shadowsocks.
pub struct VlessResponseStream {
inner: Box<dyn AsyncStream>,
response_pending: bool,
response_buffer: Vec<u8>,
}
impl VlessResponseStream {
pub fn new(inner: Box<dyn AsyncStream>) -> Self {
Self {
inner,
response_pending: true,
response_buffer: Vec::new(),
}
}
fn poll_read_response(&mut self, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
use futures::ready;
// Need to read at least 2 bytes for the response header
while self.response_buffer.len() < 2 {
let mut buf = [0u8; 2];
let mut read_buf = ReadBuf::new(&mut buf);
ready!(Pin::new(&mut self.inner).poll_read(cx, &mut read_buf))?;
let filled = read_buf.filled();
if filled.is_empty() {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"Connection closed while reading VLESS response",
)));
}
self.response_buffer.extend_from_slice(filled);
}
// Validate version
let version = self.response_buffer[0];
if version != 0 {
return Poll::Ready(Err(std::io::Error::other(format!(
"invalid server protocol version, expected 0, got {version}"
))));
}
// Check if we need to read addons
let addon_length = self.response_buffer[1] as usize;
let total_response_len = 2 + addon_length;
// Read addon data if needed
while self.response_buffer.len() < total_response_len {
let remaining = total_response_len - self.response_buffer.len();
let mut buf = vec![0u8; remaining];
let mut read_buf = ReadBuf::new(&mut buf);
ready!(Pin::new(&mut self.inner).poll_read(cx, &mut read_buf))?;
let filled = read_buf.filled();
if filled.is_empty() {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"Connection closed while reading VLESS response addons",
)));
}
self.response_buffer.extend_from_slice(filled);
}
info!(
"VLESS: Successfully read and consumed {} byte response header (version={}, addon_length={})",
total_response_len, version, addon_length
);
Poll::Ready(Ok(()))
}
}
impl AsyncRead for VlessResponseStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
// Read VLESS response header on first read
if self.response_pending {
match self.poll_read_response(cx) {
Poll::Ready(Ok(())) => {
self.response_pending = false;
// Response is consumed, now read actual data
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
// Pass through to inner stream
Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
impl AsyncWrite for VlessResponseStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
Pin::new(&mut self.inner).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.inner).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.inner).poll_shutdown(cx)
}
}
impl crate::async_stream::AsyncPing for VlessResponseStream {
fn supports_ping(&self) -> bool {
self.inner.supports_ping()
}
fn poll_write_ping(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<std::io::Result<bool>> {
Pin::new(&mut self.inner).poll_write_ping(cx)
}
}
impl AsyncStream for VlessResponseStream {}
+327
View File
@@ -0,0 +1,327 @@
use async_trait::async_trait;
use log::info;
use subtle::ConstantTimeEq;
use crate::async_stream::AsyncStream;
use crate::crypto::CryptoTlsStream;
use crate::option_util::NoneOrOne;
use crate::stream_reader::StreamReader;
use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
use crate::util::{parse_uuid, write_all};
use crate::xudp::XudpMessageStream;
use super::vision_stream::VisionStream;
use super::vless_message_stream::VlessMessageStream;
use super::vless_util::{
parse_addons_from_reader, parse_remote_location_from_reader, COMMAND_MUX, COMMAND_TCP,
COMMAND_UDP, XTLS_VISION_FLOW,
};
pub struct VlessTcpServerHandler {
user_id: Box<[u8]>,
udp_enabled: bool,
}
impl std::fmt::Debug for VlessTcpServerHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VlessTcpServerHandler")
.field("user_id", &self.user_id)
.field("udp_enabled", &self.udp_enabled)
.finish()
}
}
impl VlessTcpServerHandler {
pub fn new(user_id: &str, udp_enabled: bool) -> Self {
Self {
user_id: parse_uuid(user_id).unwrap().into_boxed_slice(),
udp_enabled,
}
}
}
const SERVER_RESPONSE_HEADER: &[u8] = &[
0u8, // version
0u8, // addons length
];
#[async_trait]
impl TcpServerHandler for VlessTcpServerHandler {
async fn setup_server_stream(
&self,
mut server_stream: Box<dyn AsyncStream>,
) -> std::io::Result<TcpServerSetupResult> {
// Parse VLESS header without TLS
let mut stream_reader = StreamReader::new_with_buffer_size(800);
let client_version = stream_reader.read_u8(&mut server_stream).await?;
if client_version != 0 {
return Err(std::io::Error::other(format!(
"invalid client protocol version, expected 0, got {client_version}"
)));
}
let target_id = stream_reader.read_slice(&mut server_stream, 16).await?;
if self.user_id.ct_eq(target_id).unwrap_u8() == 0 {
return Err(std::io::Error::other("Unknown user id"));
}
let addon_length = stream_reader.read_u8(&mut server_stream).await?;
if addon_length > 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"VLESS addons not supported in current configuration, use TLS protocol for VISION support",
));
}
let instruction = stream_reader.read_u8(&mut server_stream).await?;
match instruction {
COMMAND_TCP => {
let remote_location =
parse_remote_location_from_reader(&mut stream_reader, &mut server_stream)
.await?;
let unparsed_data = stream_reader.unparsed_data();
Ok(TcpServerSetupResult::TcpForward {
remote_location,
stream: server_stream,
need_initial_flush: false,
connection_success_response: Some(
SERVER_RESPONSE_HEADER.to_vec().into_boxed_slice(),
),
initial_remote_data: if unparsed_data.is_empty() {
None
} else {
Some(unparsed_data.to_vec().into_boxed_slice())
},
override_proxy_provider: NoneOrOne::Unspecified,
})
}
COMMAND_UDP => {
if !self.udp_enabled {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"UDP not enabled",
));
}
let remote_location =
parse_remote_location_from_reader(&mut stream_reader, &mut server_stream)
.await?;
let unparsed_data = stream_reader.unparsed_data();
write_all(&mut server_stream, SERVER_RESPONSE_HEADER).await?;
let mut vless_stream = VlessMessageStream::new(server_stream);
if !unparsed_data.is_empty() {
vless_stream.feed_initial_read_data(unparsed_data)?;
}
Ok(TcpServerSetupResult::BidirectionalUdp {
remote_location,
stream: Box::new(vless_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
})
}
COMMAND_MUX => {
if !self.udp_enabled {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"MUX/XUDP requires UDP to be enabled",
));
}
// MUX/XUDP: Destination is NOT in the VLESS header - it comes in XUDP frames
// Don't read destination from wire - protocol spec says it's not present for command 3
info!(
"MUX/XUDP: No destination in VLESS header (destinations come in XUDP frames)"
);
let unparsed_data = stream_reader.unparsed_data();
// Send VLESS response header immediately
write_all(&mut server_stream, SERVER_RESPONSE_HEADER).await?;
// Wrap raw stream in XUDP stream
let mut xudp_stream = XudpMessageStream::new(server_stream);
// Feed any unparsed data to XUDP stream
if !unparsed_data.is_empty() {
xudp_stream.feed_initial_read_data(unparsed_data)?;
}
Ok(TcpServerSetupResult::SessionBasedUdp {
stream: Box::new(xudp_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
})
}
unknown_protocol_type => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Unknown requested protocol: {unknown_protocol_type}"),
));
}
}
}
}
// REMOVED: Old tokio-rustls vision function - now using setup_custom_tls_vision_vless_server_stream
/// Setup a VISION+VLESS stream from a CryptoTlsStream (for REALITY+Vision support)
pub async fn setup_custom_tls_vision_vless_server_stream<IO>(
mut tls_stream: CryptoTlsStream<IO>,
user_id: &[u8],
udp_enabled: bool,
) -> std::io::Result<TcpServerSetupResult>
where
IO: AsyncStream + 'static,
{
// Parse VLESS header from TLS stream
let mut stream_reader = StreamReader::new_with_buffer_size(800);
let client_version = stream_reader.read_u8(&mut tls_stream).await?;
if client_version != 0 {
return Err(std::io::Error::other(format!(
"invalid client protocol version, expected 0, got {client_version}"
)));
}
let target_id = stream_reader.read_slice(&mut tls_stream, 16).await?;
let mut user_uuid = [0u8; 16];
user_uuid.copy_from_slice(target_id);
// Verify user ID using constant-time comparison to prevent timing attacks
if user_id.ct_eq(target_id).unwrap_u8() == 0 {
return Err(std::io::Error::other("Unknown user id"));
}
let addon_length = stream_reader.read_u8(&mut tls_stream).await?;
let flow = if addon_length > 0 {
parse_addons_from_reader(&mut stream_reader, &mut tls_stream, addon_length).await?
} else {
String::new()
};
let instruction = stream_reader.read_u8(&mut tls_stream).await?;
match instruction {
COMMAND_TCP => {
if flow != XTLS_VISION_FLOW {
return Err(std::io::Error::other("expected vision flow for TCP"));
}
info!("Parsing remote location...");
let remote_location =
parse_remote_location_from_reader(&mut stream_reader, &mut tls_stream).await?;
info!("Remote location parsed: {}", remote_location);
let unparsed_data = stream_reader.unparsed_data();
let flow_stream: Box<dyn AsyncStream> = if flow == XTLS_VISION_FLOW {
info!("Creating VISION stream (Custom TLS) for flow: {}", flow);
let (io, session) = tls_stream.into_inner();
Box::new(VisionStream::new_server(
io,
session,
user_uuid,
unparsed_data,
)?)
} else {
Box::new(tls_stream)
};
Ok(TcpServerSetupResult::TcpForward {
remote_location,
stream: flow_stream,
need_initial_flush: false,
connection_success_response: None, // VisionStream will send VLESS response with first write
initial_remote_data: None, // Data fed to VisionStream instead
override_proxy_provider: NoneOrOne::Unspecified,
})
}
COMMAND_UDP => {
if !udp_enabled {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"UDP not enabled",
));
}
info!("Parsing remote location...");
let remote_location =
parse_remote_location_from_reader(&mut stream_reader, &mut tls_stream).await?;
info!("Remote location parsed: {}", remote_location);
let unparsed_data = stream_reader.unparsed_data();
write_all(&mut tls_stream, SERVER_RESPONSE_HEADER).await?;
let mut vless_stream = VlessMessageStream::new(Box::new(tls_stream));
if !unparsed_data.is_empty() {
vless_stream.feed_initial_read_data(unparsed_data)?;
}
Ok(TcpServerSetupResult::BidirectionalUdp {
remote_location,
stream: Box::new(vless_stream),
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
})
}
COMMAND_MUX => {
if !udp_enabled {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"MUX/XUDP requires UDP to be enabled",
));
}
// MUX/XUDP: Destination is NOT in the VLESS header - it comes in XUDP frames
info!("MUX/XUDP: No destination in VLESS header (destinations come in XUDP frames)");
let unparsed_data = stream_reader.unparsed_data();
if flow == XTLS_VISION_FLOW {
info!("Creating VISION+XUDP stream (Custom TLS) with session-based UDP sockets");
// Extract components from CryptoTlsStream
let (io, session) = tls_stream.into_inner();
// Create VISION stream (will send VLESS response automatically on first write)
let vision_stream =
VisionStream::new_server(io, session, user_uuid, unparsed_data)?;
// Wrap VISION stream in XUDP stream
let xudp_stream = XudpMessageStream::new(Box::new(vision_stream));
Ok(TcpServerSetupResult::SessionBasedUdp {
stream: Box::new(xudp_stream),
need_initial_flush: false, // VisionStream sends VLESS response on first write
override_proxy_provider: NoneOrOne::Unspecified,
})
} else {
info!(
"Creating XUDP stream (Custom TLS, no VISION) with session-based UDP sockets"
);
// Send VLESS response header immediately
write_all(&mut tls_stream, SERVER_RESPONSE_HEADER).await?;
// Wrap TLS stream in XUDP stream
let mut xudp_stream = XudpMessageStream::new(Box::new(tls_stream));
// Feed any unparsed data to XUDP stream
if !unparsed_data.is_empty() {
xudp_stream.feed_initial_read_data(unparsed_data)?;
}
Ok(TcpServerSetupResult::SessionBasedUdp {
stream: Box::new(xudp_stream),
need_initial_flush: false, // Response already sent above
override_proxy_provider: NoneOrOne::Unspecified,
})
}
}
unknown_protocol_type => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Unknown requested protocol: {unknown_protocol_type}"),
)),
}
}
+214
View File
@@ -0,0 +1,214 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::LazyLock;
use tokio::io::AsyncReadExt;
use crate::address::{Address, NetLocation};
use crate::stream_reader::StreamReader;
// VLESS protocol command types
pub const COMMAND_TCP: u8 = 1;
pub const COMMAND_UDP: u8 = 2;
pub const COMMAND_MUX: u8 = 3; // Also known as XUDP - multiplexes UDP over single TCP connection
pub const XTLS_VISION_FLOW: &str = "xtls-rprx-vision";
pub async fn parse_addons_from_reader<S: AsyncReadExt + Unpin>(
stream_reader: &mut StreamReader,
stream: &mut S,
addon_length: u8,
) -> std::io::Result<String> {
let addon_bytes = stream_reader
.read_slice(stream, addon_length as usize)
.await?;
log::debug!(
"Parsing addons: length={}, bytes={:?}",
addon_length,
addon_bytes
);
// Parse protobuf-encoded addons
// Format: field_tag length data [field_tag length data ...]
// Field 1 = flow (string)
// Field 2 = seed (bytes)
let mut addon_cursor = 0;
let mut flow_string = String::new();
while addon_cursor < addon_bytes.len() {
// Read field tag
let field_tag = addon_bytes[addon_cursor];
addon_cursor += 1;
let field_number = field_tag >> 3;
let wire_type = field_tag & 0x07;
if wire_type != 2 {
return Err(std::io::Error::other(format!(
"Unexpected wire type {} for field {}",
wire_type, field_number
)));
}
// Read length
if addon_cursor >= addon_bytes.len() {
return Err(std::io::Error::other(
"Unexpected end of addon data reading length",
));
}
let (field_length, bytes_used) = read_varint(&addon_bytes[addon_cursor..])?;
addon_cursor += bytes_used;
// Validate field_length is within bounds
if addon_cursor + field_length as usize > addon_bytes.len() {
return Err(std::io::Error::other(format!(
"Field {} length {} exceeds remaining addon bytes (cursor: {}, total: {})",
field_number,
field_length,
addon_cursor,
addon_bytes.len()
)));
}
// Read field data
let field_data = &addon_bytes[addon_cursor..addon_cursor + field_length as usize];
addon_cursor += field_length as usize;
match field_number {
1 => {
// Flow field
flow_string = std::str::from_utf8(field_data)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?
.to_string();
log::debug!("Parsed flow: {}", flow_string);
}
2 => {
// Seed field (ignored for now)
log::debug!("Parsed seed: {} bytes", field_data.len());
}
_ => {
log::debug!(
"Unknown field {}, skipping {} bytes",
field_number,
field_data.len()
);
}
}
}
Ok(flow_string)
}
/// Parse VLESS remote location from stream
pub async fn parse_remote_location_from_reader<S: AsyncReadExt + Unpin>(
stream_reader: &mut StreamReader,
stream: &mut S,
) -> std::io::Result<NetLocation> {
let port = stream_reader.read_u16_be(stream).await?;
let address_type = stream_reader.read_u8(stream).await?;
match address_type {
1 => {
// 4 byte ipv4 address
let address_bytes = stream_reader.read_slice(stream, 4).await?;
let v4addr = Ipv4Addr::new(
address_bytes[0],
address_bytes[1],
address_bytes[2],
address_bytes[3],
);
Ok(NetLocation::new(Address::Ipv4(v4addr), port))
}
2 => {
// domain name
let domain_name_len = stream_reader.read_u8(stream).await?;
let domain_name_bytes = stream_reader
.read_slice(stream, domain_name_len as usize)
.await?;
let address_str = std::str::from_utf8(domain_name_bytes).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to decode address: {e}"),
)
})?;
Ok(NetLocation::new(Address::from(address_str)?, port))
}
3 => {
// 16 byte ipv6 address
let address_bytes = stream_reader.read_slice(stream, 16).await?;
let v6addr = Ipv6Addr::new(
((address_bytes[0] as u16) << 8) | (address_bytes[1] as u16),
((address_bytes[2] as u16) << 8) | (address_bytes[3] as u16),
((address_bytes[4] as u16) << 8) | (address_bytes[5] as u16),
((address_bytes[6] as u16) << 8) | (address_bytes[7] as u16),
((address_bytes[8] as u16) << 8) | (address_bytes[9] as u16),
((address_bytes[10] as u16) << 8) | (address_bytes[11] as u16),
((address_bytes[12] as u16) << 8) | (address_bytes[13] as u16),
((address_bytes[14] as u16) << 8) | (address_bytes[15] as u16),
);
Ok(NetLocation::new(Address::Ipv6(v6addr), port))
}
invalid_type => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid address type: {invalid_type}"),
)),
}
}
pub fn vision_flow_addon_data() -> &'static [u8] {
static INSTANCE: LazyLock<Vec<u8>> = LazyLock::new(|| {
encode_flow_addon(XTLS_VISION_FLOW)
.expect("Failed to encode vision flow addon at initialization")
});
&INSTANCE
}
fn read_varint(data: &[u8]) -> std::io::Result<(u64, usize)> {
let mut cursor = 0usize;
let mut length = 0u64;
loop {
let byte = data[cursor];
if (byte & 0b10000000) != 0 {
length = (length << 8) | ((byte ^ 0b10000000) as u64);
} else {
length = (length << 8) | (byte as u64);
return Ok((length, cursor + 1));
}
if cursor == 7 || cursor == data.len() {
return Err(std::io::Error::other("Varint is too long"));
}
cursor += 1;
}
}
/// Encode a flow string as protobuf addon data
/// Format: field_tag(0x0a) + length + data
/// Field 1 = flow (string), wire type 2 (length-delimited)
fn encode_flow_addon(flow: &str) -> std::io::Result<Vec<u8>> {
let flow_bytes = flow.as_bytes();
let flow_len = flow_bytes.len();
if flow_len > 127 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Flow string too long for simple varint encoding",
));
}
let mut result = Vec::new();
// Field 1, wire type 2 (0x0a = (1 << 3) | 2)
result.push(0x0a);
// Length as varint (simple case: < 128)
result.push(flow_len as u8);
// Flow string data
result.extend_from_slice(flow_bytes);
Ok(result)
}
-324
View File
@@ -1,324 +0,0 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use async_trait::async_trait;
use log::info;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::address::{Address, NetLocation};
use crate::async_stream::AsyncStream;
use crate::option_util::NoneOrOne;
use crate::stream_reader::StreamReader;
use crate::tcp_handler::{
TcpClientHandler, TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult,
};
use crate::util::{allocate_vec, parse_uuid, write_all};
use crate::vless_message_stream::VlessMessageStream;
#[derive(Debug)]
pub struct VlessTcpServerHandler {
user_id: Box<[u8]>,
udp_enabled: bool,
}
impl VlessTcpServerHandler {
pub fn new(user_id: &str, udp_enabled: bool) -> Self {
Self {
user_id: parse_uuid(user_id).unwrap().into_boxed_slice(),
udp_enabled,
}
}
}
const SERVER_RESPONSE_HEADER: &[u8] = &[
0u8, // version
0u8, // addons length
];
#[async_trait]
impl TcpServerHandler for VlessTcpServerHandler {
async fn setup_server_stream(
&self,
mut server_stream: Box<dyn AsyncStream>,
) -> std::io::Result<TcpServerSetupResult> {
// this needs to be less than `read_buf` size in VlessMessageStream so that
// feed_initial_data can handle the unparsed data.
let mut stream_reader = StreamReader::new_with_buffer_size(800);
let client_version = stream_reader.read_u8(&mut server_stream).await?;
if client_version != 0 {
return Err(std::io::Error::other(format!(
"invalid client protocol version, expected 0, got {client_version}"
)));
}
let target_id = stream_reader.read_slice(&mut server_stream, 16).await?;
for (b1, b2) in self.user_id.iter().zip(target_id.iter()) {
if b1 != b2 {
return Err(std::io::Error::other("Unknown user id"));
}
}
let addon_length = stream_reader.read_u8(&mut server_stream).await?;
if addon_length > 0 {
stream_reader
.read_slice(&mut server_stream, addon_length as usize)
.await?;
}
let instruction = stream_reader.read_u8(&mut server_stream).await?;
let is_udp = match instruction {
1 => {
// tcp
false
}
2 => {
if !self.udp_enabled {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"UDP not enabled",
));
}
true
}
unknown_protocol_type => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Unknown requested protocol: {unknown_protocol_type}"),
));
}
};
let port = stream_reader.read_u16_be(&mut server_stream).await?;
let address_type = stream_reader.read_u8(&mut server_stream).await?;
let remote_location = match address_type {
1 => {
// 4 byte ipv4 address
let address_bytes = stream_reader.read_slice(&mut server_stream, 4).await?;
let v4addr = Ipv4Addr::new(
address_bytes[0],
address_bytes[1],
address_bytes[2],
address_bytes[3],
);
NetLocation::new(Address::Ipv4(v4addr), port)
}
2 => {
// domain name
let domain_name_len = stream_reader.read_u8(&mut server_stream).await?;
let domain_name_bytes = stream_reader
.read_slice(&mut server_stream, domain_name_len as usize)
.await?;
let address_str = match std::str::from_utf8(domain_name_bytes) {
Ok(s) => s,
Err(e) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to decode address: {e}"),
));
}
};
// Although this is supposed to be a hostname, some clients will pass
// ipv4 and ipv6 addresses as well, so parse it rather than directly
// using Address:Hostname enum.
NetLocation::new(Address::from(address_str)?, port)
}
3 => {
// 16 byte ipv6 address
let address_bytes = stream_reader.read_slice(&mut server_stream, 16).await?;
let v6addr = Ipv6Addr::new(
((address_bytes[0] as u16) << 8) | (address_bytes[1] as u16),
((address_bytes[2] as u16) << 8) | (address_bytes[3] as u16),
((address_bytes[4] as u16) << 8) | (address_bytes[5] as u16),
((address_bytes[6] as u16) << 8) | (address_bytes[7] as u16),
((address_bytes[8] as u16) << 8) | (address_bytes[9] as u16),
((address_bytes[10] as u16) << 8) | (address_bytes[11] as u16),
((address_bytes[12] as u16) << 8) | (address_bytes[13] as u16),
((address_bytes[14] as u16) << 8) | (address_bytes[15] as u16),
);
NetLocation::new(Address::Ipv6(v6addr), port)
}
invalid_type => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid address type: {invalid_type}"),
));
}
};
let unparsed_data = stream_reader.unparsed_data();
if !is_udp {
Ok(TcpServerSetupResult::TcpForward {
remote_location,
stream: server_stream,
need_initial_flush: false,
connection_success_response: Some(
SERVER_RESPONSE_HEADER.to_vec().into_boxed_slice(),
),
initial_remote_data: if unparsed_data.is_empty() {
None
} else {
Some(unparsed_data.to_vec().into_boxed_slice())
},
override_proxy_provider: NoneOrOne::Unspecified,
})
} else {
write_all(&mut server_stream, SERVER_RESPONSE_HEADER).await?;
let mut vless_stream = VlessMessageStream::new(server_stream);
if !unparsed_data.is_empty() {
vless_stream.feed_initial_read_data(unparsed_data)?;
}
Ok(TcpServerSetupResult::BidirectionalUdp {
remote_location,
stream: Box::new(vless_stream),
// don't flush and write the server response with the initial data.
need_initial_flush: false,
override_proxy_provider: NoneOrOne::Unspecified,
})
}
}
}
#[derive(Debug)]
pub struct VlessTcpClientHandler {
user_id: Box<[u8]>,
}
impl VlessTcpClientHandler {
pub fn new(user_id: &str) -> Self {
Self {
user_id: parse_uuid(user_id).unwrap().into_boxed_slice(),
}
}
}
#[async_trait]
impl TcpClientHandler for VlessTcpClientHandler {
async fn setup_client_stream(
&self,
_server_stream: &mut Box<dyn AsyncStream>,
mut client_stream: Box<dyn AsyncStream>,
remote_location: NetLocation,
) -> std::io::Result<TcpClientSetupResult> {
// version + user id + header addon length + command + port + address type
let mut header_bytes = [0u8; 1 + 16 + 1 + 1 + 2 + 1];
// version 0 is fine, no need to write.
header_bytes[1..17].copy_from_slice(&self.user_id);
// header addon length of 0 is fine, no need to write.
// tcp
header_bytes[18] = 1;
let (remote_address, remote_port) = remote_location.unwrap_components();
header_bytes[19] = (remote_port >> 8) as u8;
header_bytes[20] = (remote_port & 0xff) as u8;
match remote_address {
Address::Ipv4(v4addr) => {
header_bytes[21] = 1;
write_all(&mut client_stream, &header_bytes).await?;
let address_bytes = v4addr.octets();
write_all(&mut client_stream, &address_bytes).await?;
}
Address::Ipv6(v6addr) => {
header_bytes[21] = 3;
write_all(&mut client_stream, &header_bytes).await?;
let address_bytes = v6addr.octets();
write_all(&mut client_stream, &address_bytes).await?;
}
Address::Hostname(hostname) => {
if hostname.len() > 255 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Hostname is too long: {hostname}"),
));
}
header_bytes[21] = 2;
write_all(&mut client_stream, &header_bytes).await?;
let hostname_len_byte: [u8; 1] = [hostname.len() as u8];
write_all(&mut client_stream, &hostname_len_byte).await?;
let hostname_bytes = hostname.into_bytes();
write_all(&mut client_stream, &hostname_bytes).await?;
}
}
client_stream.flush().await?;
// TODO: remove read_exact, used buffered writes above.
let mut response_header = [0u8; 2];
client_stream.read_exact(&mut response_header).await?;
if response_header[0] != 0 {
return Err(std::io::Error::other(format!(
"invalid server protocol version, expected 0, got {}",
response_header[0]
)));
}
let addon_length = response_header[1];
if addon_length > 0 {
read_addons(&mut client_stream, addon_length).await?;
}
Ok(TcpClientSetupResult { client_stream })
}
}
fn read_varint(data: &[u8]) -> std::io::Result<(u64, usize)> {
let mut cursor = 0usize;
let mut length = 0u64;
loop {
let byte = data[cursor];
if (byte & 0b10000000) != 0 {
length = (length << 8) | ((byte ^ 0b10000000) as u64);
} else {
length = (length << 8) | (byte as u64);
return Ok((length, cursor + 1));
}
if cursor == 7 || cursor == data.len() {
return Err(std::io::Error::other("Varint is too long"));
}
cursor += 1;
}
}
async fn read_addons(stream: &mut Box<dyn AsyncStream>, addon_length: u8) -> std::io::Result<()> {
let mut addon_bytes = allocate_vec(addon_length as usize).into_boxed_slice();
stream.read_exact(&mut addon_bytes).await?;
let mut addon_cursor = 0;
let (flow_length, bytes_used) = read_varint(&addon_bytes)?;
addon_cursor += bytes_used;
let flow_bytes = &addon_bytes[addon_cursor..addon_cursor + flow_length as usize];
addon_cursor += flow_length as usize;
let (seed_length, bytes_used) = read_varint(&addon_bytes[addon_cursor..])?;
addon_cursor += bytes_used;
let seed_bytes = &addon_bytes[addon_cursor..addon_cursor + seed_length as usize];
addon_cursor += seed_length as usize;
if addon_cursor as u8 != addon_length {
return Err(std::io::Error::other(format!(
"Did not consume all addon bytes, cursor is at {addon_cursor}, length is {addon_length}"
)));
}
info!(
"Read addon bytes: flow: {:?}, seed: {:?}",
&flow_bytes, &seed_bytes
);
Ok(())
}
-19
View File
@@ -1,4 +1,3 @@
use hmac::{Hmac, Mac};
use md5::{Digest, Md5};
#[inline]
@@ -8,15 +7,6 @@ pub fn compute_md5(data: &[u8]) -> [u8; 16] {
context.finalize().into()
}
#[inline]
pub fn compute_md5_repeating(data: &[u8], data_times: usize) -> [u8; 16] {
let mut context = Md5::new();
for _ in 0..data_times {
md5::Digest::update(&mut context, data);
}
context.finalize().into()
}
#[inline]
pub fn create_chacha_key(data: &[u8]) -> [u8; 32] {
let mut ret = [0u8; 32];
@@ -27,12 +17,3 @@ pub fn create_chacha_key(data: &[u8]) -> [u8; 32] {
context.finalize_into((&mut ret[16..]).into());
ret
}
type HmacMd5 = Hmac<Md5>;
#[inline]
pub fn compute_hmac_md5(key: &[u8], data: &[u8]) -> [u8; 16] {
let mut mac = HmacMd5::new_from_slice(key).unwrap();
Mac::update(&mut mac, data);
mac.finalize().into_bytes().into()
}
-2
View File
@@ -1,3 +1 @@
pub type Aes128CfbEnc = cfb_mode::Encryptor<aes::Aes128>;
pub type Aes128CfbDec = cfb_mode::Decryptor<aes::Aes128>;
pub type VmessReader = digest::core_api::XofReaderCoreWrapper<sha3::Shake128ReaderCore>;
+401 -515
View File
File diff suppressed because it is too large Load Diff
+2 -53
View File
@@ -9,14 +9,13 @@ use rand::RngCore;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use super::nonce::{SingleUseNonce, VmessNonceSequence};
use super::typed::{Aes128CfbDec, VmessReader};
use super::typed::VmessReader;
use crate::async_stream::{
AsyncFlushMessage, AsyncMessageStream, AsyncPing, AsyncReadMessage, AsyncShutdownMessage,
AsyncStream, AsyncWriteMessage,
};
use crate::util::allocate_vec;
use aes::cipher::{AsyncStreamCipher, KeyIvInit};
// this should be the same as vmess_handler.rs TAG_LEN.
const HEADER_TAG_LEN: usize = 16;
const ENCRYPTION_TAG_LEN: usize = 16;
@@ -111,7 +110,6 @@ enum DecryptState {
// Expected VMess header response, when we're a client.
pub struct ReadHeaderInfo {
pub is_aead: bool,
pub response_header_key: [u8; 16],
pub response_header_iv: [u8; 16],
pub response_authentication_v: u8,
@@ -121,7 +119,6 @@ pub struct ReadHeaderInfo {
enum ReadHeaderState {
ReadAeadLength,
ReadAeadContent(usize),
ReadLegacyContent,
Done,
}
@@ -206,13 +203,7 @@ impl VmessStream {
};
let read_header_state = match read_header_info {
Some(ref info) => {
if info.is_aead {
ReadHeaderState::ReadAeadLength
} else {
ReadHeaderState::ReadLegacyContent
}
}
Some(_) => ReadHeaderState::ReadAeadLength,
None => ReadHeaderState::Done,
};
@@ -290,7 +281,6 @@ impl VmessStream {
ReadHeaderState::ReadAeadContent(content_len) => {
self.process_read_header_aead_content(content_len)
}
ReadHeaderState::ReadLegacyContent => self.process_read_header_legacy_content(),
ReadHeaderState::Done => {
panic!("process_read_header called with Done state");
}
@@ -400,47 +390,6 @@ impl VmessStream {
check_header_response(encrypted_response_header, response_authentication_v)
}
fn process_read_header_legacy_content(&mut self) -> std::io::Result<()> {
if self.unprocessed_end_offset - self.unprocessed_start_offset < 4 {
return Ok(());
}
let ReadHeaderInfo {
response_header_key,
response_header_iv,
..
} = self.read_header_info.as_ref().unwrap();
let response_header_bytes = &mut self.unprocessed_buf
[self.unprocessed_start_offset..self.unprocessed_start_offset + 4];
let response_cipher = Aes128CfbDec::new(
(&response_header_key[..]).into(),
(&response_header_iv[..]).into(),
);
response_cipher.decrypt(response_header_bytes);
// do this here, because we would already have read/decrypted it in the aead clause.
let command_len = response_header_bytes[3];
if command_len > 0 {
// if this becomes an issue, we should read the command bytes and ignore them.
return Err(std::io::Error::other("extra command bytes"));
}
self.read_header_state = ReadHeaderState::Done;
self.unprocessed_start_offset += 4;
if self.unprocessed_start_offset == self.unprocessed_end_offset {
self.unprocessed_start_offset = 0;
self.unprocessed_end_offset = 0;
}
let ReadHeaderInfo {
response_authentication_v,
..
} = self.read_header_info.take().unwrap();
check_header_response(response_header_bytes, response_authentication_v)
}
fn try_decrypt(&mut self) -> std::io::Result<DecryptState> {
// returns true if a full packet was decrypted, false if not (ie. more data required)
let available_len = self.unprocessed_end_offset - self.unprocessed_start_offset;
+846
View File
@@ -0,0 +1,846 @@
// XUDP frame protocol implementation
// Protocol-agnostic UDP multiplexing used by VLESS and VMess
use bytes::{Buf, BufMut, BytesMut};
use std::net::{Ipv4Addr, Ipv6Addr};
use crate::address::{Address, NetLocation};
/// XUDP session status
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SessionStatus {
New = 0x01, // Create new session
Keep = 0x02, // Continue session
End = 0x03, // Close session
KeepAlive = 0x04, // Heartbeat
}
impl TryFrom<u8> for SessionStatus {
type Error = std::io::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x01 => Ok(SessionStatus::New),
0x02 => Ok(SessionStatus::Keep),
0x03 => Ok(SessionStatus::End),
0x04 => Ok(SessionStatus::KeepAlive),
other => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid session status: {}", other),
)),
}
}
}
/// XUDP frame options (bitmask)
#[derive(Debug, Clone, Copy)]
pub struct FrameOption(u8);
impl FrameOption {
pub const DATA: u8 = 0x01;
pub const ERROR: u8 = 0x02;
pub fn new() -> Self {
Self(0)
}
pub fn with_data(mut self) -> Self {
self.0 |= Self::DATA;
self
}
pub fn has_data(&self) -> bool {
(self.0 & Self::DATA) != 0
}
pub fn has_error(&self) -> bool {
(self.0 & Self::ERROR) != 0
}
pub fn raw(&self) -> u8 {
self.0
}
}
impl Default for FrameOption {
fn default() -> Self {
Self::new()
}
}
impl From<u8> for FrameOption {
fn from(value: u8) -> Self {
Self(value)
}
}
/// Target network type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TargetNetwork {
Tcp = 0x01,
Udp = 0x02,
}
impl TryFrom<u8> for TargetNetwork {
type Error = std::io::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x01 => Ok(TargetNetwork::Tcp),
0x02 => Ok(TargetNetwork::Udp),
other => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid network type: {}", other),
)),
}
}
}
/// XUDP frame metadata
#[derive(Debug)]
pub struct FrameMetadata {
pub session_id: u16,
pub status: SessionStatus,
pub option: FrameOption,
pub target: Option<NetLocation>,
pub network: Option<TargetNetwork>,
}
impl FrameMetadata {
/// Encode frame metadata to bytes
pub fn encode(&self, buf: &mut BytesMut) -> std::io::Result<()> {
// Reserve space for length (will fill in at end)
let length_pos = buf.len();
buf.put_u16(0);
let metadata_start = buf.len();
// Session ID
buf.put_u16(self.session_id);
// Status and Option
buf.put_u8(self.status as u8);
buf.put_u8(self.option.raw());
// For New status or Keep+UDP: write destination
if self.status == SessionStatus::New {
let network = self.network.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"network required for SessionStatusNew",
)
})?;
buf.put_u8(network as u8);
let target = self.target.as_ref().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"target required for SessionStatusNew",
)
})?;
// Port
buf.put_u16(target.port());
// Address
encode_address(buf, target.address())?;
} else if matches!(self.status, SessionStatus::Keep)
&& matches!(self.network, Some(TargetNetwork::Udp))
{
// For Keep frames with UDP, include destination
buf.put_u8(TargetNetwork::Udp as u8);
let target = self.target.as_ref().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"target required for Keep+UDP",
)
})?;
buf.put_u16(target.port());
encode_address(buf, target.address())?;
}
// Calculate and write length
let metadata_len = buf.len() - metadata_start;
let length_bytes = &mut buf[length_pos..length_pos + 2];
length_bytes.copy_from_slice(&(metadata_len as u16).to_be_bytes());
Ok(())
}
/// Decode frame metadata from bytes
pub fn decode(buf: &mut BytesMut) -> std::io::Result<Option<Self>> {
// Need at least length field
if buf.len() < 2 {
log::debug!(
"[XUDP DECODE] Buffer too short for length field: {} bytes",
buf.len()
);
return Ok(None);
}
let metadata_len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
log::debug!(
"[XUDP DECODE] metadata_len={}, buf.len()={}, need={}",
metadata_len,
buf.len(),
2 + metadata_len
);
// Need complete metadata
if buf.len() < 2 + metadata_len {
log::debug!("[XUDP DECODE] Incomplete metadata, need more data");
return Ok(None);
}
// Skip length field
buf.advance(2);
if metadata_len < 4 {
log::error!(
"[XUDP DECODE] Metadata too short: {} (buffer was {} bytes, first 8 bytes: {:?})",
metadata_len,
buf.len() + 2,
&buf[..std::cmp::min(8, buf.len())]
);
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("metadata too short: {}", metadata_len),
));
}
// Track how many metadata bytes we consume
let metadata_start = buf.len();
let session_id = buf.get_u16();
let status = SessionStatus::try_from(buf.get_u8())?;
let option = FrameOption::from(buf.get_u8());
let mut network = None;
let mut target = None;
// Calculate remaining metadata bytes (after session_id, status, option = 4 bytes)
let remaining_metadata = metadata_len.saturating_sub(4);
// Parse destination for New or Keep+UDP
// Check remaining_metadata > 0 to know if there's address data
if remaining_metadata > 0
&& (status == SessionStatus::New
|| (status == SessionStatus::Keep && buf.remaining() > 0 && buf[0] == 0x02))
{
let net_byte = buf.get_u8();
network = Some(TargetNetwork::try_from(net_byte)?);
let port = buf.get_u16();
let address = decode_address(buf)?;
target = Some(NetLocation::new(address, port));
}
// Consume any remaining metadata bytes we didn't parse
let consumed = metadata_start - buf.len();
let unconsumed = metadata_len.saturating_sub(consumed);
if unconsumed > 0 {
log::debug!(
"[XUDP DECODE] Skipping {} unconsumed metadata bytes (GlobalID or padding)",
unconsumed
);
buf.advance(unconsumed);
}
Ok(Some(FrameMetadata {
session_id,
status,
option,
target,
network,
}))
}
}
/// Encode address to buffer (VLESS address format)
fn encode_address(buf: &mut BytesMut, address: &Address) -> std::io::Result<()> {
match address {
Address::Ipv4(v4) => {
buf.put_u8(0x01);
buf.put_slice(&v4.octets());
}
Address::Hostname(hostname) => {
if hostname.len() > 255 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"hostname too long",
));
}
buf.put_u8(0x02);
buf.put_u8(hostname.len() as u8);
buf.put_slice(hostname.as_bytes());
}
Address::Ipv6(v6) => {
buf.put_u8(0x03);
buf.put_slice(&v6.octets());
}
}
Ok(())
}
/// Decode address from buffer (VLESS address format)
fn decode_address(buf: &mut BytesMut) -> std::io::Result<Address> {
if buf.remaining() < 1 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"incomplete address type",
));
}
let addr_type = buf.get_u8();
match addr_type {
0x01 => {
// IPv4
if buf.remaining() < 4 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"incomplete IPv4 address",
));
}
let mut octets = [0u8; 4];
buf.copy_to_slice(&mut octets);
Ok(Address::Ipv4(Ipv4Addr::from(octets)))
}
0x02 => {
// Hostname
if buf.remaining() < 1 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"incomplete hostname length",
));
}
let len = buf.get_u8() as usize;
if buf.remaining() < len {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"incomplete hostname",
));
}
let hostname_bytes = buf.copy_to_bytes(len);
let hostname = std::str::from_utf8(&hostname_bytes)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(Address::from(hostname)?)
}
0x03 => {
// IPv6
if buf.remaining() < 16 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"incomplete IPv6 address",
));
}
let mut octets = [0u8; 16];
buf.copy_to_slice(&mut octets);
Ok(Address::Ipv6(Ipv6Addr::from(octets)))
}
other => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid address type: {}", other),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
// ==========================================================================
// Basic encode/decode tests
// ==========================================================================
#[test]
fn test_encode_decode_new_frame_udp() {
let metadata = FrameMetadata {
session_id: 42,
status: SessionStatus::New,
option: FrameOption::new().with_data(),
target: Some(NetLocation::new(
Address::Ipv4("1.1.1.1".parse().unwrap()),
53,
)),
network: Some(TargetNetwork::Udp),
};
let mut buf = BytesMut::new();
metadata.encode(&mut buf).unwrap();
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 42);
assert_eq!(decoded.status, SessionStatus::New);
assert!(decoded.option.has_data());
assert_eq!(decoded.network, Some(TargetNetwork::Udp));
assert!(decoded.target.is_some());
let target = decoded.target.unwrap();
assert_eq!(target.port(), 53);
}
#[test]
fn test_encode_decode_keep_frame_udp() {
let metadata = FrameMetadata {
session_id: 100,
status: SessionStatus::Keep,
option: FrameOption::new().with_data(),
target: Some(NetLocation::new(
Address::Ipv4("8.8.8.8".parse().unwrap()),
53,
)),
network: Some(TargetNetwork::Udp),
};
let mut buf = BytesMut::new();
metadata.encode(&mut buf).unwrap();
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 100);
assert_eq!(decoded.status, SessionStatus::Keep);
assert!(decoded.option.has_data());
assert_eq!(decoded.network, Some(TargetNetwork::Udp));
}
#[test]
fn test_encode_decode_end_frame() {
let metadata = FrameMetadata {
session_id: 50,
status: SessionStatus::End,
option: FrameOption::new(),
target: None,
network: None,
};
let mut buf = BytesMut::new();
metadata.encode(&mut buf).unwrap();
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 50);
assert_eq!(decoded.status, SessionStatus::End);
assert!(!decoded.option.has_data());
}
#[test]
fn test_encode_decode_hostname() {
let metadata = FrameMetadata {
session_id: 1,
status: SessionStatus::New,
option: FrameOption::new(),
target: Some(NetLocation::new(
Address::Hostname("example.com".to_string()),
443,
)),
network: Some(TargetNetwork::Tcp),
};
let mut buf = BytesMut::new();
metadata.encode(&mut buf).unwrap();
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 1);
let target = decoded.target.unwrap();
assert_eq!(target.port(), 443);
match target.address() {
Address::Hostname(h) => assert_eq!(h, "example.com"),
_ => panic!("expected hostname"),
}
}
/// Test decoding a frame with extra metadata bytes (GlobalID simulation)
/// Xray-core appends 8-byte GlobalID for UDP session continuity.
/// We skip these bytes like sing-box
#[test]
fn test_decode_frame_with_globalid_extra_bytes() {
let mut buf = BytesMut::new();
// Manually construct a frame with GlobalID (8 extra bytes)
// Format: length(2) + session_id(2) + status(1) + option(1) + network(1) + port(2) + addr_type(1) + ipv4(4) + GlobalID(8)
let metadata_len: u16 = 4 + 1 + 2 + 1 + 4 + 8; // 20 bytes
buf.put_u16(metadata_len);
buf.put_u16(123); // session_id
buf.put_u8(0x01); // SessionStatus::New
buf.put_u8(0x01); // FrameOption::DATA
buf.put_u8(0x02); // TargetNetwork::Udp
buf.put_u16(53); // port
buf.put_u8(0x01); // address type: IPv4
buf.put_slice(&[8, 8, 8, 8]); // 8.8.8.8
buf.put_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); // GlobalID (8 bytes)
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 123);
assert_eq!(decoded.status, SessionStatus::New);
assert!(decoded.option.has_data());
assert_eq!(decoded.network, Some(TargetNetwork::Udp));
let target = decoded.target.unwrap();
assert_eq!(target.port(), 53);
assert_eq!(target.address(), &Address::Ipv4("8.8.8.8".parse().unwrap()));
// Buffer should be fully consumed
assert_eq!(buf.len(), 0, "Buffer should be fully consumed after decode");
}
/// Test Keep frame without destination (TCP keep)
/// This is a minimal Keep frame with just session_id + status + option
#[test]
fn test_decode_keep_frame_tcp_no_destination() {
let mut buf = BytesMut::new();
// Minimal Keep frame: length(2) + session_id(2) + status(1) + option(1) = 4 bytes metadata
buf.put_u16(4); // metadata length
buf.put_u16(42); // session_id
buf.put_u8(0x02); // SessionStatus::Keep
buf.put_u8(0x01); // FrameOption::DATA
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 42);
assert_eq!(decoded.status, SessionStatus::Keep);
assert!(decoded.option.has_data());
assert!(decoded.network.is_none(), "TCP Keep should have no network");
assert!(
decoded.target.is_none(),
"TCP Keep should have no destination"
);
assert_eq!(buf.len(), 0);
}
/// Test Keep frame with UDP destination change
/// Per Xray-core: Keep frames CAN have destination if network byte is 0x02 (UDP)
#[test]
fn test_decode_keep_frame_udp_with_destination() {
let mut buf = BytesMut::new();
// Keep+UDP frame with destination
// Format: length(2) + session_id(2) + status(1) + option(1) + network(1) + port(2) + addr_type(1) + ipv4(4)
let metadata_len: u16 = 4 + 1 + 2 + 1 + 4; // 12 bytes
buf.put_u16(metadata_len);
buf.put_u16(99); // session_id
buf.put_u8(0x02); // SessionStatus::Keep
buf.put_u8(0x01); // FrameOption::DATA
buf.put_u8(0x02); // TargetNetwork::Udp - this triggers destination parsing
buf.put_u16(1234); // port
buf.put_u8(0x01); // address type: IPv4
buf.put_slice(&[192, 168, 1, 1]); // 192.168.1.1
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 99);
assert_eq!(decoded.status, SessionStatus::Keep);
assert_eq!(decoded.network, Some(TargetNetwork::Udp));
let target = decoded.target.unwrap();
assert_eq!(target.port(), 1234);
assert_eq!(buf.len(), 0);
}
/// Test Keep frame where first byte after header is NOT 0x02 (not UDP)
/// Should NOT parse destination even if there's extra data
#[test]
fn test_decode_keep_frame_non_udp_extra_bytes() {
let mut buf = BytesMut::new();
// Keep frame where network byte is 0x01 (TCP) - should NOT parse destination
let metadata_len: u16 = 4 + 1 + 2 + 1 + 4; // 12 bytes total
buf.put_u16(metadata_len);
buf.put_u16(50); // session_id
buf.put_u8(0x02); // SessionStatus::Keep
buf.put_u8(0x01); // FrameOption::DATA
buf.put_u8(0x01); // TargetNetwork::Tcp (NOT UDP!)
buf.put_u16(443); // port (should be skipped)
buf.put_u8(0x01); // address type
buf.put_slice(&[1, 2, 3, 4]); // address (should be skipped)
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 50);
assert_eq!(decoded.status, SessionStatus::Keep);
// Network and target should be None because first byte wasn't 0x02
assert!(
decoded.network.is_none(),
"Keep+TCP should not parse network"
);
assert!(
decoded.target.is_none(),
"Keep+TCP should not parse destination"
);
// Extra bytes should be consumed/skipped
assert_eq!(buf.len(), 0, "Extra bytes should be consumed");
}
/// Test KeepAlive frame (status 0x04)
#[test]
fn test_decode_keepalive_frame() {
let mut buf = BytesMut::new();
buf.put_u16(4); // metadata length
buf.put_u16(1); // session_id
buf.put_u8(0x04); // SessionStatus::KeepAlive
buf.put_u8(0x00); // no options
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 1);
assert_eq!(decoded.status, SessionStatus::KeepAlive);
assert!(!decoded.option.has_data());
assert!(!decoded.option.has_error());
assert_eq!(buf.len(), 0);
}
/// Test End frame with ERROR option
#[test]
fn test_decode_end_frame_with_error() {
let mut buf = BytesMut::new();
buf.put_u16(4); // metadata length
buf.put_u16(77); // session_id
buf.put_u8(0x03); // SessionStatus::End
buf.put_u8(0x02); // FrameOption::ERROR
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 77);
assert_eq!(decoded.status, SessionStatus::End);
assert!(!decoded.option.has_data());
assert!(decoded.option.has_error());
assert_eq!(buf.len(), 0);
}
/// Test IPv6 destination
#[test]
fn test_decode_new_frame_ipv6() {
let mut buf = BytesMut::new();
// New frame with IPv6 destination
let metadata_len: u16 = 4 + 1 + 2 + 1 + 16; // 24 bytes
buf.put_u16(metadata_len);
buf.put_u16(200); // session_id
buf.put_u8(0x01); // SessionStatus::New
buf.put_u8(0x01); // FrameOption::DATA
buf.put_u8(0x02); // TargetNetwork::Udp
buf.put_u16(53); // port
buf.put_u8(0x03); // address type: IPv6
buf.put_slice(&[
0x20, 0x01, 0x48, 0x60, 0x48, 0x60, 0, 0, 0, 0, 0, 0, 0, 0, 0x88, 0x88,
]); // 2001:4860:4860::8888
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 200);
let target = decoded.target.unwrap();
assert_eq!(target.port(), 53);
match target.address() {
Address::Ipv6(v6) => {
assert_eq!(v6.to_string(), "2001:4860:4860::8888");
}
_ => panic!("expected IPv6"),
}
assert_eq!(buf.len(), 0);
}
// ==========================================================================
// Error handling tests
// ==========================================================================
/// Test incomplete buffer (not enough for length field)
#[test]
fn test_decode_incomplete_length() {
let mut buf = BytesMut::new();
buf.put_u8(0x00); // Only 1 byte, need 2 for length
let result = FrameMetadata::decode(&mut buf).unwrap();
assert!(result.is_none(), "Should return None for incomplete length");
assert_eq!(buf.len(), 1, "Buffer should not be consumed");
}
/// Test incomplete metadata (length says more than available)
#[test]
fn test_decode_incomplete_metadata() {
let mut buf = BytesMut::new();
buf.put_u16(10); // metadata length = 10
buf.put_u16(1); // session_id
// Only 2 bytes of metadata, but length says 10
let result = FrameMetadata::decode(&mut buf).unwrap();
assert!(
result.is_none(),
"Should return None for incomplete metadata"
);
}
/// Test metadata too short (less than 4 bytes)
#[test]
fn test_decode_metadata_too_short() {
let mut buf = BytesMut::new();
buf.put_u16(2); // metadata length = 2 (too short, need at least 4)
buf.put_u16(1); // only 2 bytes of metadata
let result = FrameMetadata::decode(&mut buf);
assert!(result.is_err(), "Should error on metadata too short");
}
/// Test invalid session status
#[test]
fn test_decode_invalid_session_status() {
let mut buf = BytesMut::new();
buf.put_u16(4); // metadata length
buf.put_u16(1); // session_id
buf.put_u8(0x99); // Invalid status!
buf.put_u8(0x00); // option
let result = FrameMetadata::decode(&mut buf);
assert!(result.is_err(), "Should error on invalid session status");
}
/// Test invalid network type in New frame
#[test]
fn test_decode_invalid_network_type() {
let mut buf = BytesMut::new();
let metadata_len: u16 = 4 + 1 + 2 + 1 + 4;
buf.put_u16(metadata_len);
buf.put_u16(1); // session_id
buf.put_u8(0x01); // SessionStatus::New
buf.put_u8(0x01); // FrameOption::DATA
buf.put_u8(0x99); // Invalid network type!
buf.put_u16(53); // port
buf.put_u8(0x01); // address type
buf.put_slice(&[1, 2, 3, 4]);
let result = FrameMetadata::decode(&mut buf);
assert!(result.is_err(), "Should error on invalid network type");
}
/// Test invalid address type
#[test]
fn test_decode_invalid_address_type() {
let mut buf = BytesMut::new();
let metadata_len: u16 = 4 + 1 + 2 + 1 + 4;
buf.put_u16(metadata_len);
buf.put_u16(1); // session_id
buf.put_u8(0x01); // SessionStatus::New
buf.put_u8(0x01); // FrameOption::DATA
buf.put_u8(0x02); // TargetNetwork::Udp
buf.put_u16(53); // port
buf.put_u8(0x99); // Invalid address type!
buf.put_slice(&[1, 2, 3, 4]);
let result = FrameMetadata::decode(&mut buf);
assert!(result.is_err(), "Should error on invalid address type");
}
#[test]
fn test_frame_format_compatibility() {
let mut buf = BytesMut::new();
// Xray-core format for New+UDP:
// 2 bytes - length (of metadata after this field)
// 2 bytes - session id
// 1 byte - status
// 1 byte - option
// 1 byte - network
// 2 bytes - port
// n bytes - address (1 byte type + address bytes)
let metadata_len: u16 = 2 + 1 + 1 + 1 + 2 + 1 + 4; // = 12
buf.put_u16(metadata_len);
buf.put_u16(0x0001); // session_id = 1
buf.put_u8(0x01); // SessionStatusNew
buf.put_u8(0x01); // OptionData
buf.put_u8(0x02); // TargetNetworkUDP
buf.put_u16(443); // port (big-endian)
buf.put_u8(0x01); // AddressTypeIPv4
buf.put_slice(&[93, 184, 216, 34]); // 93.184.216.34 (example.com)
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 1);
assert_eq!(decoded.status, SessionStatus::New);
assert!(decoded.option.has_data());
assert!(!decoded.option.has_error());
assert_eq!(decoded.network, Some(TargetNetwork::Udp));
let target = decoded.target.unwrap();
assert_eq!(target.port(), 443);
assert_eq!(
target.address(),
&Address::Ipv4("93.184.216.34".parse().unwrap())
);
}
/// Test domain name encoding
#[test]
fn test_domain_format() {
let mut buf = BytesMut::new();
let domain = "dns.google";
let metadata_len: u16 = 4 + 1 + 2 + 1 + 1 + domain.len() as u16; // 19
buf.put_u16(metadata_len);
buf.put_u16(5); // session_id
buf.put_u8(0x01); // SessionStatusNew
buf.put_u8(0x01); // OptionData
buf.put_u8(0x02); // TargetNetworkUDP
buf.put_u16(853); // DNS over TLS port
buf.put_u8(0x02); // AddressTypeDomain
buf.put_u8(domain.len() as u8);
buf.put_slice(domain.as_bytes());
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
let target = decoded.target.unwrap();
assert_eq!(target.port(), 853);
match target.address() {
Address::Hostname(h) => assert_eq!(h, "dns.google"),
_ => panic!("expected hostname"),
}
}
/// Test that our encoder produces Xray-compatible output
#[test]
fn test_encode_xray_compatible() {
let metadata = FrameMetadata {
session_id: 100,
status: SessionStatus::New,
option: FrameOption::new().with_data(),
target: Some(NetLocation::new(
Address::Ipv4("1.1.1.1".parse().unwrap()),
53,
)),
network: Some(TargetNetwork::Udp),
};
let mut buf = BytesMut::new();
metadata.encode(&mut buf).unwrap();
// Verify the wire format
assert_eq!(buf[0..2], [0x00, 0x0c]); // length = 12 (big-endian)
assert_eq!(buf[2..4], [0x00, 0x64]); // session_id = 100 (big-endian)
assert_eq!(buf[4], 0x01); // SessionStatusNew
assert_eq!(buf[5], 0x01); // OptionData
assert_eq!(buf[6], 0x02); // TargetNetworkUDP
assert_eq!(buf[7..9], [0x00, 0x35]); // port = 53 (big-endian)
assert_eq!(buf[9], 0x01); // AddressTypeIPv4
assert_eq!(buf[10..14], [1, 1, 1, 1]); // 1.1.1.1
}
/// Test maximum session ID (u16::MAX)
#[test]
fn test_max_session_id() {
let mut buf = BytesMut::new();
buf.put_u16(4);
buf.put_u16(0xFFFF); // max session_id
buf.put_u8(0x02); // Keep
buf.put_u8(0x00);
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.session_id, 65535);
}
/// Test both DATA and ERROR options set
#[test]
fn test_data_and_error_options() {
let mut buf = BytesMut::new();
buf.put_u16(4);
buf.put_u16(1);
buf.put_u8(0x03); // End
buf.put_u8(0x03); // DATA | ERROR
let decoded = FrameMetadata::decode(&mut buf).unwrap().unwrap();
assert!(decoded.option.has_data());
assert!(decoded.option.has_error());
}
}
+621
View File
@@ -0,0 +1,621 @@
// XUDP message stream - protocol-agnostic UDP session multiplexing
// Wraps any AsyncStream and provides XUDP frame encoding/decoding with session management
// Used by both VLESS and VMess protocols
use bytes::{Buf, BufMut, BytesMut};
use futures::ready;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use crate::address::{Address, NetLocation};
use crate::async_stream::{
AsyncFlushMessage, AsyncPing, AsyncReadSessionMessage, AsyncSessionMessageStream,
AsyncShutdownMessage, AsyncStream, AsyncWriteSessionMessage,
};
use crate::resolver::{NativeResolver, ResolverCache};
use super::frame::{FrameMetadata, FrameOption, SessionStatus, TargetNetwork};
pub struct XudpMessageStream {
/// Underlying byte stream (VLESS VisionStream, VMess stream, or any TLS stream) that reads/writes raw XUDP frame bytes
inner_stream: Box<dyn AsyncStream>,
/// Read buffer for incoming XUDP frames
read_buffer: BytesMut,
/// Write buffer for outgoing XUDP frames
write_buffer: BytesMut,
/// Session ID counter (starts at 1, wraps at u16::MAX)
next_session_id: u16,
/// Active sessions: destination (RESOLVED IP) -> session_id
/// IMPORTANT: Keys are always resolved IP addresses, never hostnames
/// This ensures responses from UDP sockets (which give us IPs) can find the correct session
destination_to_session: HashMap<NetLocation, u16>,
/// Reverse mapping: session_id -> destination (RESOLVED IP)
/// Used to remember which session_id maps to which destination
session_to_destination: HashMap<u16, NetLocation>,
/// Maps session_id -> ORIGINAL destination (before resolution)
/// This preserves hostnames for encoding in response frames
/// CRITICAL FOR WEBRTC: Client expects responses to reference original hostname, not resolved IP
session_to_original_destination: HashMap<u16, NetLocation>,
/// Resolver cache for hostname resolution
/// Resolves hostnames to IPs before storing in session maps
resolver_cache: ResolverCache,
/// Buffered incoming message (if we read a complete frame)
/// Stores: (data, original_destination, optional_resolved_destination)
/// If resolved_destination is None, we still need to resolve it
incoming_message: Option<(Vec<u8>, NetLocation, Option<NetLocation>)>,
/// EOF flag
is_eof: bool,
}
impl XudpMessageStream {
pub fn new(inner_stream: Box<dyn AsyncStream>) -> Self {
// Create resolver implicitly like UdpMessageStream does
let resolver = Arc::new(NativeResolver::new());
Self {
inner_stream,
read_buffer: BytesMut::with_capacity(65536),
write_buffer: BytesMut::with_capacity(65536),
next_session_id: 1,
destination_to_session: HashMap::new(),
session_to_destination: HashMap::new(),
session_to_original_destination: HashMap::new(),
resolver_cache: ResolverCache::new(resolver),
incoming_message: None,
is_eof: false,
}
}
/// Feed initial unparsed data into the read buffer
/// Used when protocol header parsing (VLESS/VMess) consumed data that belongs to XUDP frames
pub fn feed_initial_read_data(&mut self, data: &[u8]) -> std::io::Result<()> {
if data.is_empty() {
return Ok(());
}
log::debug!(
"[XUDP] Feeding {} bytes of initial data to read buffer",
data.len()
);
self.read_buffer.extend_from_slice(data);
Ok(())
}
/// Allocate new session ID
fn allocate_session_id(&mut self) -> u16 {
loop {
let id = self.next_session_id;
self.next_session_id = self.next_session_id.wrapping_add(1);
if self.next_session_id == 0 {
self.next_session_id = 1; // Skip 0
}
// Check if ID is already in use (unlikely with u16 space)
if !self.session_to_destination.contains_key(&id) {
return id;
}
}
}
/// Get or create session ID for destination, preserving original address
/// resolved_destination: IP address (used for reverse lookup from UDP responses)
/// original_destination: Original address from XUDP frame (may be hostname, used in response frames)
fn get_or_create_session(
&mut self,
resolved_destination: &NetLocation,
original_destination: &NetLocation,
) -> (u16, bool) {
if let Some(&session_id) = self.destination_to_session.get(resolved_destination) {
log::debug!(
"[XUDP] Found existing session {} for destination {}",
session_id,
resolved_destination
);
(session_id, false) // Existing session
} else {
let session_id = self.allocate_session_id();
log::debug!(
"[XUDP] Creating NEW session {} for resolved dest {} (original: {})",
session_id,
resolved_destination,
original_destination
);
self.destination_to_session
.insert(resolved_destination.clone(), session_id);
self.session_to_destination
.insert(session_id, resolved_destination.clone());
// CRITICAL: Store original destination for use in response frames
self.session_to_original_destination
.insert(session_id, original_destination.clone());
log::debug!(
"[XUDP] Session maps updated. Total sessions: {}",
self.destination_to_session.len()
);
(session_id, true) // New session
}
}
/// Try to decode one complete XUDP frame from the read buffer.
/// Returns:
/// Ok(Some((data, destination))) - Successfully decoded a complete frame
/// Ok(None) - Buffer doesn't contain a complete frame yet (need more data)
/// Err(e) - Error during decoding
///
/// IMPORTANT: This function must NOT consume any bytes from the buffer unless
/// it successfully decodes a complete frame. Otherwise, partial frames would
/// be lost when the function is called again with more data.
fn try_decode_one_frame(&mut self) -> std::io::Result<Option<(Vec<u8>, NetLocation)>> {
log::debug!(
"[XUDP READ] Attempting to decode frame, buffer len: {}",
self.read_buffer.len()
);
// First, peek at the buffer to determine total frame size WITHOUT consuming anything.
// We need to check: metadata_len (2 bytes) + metadata + data_len (2 bytes) + data
// Need at least 2 bytes for metadata length
if self.read_buffer.len() < 2 {
log::debug!("[XUDP READ] Buffer too short for metadata length field");
return Ok(None);
}
let metadata_len = u16::from_be_bytes([self.read_buffer[0], self.read_buffer[1]]) as usize;
// Check if we have complete metadata
if self.read_buffer.len() < 2 + metadata_len {
log::debug!("[XUDP READ] Buffer too short for complete metadata");
return Ok(None);
}
// Peek at metadata to check if frame has data
if metadata_len < 4 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("metadata too short: {}", metadata_len),
));
}
// Peek at status and option bytes (at offset 2 + 2 + 1 = 5 for status, 6 for option)
let status_byte = self.read_buffer[2 + 2]; // Skip metadata_len(2) + session_id(2)
let option_byte = self.read_buffer[2 + 3]; // Skip metadata_len(2) + session_id(2) + status(1)
let has_data = (option_byte & 0x01) != 0; // FrameOption::DATA = 0x01
let is_end = status_byte == 0x03; // SessionStatus::End
let is_keepalive = status_byte == 0x04; // SessionStatus::KeepAlive
// For End or KeepAlive frames, or frames without data, we only need the metadata
if is_end || is_keepalive || !has_data {
// We have enough data to decode this frame - proceed with actual decode
let metadata = FrameMetadata::decode(&mut self.read_buffer)?
.expect("metadata decode should succeed after length check");
log::debug!("[XUDP READ] Decoded frame: session_id={}, status={:?}, has_data={}, target={:?}, network={:?}",
metadata.session_id, metadata.status, metadata.option.has_data(), metadata.target, metadata.network);
// Handle session mappings
if let Some(ref target) = metadata.target {
log::debug!(
"[XUDP READ] Updating session {} mapping to target {}",
metadata.session_id,
target
);
self.session_to_destination
.insert(metadata.session_id, target.clone());
self.session_to_original_destination
.insert(metadata.session_id, target.clone());
}
if metadata.status == SessionStatus::End {
if let Some(destination) = self.session_to_destination.remove(&metadata.session_id)
{
self.destination_to_session.remove(&destination);
}
self.session_to_original_destination
.remove(&metadata.session_id);
return self.try_decode_one_frame();
}
// No data, try to decode next frame
return self.try_decode_one_frame();
}
// Frame has data - check if we have the data length and data
let data_len_offset = 2 + metadata_len;
if self.read_buffer.len() < data_len_offset + 2 {
log::debug!("[XUDP READ] Buffer too short for data length field");
return Ok(None);
}
let data_len = u16::from_be_bytes([
self.read_buffer[data_len_offset],
self.read_buffer[data_len_offset + 1],
]) as usize;
// Check if we have all the data
let total_frame_len = data_len_offset + 2 + data_len;
if self.read_buffer.len() < total_frame_len {
log::debug!(
"[XUDP READ] Buffer too short for complete frame: have {}, need {}",
self.read_buffer.len(),
total_frame_len
);
return Ok(None);
}
// Now we know we have a complete frame - consume it all
let metadata = FrameMetadata::decode(&mut self.read_buffer)?
.expect("metadata decode should succeed after length check");
log::debug!("[XUDP READ] Decoded frame: session_id={}, status={:?}, has_data={}, target={:?}, network={:?}",
metadata.session_id, metadata.status, metadata.option.has_data(), metadata.target, metadata.network);
// Check for TCP destination - we don't support TCP over XUDP
if let Some(TargetNetwork::Tcp) = metadata.network {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"XUDP with TCP destinations is not supported. Only UDP destinations are supported.",
));
}
// Check for ERROR option bit - remote side is signaling an error
if metadata.option.has_error() {
log::error!(
"[XUDP READ] Received frame with ERROR option set for session {}",
metadata.session_id
);
return Err(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"XUDP session closed by remote with error",
));
}
// Store/update session mapping for session_id → destination
if let Some(ref target) = metadata.target {
log::debug!(
"[XUDP READ] Updating session {} mapping to target {}",
metadata.session_id,
target
);
self.session_to_destination
.insert(metadata.session_id, target.clone());
self.session_to_original_destination
.insert(metadata.session_id, target.clone());
}
// Consume data length (already verified we have it)
self.read_buffer.advance(2);
if data_len == 0 {
// Empty data, try to decode next frame
return self.try_decode_one_frame();
}
// Extract data (already verified we have it)
let data = self.read_buffer[..data_len].to_vec();
self.read_buffer.advance(data_len);
// Determine destination
let destination = if let Some(ref target) = metadata.target {
target.clone()
} else {
// Look up in session map
self.session_to_destination
.get(&metadata.session_id)
.cloned()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("unknown session ID: {}", metadata.session_id),
)
})?
};
log::debug!(
"[XUDP READ] Decoded complete frame with {} bytes for destination {}",
data.len(),
destination
);
Ok(Some((data, destination)))
}
}
impl AsyncFlushMessage for XudpMessageStream {
fn poll_flush_message(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
let this = self.get_mut();
while !this.write_buffer.is_empty() {
let n = ready!(Pin::new(&mut this.inner_stream).poll_write(cx, &this.write_buffer))?;
this.write_buffer.advance(n);
}
Pin::new(&mut this.inner_stream).poll_flush(cx)
}
}
impl AsyncShutdownMessage for XudpMessageStream {
fn poll_shutdown_message(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
ready!(self.as_mut().poll_flush_message(cx))?;
Pin::new(&mut self.get_mut().inner_stream).poll_shutdown(cx)
}
}
impl AsyncPing for XudpMessageStream {
fn supports_ping(&self) -> bool {
self.inner_stream.supports_ping()
}
fn poll_write_ping(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
Pin::new(&mut self.get_mut().inner_stream).poll_write_ping(cx)
}
}
impl AsyncReadSessionMessage for XudpMessageStream {
fn poll_read_session_message(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<(u16, SocketAddr)>> {
let this = self.get_mut();
// Return buffered message if available
if let Some((data, original_destination, resolved_opt)) = this.incoming_message.take() {
if data.len() > buf.remaining() {
// Re-buffer it
this.incoming_message = Some((data, original_destination, resolved_opt));
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"buffer too small for incoming message",
)));
}
// Resolve if not already resolved
let resolved_destination = if let Some(resolved) = resolved_opt {
resolved
} else {
match this
.resolver_cache
.poll_resolve_location(cx, &original_destination)
{
Poll::Ready(Ok(socket_addr)) => {
let resolved = match socket_addr {
SocketAddr::V4(addr) => {
NetLocation::new(Address::Ipv4(*addr.ip()), addr.port())
}
SocketAddr::V6(addr) => {
NetLocation::new(Address::Ipv6(*addr.ip()), addr.port())
}
};
resolved
}
Poll::Ready(Err(e)) => {
return Poll::Ready(Err(e));
}
Poll::Pending => {
// Re-buffer and wait for DNS
this.incoming_message = Some((data, original_destination, None));
return Poll::Pending;
}
}
};
// Find session ID for resolved destination, preserving original
let (session_id, _is_new) =
this.get_or_create_session(&resolved_destination, &original_destination);
// Convert to SocketAddr for return
let socket_addr = futures::ready!(this
.resolver_cache
.poll_resolve_location(cx, &original_destination))?;
buf.put_slice(&data);
return Poll::Ready(Ok((session_id, socket_addr)));
}
if this.is_eof {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF reached",
)));
}
loop {
// Try to decode a complete frame from the read buffer
match this.try_decode_one_frame()? {
Some((data, destination)) => {
// Resolve hostname to IP if needed
log::debug!("[XUDP SESSION READ] Resolving destination: {}", destination);
let socket_addr =
match this.resolver_cache.poll_resolve_location(cx, &destination) {
Poll::Ready(Ok(addr)) => addr,
Poll::Ready(Err(e)) => {
return Poll::Ready(Err(e));
}
Poll::Pending => {
// DNS resolution pending - buffer the frame and wait
this.incoming_message = Some((data, destination, None));
return Poll::Pending;
}
};
let resolved_destination = match socket_addr {
SocketAddr::V4(addr) => {
NetLocation::new(Address::Ipv4(*addr.ip()), addr.port())
}
SocketAddr::V6(addr) => {
NetLocation::new(Address::Ipv6(*addr.ip()), addr.port())
}
};
log::debug!(
"[XUDP SESSION READ] Resolved {} -> {}",
destination,
resolved_destination
);
// Get or create session, preserving original destination (may be hostname)
let (session_id, _is_new) =
this.get_or_create_session(&resolved_destination, &destination);
log::debug!(
"[XUDP SESSION READ] Session {} mapped to {}",
session_id,
resolved_destination
);
// Successfully decoded a frame
if data.len() > buf.remaining() {
// Buffer it with resolved destination for next read
this.incoming_message =
Some((data, destination, Some(resolved_destination)));
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"buffer too small for incoming message",
)));
}
buf.put_slice(&data);
log::debug!(
"[XUDP SESSION READ] Returning {} bytes for session {} to {}",
data.len(),
session_id,
socket_addr
);
return Poll::Ready(Ok((session_id, socket_addr)));
}
None => {
// Buffer doesn't have a complete frame, need to read more data
}
}
// Read more data from inner stream
let original_filled = this.read_buffer.len();
this.read_buffer.resize(original_filled + 8192, 0);
let mut temp_buf = ReadBuf::new(&mut this.read_buffer[original_filled..]);
log::debug!(
"[XUDP SESSION READ] Reading from inner stream, current buffer has {} bytes",
original_filled
);
let poll_result = Pin::new(&mut this.inner_stream).poll_read(cx, &mut temp_buf);
let n = temp_buf.filled().len();
this.read_buffer.truncate(original_filled + n);
match ready!(poll_result) {
Ok(()) => {
log::debug!(
"[XUDP SESSION READ] Got {} bytes from inner stream (total buffer: {})",
n,
this.read_buffer.len()
);
if n == 0 {
this.is_eof = true;
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF reached",
)));
}
// Got new data, continue loop to try decoding again
continue;
}
Err(e) => {
log::error!("[XUDP SESSION READ] Error reading from inner stream: {}", e);
return Poll::Ready(Err(e));
}
}
}
}
}
impl AsyncWriteSessionMessage for XudpMessageStream {
fn poll_write_session_message(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
session_id: u16,
buf: &[u8],
target: &SocketAddr,
) -> Poll<std::io::Result<()>> {
// This is the reverse direction: UDP response from internet → XUDP client
// CRITICAL: Use ORIGINAL destination (may be hostname) in response frame, NOT resolved IP!
// WebRTC and other protocols expect responses to reference the hostname they requested.
log::debug!(
"[XUDP SESSION WRITE] Writing {} bytes for session {} from source {}",
buf.len(),
session_id,
target
);
// Flush any pending write buffer first
if !self.write_buffer.is_empty() {
ready!(self.as_mut().poll_flush_message(cx))?;
}
// CRITICAL FIX: Use original destination (hostname) instead of resolved IP
// Look up the original destination that the client requested
let target_location = self
.session_to_original_destination
.get(&session_id)
.cloned()
.ok_or_else(|| {
log::error!(
"[XUDP SESSION WRITE] No original destination found for session {}! \
This should not happen - session was created but original dest not stored.",
session_id
);
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("No original destination for session {}", session_id),
)
})?;
log::debug!("[XUDP SESSION WRITE] Using original destination {} for session {} (response came from {})",
target_location, session_id, target);
// Build Keep frame with data
let metadata = FrameMetadata {
session_id,
status: SessionStatus::Keep,
option: FrameOption::new().with_data(),
target: Some(target_location.clone()), // Use ORIGINAL (hostname), not resolved IP!
network: Some(TargetNetwork::Udp),
};
log::debug!(
"[XUDP SESSION WRITE] Encoding Keep frame: session_id={}, target={}, data_len={}",
session_id,
target_location,
buf.len()
);
// Encode metadata
metadata.encode(&mut self.write_buffer)?;
// Write data length
self.write_buffer.put_u16(buf.len() as u16);
// Write data
self.write_buffer.extend_from_slice(buf);
// Flush immediately
self.poll_flush_message(cx)
}
}
impl AsyncSessionMessageStream for XudpMessageStream {}
+8
View File
@@ -0,0 +1,8 @@
// XUDP (Extended UDP) protocol implementation
// Protocol-agnostic UDP multiplexing over TCP connections
// Used by both VLESS and VMess protocols
pub mod frame;
pub mod message_stream;
pub use message_stream::XudpMessageStream;