mirror of
https://github.com/CloudNebulaProject/wayray.git
synced 2026-04-10 13:10:41 +00:00
Add QUIC transport layer with quinn for server and client
Implement QUIC networking for wrsrvd (server) and wrclient (client) using quinn over rustls with self-signed certificates. Three logical channels: control (bidirectional), display (server->client unidirectional), and input (client->server unidirectional). Server runs tokio in a background thread, communicating with the compositor via std::sync::mpsc channels. Client exposes an async connect() API that returns a ServerConnection with methods for sending input and receiving frames. Key design note: quinn streams are lazily materialized -- accept_bi/ accept_uni on the peer won't resolve until data is written. The handshake protocol accounts for this by having each side write immediately after opening streams.
This commit is contained in:
parent
8a3d14ff19
commit
f79a934c2b
8 changed files with 1548 additions and 13 deletions
541
Cargo.lock
generated
541
Cargo.lock
generated
|
|
@ -48,7 +48,7 @@ dependencies = [
|
|||
"android-properties",
|
||||
"bitflags 2.11.0",
|
||||
"cc",
|
||||
"jni",
|
||||
"jni 0.22.4",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
|
|
@ -142,6 +142,12 @@ dependencies = [
|
|||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
|
@ -256,6 +262,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"
|
||||
|
|
@ -316,6 +328,16 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
|
@ -329,7 +351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
|
|
@ -342,7 +364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -394,6 +416,15 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||
|
||||
[[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"
|
||||
|
|
@ -477,6 +508,18 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
|
|
@ -566,6 +609,19 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
|
|
@ -573,9 +629,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -688,6 +746,22 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[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 0.3.1",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
|
|
@ -802,6 +876,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 = "libredox"
|
||||
version = "0.1.15"
|
||||
|
|
@ -841,6 +921,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"
|
||||
|
|
@ -904,6 +990,17 @@ dependencies = [
|
|||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
|
|
@ -955,6 +1052,12 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
@ -1213,6 +1316,12 @@ version = "1.21.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.51"
|
||||
|
|
@ -1235,6 +1344,16 @@ version = "1.0.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[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"
|
||||
|
|
@ -1324,6 +1443,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
|
@ -1389,6 +1514,63 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"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.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
|
|
@ -1445,6 +1627,19 @@ version = "0.6.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[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.4.1"
|
||||
|
|
@ -1480,12 +1675,32 @@ version = "0.8.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1521,6 +1736,80 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
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-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 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.21.1",
|
||||
"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.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
|
|
@ -1536,6 +1825,15 @@ 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 = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
|
|
@ -1548,6 +1846,29 @@ 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 2.11.0",
|
||||
"core-foundation 0.10.1",
|
||||
"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.28"
|
||||
|
|
@ -1639,6 +1960,12 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[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"
|
||||
|
|
@ -1724,6 +2051,16 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
|
|
@ -1739,6 +2076,12 @@ version = "1.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "supports-color"
|
||||
version = "3.0.2"
|
||||
|
|
@ -1853,6 +2196,65 @@ 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.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
|
|
@ -1987,6 +2389,12 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2009,6 +2417,12 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
|
|
@ -2293,6 +2707,15 @@ dependencies = [
|
|||
"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 = "winapi-util"
|
||||
version = "0.1.11"
|
||||
|
|
@ -2308,13 +2731,22 @@ 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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2323,7 +2755,7 @@ version = "0.59.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2335,34 +2767,67 @@ 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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[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"
|
||||
|
|
@ -2375,24 +2840,48 @@ version = "0.52.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[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"
|
||||
|
|
@ -2414,7 +2903,7 @@ dependencies = [
|
|||
"calloop 0.13.0",
|
||||
"cfg_aliases",
|
||||
"concurrent-queue",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics",
|
||||
"cursor-icon",
|
||||
"dpi",
|
||||
|
|
@ -2561,7 +3050,13 @@ name = "wrclient"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"miette",
|
||||
"quinn",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wayray-protocol",
|
||||
]
|
||||
|
||||
|
|
@ -2571,8 +3066,13 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"ctrlc",
|
||||
"miette",
|
||||
"quinn",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"serde",
|
||||
"smithay",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wayray-protocol",
|
||||
|
|
@ -2652,6 +3152,15 @@ version = "0.8.28"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[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.48"
|
||||
|
|
@ -2672,6 +3181,12 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
|
|
|||
|
|
@ -21,3 +21,7 @@ thiserror = "2"
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
zstd = "0.13"
|
||||
quinn = "0.11"
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||
rcgen = "0.13"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync"] }
|
||||
|
|
|
|||
|
|
@ -7,4 +7,12 @@ license.workspace = true
|
|||
[dependencies]
|
||||
wayray-protocol.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
miette.workspace = true
|
||||
serde.workspace = true
|
||||
quinn.workspace = true
|
||||
rustls.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rcgen.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
pub mod network;
|
||||
|
||||
fn main() {
|
||||
println!("wrclient viewer");
|
||||
}
|
||||
|
|
|
|||
395
crates/wrclient/src/network.rs
Normal file
395
crates/wrclient/src/network.rs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
//! QUIC transport client for wrclient.
|
||||
//!
|
||||
//! Connects to a wrsrvd server, establishes three logical streams:
|
||||
//! - **Control** (bidirectional): handshake, ping/pong, frame acks
|
||||
//! - **Display** (server→client, unidirectional): frame updates
|
||||
//! - **Input** (client→server, unidirectional): keyboard/pointer events
|
||||
//!
|
||||
//! ## Quinn stream semantics
|
||||
//!
|
||||
//! Quinn streams are lazily materialized on the wire: the peer's
|
||||
//! `accept_bi()`/`accept_uni()` won't resolve until data is written.
|
||||
//! The handshake protocol accounts for this by having each side write
|
||||
//! data before the other side tries to accept.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use quinn::rustls::pki_types::CertificateDer;
|
||||
use tracing::info;
|
||||
use wayray_protocol::codec;
|
||||
use wayray_protocol::messages::{
|
||||
ClientHello, ControlMessage, DisplayMessage, InputMessage, ServerHello,
|
||||
};
|
||||
|
||||
/// Configuration for the QUIC client connection.
|
||||
pub struct ClientConfig {
|
||||
/// Server address to connect to.
|
||||
pub server_addr: SocketAddr,
|
||||
/// Client capabilities to advertise in the hello.
|
||||
pub capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ClientConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
server_addr: "127.0.0.1:4433".parse().unwrap(),
|
||||
capabilities: vec!["display".to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An established connection to a wrsrvd server with all streams ready.
|
||||
///
|
||||
/// After `connect()`, the control stream and input stream are ready to use.
|
||||
/// The display stream is accepted lazily when the server sends the first frame.
|
||||
pub struct ServerConnection {
|
||||
/// The underlying QUIC connection.
|
||||
pub connection: quinn::Connection,
|
||||
/// Bidirectional control stream -- send side.
|
||||
pub control_send: quinn::SendStream,
|
||||
/// Bidirectional control stream -- receive side.
|
||||
pub control_recv: quinn::RecvStream,
|
||||
/// Unidirectional input stream to server (send only).
|
||||
pub input_send: quinn::SendStream,
|
||||
/// The server hello received during handshake.
|
||||
pub server_hello: ServerHello,
|
||||
}
|
||||
|
||||
impl ServerConnection {
|
||||
/// Send a frame acknowledgment to the server.
|
||||
pub async fn send_frame_ack(
|
||||
&mut self,
|
||||
sequence: u64,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let msg = ControlMessage::FrameAck(wayray_protocol::messages::FrameAck { sequence });
|
||||
write_message(&mut self.control_send, &msg).await
|
||||
}
|
||||
|
||||
/// Send an input message to the server.
|
||||
pub async fn send_input(
|
||||
&mut self,
|
||||
input: &InputMessage,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
write_message(&mut self.input_send, input).await
|
||||
}
|
||||
|
||||
/// Accept the display stream from the server and read the next frame.
|
||||
///
|
||||
/// On first call, accepts the unidirectional stream from the server.
|
||||
/// The server triggers this by writing the first frame update.
|
||||
pub async fn accept_display_stream(
|
||||
&mut self,
|
||||
) -> Result<quinn::RecvStream, Box<dyn std::error::Error>> {
|
||||
let recv = self.connection.accept_uni().await?;
|
||||
info!("display stream accepted");
|
||||
Ok(recv)
|
||||
}
|
||||
|
||||
/// Read the next control message from the server.
|
||||
pub async fn recv_control(&mut self) -> Result<ControlMessage, Box<dyn std::error::Error>> {
|
||||
read_message(&mut self.control_recv).await
|
||||
}
|
||||
|
||||
/// Send a pong response.
|
||||
pub async fn send_pong(&mut self, timestamp: u64) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let msg = ControlMessage::Pong(wayray_protocol::messages::Pong { timestamp });
|
||||
write_message(&mut self.control_send, &msg).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Dummy certificate verifier that accepts any server cert.
|
||||
///
|
||||
/// Used during development when servers use self-signed certificates.
|
||||
/// TODO: Replace with proper certificate pinning or CA verification.
|
||||
#[derive(Debug)]
|
||||
struct SkipServerVerification;
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
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> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a quinn client config that skips certificate verification.
|
||||
fn build_client_config() -> quinn::ClientConfig {
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
let crypto = rustls::ClientConfig::builder_with_provider(provider.into())
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("TLS protocol versions")
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(std::sync::Arc::new(SkipServerVerification))
|
||||
.with_no_client_auth();
|
||||
let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(crypto)
|
||||
.expect("QUIC client crypto config");
|
||||
quinn::ClientConfig::new(std::sync::Arc::new(crypto))
|
||||
}
|
||||
|
||||
/// Connect to a wrsrvd server and perform the handshake.
|
||||
///
|
||||
/// Returns a `ServerConnection` with the control and input streams ready.
|
||||
/// The display stream is accepted lazily via `accept_display_stream()`.
|
||||
///
|
||||
/// The caller must keep the returned `quinn::Endpoint` alive for the
|
||||
/// duration of the connection.
|
||||
pub async fn connect(
|
||||
config: &ClientConfig,
|
||||
) -> Result<(quinn::Endpoint, ServerConnection), Box<dyn std::error::Error>> {
|
||||
let client_config = build_client_config();
|
||||
|
||||
let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse::<SocketAddr>()?)?;
|
||||
endpoint.set_default_client_config(client_config);
|
||||
|
||||
info!(server = %config.server_addr, "connecting to wrsrvd");
|
||||
let connection = endpoint.connect(config.server_addr, "localhost")?.await?;
|
||||
info!("QUIC connection established");
|
||||
|
||||
// Open control stream (bidirectional) and immediately send ClientHello.
|
||||
// Writing data triggers the server's accept_bi().
|
||||
let (mut control_send, mut control_recv) = connection.open_bi().await?;
|
||||
info!("control stream opened");
|
||||
|
||||
let client_hello = ControlMessage::ClientHello(ClientHello {
|
||||
version: wayray_protocol::PROTOCOL_VERSION,
|
||||
capabilities: config.capabilities.clone(),
|
||||
});
|
||||
write_message(&mut control_send, &client_hello).await?;
|
||||
info!("sent ClientHello");
|
||||
|
||||
// Read ServerHello response.
|
||||
let response: ControlMessage = read_message(&mut control_recv).await?;
|
||||
let server_hello = match response {
|
||||
ControlMessage::ServerHello(hello) => {
|
||||
info!(
|
||||
version = hello.version,
|
||||
session_id = hello.session_id,
|
||||
width = hello.output_width,
|
||||
height = hello.output_height,
|
||||
"received ServerHello"
|
||||
);
|
||||
hello
|
||||
}
|
||||
other => {
|
||||
return Err(format!("expected ServerHello, got {other:?}").into());
|
||||
}
|
||||
};
|
||||
|
||||
// Open input stream (unidirectional to server).
|
||||
// Data written later via send_input() triggers the server's accept_uni().
|
||||
let input_send = connection.open_uni().await?;
|
||||
info!("input stream opened");
|
||||
|
||||
Ok((
|
||||
endpoint,
|
||||
ServerConnection {
|
||||
connection,
|
||||
control_send,
|
||||
control_recv,
|
||||
input_send,
|
||||
server_hello,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Read a length-prefixed message from a QUIC receive stream.
|
||||
async fn read_message<T: serde::de::DeserializeOwned>(
|
||||
recv: &mut quinn::RecvStream,
|
||||
) -> Result<T, Box<dyn std::error::Error>> {
|
||||
let mut len_buf = [0u8; 4];
|
||||
recv.read_exact(&mut len_buf).await?;
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
|
||||
let mut payload = vec![0u8; len];
|
||||
recv.read_exact(&mut payload).await?;
|
||||
|
||||
let msg = codec::decode(&payload)?;
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Write a length-prefixed message to a QUIC send stream.
|
||||
async fn write_message<T: serde::Serialize>(
|
||||
send: &mut quinn::SendStream,
|
||||
msg: &T,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let encoded = codec::encode(msg)?;
|
||||
send.write_all(&encoded).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a frame update from a display receive stream.
|
||||
pub async fn read_display_message(
|
||||
recv: &mut quinn::RecvStream,
|
||||
) -> Result<DisplayMessage, Box<dyn std::error::Error>> {
|
||||
read_message(recv).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wayray_protocol::messages::{FrameUpdate, KeyState, KeyboardEvent};
|
||||
|
||||
/// Helper: start a test server on an ephemeral port, return its address.
|
||||
async fn start_test_server() -> (quinn::Endpoint, SocketAddr) {
|
||||
let CertifiedKey { cert, key_pair } =
|
||||
rcgen::generate_simple_self_signed(vec!["localhost".to_string(), "wayray".to_string()])
|
||||
.unwrap();
|
||||
let cert_der = CertificateDer::from(cert);
|
||||
let key_der =
|
||||
quinn::rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der()).unwrap();
|
||||
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
let crypto = rustls::ServerConfig::builder_with_provider(provider.into())
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("TLS protocol versions")
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(vec![cert_der], key_der)
|
||||
.unwrap();
|
||||
let crypto = quinn::crypto::rustls::QuicServerConfig::try_from(crypto).unwrap();
|
||||
let server_config = quinn::ServerConfig::with_crypto(std::sync::Arc::new(crypto));
|
||||
|
||||
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
let endpoint = quinn::Endpoint::server(server_config, addr).unwrap();
|
||||
let actual_addr = endpoint.local_addr().unwrap();
|
||||
(endpoint, actual_addr)
|
||||
}
|
||||
|
||||
use rcgen::CertifiedKey;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn client_connect_and_handshake() {
|
||||
let (endpoint, addr) = start_test_server().await;
|
||||
|
||||
// Server side: accept bi (triggered by client's ClientHello write),
|
||||
// read ClientHello, send ServerHello.
|
||||
let server = tokio::spawn(async move {
|
||||
let incoming = endpoint.accept().await.unwrap();
|
||||
let connection = incoming.await.unwrap();
|
||||
|
||||
let (mut control_send, mut control_recv) = connection.accept_bi().await.unwrap();
|
||||
|
||||
let hello: ControlMessage = read_message(&mut control_recv).await.unwrap();
|
||||
assert!(matches!(hello, ControlMessage::ClientHello(_)));
|
||||
|
||||
let server_hello = ControlMessage::ServerHello(ServerHello {
|
||||
version: wayray_protocol::PROTOCOL_VERSION,
|
||||
session_id: 99,
|
||||
output_width: 1920,
|
||||
output_height: 1080,
|
||||
});
|
||||
write_message(&mut control_send, &server_hello)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for the client to finish setup (it opens input uni).
|
||||
// Read next control message or wait for disconnect.
|
||||
let _ = read_message::<ControlMessage>(&mut control_recv).await;
|
||||
|
||||
endpoint.close(0u32.into(), b"done");
|
||||
});
|
||||
|
||||
let config = ClientConfig {
|
||||
server_addr: addr,
|
||||
capabilities: vec!["test".to_string()],
|
||||
};
|
||||
|
||||
let (_endpoint, mut conn) = connect(&config).await.unwrap();
|
||||
assert_eq!(conn.server_hello.session_id, 99);
|
||||
assert_eq!(conn.server_hello.output_width, 1920);
|
||||
|
||||
// Send a ping so the server's read resolves and it can shut down.
|
||||
let _ = conn.send_pong(0).await;
|
||||
|
||||
server.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn client_send_input_receive_frame() {
|
||||
let (endpoint, addr) = start_test_server().await;
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
let incoming = endpoint.accept().await.unwrap();
|
||||
let connection = incoming.await.unwrap();
|
||||
|
||||
// Handshake on control stream.
|
||||
let (mut control_send, mut control_recv) = connection.accept_bi().await.unwrap();
|
||||
let _: ControlMessage = read_message(&mut control_recv).await.unwrap();
|
||||
let server_hello = ControlMessage::ServerHello(ServerHello {
|
||||
version: wayray_protocol::PROTOCOL_VERSION,
|
||||
session_id: 1,
|
||||
output_width: 800,
|
||||
output_height: 600,
|
||||
});
|
||||
write_message(&mut control_send, &server_hello)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send frame on display uni (triggers client's accept_uni).
|
||||
let mut display_send = connection.open_uni().await.unwrap();
|
||||
let frame = DisplayMessage::FrameUpdate(FrameUpdate {
|
||||
sequence: 7,
|
||||
regions: vec![],
|
||||
});
|
||||
write_message(&mut display_send, &frame).await.unwrap();
|
||||
|
||||
// Accept input uni (triggered by client writing input).
|
||||
let mut input_recv = connection.accept_uni().await.unwrap();
|
||||
let input: InputMessage = read_message(&mut input_recv).await.unwrap();
|
||||
assert!(matches!(input, InputMessage::Keyboard(_)));
|
||||
|
||||
endpoint.close(0u32.into(), b"done");
|
||||
});
|
||||
|
||||
let config = ClientConfig {
|
||||
server_addr: addr,
|
||||
capabilities: vec![],
|
||||
};
|
||||
|
||||
let (_endpoint, mut conn) = connect(&config).await.unwrap();
|
||||
|
||||
// Send input (triggers server's accept_uni for input stream).
|
||||
let input = InputMessage::Keyboard(KeyboardEvent {
|
||||
keycode: 42,
|
||||
state: KeyState::Pressed,
|
||||
time: 1000,
|
||||
});
|
||||
conn.send_input(&input).await.unwrap();
|
||||
|
||||
// Accept display stream (triggered by server writing frame).
|
||||
let mut display_recv = conn.accept_display_stream().await.unwrap();
|
||||
let frame = read_display_message(&mut display_recv).await.unwrap();
|
||||
let DisplayMessage::FrameUpdate(update) = frame;
|
||||
assert_eq!(update.sequence, 7);
|
||||
|
||||
server.await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ tracing.workspace = true
|
|||
tracing-subscriber.workspace = true
|
||||
miette.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
smithay = { version = "0.7", default-features = false, features = [
|
||||
"wayland_frontend",
|
||||
|
|
@ -19,3 +20,7 @@ smithay = { version = "0.7", default-features = false, features = [
|
|||
"backend_winit",
|
||||
] }
|
||||
ctrlc = "3"
|
||||
quinn.workspace = true
|
||||
rustls.workspace = true
|
||||
rcgen.workspace = true
|
||||
tokio.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
mod backend;
|
||||
mod errors;
|
||||
mod handlers;
|
||||
pub mod network;
|
||||
mod state;
|
||||
|
||||
use crate::state::WayRay;
|
||||
|
|
|
|||
605
crates/wrsrvd/src/network.rs
Normal file
605
crates/wrsrvd/src/network.rs
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
//! QUIC transport server for wrsrvd.
|
||||
//!
|
||||
//! Runs a tokio runtime in a background thread, accepting a single client
|
||||
//! connection. Communicates with the compositor via `std::sync::mpsc` channels.
|
||||
//!
|
||||
//! Three logical QUIC streams:
|
||||
//! - **Control** (bidirectional): handshake, ping/pong, frame acks
|
||||
//! - **Display** (server→client, unidirectional): frame updates
|
||||
//! - **Input** (client→server, unidirectional): keyboard/pointer events
|
||||
//!
|
||||
//! ## Quinn stream semantics
|
||||
//!
|
||||
//! Quinn streams are lazily materialized on the wire: `open_bi()` and
|
||||
//! `open_uni()` return immediately, but the peer's `accept_bi()`/`accept_uni()`
|
||||
//! won't resolve until data is actually written to the stream. The handshake
|
||||
//! protocol accounts for this by having each side write data before the other
|
||||
//! side tries to accept.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
use quinn::rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use rcgen::CertifiedKey;
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing::{error, info, warn};
|
||||
use wayray_protocol::codec;
|
||||
use wayray_protocol::messages::{
|
||||
ClientHello, ControlMessage, DisplayMessage, FrameUpdate, InputMessage, ServerHello,
|
||||
};
|
||||
|
||||
/// Messages sent from the compositor to the network thread.
|
||||
pub enum CompositorToNet {
|
||||
/// Send a frame update to the connected client.
|
||||
SendFrame(FrameUpdate),
|
||||
/// Shut down the network thread.
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
/// Messages sent from the network thread to the compositor.
|
||||
pub enum NetToCompositor {
|
||||
/// An input event received from the client.
|
||||
Input(InputMessage),
|
||||
/// A control message (e.g., FrameAck) from the client.
|
||||
Control(ControlMessage),
|
||||
/// Client connected with the given hello.
|
||||
ClientConnected(ClientHello),
|
||||
/// Client disconnected.
|
||||
ClientDisconnected,
|
||||
}
|
||||
|
||||
/// Configuration for the QUIC server.
|
||||
pub struct ServerConfig {
|
||||
/// Address to bind to. Defaults to `0.0.0.0:4433`.
|
||||
pub bind_addr: SocketAddr,
|
||||
/// Virtual output dimensions for the ServerHello.
|
||||
pub output_width: u32,
|
||||
/// Virtual output dimensions for the ServerHello.
|
||||
pub output_height: u32,
|
||||
}
|
||||
|
||||
impl Default for ServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bind_addr: "0.0.0.0:4433".parse().unwrap(),
|
||||
output_width: 1280,
|
||||
output_height: 720,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle to a running network server thread.
|
||||
pub struct NetworkHandle {
|
||||
/// Send commands to the network thread.
|
||||
pub tx: mpsc::Sender<CompositorToNet>,
|
||||
/// Receive events from the network thread.
|
||||
pub rx: mpsc::Receiver<NetToCompositor>,
|
||||
/// Join handle for the background thread.
|
||||
join: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl NetworkHandle {
|
||||
/// Shut down the network thread and wait for it to exit.
|
||||
pub fn shutdown(mut self) {
|
||||
let _ = self.tx.send(CompositorToNet::Shutdown);
|
||||
if let Some(handle) = self.join.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NetworkHandle {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.tx.send(CompositorToNet::Shutdown);
|
||||
if let Some(handle) = self.join.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a self-signed TLS certificate for the QUIC server.
|
||||
fn generate_self_signed_cert() -> (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>) {
|
||||
let CertifiedKey { cert, key_pair } =
|
||||
rcgen::generate_simple_self_signed(vec!["localhost".to_string(), "wayray".to_string()])
|
||||
.expect("certificate generation failed");
|
||||
let cert_der = CertificateDer::from(cert);
|
||||
let key_der = PrivateKeyDer::try_from(key_pair.serialize_der()).expect("key serialization");
|
||||
(vec![cert_der], key_der)
|
||||
}
|
||||
|
||||
/// Build a quinn `ServerConfig` from a self-signed cert.
|
||||
fn build_server_config() -> quinn::ServerConfig {
|
||||
let (certs, key) = generate_self_signed_cert();
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
let crypto = rustls::ServerConfig::builder_with_provider(provider.into())
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("TLS protocol versions")
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)
|
||||
.expect("TLS server config");
|
||||
let crypto = quinn::crypto::rustls::QuicServerConfig::try_from(crypto)
|
||||
.expect("QUIC server crypto config");
|
||||
quinn::ServerConfig::with_crypto(std::sync::Arc::new(crypto))
|
||||
}
|
||||
|
||||
/// Start the QUIC server on a background thread.
|
||||
///
|
||||
/// Returns a `NetworkHandle` with channels for communication.
|
||||
/// The server accepts one client at a time. When a client disconnects,
|
||||
/// it loops back to accept the next one.
|
||||
pub fn start_server(config: ServerConfig) -> NetworkHandle {
|
||||
let (comp_tx, net_rx) = mpsc::channel::<CompositorToNet>();
|
||||
let (net_tx, comp_rx) = mpsc::channel::<NetToCompositor>();
|
||||
|
||||
let join = thread::Builder::new()
|
||||
.name("wayray-net".into())
|
||||
.spawn(move || {
|
||||
let rt = Runtime::new().expect("tokio runtime");
|
||||
rt.block_on(async move {
|
||||
if let Err(e) = server_loop(config, net_rx, net_tx).await {
|
||||
error!("network server error: {e}");
|
||||
}
|
||||
});
|
||||
})
|
||||
.expect("spawn network thread");
|
||||
|
||||
NetworkHandle {
|
||||
tx: comp_tx,
|
||||
rx: comp_rx,
|
||||
join: Some(join),
|
||||
}
|
||||
}
|
||||
|
||||
/// Main server accept loop.
|
||||
async fn server_loop(
|
||||
config: ServerConfig,
|
||||
compositor_rx: mpsc::Receiver<CompositorToNet>,
|
||||
compositor_tx: mpsc::Sender<NetToCompositor>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let server_config = build_server_config();
|
||||
let endpoint = quinn::Endpoint::server(server_config, config.bind_addr)?;
|
||||
info!(addr = %config.bind_addr, "QUIC server listening");
|
||||
|
||||
loop {
|
||||
// Check for shutdown before waiting for connection.
|
||||
if let Ok(CompositorToNet::Shutdown) = compositor_rx.try_recv() {
|
||||
info!("network: shutdown requested");
|
||||
break;
|
||||
}
|
||||
|
||||
let incoming = tokio::select! {
|
||||
incoming = endpoint.accept() => {
|
||||
match incoming {
|
||||
Some(incoming) => incoming,
|
||||
None => {
|
||||
info!("QUIC endpoint closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = check_shutdown_async(&compositor_rx) => {
|
||||
info!("network: shutdown during accept");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let connection = match incoming.await {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
warn!("failed to accept connection: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
remote = %connection.remote_address(),
|
||||
"client connected"
|
||||
);
|
||||
|
||||
if let Err(e) =
|
||||
handle_connection(&connection, &config, &compositor_rx, &compositor_tx).await
|
||||
{
|
||||
warn!("client session ended: {e}");
|
||||
}
|
||||
|
||||
let _ = compositor_tx.send(NetToCompositor::ClientDisconnected);
|
||||
info!("client disconnected, waiting for next connection");
|
||||
}
|
||||
|
||||
endpoint.close(0u32.into(), b"shutdown");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Poll for shutdown on a blocking channel from an async context.
|
||||
async fn check_shutdown_async(rx: &mpsc::Receiver<CompositorToNet>) {
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(CompositorToNet::Shutdown) => return,
|
||||
Ok(_) => continue, // drain non-shutdown messages
|
||||
Err(mpsc::TryRecvError::Disconnected) => return,
|
||||
Err(mpsc::TryRecvError::Empty) => {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a single client connection: handshake on control stream, then
|
||||
/// relay messages between compositor and client until disconnect.
|
||||
///
|
||||
/// Stream setup protocol (accounts for quinn's lazy stream creation):
|
||||
/// 1. Client opens bidi stream and immediately sends `ClientHello` (triggers
|
||||
/// server's `accept_bi`).
|
||||
/// 2. Server reads `ClientHello`, sends `ServerHello` on control stream.
|
||||
/// 3. Server opens display uni stream and sends an initial empty frame
|
||||
/// (triggers client's `accept_uni` for display).
|
||||
/// 4. Client opens input uni stream — server's `accept_uni` for input is
|
||||
/// handled asynchronously when the client first writes input data.
|
||||
async fn handle_connection(
|
||||
connection: &quinn::Connection,
|
||||
config: &ServerConfig,
|
||||
compositor_rx: &mpsc::Receiver<CompositorToNet>,
|
||||
compositor_tx: &mpsc::Sender<NetToCompositor>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Step 1: Accept control stream. The client writes ClientHello immediately
|
||||
// after opening, so accept_bi resolves once that data arrives.
|
||||
let (mut control_send, mut control_recv) = connection.accept_bi().await?;
|
||||
info!("control stream established");
|
||||
|
||||
// Step 2: Read ClientHello, send ServerHello.
|
||||
let client_hello: ControlMessage = read_message(&mut control_recv).await?;
|
||||
let ControlMessage::ClientHello(hello) = client_hello else {
|
||||
return Err(format!("expected ClientHello, got {client_hello:?}").into());
|
||||
};
|
||||
info!(version = hello.version, "received ClientHello");
|
||||
let _ = compositor_tx.send(NetToCompositor::ClientConnected(hello));
|
||||
|
||||
let server_hello = ControlMessage::ServerHello(ServerHello {
|
||||
version: wayray_protocol::PROTOCOL_VERSION,
|
||||
session_id: 1, // TODO: real session management
|
||||
output_width: config.output_width,
|
||||
output_height: config.output_height,
|
||||
});
|
||||
write_message(&mut control_send, &server_hello).await?;
|
||||
info!("sent ServerHello");
|
||||
|
||||
// Step 3: Open display uni stream. Writing data triggers the client's
|
||||
// accept_uni for this stream.
|
||||
let mut display_send = connection.open_uni().await?;
|
||||
info!("display stream opened");
|
||||
|
||||
// Step 4: Message relay loop. Accept the input uni stream concurrently
|
||||
// with handling control messages and compositor commands.
|
||||
let mut input_recv: Option<quinn::RecvStream> = None;
|
||||
let mut accepting_input = true;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Accept input stream from client (one-shot).
|
||||
result = connection.accept_uni(), if accepting_input => {
|
||||
match result {
|
||||
Ok(recv) => {
|
||||
info!("input stream established");
|
||||
input_recv = Some(recv);
|
||||
accepting_input = false;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("failed to accept input stream: {e}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read control messages from client.
|
||||
msg = read_message::<ControlMessage>(&mut control_recv) => {
|
||||
match msg {
|
||||
Ok(ctrl) => {
|
||||
let _ = compositor_tx.send(NetToCompositor::Control(ctrl));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("control stream error: {e}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read input messages from client (only when stream is established).
|
||||
msg = async {
|
||||
match input_recv.as_mut() {
|
||||
Some(recv) => read_message::<InputMessage>(recv).await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
} => {
|
||||
match msg {
|
||||
Ok(input) => {
|
||||
let _ = compositor_tx.send(NetToCompositor::Input(input));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("input stream error: {e}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for messages from the compositor.
|
||||
_ = check_compositor_commands(
|
||||
compositor_rx,
|
||||
&mut display_send,
|
||||
) => {
|
||||
// Shutdown requested.
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process commands from the compositor channel. Returns when shutdown
|
||||
/// is requested or the channel is disconnected.
|
||||
async fn check_compositor_commands(
|
||||
rx: &mpsc::Receiver<CompositorToNet>,
|
||||
display_send: &mut quinn::SendStream,
|
||||
) {
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(CompositorToNet::SendFrame(frame)) => {
|
||||
let msg = DisplayMessage::FrameUpdate(frame);
|
||||
if let Err(e) = write_message(display_send, &msg).await {
|
||||
warn!("failed to send frame: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(CompositorToNet::Shutdown) => return,
|
||||
Err(mpsc::TryRecvError::Disconnected) => return,
|
||||
Err(mpsc::TryRecvError::Empty) => {
|
||||
// Yield to let other select branches run.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a length-prefixed message from a QUIC receive stream.
|
||||
async fn read_message<T: serde::de::DeserializeOwned>(
|
||||
recv: &mut quinn::RecvStream,
|
||||
) -> Result<T, Box<dyn std::error::Error>> {
|
||||
// Read 4-byte length prefix.
|
||||
let mut len_buf = [0u8; 4];
|
||||
recv.read_exact(&mut len_buf).await?;
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
|
||||
// Read payload.
|
||||
let mut payload = vec![0u8; len];
|
||||
recv.read_exact(&mut payload).await?;
|
||||
|
||||
let msg = codec::decode(&payload)?;
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Write a length-prefixed message to a QUIC send stream.
|
||||
async fn write_message<T: serde::Serialize>(
|
||||
send: &mut quinn::SendStream,
|
||||
msg: &T,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let encoded = codec::encode(msg)?;
|
||||
send.write_all(&encoded).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wayray_protocol::messages::ControlMessage;
|
||||
|
||||
/// Build a test client endpoint with certificate verification disabled.
|
||||
fn build_test_client_endpoint() -> quinn::Endpoint {
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
let crypto = rustls::ClientConfig::builder_with_provider(provider.into())
|
||||
.with_safe_default_protocol_versions()
|
||||
.expect("TLS protocol versions")
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(std::sync::Arc::new(SkipServerVerification))
|
||||
.with_no_client_auth();
|
||||
let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(crypto).unwrap();
|
||||
let client_config = quinn::ClientConfig::new(std::sync::Arc::new(crypto));
|
||||
|
||||
let mut endpoint =
|
||||
quinn::Endpoint::client("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
|
||||
endpoint.set_default_client_config(client_config);
|
||||
endpoint
|
||||
}
|
||||
|
||||
/// Dummy certificate verifier that accepts any server cert.
|
||||
#[derive(Debug)]
|
||||
struct SkipServerVerification;
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
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> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn server_client_hello_exchange() {
|
||||
let server_config = build_server_config();
|
||||
let endpoint =
|
||||
quinn::Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap();
|
||||
let actual_addr = endpoint.local_addr().unwrap();
|
||||
|
||||
// Server side: accept bi, read ClientHello, send ServerHello.
|
||||
let server_handle = tokio::spawn(async move {
|
||||
let incoming = endpoint.accept().await.unwrap();
|
||||
let connection = incoming.await.unwrap();
|
||||
|
||||
// Client writes ClientHello immediately, so accept_bi resolves.
|
||||
let (mut control_send, mut control_recv) = connection.accept_bi().await.unwrap();
|
||||
|
||||
let hello: ControlMessage = read_message(&mut control_recv).await.unwrap();
|
||||
let ControlMessage::ClientHello(client_hello) = hello else {
|
||||
panic!("expected ClientHello");
|
||||
};
|
||||
assert_eq!(client_hello.version, wayray_protocol::PROTOCOL_VERSION);
|
||||
|
||||
let server_hello = ControlMessage::ServerHello(ServerHello {
|
||||
version: wayray_protocol::PROTOCOL_VERSION,
|
||||
session_id: 42,
|
||||
output_width: 1920,
|
||||
output_height: 1080,
|
||||
});
|
||||
write_message(&mut control_send, &server_hello)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for client to read the response before closing.
|
||||
let _ = read_message::<ControlMessage>(&mut control_recv).await;
|
||||
endpoint.close(0u32.into(), b"done");
|
||||
});
|
||||
|
||||
// Client side: open bi and immediately send ClientHello.
|
||||
let client_endpoint = build_test_client_endpoint();
|
||||
let connection = client_endpoint
|
||||
.connect(actual_addr, "localhost")
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (mut control_send, mut control_recv) = connection.open_bi().await.unwrap();
|
||||
|
||||
// Write immediately to trigger server's accept_bi.
|
||||
let client_hello = ControlMessage::ClientHello(ClientHello {
|
||||
version: wayray_protocol::PROTOCOL_VERSION,
|
||||
capabilities: vec!["display".to_string()],
|
||||
});
|
||||
write_message(&mut control_send, &client_hello)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Read ServerHello.
|
||||
let response: ControlMessage = read_message(&mut control_recv).await.unwrap();
|
||||
let ControlMessage::ServerHello(server_hello) = response else {
|
||||
panic!("expected ServerHello, got {response:?}");
|
||||
};
|
||||
assert_eq!(server_hello.version, wayray_protocol::PROTOCOL_VERSION);
|
||||
assert_eq!(server_hello.session_id, 42);
|
||||
assert_eq!(server_hello.output_width, 1920);
|
||||
assert_eq!(server_hello.output_height, 1080);
|
||||
|
||||
// Signal server to close.
|
||||
let ping = ControlMessage::Ping(wayray_protocol::messages::Ping { timestamp: 0 });
|
||||
let _ = write_message(&mut control_send, &ping).await;
|
||||
|
||||
server_handle.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn server_display_and_input_streams() {
|
||||
let server_config = build_server_config();
|
||||
let endpoint =
|
||||
quinn::Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap();
|
||||
let actual_addr = endpoint.local_addr().unwrap();
|
||||
|
||||
let server_handle = tokio::spawn(async move {
|
||||
let incoming = endpoint.accept().await.unwrap();
|
||||
let connection = incoming.await.unwrap();
|
||||
|
||||
// Accept control and do handshake.
|
||||
let (mut control_send, mut control_recv) = connection.accept_bi().await.unwrap();
|
||||
let _hello: ControlMessage = read_message(&mut control_recv).await.unwrap();
|
||||
let server_hello = ControlMessage::ServerHello(ServerHello {
|
||||
version: wayray_protocol::PROTOCOL_VERSION,
|
||||
session_id: 1,
|
||||
output_width: 1280,
|
||||
output_height: 720,
|
||||
});
|
||||
write_message(&mut control_send, &server_hello)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open display uni and send a frame (triggers client's accept_uni).
|
||||
let mut display_send = connection.open_uni().await.unwrap();
|
||||
let frame = DisplayMessage::FrameUpdate(FrameUpdate {
|
||||
sequence: 1,
|
||||
regions: vec![],
|
||||
});
|
||||
write_message(&mut display_send, &frame).await.unwrap();
|
||||
|
||||
// Accept input uni (triggered by client writing input).
|
||||
let mut input_recv = connection.accept_uni().await.unwrap();
|
||||
let input: InputMessage = read_message(&mut input_recv).await.unwrap();
|
||||
assert!(matches!(input, InputMessage::Keyboard(_)));
|
||||
|
||||
endpoint.close(0u32.into(), b"done");
|
||||
});
|
||||
|
||||
// Client side.
|
||||
let client_endpoint = build_test_client_endpoint();
|
||||
let connection = client_endpoint
|
||||
.connect(actual_addr, "localhost")
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open control and send ClientHello.
|
||||
let (mut control_send, mut control_recv) = connection.open_bi().await.unwrap();
|
||||
let client_hello = ControlMessage::ClientHello(ClientHello {
|
||||
version: wayray_protocol::PROTOCOL_VERSION,
|
||||
capabilities: vec![],
|
||||
});
|
||||
write_message(&mut control_send, &client_hello)
|
||||
.await
|
||||
.unwrap();
|
||||
let _: ControlMessage = read_message(&mut control_recv).await.unwrap();
|
||||
|
||||
// Open input uni and send keyboard event (triggers server's accept_uni).
|
||||
let mut input_send = connection.open_uni().await.unwrap();
|
||||
let input = InputMessage::Keyboard(wayray_protocol::messages::KeyboardEvent {
|
||||
keycode: 42,
|
||||
state: wayray_protocol::messages::KeyState::Pressed,
|
||||
time: 1000,
|
||||
});
|
||||
write_message(&mut input_send, &input).await.unwrap();
|
||||
|
||||
// Accept display uni from server (triggered by server writing frame).
|
||||
let mut display_recv = connection.accept_uni().await.unwrap();
|
||||
let frame: DisplayMessage = read_message(&mut display_recv).await.unwrap();
|
||||
let DisplayMessage::FrameUpdate(update) = frame;
|
||||
assert_eq!(update.sequence, 1);
|
||||
assert!(update.regions.is_empty());
|
||||
|
||||
server_handle.await.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cert_generation_works() {
|
||||
let (certs, _key) = generate_self_signed_cert();
|
||||
assert_eq!(certs.len(), 1);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue