feat(vpn transport): add QUIC transport support with auto fallback to WebSocket

This commit is contained in:
2026-03-19 21:53:30 +00:00
parent e14c357ba0
commit e81dd377d8
16 changed files with 2952 additions and 1888 deletions

556
rust/Cargo.lock generated
View File

@@ -120,6 +120,17 @@ version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -169,6 +180,12 @@ dependencies = [
"piper",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -205,6 +222,12 @@ dependencies = [
"shlex",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -298,6 +321,16 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -307,6 +340,22 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -374,6 +423,15 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -416,6 +474,18 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "fastbloom"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4"
dependencies = [
"getrandom 0.3.4",
"libm",
"rand 0.9.2",
"siphasher",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -549,8 +619,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@@ -560,9 +632,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasip2",
"wasm-bindgen",
]
[[package]]
@@ -624,6 +698,38 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "js-sys"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -646,6 +752,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libmimalloc-sys"
version = "0.1.44"
@@ -671,6 +783,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "matchers"
version = "0.2.0"
@@ -727,6 +845,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-conv"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -745,6 +869,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "parking"
version = "2.2.1"
@@ -774,6 +904,16 @@ dependencies = [
"windows-link",
]
[[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 = "pin-project-lite"
version = "0.2.16"
@@ -814,6 +954,12 @@ dependencies = [
"universal-hash",
]
[[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"
@@ -832,6 +978,63 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"fastbloom",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"slab",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
]
[[package]]
name = "quote"
version = "1.0.44"
@@ -906,6 +1109,19 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rcgen"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
dependencies = [
"pem",
"ring",
"rustls-pki-types",
"time",
"yasna",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -946,6 +1162,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -962,21 +1184,71 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"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.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.9"
@@ -988,12 +1260,59 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.27"
@@ -1090,6 +1409,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]]
name = "slab"
version = "0.4.12"
@@ -1107,23 +1432,31 @@ name = "smartvpn_daemon"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"base64",
"bytes",
"chacha20poly1305",
"clap",
"futures-util",
"mimalloc",
"quinn",
"rand 0.8.5",
"rcgen",
"ring",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"snow",
"thiserror",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite",
"tokio-util",
"tracing",
"tracing-subscriber",
"tun",
"webpki-roots 1.0.6",
]
[[package]]
@@ -1175,13 +1508,33 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.18",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@@ -1204,6 +1557,40 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.49.0"
@@ -1277,6 +1664,7 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -1346,7 +1734,7 @@ dependencies = [
"libc",
"log",
"nix",
"thiserror",
"thiserror 2.0.18",
"tokio",
"tokio-util",
"windows-sys 0.59.0",
@@ -1368,7 +1756,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"sha1",
"thiserror",
"thiserror 2.0.18",
"utf-8",
]
@@ -1424,6 +1812,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -1439,6 +1837,70 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
@@ -1457,12 +1919,30 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -1499,6 +1979,21 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -1532,6 +2027,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -1544,6 +2045,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -1556,6 +2063,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -1580,6 +2093,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -1592,6 +2111,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -1604,6 +2129,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -1616,6 +2147,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -1649,7 +2186,7 @@ dependencies = [
"futures",
"libloading",
"log",
"thiserror",
"thiserror 2.0.18",
"windows-sys 0.61.2",
"winreg",
]
@@ -1660,6 +2197,15 @@ version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[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.39"

View File

@@ -25,6 +25,14 @@ tun = { version = "0.7", features = ["async"] }
bytes = "1"
tokio-util = "0.7"
futures-util = "0.3"
async-trait = "0.1"
quinn = "0.11"
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
rcgen = "0.13"
ring = "0.17"
rustls-pki-types = "1"
rustls-pemfile = "2"
webpki-roots = "1"
mimalloc = "0.1"
[profile.release]

View File

@@ -1,10 +1,8 @@
use anyhow::Result;
use bytes::BytesMut;
use futures_util::{SinkExt, StreamExt};
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::{mpsc, watch, RwLock};
use tokio_tungstenite::tungstenite::Message;
use tracing::{info, error, warn, debug};
use crate::codec::{Frame, FrameCodec, PacketType};
@@ -12,6 +10,8 @@ use crate::crypto;
use crate::keepalive::{self, KeepaliveSignal, LinkHealth};
use crate::telemetry::ConnectionQuality;
use crate::transport;
use crate::transport_trait::{self, TransportSink, TransportStream};
use crate::quic_transport;
/// Client configuration (matches TS IVpnClientConfig).
#[derive(Debug, Clone, Deserialize)]
@@ -22,6 +22,10 @@ pub struct ClientConfig {
pub dns: Option<Vec<String>>,
pub mtu: Option<u16>,
pub keepalive_interval_secs: Option<u64>,
/// Transport type: "websocket" (default) or "quic".
pub transport: Option<String>,
/// For QUIC: SHA-256 hash of server certificate (base64) for cert pinning.
pub server_cert_hash: Option<String>,
}
/// Client statistics.
@@ -106,9 +110,66 @@ impl VpnClient {
&config.server_public_key,
)?;
// Connect to WebSocket server
let ws = transport::connect_to_server(&config.server_url).await?;
let (mut ws_sink, mut ws_stream) = ws.split();
// Create transport based on configuration
let (mut sink, mut stream): (Box<dyn TransportSink>, Box<dyn TransportStream>) = {
let transport_type = config.transport.as_deref().unwrap_or("auto");
match transport_type {
"quic" => {
let server_addr = &config.server_url; // For QUIC, serverUrl is host:port
let cert_hash = config.server_cert_hash.as_deref();
let conn = quic_transport::connect_quic(server_addr, cert_hash).await?;
let (quic_sink, quic_stream) = quic_transport::open_quic_streams(conn).await?;
info!("Connected via QUIC");
(Box::new(quic_sink) as Box<dyn TransportSink>,
Box::new(quic_stream) as Box<dyn TransportStream>)
}
"websocket" => {
let ws = transport::connect_to_server(&config.server_url).await?;
let (ws_sink, ws_stream) = transport_trait::split_ws(ws);
info!("Connected via WebSocket");
(Box::new(ws_sink), Box::new(ws_stream))
}
_ => {
// "auto" (default): try QUIC first, fall back to WebSocket
// Extract host:port from the URL for QUIC attempt
let quic_addr = extract_host_port(&config.server_url);
let cert_hash = config.server_cert_hash.as_deref();
if let Some(ref addr) = quic_addr {
match tokio::time::timeout(
std::time::Duration::from_secs(3),
try_quic_connect(addr, cert_hash),
).await {
Ok(Ok((quic_sink, quic_stream))) => {
info!("Auto: connected via QUIC to {}", addr);
(Box::new(quic_sink) as Box<dyn TransportSink>,
Box::new(quic_stream) as Box<dyn TransportStream>)
}
Ok(Err(e)) => {
debug!("Auto: QUIC failed ({}), falling back to WebSocket", e);
let ws = transport::connect_to_server(&config.server_url).await?;
let (ws_sink, ws_stream) = transport_trait::split_ws(ws);
info!("Auto: connected via WebSocket (QUIC unavailable)");
(Box::new(ws_sink), Box::new(ws_stream))
}
Err(_) => {
debug!("Auto: QUIC timed out, falling back to WebSocket");
let ws = transport::connect_to_server(&config.server_url).await?;
let (ws_sink, ws_stream) = transport_trait::split_ws(ws);
info!("Auto: connected via WebSocket (QUIC timed out)");
(Box::new(ws_sink), Box::new(ws_stream))
}
}
} else {
// Can't extract host:port for QUIC, use WebSocket directly
let ws = transport::connect_to_server(&config.server_url).await?;
let (ws_sink, ws_stream) = transport_trait::split_ws(ws);
info!("Connected via WebSocket");
(Box::new(ws_sink), Box::new(ws_stream))
}
}
}
};
// Noise NK handshake (client side = initiator)
*state.write().await = ClientState::Handshaking;
@@ -123,13 +184,11 @@ impl VpnClient {
};
let mut frame_bytes = BytesMut::new();
<FrameCodec as tokio_util::codec::Encoder<Frame>>::encode(&mut FrameCodec, init_frame, &mut frame_bytes)?;
ws_sink.send(Message::Binary(frame_bytes.to_vec().into())).await?;
sink.send_reliable(frame_bytes.to_vec()).await?;
// <- e, ee
let resp_msg = match ws_stream.next().await {
Some(Ok(Message::Binary(data))) => data.to_vec(),
Some(Ok(_)) => anyhow::bail!("Expected binary handshake response"),
Some(Err(e)) => anyhow::bail!("WebSocket error during handshake: {}", e),
let resp_msg = match stream.recv_reliable().await? {
Some(data) => data,
None => anyhow::bail!("Connection closed during handshake"),
};
@@ -145,9 +204,9 @@ impl VpnClient {
let mut noise_transport = initiator.into_transport_mode()?;
// Receive assigned IP info (encrypted)
let info_msg = match ws_stream.next().await {
Some(Ok(Message::Binary(data))) => data.to_vec(),
_ => anyhow::bail!("Expected IP info message"),
let info_msg = match stream.recv_reliable().await? {
Some(data) => data,
None => anyhow::bail!("Connection closed before IP info"),
};
let mut frame_buf = BytesMut::from(&info_msg[..]);
@@ -184,8 +243,8 @@ impl VpnClient {
// Spawn packet forwarding loop
let assigned_ip_clone = assigned_ip.clone();
tokio::spawn(client_loop(
ws_sink,
ws_stream,
sink,
stream,
noise_transport,
state,
stats,
@@ -280,8 +339,8 @@ impl VpnClient {
/// The main client packet forwarding loop (runs in a spawned task).
async fn client_loop(
mut ws_sink: futures_util::stream::SplitSink<transport::WsStream, Message>,
mut ws_stream: futures_util::stream::SplitStream<transport::WsStream>,
mut sink: Box<dyn TransportSink>,
mut stream: Box<dyn TransportStream>,
mut noise_transport: snow::TransportState,
state: Arc<RwLock<ClientState>>,
stats: Arc<RwLock<ClientStatistics>>,
@@ -294,10 +353,10 @@ async fn client_loop(
loop {
tokio::select! {
msg = ws_stream.next() => {
msg = stream.recv_reliable() => {
match msg {
Some(Ok(Message::Binary(data))) => {
let mut frame_buf = BytesMut::from(&data[..][..]);
Ok(Some(data)) => {
let mut frame_buf = BytesMut::from(&data[..]);
if let Ok(Some(frame)) = <FrameCodec as tokio_util::codec::Decoder>::decode(&mut FrameCodec, &mut frame_buf) {
match frame.packet_type {
PacketType::IpPacket => {
@@ -328,17 +387,13 @@ async fn client_loop(
}
}
}
Some(Ok(Message::Close(_))) | None => {
Ok(None) => {
info!("Connection closed");
*state.write().await = ClientState::Disconnected;
break;
}
Some(Ok(Message::Ping(data))) => {
let _ = ws_sink.send(Message::Pong(data)).await;
}
Some(Ok(_)) => continue,
Some(Err(e)) => {
error!("WebSocket error: {}", e);
Err(e) => {
error!("Transport error: {}", e);
*state.write().await = ClientState::Error(e.to_string());
break;
}
@@ -354,7 +409,7 @@ async fn client_loop(
};
let mut frame_bytes = BytesMut::new();
if <FrameCodec as tokio_util::codec::Encoder<Frame>>::encode(&mut FrameCodec, ka_frame, &mut frame_bytes).is_ok() {
if ws_sink.send(Message::Binary(frame_bytes.to_vec().into())).await.is_err() {
if sink.send_reliable(frame_bytes.to_vec()).await.is_err() {
warn!("Failed to send keepalive");
*state.write().await = ClientState::Disconnected;
break;
@@ -385,12 +440,51 @@ async fn client_loop(
};
let mut frame_bytes = BytesMut::new();
if <FrameCodec as tokio_util::codec::Encoder<Frame>>::encode(&mut FrameCodec, dc_frame, &mut frame_bytes).is_ok() {
let _ = ws_sink.send(Message::Binary(frame_bytes.to_vec().into())).await;
let _ = sink.send_reliable(frame_bytes.to_vec()).await;
}
let _ = ws_sink.close().await;
let _ = sink.close().await;
*state.write().await = ClientState::Disconnected;
break;
}
}
}
}
/// Try to connect via QUIC. Returns transport halves on success.
async fn try_quic_connect(
addr: &str,
cert_hash: Option<&str>,
) -> Result<(quic_transport::QuicTransportSink, quic_transport::QuicTransportStream)> {
let conn = quic_transport::connect_quic(addr, cert_hash).await?;
let (sink, stream) = quic_transport::open_quic_streams(conn).await?;
Ok((sink, stream))
}
/// Extract host:port from a WebSocket URL for QUIC auto-fallback.
/// e.g. "ws://127.0.0.1:8080" -> Some("127.0.0.1:8080")
/// "wss://vpn.example.com/tunnel" -> Some("vpn.example.com:443")
/// "127.0.0.1:8080" -> Some("127.0.0.1:8080") (already host:port)
fn extract_host_port(url: &str) -> Option<String> {
if url.starts_with("ws://") || url.starts_with("wss://") {
// Parse as URL
let stripped = if url.starts_with("wss://") {
&url[6..]
} else {
&url[5..]
};
// Remove path
let host_port = stripped.split('/').next()?;
if host_port.contains(':') {
Some(host_port.to_string())
} else {
// Default port
let default_port = if url.starts_with("wss://") { 443 } else { 80 };
Some(format!("{}:{}", host_port, default_port))
}
} else if url.contains(':') {
// Already host:port
Some(url.to_string())
} else {
None
}
}

View File

@@ -5,6 +5,8 @@ pub mod management;
pub mod codec;
pub mod crypto;
pub mod transport;
pub mod transport_trait;
pub mod quic_transport;
pub mod keepalive;
pub mod tunnel;
pub mod network;

546
rust/src/quic_transport.rs Normal file
View File

@@ -0,0 +1,546 @@
use anyhow::Result;
use async_trait::async_trait;
use quinn::crypto::rustls::QuicClientConfig;
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tracing::{info, warn, debug};
use crate::transport_trait::{TransportSink, TransportStream};
// ============================================================================
// TLS / Certificate helpers
// ============================================================================
/// Generate a self-signed certificate and private key for QUIC.
pub fn generate_self_signed_cert() -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
let cert = rcgen::generate_simple_self_signed(vec!["smartvpn".to_string()])?;
let cert_der = CertificateDer::from(cert.cert);
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()));
Ok((vec![cert_der], key_der))
}
/// Compute the SHA-256 hash of a DER-encoded certificate and return it as base64.
pub fn cert_hash(cert_der: &CertificateDer<'_>) -> String {
use ring::digest;
let hash = digest::digest(&digest::SHA256, cert_der.as_ref());
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash.as_ref())
}
// ============================================================================
// Server-side QUIC endpoint
// ============================================================================
/// Configuration for the QUIC server endpoint.
pub struct QuicServerConfig {
pub listen_addr: String,
pub cert_chain: Vec<CertificateDer<'static>>,
pub private_key: PrivateKeyDer<'static>,
pub idle_timeout_secs: u64,
}
/// Create a QUIC server endpoint bound to the given address.
pub fn create_quic_server(config: QuicServerConfig) -> Result<quinn::Endpoint> {
let addr: SocketAddr = config.listen_addr.parse()?;
let provider = Arc::new(rustls::crypto::ring::default_provider());
let mut tls_config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.with_no_client_auth()
.with_single_cert(config.cert_chain, config.private_key)?;
tls_config.alpn_protocols = vec![b"smartvpn".to_vec()];
let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(
quinn::crypto::rustls::QuicServerConfig::try_from(tls_config)?,
));
let mut transport = quinn::TransportConfig::default();
transport.max_idle_timeout(Some(
quinn::IdleTimeout::try_from(Duration::from_secs(config.idle_timeout_secs))?,
));
// Enable datagrams with a generous max size
transport.datagram_receive_buffer_size(Some(65535));
transport.datagram_send_buffer_size(65535);
server_config.transport_config(Arc::new(transport));
let endpoint = quinn::Endpoint::server(server_config, addr)?;
info!("QUIC server listening on {}", addr);
Ok(endpoint)
}
// ============================================================================
// Client-side QUIC connection
// ============================================================================
/// A certificate verifier that accepts any server certificate.
/// Safe when Noise NK provides server authentication at the application layer.
#[derive(Debug)]
struct AcceptAnyCert;
impl rustls::client::danger::ServerCertVerifier for AcceptAnyCert {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Err(rustls::Error::General("TLS 1.2 not supported".to_string()))
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}
/// A certificate verifier that accepts any certificate matching a given SHA-256 hash.
#[derive(Debug)]
struct CertHashVerifier {
expected_hash: String,
}
impl rustls::client::danger::ServerCertVerifier for CertHashVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
let actual_hash = cert_hash(end_entity);
if actual_hash == self.expected_hash {
Ok(rustls::client::danger::ServerCertVerified::assertion())
} else {
Err(rustls::Error::General(format!(
"Certificate hash mismatch: expected {}, got {}",
self.expected_hash, actual_hash
)))
}
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
// QUIC always uses TLS 1.3
Err(rustls::Error::General("TLS 1.2 not supported".to_string()))
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}
/// Connect to a QUIC server.
///
/// - If `server_cert_hash` is provided, verifies the server certificate matches
/// the given SHA-256 hash (cert pinning).
/// - If `server_cert_hash` is `None`, accepts any server certificate. This is
/// safe because the Noise NK handshake (which runs over the QUIC stream)
/// authenticates the server via its pre-shared public key — the same trust
/// model as WireGuard.
pub async fn connect_quic(
addr: &str,
server_cert_hash: Option<&str>,
) -> Result<quinn::Connection> {
let remote: SocketAddr = addr.parse()?;
let provider = Arc::new(rustls::crypto::ring::default_provider());
let tls_config = if let Some(hash) = server_cert_hash {
// Pin to a specific certificate hash
let mut config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.dangerous()
.with_custom_certificate_verifier(Arc::new(CertHashVerifier {
expected_hash: hash.to_string(),
}))
.with_no_client_auth();
config.alpn_protocols = vec![b"smartvpn".to_vec()];
config
} else {
// Accept any cert — Noise NK provides server authentication
let mut config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.dangerous()
.with_custom_certificate_verifier(Arc::new(AcceptAnyCert))
.with_no_client_auth();
config.alpn_protocols = vec![b"smartvpn".to_vec()];
config
};
let client_config = quinn::ClientConfig::new(Arc::new(
QuicClientConfig::try_from(tls_config)?,
));
let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse()?)?;
endpoint.set_default_client_config(client_config);
info!("Connecting to QUIC server at {}", addr);
let connection = endpoint.connect(remote, "smartvpn")?.await?;
info!("QUIC connection established");
Ok(connection)
}
// ============================================================================
// QUIC Transport Sink / Stream implementations
// ============================================================================
/// QUIC transport sink — wraps a SendStream (reliable) and Connection (datagrams).
pub struct QuicTransportSink {
send_stream: quinn::SendStream,
connection: quinn::Connection,
}
impl QuicTransportSink {
pub fn new(send_stream: quinn::SendStream, connection: quinn::Connection) -> Self {
Self {
send_stream,
connection,
}
}
}
#[async_trait]
impl TransportSink for QuicTransportSink {
async fn send_reliable(&mut self, data: Vec<u8>) -> Result<()> {
// Length-prefix framing: [4-byte big-endian length][payload]
let len = data.len() as u32;
self.send_stream.write_all(&len.to_be_bytes()).await?;
self.send_stream.write_all(&data).await?;
Ok(())
}
async fn send_datagram(&mut self, data: Vec<u8>) -> Result<()> {
let max_size = self.connection.max_datagram_size();
match max_size {
Some(max) if data.len() <= max => {
self.connection.send_datagram(data.into())?;
Ok(())
}
_ => {
// Datagram too large or datagrams disabled — fall back to reliable
debug!("Datagram too large ({}B), falling back to reliable stream", data.len());
self.send_reliable(data).await
}
}
}
async fn close(&mut self) -> Result<()> {
self.send_stream.finish()?;
Ok(())
}
}
/// QUIC transport stream — wraps a RecvStream (reliable) and Connection (datagrams).
pub struct QuicTransportStream {
recv_stream: quinn::RecvStream,
connection: quinn::Connection,
}
impl QuicTransportStream {
pub fn new(recv_stream: quinn::RecvStream, connection: quinn::Connection) -> Self {
Self {
recv_stream,
connection,
}
}
}
#[async_trait]
impl TransportStream for QuicTransportStream {
async fn recv_reliable(&mut self) -> Result<Option<Vec<u8>>> {
// Read length prefix
let mut len_buf = [0u8; 4];
match self.recv_stream.read_exact(&mut len_buf).await {
Ok(()) => {}
Err(quinn::ReadExactError::FinishedEarly(_)) => return Ok(None),
Err(quinn::ReadExactError::ReadError(quinn::ReadError::ConnectionLost(e))) => {
warn!("QUIC connection lost: {}", e);
return Ok(None);
}
Err(e) => return Err(anyhow::anyhow!("QUIC read error: {}", e)),
}
let len = u32::from_be_bytes(len_buf) as usize;
if len > 65536 {
return Err(anyhow::anyhow!("Frame too large: {} bytes", len));
}
let mut data = vec![0u8; len];
match self.recv_stream.read_exact(&mut data).await {
Ok(()) => Ok(Some(data)),
Err(quinn::ReadExactError::FinishedEarly(_)) => Ok(None),
Err(e) => Err(anyhow::anyhow!("QUIC read error: {}", e)),
}
}
async fn recv_datagram(&mut self) -> Result<Option<Vec<u8>>> {
match self.connection.read_datagram().await {
Ok(data) => Ok(Some(data.to_vec())),
Err(quinn::ConnectionError::ApplicationClosed(_)) => Ok(None),
Err(quinn::ConnectionError::LocallyClosed) => Ok(None),
Err(e) => Err(anyhow::anyhow!("QUIC datagram error: {}", e)),
}
}
fn supports_datagrams(&self) -> bool {
self.connection.max_datagram_size().is_some()
}
}
/// Accept a QUIC connection and open a bidirectional control stream.
/// Returns the transport sink/stream pair ready for the VPN handshake.
pub async fn accept_quic_connection(
conn: quinn::Connection,
) -> Result<(QuicTransportSink, QuicTransportStream)> {
// The client opens the bidirectional control stream
let (send, recv) = conn.accept_bi().await?;
info!("QUIC bidirectional control stream accepted");
Ok((
QuicTransportSink::new(send, conn.clone()),
QuicTransportStream::new(recv, conn),
))
}
/// Open a QUIC connection's bidirectional control stream (client side).
pub async fn open_quic_streams(
conn: quinn::Connection,
) -> Result<(QuicTransportSink, QuicTransportStream)> {
let (send, recv) = conn.open_bi().await?;
info!("QUIC bidirectional control stream opened");
Ok((
QuicTransportSink::new(send, conn.clone()),
QuicTransportStream::new(recv, conn),
))
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cert_generation_and_hash() {
let (certs, _key) = generate_self_signed_cert().unwrap();
assert_eq!(certs.len(), 1);
let hash = cert_hash(&certs[0]);
// SHA-256 base64 is 44 characters
assert_eq!(hash.len(), 44);
}
#[test]
fn test_cert_hash_deterministic() {
let (certs, _key) = generate_self_signed_cert().unwrap();
let hash1 = cert_hash(&certs[0]);
let hash2 = cert_hash(&certs[0]);
assert_eq!(hash1, hash2);
}
/// Helper: create QUIC server and client endpoints.
fn create_quic_endpoints() -> (quinn::Endpoint, quinn::Endpoint, String) {
let (certs, key) = generate_self_signed_cert().unwrap();
let hash = cert_hash(&certs[0]);
let provider = Arc::new(rustls::crypto::ring::default_provider());
let mut server_tls = rustls::ServerConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions().unwrap()
.with_no_client_auth()
.with_single_cert(certs, key).unwrap();
server_tls.alpn_protocols = vec![b"smartvpn".to_vec()];
let server_qcfg = quinn::ServerConfig::with_crypto(Arc::new(
quinn::crypto::rustls::QuicServerConfig::try_from(server_tls).unwrap(),
));
let server_ep = quinn::Endpoint::server(server_qcfg, "127.0.0.1:0".parse().unwrap()).unwrap();
let mut client_tls = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions().unwrap()
.dangerous()
.with_custom_certificate_verifier(Arc::new(CertHashVerifier {
expected_hash: hash,
}))
.with_no_client_auth();
client_tls.alpn_protocols = vec![b"smartvpn".to_vec()];
let client_config = quinn::ClientConfig::new(Arc::new(
QuicClientConfig::try_from(client_tls).unwrap(),
));
let mut client_ep = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap()).unwrap();
client_ep.set_default_client_config(client_config);
let server_addr = server_ep.local_addr().unwrap().to_string();
(server_ep, client_ep, server_addr)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_quic_server_client_roundtrip() {
let (server_ep, client_ep, server_addr) = create_quic_endpoints();
let addr: std::net::SocketAddr = server_addr.parse().unwrap();
// Server: accept, accept_bi, read, echo, finish
let server_task = tokio::spawn(async move {
let conn = server_ep.accept().await.unwrap().await.unwrap();
let (mut s_send, mut s_recv) = conn.accept_bi().await.unwrap();
let data = s_recv.read_to_end(1024).await.unwrap();
s_send.write_all(&data).await.unwrap();
s_send.finish().unwrap();
tokio::time::sleep(Duration::from_secs(1)).await;
server_ep
});
// Client: connect, open_bi, write, finish, read
let conn = client_ep.connect(addr, "smartvpn").unwrap().await.unwrap();
let (mut c_send, mut c_recv) = conn.open_bi().await.unwrap();
c_send.write_all(b"hello quinn").await.unwrap();
c_send.finish().unwrap();
let data = c_recv.read_to_end(1024).await.unwrap();
assert_eq!(&data[..], b"hello quinn");
let _ = server_task.await;
drop(client_ep);
}
/// Test transport trait wrappers over QUIC.
/// Key: client must send data first (QUIC streams are opened implicitly by data).
/// The server accept_bi runs concurrently with the client's first send_reliable.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_quic_transport_trait_roundtrip() {
let (server_ep, client_ep, server_addr) = create_quic_endpoints();
let addr: std::net::SocketAddr = server_addr.parse().unwrap();
// Server task: accept connection, then accept_bi (blocks until client sends data)
let server_task = tokio::spawn(async move {
let conn = server_ep.accept().await.unwrap().await.unwrap();
let (s_sink, s_stream) = accept_quic_connection(conn).await.unwrap();
(s_sink, s_stream, server_ep)
});
// Client: connect, open_bi via wrapper
let conn = client_ep.connect(addr, "smartvpn").unwrap().await.unwrap();
let (mut c_sink, mut c_stream) = open_quic_streams(conn).await.unwrap();
// Client sends first — this triggers the QUIC stream to become visible to the server
c_sink.send_reliable(b"hello-from-client".to_vec()).await.unwrap();
// Now server's accept_bi unblocks
let (mut s_sink, mut s_stream, _sep) = server_task.await.unwrap();
// Server reads the message
let msg = s_stream.recv_reliable().await.unwrap().unwrap();
assert_eq!(msg, b"hello-from-client");
// Server -> Client
s_sink.send_reliable(b"hello-from-server".to_vec()).await.unwrap();
let msg = c_stream.recv_reliable().await.unwrap().unwrap();
assert_eq!(msg, b"hello-from-server");
drop(client_ep);
}
/// Test QUIC datagram support.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_quic_datagram_exchange() {
let (server_ep, client_ep, server_addr) = create_quic_endpoints();
let addr: std::net::SocketAddr = server_addr.parse().unwrap();
// Server: accept, accept_bi (opens control stream), then read datagram
let server_task = tokio::spawn(async move {
let conn = server_ep.accept().await.unwrap().await.unwrap();
// Accept bi stream (control channel)
let (_s_sink, _s_stream) = accept_quic_connection(conn.clone()).await.unwrap();
// Read datagram
let dgram = conn.read_datagram().await.unwrap();
assert_eq!(&dgram[..], b"dgram-payload");
server_ep
});
// Client: connect, open bi stream (triggers server accept_bi), then send datagram
let conn = client_ep.connect(addr, "smartvpn").unwrap().await.unwrap();
let (mut c_sink, _c_stream) = open_quic_streams(conn.clone()).await.unwrap();
// Send initial data to open the stream (required for QUIC)
c_sink.send_reliable(b"init".to_vec()).await.unwrap();
// Small yield to let the server process the bi stream
tokio::task::yield_now().await;
// Send datagram
assert!(conn.max_datagram_size().is_some());
conn.send_datagram(bytes::Bytes::from_static(b"dgram-payload")).unwrap();
let _ = server_task.await.unwrap();
drop(client_ep);
}
/// Test that supports_datagrams returns true for QUIC transports.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_quic_supports_datagrams() {
let (server_ep, client_ep, server_addr) = create_quic_endpoints();
let addr: std::net::SocketAddr = server_addr.parse().unwrap();
let server_task = tokio::spawn(async move {
let conn = server_ep.accept().await.unwrap().await.unwrap();
let (_s_sink, s_stream) = accept_quic_connection(conn).await.unwrap();
assert!(s_stream.supports_datagrams());
server_ep
});
let conn = client_ep.connect(addr, "smartvpn").unwrap().await.unwrap();
let (mut c_sink, c_stream) = open_quic_streams(conn).await.unwrap();
assert!(c_stream.supports_datagrams());
// Send data to trigger server's accept_bi
c_sink.send_reliable(b"ping".to_vec()).await.unwrap();
let _ = server_task.await.unwrap();
drop(client_ep);
}
}

View File

@@ -130,10 +130,12 @@ mod tests {
#[test]
fn tokens_do_not_exceed_burst() {
let mut tb = TokenBucket::new(1_000_000, 1_000);
// Use a low rate so refill between consecutive calls is negligible
let mut tb = TokenBucket::new(100, 1_000);
// Wait to accumulate — but should cap at burst
std::thread::sleep(Duration::from_millis(50));
assert!(tb.try_consume(1_000));
// At 100 bytes/sec, the few μs between calls add ~0 tokens
assert!(!tb.try_consume(1));
}
}

View File

@@ -1,6 +1,5 @@
use anyhow::Result;
use bytes::BytesMut;
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::Ipv4Addr;
@@ -8,7 +7,6 @@ use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex, RwLock};
use tokio_tungstenite::tungstenite::Message;
use tracing::{info, error, warn};
use crate::codec::{Frame, FrameCodec, PacketType};
@@ -17,6 +15,8 @@ use crate::mtu::{MtuConfig, TunnelOverhead};
use crate::network::IpPool;
use crate::ratelimit::TokenBucket;
use crate::transport;
use crate::transport_trait::{self, TransportSink, TransportStream};
use crate::quic_transport;
/// Dead-peer timeout: 3x max keepalive interval (Healthy=60s).
const DEAD_PEER_TIMEOUT: Duration = Duration::from_secs(180);
@@ -39,6 +39,12 @@ pub struct ServerConfig {
pub default_rate_limit_bytes_per_sec: Option<u64>,
/// Default burst size for new clients (bytes). None = unlimited.
pub default_burst_bytes: Option<u64>,
/// Transport mode: "websocket" (default), "quic", or "both".
pub transport_mode: Option<String>,
/// QUIC listen address (host:port). Defaults to listen_addr.
pub quic_listen_addr: Option<String>,
/// QUIC idle timeout in seconds (default: 30).
pub quic_idle_timeout_secs: Option<u64>,
}
/// Information about a connected client.
@@ -135,14 +141,58 @@ impl VpnServer {
self.state = Some(state.clone());
self.shutdown_tx = Some(shutdown_tx);
let transport_mode = config.transport_mode.as_deref().unwrap_or("both");
let listen_addr = config.listen_addr.clone();
tokio::spawn(async move {
if let Err(e) = run_listener(state, listen_addr, &mut shutdown_rx).await {
error!("Server listener error: {}", e);
}
});
info!("VPN server started");
match transport_mode {
"quic" => {
let quic_addr = config.quic_listen_addr.clone().unwrap_or_else(|| listen_addr.clone());
let idle_timeout = config.quic_idle_timeout_secs.unwrap_or(30);
tokio::spawn(async move {
if let Err(e) = run_quic_listener(state, quic_addr, idle_timeout, &mut shutdown_rx).await {
error!("QUIC listener error: {}", e);
}
});
}
"both" => {
let quic_addr = config.quic_listen_addr.clone().unwrap_or_else(|| listen_addr.clone());
let idle_timeout = config.quic_idle_timeout_secs.unwrap_or(30);
let state2 = state.clone();
let (shutdown_tx2, mut shutdown_rx2) = mpsc::channel::<()>(1);
// Store second shutdown sender so both listeners stop
let shutdown_tx_orig = self.shutdown_tx.take().unwrap();
let (combined_tx, mut combined_rx) = mpsc::channel::<()>(1);
self.shutdown_tx = Some(combined_tx);
// Forward combined shutdown to both listeners
tokio::spawn(async move {
combined_rx.recv().await;
let _ = shutdown_tx_orig.send(()).await;
let _ = shutdown_tx2.send(()).await;
});
tokio::spawn(async move {
if let Err(e) = run_ws_listener(state, listen_addr, &mut shutdown_rx).await {
error!("WebSocket listener error: {}", e);
}
});
tokio::spawn(async move {
if let Err(e) = run_quic_listener(state2, quic_addr, idle_timeout, &mut shutdown_rx2).await {
error!("QUIC listener error: {}", e);
}
});
}
_ => {
// "websocket" (default)
tokio::spawn(async move {
if let Err(e) = run_ws_listener(state, listen_addr, &mut shutdown_rx).await {
error!("Server listener error: {}", e);
}
});
}
}
info!("VPN server started (transport: {})", transport_mode);
Ok(())
}
@@ -239,7 +289,9 @@ impl VpnServer {
}
}
async fn run_listener(
/// WebSocket listener — accepts TCP connections, upgrades to WS, then hands off
/// to the transport-agnostic `handle_client_connection`.
async fn run_ws_listener(
state: Arc<ServerState>,
listen_addr: String,
shutdown_rx: &mut mpsc::Receiver<()>,
@@ -255,8 +307,20 @@ async fn run_listener(
info!("New connection from {}", addr);
let state = state.clone();
tokio::spawn(async move {
if let Err(e) = handle_client_connection(state, stream).await {
warn!("Client connection error: {}", e);
match transport::accept_connection(stream).await {
Ok(ws) => {
let (sink, stream) = transport_trait::split_ws(ws);
if let Err(e) = handle_client_connection(
state,
Box::new(sink),
Box::new(stream),
).await {
warn!("Client connection error: {}", e);
}
}
Err(e) => {
warn!("WebSocket upgrade failed: {}", e);
}
}
});
}
@@ -275,13 +339,95 @@ async fn run_listener(
Ok(())
}
/// QUIC listener — accepts QUIC connections and hands off to the transport-agnostic
/// `handle_client_connection`.
async fn run_quic_listener(
state: Arc<ServerState>,
listen_addr: String,
idle_timeout_secs: u64,
shutdown_rx: &mut mpsc::Receiver<()>,
) -> Result<()> {
// Generate or use configured TLS certificate for QUIC
let (cert_chain, private_key) = if let (Some(ref cert_pem), Some(ref key_pem)) =
(&state.config.tls_cert, &state.config.tls_key)
{
// Parse PEM certificates
let certs: Vec<rustls_pki_types::CertificateDer<'static>> =
rustls_pemfile::certs(&mut cert_pem.as_bytes())
.collect::<Result<Vec<_>, _>>()?;
let key = rustls_pemfile::private_key(&mut key_pem.as_bytes())?
.ok_or_else(|| anyhow::anyhow!("No private key found in PEM"))?;
(certs, key)
} else {
// Generate self-signed certificate
let (certs, key) = quic_transport::generate_self_signed_cert()?;
info!("QUIC using self-signed certificate (hash: {})", quic_transport::cert_hash(&certs[0]));
(certs, key)
};
let endpoint = quic_transport::create_quic_server(quic_transport::QuicServerConfig {
listen_addr,
cert_chain,
private_key,
idle_timeout_secs,
})?;
loop {
tokio::select! {
incoming = endpoint.accept() => {
match incoming {
Some(incoming) => {
let state = state.clone();
tokio::spawn(async move {
match incoming.await {
Ok(conn) => {
let remote = conn.remote_address();
info!("New QUIC connection from {}", remote);
match quic_transport::accept_quic_connection(conn).await {
Ok((sink, stream)) => {
if let Err(e) = handle_client_connection(
state,
Box::new(sink),
Box::new(stream),
).await {
warn!("QUIC client error: {}", e);
}
}
Err(e) => {
warn!("QUIC stream accept failed: {}", e);
}
}
}
Err(e) => {
warn!("QUIC handshake failed: {}", e);
}
}
});
}
None => {
info!("QUIC endpoint closed");
break;
}
}
}
_ = shutdown_rx.recv() => {
info!("QUIC shutdown signal received");
endpoint.close(0u32.into(), b"shutdown");
break;
}
}
}
Ok(())
}
/// Transport-agnostic client handler. Performs the Noise NK handshake, registers
/// the client, and runs the main packet forwarding loop.
async fn handle_client_connection(
state: Arc<ServerState>,
stream: tokio::net::TcpStream,
mut sink: Box<dyn TransportSink>,
mut stream: Box<dyn TransportStream>,
) -> Result<()> {
let ws = transport::accept_connection(stream).await?;
let (mut ws_sink, mut ws_stream) = ws.split();
let client_id = uuid_v4();
let assigned_ip = state.ip_pool.lock().await.allocate(&client_id)?;
@@ -295,9 +441,9 @@ async fn handle_client_connection(
let mut buf = vec![0u8; 65535];
// Receive handshake init
let init_msg = match ws_stream.next().await {
Some(Ok(Message::Binary(data))) => data.to_vec(),
_ => anyhow::bail!("Expected handshake init message"),
let init_msg = match stream.recv_reliable().await? {
Some(data) => data,
None => anyhow::bail!("Connection closed before handshake"),
};
let mut frame_buf = BytesMut::from(&init_msg[..]);
@@ -318,7 +464,7 @@ async fn handle_client_connection(
};
let mut frame_bytes = BytesMut::new();
<FrameCodec as tokio_util::codec::Encoder<Frame>>::encode(&mut FrameCodec, response_frame, &mut frame_bytes)?;
ws_sink.send(Message::Binary(frame_bytes.to_vec().into())).await?;
sink.send_reliable(frame_bytes.to_vec()).await?;
let mut noise_transport = responder.into_transport_mode()?;
@@ -369,7 +515,7 @@ async fn handle_client_connection(
};
let mut frame_bytes = BytesMut::new();
<FrameCodec as tokio_util::codec::Encoder<Frame>>::encode(&mut FrameCodec, encrypted_info, &mut frame_bytes)?;
ws_sink.send(Message::Binary(frame_bytes.to_vec().into())).await?;
sink.send_reliable(frame_bytes.to_vec()).await?;
info!("Client {} connected with IP {}", client_id, assigned_ip);
@@ -378,11 +524,11 @@ async fn handle_client_connection(
loop {
tokio::select! {
msg = ws_stream.next() => {
msg = stream.recv_reliable() => {
match msg {
Some(Ok(Message::Binary(data))) => {
Ok(Some(data)) => {
last_activity = tokio::time::Instant::now();
let mut frame_buf = BytesMut::from(&data[..][..]);
let mut frame_buf = BytesMut::from(&data[..]);
match <FrameCodec as tokio_util::codec::Decoder>::decode(&mut FrameCodec, &mut frame_buf) {
Ok(Some(frame)) => match frame.packet_type {
PacketType::IpPacket => {
@@ -432,7 +578,7 @@ async fn handle_client_connection(
};
let mut frame_bytes = BytesMut::new();
<FrameCodec as tokio_util::codec::Encoder<Frame>>::encode(&mut FrameCodec, ack_frame, &mut frame_bytes)?;
ws_sink.send(Message::Binary(frame_bytes.to_vec().into())).await?;
sink.send_reliable(frame_bytes.to_vec()).await?;
let mut stats = state.stats.write().await;
stats.keepalives_received += 1;
@@ -463,20 +609,12 @@ async fn handle_client_connection(
}
}
}
Some(Ok(Message::Close(_))) | None => {
Ok(None) => {
info!("Client {} connection closed", client_id);
break;
}
Some(Ok(Message::Ping(data))) => {
last_activity = tokio::time::Instant::now();
ws_sink.send(Message::Pong(data)).await?;
}
Some(Ok(_)) => {
last_activity = tokio::time::Instant::now();
continue;
}
Some(Err(e)) => {
warn!("WebSocket error from {}: {}", client_id, e);
Err(e) => {
warn!("Transport error from {}: {}", client_id, e);
break;
}
}

116
rust/src/transport_trait.rs Normal file
View File

@@ -0,0 +1,116 @@
use anyhow::Result;
use async_trait::async_trait;
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::Message;
use crate::transport::WsStream;
// ============================================================================
// Transport trait abstraction
// ============================================================================
/// Outbound half of a VPN transport connection.
#[async_trait]
pub trait TransportSink: Send + 'static {
/// Send a framed binary message on the reliable channel.
async fn send_reliable(&mut self, data: Vec<u8>) -> Result<()>;
/// Send a datagram (unreliable, best-effort).
/// Falls back to reliable if the transport does not support datagrams.
async fn send_datagram(&mut self, data: Vec<u8>) -> Result<()>;
/// Gracefully close the transport.
async fn close(&mut self) -> Result<()>;
}
/// Inbound half of a VPN transport connection.
#[async_trait]
pub trait TransportStream: Send + 'static {
/// Receive the next reliable binary message. Returns `None` on close.
async fn recv_reliable(&mut self) -> Result<Option<Vec<u8>>>;
/// Receive the next datagram. Returns `None` if datagrams are unsupported
/// or the connection is closed.
async fn recv_datagram(&mut self) -> Result<Option<Vec<u8>>>;
/// Whether this transport supports unreliable datagrams.
fn supports_datagrams(&self) -> bool;
}
// ============================================================================
// WebSocket implementation
// ============================================================================
/// WebSocket transport sink (wraps the write half of a split WsStream).
pub struct WsTransportSink {
inner: futures_util::stream::SplitSink<WsStream, Message>,
}
impl WsTransportSink {
pub fn new(inner: futures_util::stream::SplitSink<WsStream, Message>) -> Self {
Self { inner }
}
}
#[async_trait]
impl TransportSink for WsTransportSink {
async fn send_reliable(&mut self, data: Vec<u8>) -> Result<()> {
self.inner.send(Message::Binary(data.into())).await?;
Ok(())
}
async fn send_datagram(&mut self, data: Vec<u8>) -> Result<()> {
// WebSocket has no datagram support — fall back to reliable.
self.send_reliable(data).await
}
async fn close(&mut self) -> Result<()> {
self.inner.close().await?;
Ok(())
}
}
/// WebSocket transport stream (wraps the read half of a split WsStream).
pub struct WsTransportStream {
inner: futures_util::stream::SplitStream<WsStream>,
}
impl WsTransportStream {
pub fn new(inner: futures_util::stream::SplitStream<WsStream>) -> Self {
Self { inner }
}
}
#[async_trait]
impl TransportStream for WsTransportStream {
async fn recv_reliable(&mut self) -> Result<Option<Vec<u8>>> {
loop {
match self.inner.next().await {
Some(Ok(Message::Binary(data))) => return Ok(Some(data.to_vec())),
Some(Ok(Message::Close(_))) | None => return Ok(None),
Some(Ok(Message::Ping(_))) => {
// Ping handling is done at the tungstenite layer automatically
// when the sink side is alive. Just skip here.
continue;
}
Some(Ok(_)) => continue,
Some(Err(e)) => return Err(anyhow::anyhow!("WebSocket error: {}", e)),
}
}
}
async fn recv_datagram(&mut self) -> Result<Option<Vec<u8>>> {
// WebSocket does not support datagrams.
Ok(None)
}
fn supports_datagrams(&self) -> bool {
false
}
}
/// Split a WebSocket stream into transport sink and stream halves.
pub fn split_ws(ws: WsStream) -> (WsTransportSink, WsTransportStream) {
let (sink, stream) = ws.split();
(WsTransportSink::new(sink), WsTransportStream::new(stream))
}