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:
Till Wegmueller 2026-04-07 16:54:01 +02:00
parent 8a3d14ff19
commit f79a934c2b
8 changed files with 1548 additions and 13 deletions

541
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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

View file

@ -1,3 +1,5 @@
pub mod network;
fn main() {
println!("wrclient viewer");
}

View 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();
}
}

View file

@ -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

View file

@ -1,6 +1,7 @@
mod backend;
mod errors;
mod handlers;
pub mod network;
mod state;
use crate::state::WayRay;

View 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);
}
}