feat(core): add QUIC TLS admission transport
This commit is contained in:
Generated
+595
-1
@@ -2,18 +2,181 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[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.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -23,6 +186,62 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.4.3",
|
||||
"lru-slab",
|
||||
"rand",
|
||||
"rand_pcg",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -32,6 +251,108 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[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-hash"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -59,7 +380,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -75,6 +396,45 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
@@ -86,20 +446,254 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
|
||||
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.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e608df10a8a5f3c45a2ad4801f93083620b4f3a816551e5e407cdfcd6690f56"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "versevdi-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"getrandom 0.4.3",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[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_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[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.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[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.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[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.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
||||
@@ -9,8 +9,14 @@ publish = false
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
base64 = "=0.22.1"
|
||||
getrandom = "=0.4.3"
|
||||
quinn = { version = "=0.11.11", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
|
||||
rustls = { version = "=0.23.43", default-features = false, features = ["std", "ring"] }
|
||||
rustls-pemfile = "=2.2.0"
|
||||
serde = { version = "=1.0.229", features = ["derive"] }
|
||||
serde_json = "=1.0.151"
|
||||
tokio = { version = "=1.51.4", features = ["rt-multi-thread", "sync", "time", "net"] }
|
||||
|
||||
[[test]]
|
||||
name = "protocol_fixtures"
|
||||
|
||||
@@ -5,6 +5,9 @@ use std::fmt;
|
||||
pub enum CoreError {
|
||||
InvalidArgument,
|
||||
AuthorityRejected,
|
||||
Tls,
|
||||
Transport,
|
||||
Protocol,
|
||||
Expired,
|
||||
QueueFull,
|
||||
Cancelled,
|
||||
@@ -33,6 +36,9 @@ impl CoreError {
|
||||
match self {
|
||||
Self::InvalidArgument => "invalid_argument",
|
||||
Self::AuthorityRejected => "authority_rejected",
|
||||
Self::Tls => "tls",
|
||||
Self::Transport => "transport",
|
||||
Self::Protocol => "protocol",
|
||||
Self::Expired => "expired",
|
||||
Self::QueueFull => "queue_full",
|
||||
Self::Cancelled => "cancelled",
|
||||
|
||||
@@ -16,7 +16,10 @@ pub mod error;
|
||||
pub mod input;
|
||||
pub mod media;
|
||||
pub mod session;
|
||||
pub mod transport;
|
||||
pub mod wire;
|
||||
|
||||
mod tls;
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
mod abi;
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustls::client::ResolvesClientCert;
|
||||
use rustls::pki_types::CertificateDer;
|
||||
use rustls::sign::{CertifiedKey, Signer, SigningKey};
|
||||
use rustls::{ClientConfig, RootCertStore, SignatureAlgorithm, SignatureScheme};
|
||||
|
||||
use crate::error::{CoreError, Result};
|
||||
use crate::wire::NativeTunnelCredential;
|
||||
|
||||
pub(crate) type SignCallback = dyn Fn(&[u8]) -> Result<[u8; 64]> + Send + Sync;
|
||||
|
||||
pub(crate) struct CallbackSigningKey {
|
||||
callback: Arc<SignCallback>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for CallbackSigningKey {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("CallbackSigningKey")
|
||||
}
|
||||
}
|
||||
|
||||
impl CallbackSigningKey {
|
||||
pub(crate) fn new(callback: Arc<SignCallback>) -> Self {
|
||||
Self { callback }
|
||||
}
|
||||
}
|
||||
|
||||
impl SigningKey for CallbackSigningKey {
|
||||
fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option<Box<dyn Signer>> {
|
||||
offered
|
||||
.contains(&SignatureScheme::ED25519)
|
||||
.then(|| Box::new(CallbackSigner(Arc::clone(&self.callback))) as Box<dyn Signer>)
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> SignatureAlgorithm {
|
||||
SignatureAlgorithm::ED25519
|
||||
}
|
||||
}
|
||||
|
||||
struct CallbackSigner(Arc<SignCallback>);
|
||||
|
||||
impl fmt::Debug for CallbackSigner {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("CallbackSigner")
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer for CallbackSigner {
|
||||
fn sign(&self, message: &[u8]) -> std::result::Result<Vec<u8>, rustls::Error> {
|
||||
(self.0)(message)
|
||||
.map(|signature| signature.to_vec())
|
||||
.map_err(|_| rustls::Error::General("client signing failed".to_owned()))
|
||||
}
|
||||
|
||||
fn scheme(&self) -> SignatureScheme {
|
||||
SignatureScheme::ED25519
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientIdentity(Arc<CertifiedKey>);
|
||||
|
||||
impl ResolvesClientCert for ClientIdentity {
|
||||
fn resolve(
|
||||
&self,
|
||||
_root_hint_subjects: &[&[u8]],
|
||||
sigschemes: &[SignatureScheme],
|
||||
) -> Option<Arc<CertifiedKey>> {
|
||||
sigschemes
|
||||
.contains(&SignatureScheme::ED25519)
|
||||
.then(|| Arc::clone(&self.0))
|
||||
}
|
||||
|
||||
fn has_certs(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn client_config(
|
||||
credential: &NativeTunnelCredential,
|
||||
callback: Arc<SignCallback>,
|
||||
) -> Result<ClientConfig> {
|
||||
let certificate_chain = rustls_pemfile::certs(&mut Cursor::new(
|
||||
credential.certificate_chain_pem().as_bytes(),
|
||||
))
|
||||
.collect::<std::result::Result<Vec<CertificateDer<'static>>, _>>()
|
||||
.map_err(|_| CoreError::Tls)?;
|
||||
if certificate_chain.is_empty() {
|
||||
return Err(CoreError::Tls);
|
||||
}
|
||||
let mut roots = RootCertStore::empty();
|
||||
let trust_bundle =
|
||||
rustls_pemfile::certs(&mut Cursor::new(credential.trust_bundle_pem().as_bytes()))
|
||||
.collect::<std::result::Result<Vec<CertificateDer<'static>>, _>>()
|
||||
.map_err(|_| CoreError::Tls)?;
|
||||
if trust_bundle.is_empty() || roots.add_parsable_certificates(trust_bundle).1 != 0 {
|
||||
return Err(CoreError::Tls);
|
||||
}
|
||||
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
||||
let mut config = ClientConfig::builder_with_provider(provider)
|
||||
.with_protocol_versions(&[&rustls::version::TLS13])
|
||||
.map_err(|_| CoreError::Tls)?
|
||||
.with_root_certificates(roots)
|
||||
.with_client_cert_resolver(Arc::new(ClientIdentity(Arc::new(CertifiedKey::new(
|
||||
certificate_chain,
|
||||
Arc::new(CallbackSigningKey::new(callback)),
|
||||
)))));
|
||||
config.alpn_protocols = vec![b"versevdi-gateway-v1".to_vec()];
|
||||
config.enable_early_data = false;
|
||||
Ok(config)
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
use std::fmt;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use base64::Engine as _;
|
||||
use quinn::crypto::rustls::QuicClientConfig;
|
||||
use quinn::{Endpoint, TransportConfig, VarInt};
|
||||
|
||||
use crate::error::{CoreError, Result};
|
||||
use crate::tls::{client_config, SignCallback};
|
||||
use crate::wire::{
|
||||
decode_stable_error, CapabilityProfile, ClientSessionAuthority, ConnectionManifest,
|
||||
NativeTunnelCredential,
|
||||
};
|
||||
|
||||
const HELLO_LIMIT: usize = 16 * 1024;
|
||||
const APPLICATION_ERROR: VarInt = VarInt::from_u32(0x100);
|
||||
|
||||
pub struct AdmissionSigner(Arc<SignCallback>);
|
||||
|
||||
impl AdmissionSigner {
|
||||
pub fn new(callback: impl Fn(&[u8]) -> Result<[u8; 64]> + Send + Sync + 'static) -> Self {
|
||||
Self(Arc::new(callback))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TlsEd25519Signer(Arc<SignCallback>);
|
||||
|
||||
impl TlsEd25519Signer {
|
||||
pub fn new(callback: impl Fn(&[u8]) -> Result<[u8; 64]> + Send + Sync + 'static) -> Self {
|
||||
Self(Arc::new(callback))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Signers {
|
||||
admission: AdmissionSigner,
|
||||
tls: TlsEd25519Signer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Cancellation(Arc<AtomicBool>);
|
||||
|
||||
impl Cancellation {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.0.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
fn check(&self) -> Result<()> {
|
||||
if self.0.load(Ordering::Acquire) {
|
||||
Err(CoreError::Cancelled)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Signers {
|
||||
#[must_use]
|
||||
pub fn new(admission: AdmissionSigner, tls: TlsEd25519Signer) -> Self {
|
||||
Self { admission, tls }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransportSession {
|
||||
endpoint: Endpoint,
|
||||
connection: quinn::Connection,
|
||||
authority: ClientSessionAuthority,
|
||||
}
|
||||
|
||||
impl TransportSession {
|
||||
#[must_use]
|
||||
pub const fn authority(&self) -> &ClientSessionAuthority {
|
||||
&self.authority
|
||||
}
|
||||
|
||||
pub async fn close(self) {
|
||||
self.connection.close(APPLICATION_ERROR, b"cancelled");
|
||||
self.connection.closed().await;
|
||||
self.endpoint.wait_idle().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TransportSession {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("TransportSession")
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens one TLS 1.3 QUIC admission session using only manifest addresses and private trust.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a provider-free validation, TLS, transport, protocol, expiry, or cancellation error.
|
||||
pub async fn connect(
|
||||
manifest: &ConnectionManifest,
|
||||
credential: &NativeTunnelCredential,
|
||||
signers: Signers,
|
||||
now_utc: &str,
|
||||
deadline: Duration,
|
||||
) -> Result<TransportSession> {
|
||||
connect_with_cancellation(
|
||||
manifest,
|
||||
credential,
|
||||
signers,
|
||||
now_utc,
|
||||
deadline,
|
||||
&Cancellation::new(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Opens one cancellable TLS 1.3 QUIC admission session.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `cancelled` when the signal fires or the overall deadline expires.
|
||||
pub async fn connect_with_cancellation(
|
||||
manifest: &ConnectionManifest,
|
||||
credential: &NativeTunnelCredential,
|
||||
signers: Signers,
|
||||
now_utc: &str,
|
||||
deadline: Duration,
|
||||
cancellation: &Cancellation,
|
||||
) -> Result<TransportSession> {
|
||||
let started = Instant::now();
|
||||
cancellation.check()?;
|
||||
manifest.validate_at(now_utc)?;
|
||||
credential.validate_at(now_utc)?;
|
||||
let capabilities = CapabilityProfile::new(
|
||||
"quic-tls13",
|
||||
"datagram-v2",
|
||||
"encoded",
|
||||
"encoded",
|
||||
"server",
|
||||
vec!["h264-opus".to_owned()],
|
||||
)?;
|
||||
let admission_signer = signers.admission.0;
|
||||
let tls_signer = signers.tls.0;
|
||||
let tls_cancellation = cancellation.clone();
|
||||
let checked_tls_signer: Arc<SignCallback> = Arc::new(move |message| {
|
||||
tls_cancellation.check()?;
|
||||
if started.elapsed() >= deadline {
|
||||
return Err(CoreError::Cancelled);
|
||||
}
|
||||
let signature = tls_signer(message)?;
|
||||
tls_cancellation.check()?;
|
||||
if started.elapsed() >= deadline {
|
||||
return Err(CoreError::Cancelled);
|
||||
}
|
||||
Ok(signature)
|
||||
});
|
||||
let rustls = client_config(credential, checked_tls_signer)?;
|
||||
let crypto = QuicClientConfig::try_from(rustls).map_err(|_| CoreError::Tls)?;
|
||||
let mut quinn_config = quinn::ClientConfig::new(Arc::new(crypto));
|
||||
let mut transport = TransportConfig::default();
|
||||
transport.max_concurrent_bidi_streams(VarInt::from_u32(1));
|
||||
transport.max_concurrent_uni_streams(VarInt::from_u32(0));
|
||||
transport.datagram_receive_buffer_size(Some(64 * 1024));
|
||||
quinn_config.transport_config(Arc::new(transport));
|
||||
|
||||
let remaining = deadline
|
||||
.checked_sub(started.elapsed())
|
||||
.ok_or(CoreError::Cancelled)?;
|
||||
let result = tokio::time::timeout(remaining, async move {
|
||||
let address_count = manifest.addresses().len();
|
||||
for (address_index, address) in manifest.addresses().iter().enumerate() {
|
||||
cancellation.check()?;
|
||||
let Ok(resolved) = tokio::net::lookup_host(address).await else {
|
||||
continue;
|
||||
};
|
||||
let resolved = resolved.take(8).collect::<Vec<_>>();
|
||||
let address_budget = deadline.saturating_sub(started.elapsed())
|
||||
/ u32::try_from(address_count.saturating_sub(address_index).max(1)).unwrap_or(1);
|
||||
let address_deadline = Instant::now() + address_budget;
|
||||
let remote_count = resolved.len();
|
||||
for (remote_index, remote) in resolved.into_iter().enumerate() {
|
||||
cancellation.check()?;
|
||||
let remaining_remotes = remote_count.saturating_sub(remote_index).max(1);
|
||||
let attempt_budget = address_deadline.saturating_duration_since(Instant::now())
|
||||
/ u32::try_from(remaining_remotes).unwrap_or(1);
|
||||
match cancellable_timeout(
|
||||
attempt_budget,
|
||||
cancellation,
|
||||
dial(
|
||||
remote,
|
||||
quinn_config.clone(),
|
||||
&AttemptContext {
|
||||
manifest,
|
||||
offered: &capabilities,
|
||||
admission_signer: admission_signer.as_ref(),
|
||||
cancellation,
|
||||
now_utc,
|
||||
started,
|
||||
deadline,
|
||||
},
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(session)) => return Ok(session),
|
||||
Ok(Err(AttemptError::Retry)) | Err(CoreError::Transport) => {}
|
||||
Ok(Err(AttemptError::Terminal(error))) | Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(CoreError::Transport)
|
||||
})
|
||||
.await
|
||||
.map_err(|_| CoreError::Cancelled)?;
|
||||
if matches!(result, Err(CoreError::Transport)) && started.elapsed() >= deadline {
|
||||
Err(CoreError::Cancelled)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
struct AttemptContext<'a> {
|
||||
manifest: &'a ConnectionManifest,
|
||||
offered: &'a CapabilityProfile,
|
||||
admission_signer: &'a SignCallback,
|
||||
cancellation: &'a Cancellation,
|
||||
now_utc: &'a str,
|
||||
started: Instant,
|
||||
deadline: Duration,
|
||||
}
|
||||
|
||||
async fn cancellable_timeout<T>(
|
||||
duration: Duration,
|
||||
cancellation: &Cancellation,
|
||||
future: impl std::future::Future<Output = T>,
|
||||
) -> Result<T> {
|
||||
let started = Instant::now();
|
||||
let mut future = std::pin::pin!(future);
|
||||
loop {
|
||||
cancellation.check()?;
|
||||
let remaining = duration
|
||||
.checked_sub(started.elapsed())
|
||||
.ok_or(CoreError::Transport)?;
|
||||
let slice = remaining.min(Duration::from_millis(5));
|
||||
match tokio::time::timeout(slice, &mut future).await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(_) if started.elapsed() < duration => {}
|
||||
Err(_) => return Err(CoreError::Transport),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn dial(
|
||||
remote: SocketAddr,
|
||||
config: quinn::ClientConfig,
|
||||
context: &AttemptContext<'_>,
|
||||
) -> std::result::Result<TransportSession, AttemptError> {
|
||||
let bind = match remote.ip() {
|
||||
IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
|
||||
};
|
||||
let mut endpoint = Endpoint::client(bind).map_err(|_| AttemptError::Retry)?;
|
||||
endpoint.set_default_client_config(config);
|
||||
let mut connecting = endpoint
|
||||
.connect(remote, context.manifest.public_identity())
|
||||
.map_err(|_| AttemptError::Retry)?;
|
||||
let negotiated = connecting
|
||||
.handshake_data()
|
||||
.await
|
||||
.map_err(|_| AttemptError::Terminal(CoreError::Tls))?
|
||||
.downcast::<quinn::crypto::rustls::HandshakeData>()
|
||||
.ok()
|
||||
.and_then(|data| data.protocol.clone());
|
||||
if negotiated.as_deref() != Some(b"versevdi-gateway-v1") {
|
||||
return Err(AttemptError::Terminal(CoreError::Tls));
|
||||
}
|
||||
let connection = connecting
|
||||
.await
|
||||
.map_err(|_| AttemptError::Terminal(CoreError::Tls))?;
|
||||
let payload = admission_payload(
|
||||
context.manifest,
|
||||
context.offered,
|
||||
context.admission_signer,
|
||||
context.cancellation,
|
||||
context.started,
|
||||
context.deadline,
|
||||
)?;
|
||||
let (mut send, mut receive) = connection
|
||||
.open_bi()
|
||||
.await
|
||||
.map_err(|_| AttemptError::Terminal(connection_error(&connection)))?;
|
||||
write_frame(&mut send, &payload)
|
||||
.await
|
||||
.map_err(|_| AttemptError::Terminal(connection_error(&connection)))?;
|
||||
send.finish()
|
||||
.map_err(|_| AttemptError::Terminal(connection_error(&connection)))?;
|
||||
let response = read_frame(&mut receive)
|
||||
.await
|
||||
.map_err(|_| AttemptError::Terminal(connection_error(&connection)))?;
|
||||
let Ok(authority) = ClientSessionAuthority::decode(&response) else {
|
||||
let stable = decode_stable_error(&response)?;
|
||||
return if stable.retryable {
|
||||
Err(AttemptError::Retry)
|
||||
} else {
|
||||
Err(AttemptError::Terminal(stable.error))
|
||||
};
|
||||
};
|
||||
authority
|
||||
.validate_binding(context.manifest, context.offered, context.now_utc)
|
||||
.map_err(AttemptError::Terminal)?;
|
||||
Ok(TransportSession {
|
||||
endpoint,
|
||||
connection,
|
||||
authority,
|
||||
})
|
||||
}
|
||||
|
||||
fn admission_payload(
|
||||
manifest: &ConnectionManifest,
|
||||
offered: &CapabilityProfile,
|
||||
signer: &SignCallback,
|
||||
cancellation: &Cancellation,
|
||||
started: Instant,
|
||||
deadline: Duration,
|
||||
) -> std::result::Result<Vec<u8>, AttemptError> {
|
||||
cancellation.check()?;
|
||||
let mut nonce = [0_u8; 24];
|
||||
getrandom::fill(&mut nonce).map_err(|_| CoreError::Transport)?;
|
||||
let nonce = URL_SAFE_NO_PAD.encode(nonce);
|
||||
let unsigned = manifest.admission(nonce.clone(), "A".repeat(86), offered.clone())?;
|
||||
cancellation.check()?;
|
||||
let signature = signer(&unsigned.admission_transcript())?;
|
||||
cancellation.check()?;
|
||||
if started.elapsed() >= deadline {
|
||||
return Err(CoreError::Cancelled.into());
|
||||
}
|
||||
let admission =
|
||||
manifest.admission(nonce, URL_SAFE_NO_PAD.encode(signature), offered.clone())?;
|
||||
let payload = serde_json::to_vec(&admission).map_err(|_| CoreError::Protocol)?;
|
||||
hello_length(payload.len())?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
fn connection_error(connection: &quinn::Connection) -> CoreError {
|
||||
match connection.close_reason() {
|
||||
Some(quinn::ConnectionError::TransportError(error))
|
||||
if (0x100..0x200).contains(&u64::from(error.code)) =>
|
||||
{
|
||||
CoreError::Tls
|
||||
}
|
||||
Some(quinn::ConnectionError::ConnectionClosed(frame))
|
||||
if (0x100..0x200).contains(&u64::from(frame.error_code)) =>
|
||||
{
|
||||
CoreError::Tls
|
||||
}
|
||||
_ => CoreError::Transport,
|
||||
}
|
||||
}
|
||||
|
||||
enum AttemptError {
|
||||
Retry,
|
||||
Terminal(CoreError),
|
||||
}
|
||||
|
||||
impl From<CoreError> for AttemptError {
|
||||
fn from(error: CoreError) -> Self {
|
||||
Self::Terminal(error)
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_frame(stream: &mut quinn::SendStream, payload: &[u8]) -> Result<()> {
|
||||
let length = hello_length(payload.len())?;
|
||||
stream
|
||||
.write_all(&length.to_be_bytes())
|
||||
.await
|
||||
.map_err(|_| CoreError::Transport)?;
|
||||
stream
|
||||
.write_all(payload)
|
||||
.await
|
||||
.map_err(|_| CoreError::Transport)
|
||||
}
|
||||
|
||||
async fn read_frame(stream: &mut quinn::RecvStream) -> Result<Vec<u8>> {
|
||||
let mut header = [0_u8; 4];
|
||||
stream
|
||||
.read_exact(&mut header)
|
||||
.await
|
||||
.map_err(|_| CoreError::Transport)?;
|
||||
let length = usize::try_from(hello_length(
|
||||
usize::try_from(u32::from_be_bytes(header)).map_err(|_| CoreError::Protocol)?,
|
||||
)?)
|
||||
.map_err(|_| CoreError::Protocol)?;
|
||||
let mut payload = vec![0; length];
|
||||
stream
|
||||
.read_exact(&mut payload)
|
||||
.await
|
||||
.map_err(|_| CoreError::Transport)?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
fn hello_length(length: usize) -> Result<u32> {
|
||||
if length == 0 || length > HELLO_LIMIT {
|
||||
return Err(CoreError::Protocol);
|
||||
}
|
||||
u32::try_from(length).map_err(|_| CoreError::Protocol)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{hello_length, HELLO_LIMIT};
|
||||
use crate::error::CoreError;
|
||||
|
||||
#[test]
|
||||
fn hello_length_bound_accepts_exact_limit_and_rejects_larger() {
|
||||
assert_eq!(hello_length(HELLO_LIMIT), Ok(16_384));
|
||||
assert_eq!(hello_length(0), Err(CoreError::Protocol));
|
||||
assert_eq!(hello_length(HELLO_LIMIT + 1), Err(CoreError::Protocol));
|
||||
}
|
||||
}
|
||||
+147
-17
@@ -1,5 +1,5 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{CoreError, Result};
|
||||
|
||||
@@ -203,6 +203,8 @@ fn timestamp(value: &str, exact_seconds: bool) -> Option<Timestamp> {
|
||||
|
||||
fn valid_dns_name(value: &str) -> bool {
|
||||
bounded(value, 1, 253)
|
||||
&& value.parse::<std::net::IpAddr>().is_err()
|
||||
&& !uuid_shaped(value)
|
||||
&& value.split('.').all(|label| {
|
||||
bounded(label, 1, 63)
|
||||
&& !label.starts_with('-')
|
||||
@@ -213,7 +215,15 @@ fn valid_dns_name(value: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
fn uuid_shaped(value: &str) -> bool {
|
||||
value.len() == 36
|
||||
&& value.bytes().enumerate().all(|(index, byte)| match index {
|
||||
8 | 13 | 18 | 23 => byte == b'-',
|
||||
_ => byte.is_ascii_hexdigit(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CapabilityProfile {
|
||||
transport: String,
|
||||
@@ -432,6 +442,35 @@ impl ConnectionManifest {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn addresses(&self) -> &[String] {
|
||||
&self.gateway.addresses
|
||||
}
|
||||
|
||||
pub(crate) fn public_identity(&self) -> &str {
|
||||
&self.gateway.public_identity
|
||||
}
|
||||
|
||||
pub(crate) fn admission(
|
||||
&self,
|
||||
client_nonce: String,
|
||||
device_signature: String,
|
||||
capabilities: CapabilityProfile,
|
||||
) -> Result<TunnelAdmissionRequest> {
|
||||
let request = TunnelAdmissionRequest {
|
||||
version: "1".to_owned(),
|
||||
session_id: self.session_id.clone(),
|
||||
gateway_id: self.gateway.id.clone(),
|
||||
audience: self.grant.audience.clone(),
|
||||
grant: self.grant.opaque_value.clone(),
|
||||
reconnect_sequence: self.reconnect_sequence,
|
||||
client_nonce,
|
||||
device_signature,
|
||||
capabilities,
|
||||
};
|
||||
request.validate()?;
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -477,9 +516,17 @@ impl NativeTunnelCredential {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn certificate_chain_pem(&self) -> &str {
|
||||
&self.certificate_chain_pem
|
||||
}
|
||||
|
||||
pub(crate) fn trust_bundle_pem(&self) -> &str {
|
||||
&self.trust_bundle_pem
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TunnelAdmissionRequest {
|
||||
version: String,
|
||||
@@ -501,23 +548,25 @@ impl TunnelAdmissionRequest {
|
||||
/// Returns `invalid_argument` for malformed, unknown, or out-of-bound data.
|
||||
pub fn decode(bytes: &[u8]) -> Result<Self> {
|
||||
let request: Self = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?;
|
||||
if request.version != "1"
|
||||
|| !bounded(&request.session_id, 1, 128)
|
||||
|| !bounded(&request.gateway_id, 1, 128)
|
||||
|| !bounded(&request.audience, 1, 256)
|
||||
|| !bounded(&request.grant, 43, 256)
|
||||
|| !bounded(&request.client_nonce, 16, 128)
|
||||
|| request.device_signature.len() != 86
|
||||
|| !matches!(
|
||||
raw_base64url_decoded_len(&request.client_nonce),
|
||||
Some(12..=96)
|
||||
)
|
||||
|| raw_base64url_decoded_len(&request.device_signature) != Some(64)
|
||||
request.validate()?;
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<()> {
|
||||
if self.version != "1"
|
||||
|| !bounded(&self.session_id, 1, 128)
|
||||
|| !bounded(&self.gateway_id, 1, 128)
|
||||
|| !bounded(&self.audience, 1, 256)
|
||||
|| !bounded(&self.grant, 43, 256)
|
||||
|| !bounded(&self.client_nonce, 16, 128)
|
||||
|| self.device_signature.len() != 86
|
||||
|| !matches!(raw_base64url_decoded_len(&self.client_nonce), Some(12..=96))
|
||||
|| raw_base64url_decoded_len(&self.device_signature) != Some(64)
|
||||
{
|
||||
return Err(CoreError::InvalidArgument);
|
||||
}
|
||||
request.capabilities.validate()?;
|
||||
Ok(request)
|
||||
self.capabilities.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -561,6 +610,20 @@ pub struct ClientSessionAuthority {
|
||||
capabilities: CapabilityProfile,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct StableError {
|
||||
version: String,
|
||||
code: String,
|
||||
message: String,
|
||||
retryable: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct DecodedStableError {
|
||||
pub(crate) error: CoreError,
|
||||
pub(crate) retryable: bool,
|
||||
}
|
||||
|
||||
impl ClientSessionAuthority {
|
||||
/// Strictly decodes and validates a provider-free RC5 client authority.
|
||||
///
|
||||
@@ -608,4 +671,71 @@ impl ClientSessionAuthority {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn session_id(&self) -> &str {
|
||||
&self.session_id
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode_stable_error(bytes: &[u8]) -> Result<DecodedStableError> {
|
||||
let stable: StableError = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?;
|
||||
if stable.version != "1" || !bounded(&stable.code, 1, 128) || !bounded(&stable.message, 1, 512)
|
||||
{
|
||||
return Err(CoreError::Protocol);
|
||||
}
|
||||
let error = match stable.code.as_str() {
|
||||
"expired_grant" => CoreError::Expired,
|
||||
"admission_rejected"
|
||||
| "gateway_draining"
|
||||
| "invalid_authority"
|
||||
| "no_capability_overlap"
|
||||
| "wrong_gateway"
|
||||
| "provider_work_unavailable"
|
||||
| "clipboard_audit_unavailable" => CoreError::AuthorityRejected,
|
||||
"provider_identity_rejected"
|
||||
| "provider_malformed"
|
||||
| "provider_timeout"
|
||||
| "provider_unavailable"
|
||||
| "provider_state_unavailable" => CoreError::Transport,
|
||||
"invalid_hello" => CoreError::Protocol,
|
||||
_ => return Err(CoreError::Protocol),
|
||||
};
|
||||
Ok(DecodedStableError {
|
||||
error,
|
||||
retryable: stable.retryable,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod stable_error_tests {
|
||||
use super::decode_stable_error;
|
||||
use crate::error::CoreError;
|
||||
|
||||
#[test]
|
||||
fn stable_error_uses_exact_rc5_bounds_and_preserves_retryability() {
|
||||
let message = "m".repeat(512);
|
||||
let bytes = serde_json::to_vec(&serde_json::json!({
|
||||
"version": "1",
|
||||
"code": "gateway_draining",
|
||||
"message": message,
|
||||
"retryable": true,
|
||||
}))
|
||||
.expect("encode stable error");
|
||||
let decoded = decode_stable_error(&bytes).expect("RC5 stable error");
|
||||
assert_eq!(decoded.error, CoreError::AuthorityRejected);
|
||||
assert!(decoded.retryable);
|
||||
|
||||
for invalid in [
|
||||
serde_json::json!({"version":"1","code":"gateway_draining","message":"","retryable":true}),
|
||||
serde_json::json!({"version":"1","code":"c".repeat(129),"message":"m","retryable":true}),
|
||||
serde_json::json!({"version":"1","code":"gateway_draining","message":"m".repeat(513),"retryable":true}),
|
||||
serde_json::json!({"version":"1","code":"unknown","message":"m","retryable":true}),
|
||||
] {
|
||||
assert_eq!(
|
||||
decode_stable_error(&serde_json::to_vec(&invalid).expect("encode invalid")).err(),
|
||||
Some(CoreError::Protocol)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,806 @@
|
||||
use std::fs;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine as _;
|
||||
use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use rustls::sign::SigningKey;
|
||||
use rustls::SignatureScheme;
|
||||
use serde::Deserialize;
|
||||
use versevdi_core::error::CoreError;
|
||||
use versevdi_core::transport::{
|
||||
connect, connect_with_cancellation, AdmissionSigner, Cancellation, Signers, TlsEd25519Signer,
|
||||
};
|
||||
use versevdi_core::wire::{ConnectionManifest, NativeTunnelCredential};
|
||||
|
||||
const ORACLE_SOURCE: &str = r#"
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gateway "git.sechmachine.io.vn/sechmachine/VerseVDI-Data-Plane/gateway"
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
|
||||
type admission struct {
|
||||
mu sync.Mutex
|
||||
lastTranscript []byte
|
||||
reusable bool
|
||||
failWorkOnce bool
|
||||
authority protocol.SessionAuthority
|
||||
work protocol.ProviderSessionWork
|
||||
public ed25519.PublicKey
|
||||
}
|
||||
|
||||
func (a *admission) Admit(_ context.Context, request protocol.TunnelAdmissionRequest) (protocol.SessionAuthority, error) {
|
||||
signature, err := base64.RawURLEncoding.DecodeString(request.DeviceSignature)
|
||||
transcript := request.DeviceAdmissionTranscript()
|
||||
if err != nil || !ed25519.Verify(a.public, transcript, signature) {
|
||||
return protocol.SessionAuthority{}, gateway.ErrAdmissionRejected
|
||||
}
|
||||
a.mu.Lock(); defer a.mu.Unlock()
|
||||
if !a.reusable && bytes.Equal(a.lastTranscript, transcript) { return protocol.SessionAuthority{}, gateway.ErrAdmissionRejected }
|
||||
a.lastTranscript = append(a.lastTranscript[:0], transcript...)
|
||||
return a.authority, nil
|
||||
}
|
||||
func (a *admission) ProviderWork(context.Context, protocol.SessionAuthority) (protocol.ProviderSessionWork, error) {
|
||||
a.mu.Lock(); defer a.mu.Unlock()
|
||||
if a.failWorkOnce { a.failWorkOnce = false; return protocol.ProviderSessionWork{}, context.DeadlineExceeded }
|
||||
return a.work, nil
|
||||
}
|
||||
func (a *admission) Release(context.Context, protocol.SessionAuthority) error { return nil }
|
||||
|
||||
type provider struct{}
|
||||
type session struct { state protocol.ProviderState; video chan gateway.ProviderMedia; audio chan gateway.ProviderMedia; events chan gateway.ProviderEvent }
|
||||
func (provider) Start(_ context.Context, request gateway.LaunchRequest) (gateway.ProviderSession, error) {
|
||||
return &session{state: protocol.ProviderState{Version:"1", SessionID:request.SessionID, State:gateway.ProviderStateReady, Channels:[]string{"video","audio","input","feedback"}}, video:make(chan gateway.ProviderMedia), audio:make(chan gateway.ProviderMedia), events:make(chan gateway.ProviderEvent)}, nil
|
||||
}
|
||||
func (s *session) Ready(context.Context) error { return nil }
|
||||
func (s *session) Video() <-chan gateway.ProviderMedia { return s.video }
|
||||
func (s *session) Audio() <-chan gateway.ProviderMedia { return s.audio }
|
||||
func (s *session) Events() <-chan gateway.ProviderEvent { return s.events }
|
||||
func (s *session) Input(context.Context, gateway.InputEvent) error { return nil }
|
||||
func (s *session) Feedback(context.Context, gateway.Feedback) error { return nil }
|
||||
func (s *session) ReadClipboard(context.Context) (string, error) { return "", errors.New("disabled") }
|
||||
func (s *session) WriteClipboard(context.Context, string) error { return errors.New("disabled") }
|
||||
func (s *session) Telemetry() gateway.ProviderTelemetry { return gateway.ProviderTelemetry{State:s.state.State} }
|
||||
func (s *session) ReleaseAll(context.Context) error { return nil }
|
||||
func (s *session) Terminate(context.Context) error { return nil }
|
||||
func (s *session) State() protocol.ProviderState { return s.state }
|
||||
|
||||
func makeCertificate(parent *x509.Certificate, parentKey ed25519.PrivateKey, serial int64, dns string, usage x509.ExtKeyUsage, isCA bool) ([]byte, ed25519.PrivateKey) {
|
||||
public, private, err := ed25519.GenerateKey(rand.Reader); if err != nil { panic(err) }
|
||||
template := &x509.Certificate{SerialNumber:big.NewInt(serial), Subject:pkix.Name{CommonName:dns}, DNSNames:[]string{dns}, NotBefore:time.Now().Add(-time.Hour), NotAfter:time.Now().Add(time.Hour), IsCA:isCA, BasicConstraintsValid:true, KeyUsage:x509.KeyUsageDigitalSignature}
|
||||
if isCA { template.KeyUsage |= x509.KeyUsageCertSign } else { template.ExtKeyUsage = []x509.ExtKeyUsage{usage} }
|
||||
if parent == nil { parent = template; parentKey = private }
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, parent, public, parentKey); if err != nil { panic(err) }
|
||||
return der, private
|
||||
}
|
||||
func pemCert(ders ...[]byte) string { var out []byte; for _, der := range ders { out = append(out, pem.EncodeToMemory(&pem.Block{Type:"CERTIFICATE",Bytes:der})...) }; return string(out) }
|
||||
|
||||
func main() {
|
||||
readyPath, stopPath := os.Args[1], os.Args[2]
|
||||
mode := os.Getenv("VERSEVDI_RUST_ORACLE_MODE")
|
||||
caDER, caKey := makeCertificate(nil, nil, 1, "Verse Rust Oracle CA", 0, true)
|
||||
ca, err := x509.ParseCertificate(caDER); if err != nil { panic(err) }
|
||||
serverDER, serverKey := makeCertificate(ca, caKey, 2, "gateway.test", x509.ExtKeyUsageServerAuth, false)
|
||||
clientDER, clientKey := makeCertificate(ca, caKey, 3, "client.test", x509.ExtKeyUsageClientAuth, false)
|
||||
_, admissionKey := makeCertificate(ca, caKey, 6, "admission.test", x509.ExtKeyUsageClientAuth, false)
|
||||
badCADER, _ := makeCertificate(nil, nil, 4, "Wrong CA", 0, true)
|
||||
_, wrongClientKey := makeCertificate(ca, caKey, 5, "wrong-client.test", x509.ExtKeyUsageClientAuth, false)
|
||||
pool := x509.NewCertPool(); pool.AddCert(ca)
|
||||
server := &tls.Config{MinVersion:tls.VersionTLS13, MaxVersion:tls.VersionTLS13, Certificates:[]tls.Certificate{{Certificate:[][]byte{serverDER,caDER},PrivateKey:serverKey}}, ClientAuth:tls.RequireAndVerifyClientCert, ClientCAs:pool}
|
||||
expiry := time.Now().Add(5*time.Minute).UTC().Truncate(time.Second).Format(time.RFC3339)
|
||||
authority := protocol.SessionAuthority{Version:"1",SessionID:"session-1",GatewayID:"gateway-1",Audience:"versevdi-gateway",ExpiresAt:expiry,Capabilities:gateway.DefaultCapabilities(),ProviderProfile:gateway.ProviderProfileApollo,ProviderIdentity:"oracle#sha256:fixture"}
|
||||
if mode == "session" { authority.SessionID = "other-session" }
|
||||
if mode == "gateway" { authority.GatewayID = "other-gateway" }
|
||||
if mode == "audience" { authority.Audience = "other-audience" }
|
||||
if mode == "reconnect" { authority.ReconnectSequence = 1 }
|
||||
if mode == "expiry" { authority.ExpiresAt = time.Now().Add(-time.Minute).UTC().Truncate(time.Second).Format(time.RFC3339) }
|
||||
if mode == "capability" { authority.Capabilities.ClientDecode = []string{"hevc-opus"} }
|
||||
if mode == "alpn" { server.NextProtos = []string{"wrong-alpn"} }
|
||||
work := protocol.ProviderSessionWork{Version:"1",SessionID:authority.SessionID,GatewayID:authority.GatewayID,ExpiresAt:expiry,ProviderProfile:gateway.ProviderProfileApollo,ProviderIdentity:authority.ProviderIdentity,PolicyVersionID:"policy-1",StreamPolicy:protocol.ProviderStreamPolicy{ResolutionWidth:1920,ResolutionHeight:1080,Fps:60,Codec:"H264",BitrateKbps:8000,AudioEnabled:true},ApplicationID:"1",ClientID:"client",ManagementHost:"provider.invalid",ManagementPort:47990,StreamHost:"provider.invalid",StreamPort:47984,ClientCertificatePem:"certificate",ClientPrivateKeyPem:"private-key",ServerCertificatePem:"certificate",ClipboardPolicy:protocol.ClipboardPolicy{MaxTextBytes:65536,MaxUpdatesPerMinute:30}}
|
||||
replayGuard := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey)}
|
||||
replayRequest := protocol.TunnelAdmissionRequest{Version:"1",SessionID:"session-1",GatewayID:"gateway-1",Audience:"versevdi-gateway",Grant:"ggggggggggggggggggggggggggggggggggggggggggg",ClientNonce:"bm9uY2UtZm9yLXJlcGxheQ",Capabilities:gateway.DefaultCapabilities()}
|
||||
replayRequest.DeviceSignature = base64.RawURLEncoding.EncodeToString(ed25519.Sign(admissionKey,replayRequest.DeviceAdmissionTranscript()))
|
||||
_, firstReplayErr := replayGuard.Admit(context.Background(),replayRequest)
|
||||
_, secondReplayErr := replayGuard.Admit(context.Background(),replayRequest)
|
||||
replayRejected := firstReplayErr == nil && errors.Is(secondReplayErr,gateway.ErrAdmissionRejected)
|
||||
admissionService := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey),reusable:mode == "reusable",failWorkOnce:mode == "post-retryable"}
|
||||
newService := func() *gateway.Server {
|
||||
service, err := gateway.NewServer(gateway.ServerConfig{ListenAddress:"127.0.0.1:0",TLSConfig:server,GatewayID:authority.GatewayID,Admission:admissionService,Provider:provider{}}); if err != nil { panic(err) }
|
||||
return service
|
||||
}
|
||||
service := newService()
|
||||
services := []*gateway.Server{service}
|
||||
addresses := []string{service.Addr().String()}
|
||||
if mode == "retryable" {
|
||||
draining := newService()
|
||||
draining.BeginDrain()
|
||||
services = []*gateway.Server{draining, service}
|
||||
addresses = []string{draining.Addr().String(), service.Addr().String()}
|
||||
}
|
||||
if mode == "post-retryable" {
|
||||
second := newService()
|
||||
services = []*gateway.Server{service, second}
|
||||
addresses = []string{service.Addr().String(), second.Addr().String()}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background()); defer cancel()
|
||||
done := make(chan error,len(services)); for _, current := range services { go func(server *gateway.Server){done <- server.Serve(ctx)}(current) }
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(clientKey); if err != nil { panic(err) }
|
||||
admissionKeyDER, err := x509.MarshalPKCS8PrivateKey(admissionKey); if err != nil { panic(err) }
|
||||
manifest := map[string]any{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":map[string]any{"id":"gateway-1","addresses":addresses,"public_identity":"gateway.test"},"tunnel":map[string]any{"versions":[]string{"verse-gateway-v1/1"},"features":[]string{"control.v1"}},"profile":map[string]any{"id":"standard","bounds":map[string]any{"minimum_kbps":1000,"target_kbps":5000,"maximum_kbps":10000},"display_mode":map[string]any{"resolution_width":1920,"resolution_height":1080,"fps":60}},"grant":map[string]any{"opaque_value":"ggggggggggggggggggggggggggggggggggggggggggg","expires_at":"2099-01-01T00:00:00Z","audience":"versevdi-gateway"},"correlation_id":"oracle"}
|
||||
credential := map[string]any{"client_device_id":"device","device_key_id":"key","certificate_chain_pem":pemCert(clientDER,caDER),"trust_bundle_pem":pemCert(caDER),"expires_at":"2099-01-01T00:00:00Z"}
|
||||
manifestJSON,_ := json.Marshal(manifest); credentialJSON,_ := json.Marshal(credential)
|
||||
wrongKeyDER, err := x509.MarshalPKCS8PrivateKey(wrongClientKey); if err != nil { panic(err) }
|
||||
serverKeyDER, err := x509.MarshalPKCS8PrivateKey(serverKey); if err != nil { panic(err) }
|
||||
ready,_ := json.Marshal(map[string]any{"manifest":string(manifestJSON),"credential":string(credentialJSON),"admission_key":base64.StdEncoding.EncodeToString(admissionKeyDER),"client_key":base64.StdEncoding.EncodeToString(keyDER),"wrong_client_key":base64.StdEncoding.EncodeToString(wrongKeyDER),"server_key":base64.StdEncoding.EncodeToString(serverKeyDER),"server_chain":pemCert(serverDER,caDER),"bad_trust":pemCert(badCADER),"replay_rejected":replayRejected})
|
||||
if err := os.WriteFile(readyPath,ready,0600); err != nil { panic(err) }
|
||||
for { if _, err := os.Stat(stopPath); err == nil { break }; time.Sleep(10*time.Millisecond) }
|
||||
cancel(); for _, current := range services { _ = current.Close() }; for range services { <-done }
|
||||
}
|
||||
"#;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Ready {
|
||||
manifest: String,
|
||||
credential: String,
|
||||
admission_key: String,
|
||||
client_key: String,
|
||||
wrong_client_key: String,
|
||||
server_key: String,
|
||||
server_chain: String,
|
||||
bad_trust: String,
|
||||
replay_rejected: bool,
|
||||
}
|
||||
|
||||
struct Oracle {
|
||||
_serial: std::sync::MutexGuard<'static, ()>,
|
||||
child: Child,
|
||||
directory: std::path::PathBuf,
|
||||
ready: Ready,
|
||||
}
|
||||
|
||||
impl Oracle {
|
||||
fn start(mode: &str) -> Self {
|
||||
static SERIAL: Mutex<()> = Mutex::new(());
|
||||
static NEXT: AtomicUsize = AtomicUsize::new(1);
|
||||
let serial = SERIAL
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"versevdi-rust-gateway-oracle-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&directory).expect("create oracle directory");
|
||||
let source = directory.join("main.go");
|
||||
let ready_path = directory.join("ready.json");
|
||||
let stop_path = directory.join("stop");
|
||||
fs::write(&source, ORACLE_SOURCE).expect("write oracle source");
|
||||
let child = Command::new("go")
|
||||
.args(["run", source.to_str().expect("source path is UTF-8")])
|
||||
.arg(&ready_path)
|
||||
.arg(&stop_path)
|
||||
.env("VERSEVDI_RUST_ORACLE_MODE", mode)
|
||||
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("start Go gateway oracle");
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
while Instant::now() < deadline {
|
||||
if let Ok(bytes) = fs::read(&ready_path) {
|
||||
if let Ok(ready) = serde_json::from_slice(&bytes) {
|
||||
return Self {
|
||||
_serial: serial,
|
||||
child,
|
||||
directory,
|
||||
ready,
|
||||
};
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let output = child.wait_with_output().expect("collect Go oracle output");
|
||||
panic!(
|
||||
"Go gateway oracle did not become ready: status={} stdout={} stderr={}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Oracle {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::write(self.directory.join("stop"), []);
|
||||
let _ = self.child.wait();
|
||||
let _ = fs::remove_dir_all(&self.directory);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_key(encoded: &str) -> Arc<dyn SigningKey> {
|
||||
let der = STANDARD.decode(encoded).expect("decode test key");
|
||||
rustls::crypto::ring::default_provider()
|
||||
.key_provider
|
||||
.load_private_key(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(der)))
|
||||
.expect("load ephemeral Ed25519 test key")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_ed25519_signer_completes_tls13_quic_admission_without_private_key_input() {
|
||||
let oracle = Oracle::start("");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let admission_key = test_key(&oracle.ready.admission_key);
|
||||
let tls_key = test_key(&oracle.ready.client_key);
|
||||
let admission_inputs = Arc::new(Mutex::new(Vec::new()));
|
||||
let tls_inputs = Arc::new(Mutex::new(Vec::new()));
|
||||
let admission_calls = Arc::new(AtomicUsize::new(0));
|
||||
let tls_calls = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let sign = |key: Arc<dyn SigningKey>, input: &[u8]| -> Result<[u8; 64], CoreError> {
|
||||
let signer = key
|
||||
.choose_scheme(&[SignatureScheme::ED25519])
|
||||
.ok_or(CoreError::Tls)?;
|
||||
signer
|
||||
.sign(input)
|
||||
.map_err(|_| CoreError::Tls)?
|
||||
.try_into()
|
||||
.map_err(|_| CoreError::Tls)
|
||||
};
|
||||
let admission = {
|
||||
let key = Arc::clone(&admission_key);
|
||||
let inputs = Arc::clone(&admission_inputs);
|
||||
let calls = Arc::clone(&admission_calls);
|
||||
AdmissionSigner::new(move |input| {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
*inputs.lock().expect("admission inputs") = input.to_vec();
|
||||
sign(Arc::clone(&key), input)
|
||||
})
|
||||
};
|
||||
let tls = {
|
||||
let key = Arc::clone(&tls_key);
|
||||
let inputs = Arc::clone(&tls_inputs);
|
||||
let calls = Arc::clone(&tls_calls);
|
||||
TlsEd25519Signer::new(move |input| {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
*inputs.lock().expect("TLS inputs") = input.to_vec();
|
||||
sign(Arc::clone(&key), input)
|
||||
})
|
||||
};
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(admission, tls),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(10),
|
||||
))
|
||||
.expect("production Go gateway admission");
|
||||
|
||||
assert_eq!(session.authority().session_id(), "session-1");
|
||||
assert_eq!(admission_calls.load(Ordering::SeqCst), 1);
|
||||
assert!(tls_calls.load(Ordering::SeqCst) >= 1);
|
||||
assert!(admission_inputs
|
||||
.lock()
|
||||
.expect("admission transcript")
|
||||
.starts_with(b"versevdi/tunnel-admission/v1"));
|
||||
assert_ne!(
|
||||
*admission_inputs.lock().expect("admission transcript"),
|
||||
*tls_inputs.lock().expect("TLS transcript")
|
||||
);
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
fn signer(key: Arc<dyn SigningKey>) -> impl Fn(&[u8]) -> Result<[u8; 64], CoreError> {
|
||||
move |input| {
|
||||
key.choose_scheme(&[SignatureScheme::ED25519])
|
||||
.ok_or(CoreError::Tls)?
|
||||
.sign(input)
|
||||
.map_err(|_| CoreError::Tls)?
|
||||
.try_into()
|
||||
.map_err(|_| CoreError::Tls)
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_oracle(
|
||||
oracle: &Oracle,
|
||||
admission_key: &str,
|
||||
tls_key: &str,
|
||||
mutate_manifest: impl FnOnce(&mut serde_json::Value),
|
||||
mutate_credential: impl FnOnce(&mut serde_json::Value),
|
||||
) -> Result<versevdi_core::transport::TransportSession, CoreError> {
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
let mut credential: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.credential).expect("credential JSON");
|
||||
mutate_manifest(&mut manifest);
|
||||
mutate_credential(&mut credential);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))?;
|
||||
let credential = NativeTunnelCredential::decode(
|
||||
&serde_json::to_vec(&credential).expect("encode credential"),
|
||||
)?;
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
runtime.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(
|
||||
AdmissionSigner::new(signer(test_key(admission_key))),
|
||||
TlsEd25519Signer::new(signer(test_key(tls_key))),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_gateway_rejects_tls_and_admission_identity_mismatches() {
|
||||
for case in [
|
||||
"wrong-key",
|
||||
"wrong-signature",
|
||||
"wrong-leaf",
|
||||
"wrong-sni",
|
||||
"wrong-root",
|
||||
"wrong-alpn",
|
||||
] {
|
||||
let oracle = Oracle::start("");
|
||||
let admission_key = &oracle.ready.admission_key;
|
||||
let tls_key = &oracle.ready.client_key;
|
||||
let wrong = &oracle.ready.wrong_client_key;
|
||||
let result = match case {
|
||||
"wrong-key" => connect_oracle(&oracle, admission_key, wrong, |_| {}, |_| {}),
|
||||
"wrong-signature" => connect_oracle(&oracle, wrong, tls_key, |_| {}, |_| {}),
|
||||
"wrong-leaf" => connect_oracle(
|
||||
&oracle,
|
||||
admission_key,
|
||||
&oracle.ready.server_key,
|
||||
|_| {},
|
||||
|credential| {
|
||||
credential["certificate_chain_pem"] = oracle.ready.server_chain.clone().into();
|
||||
},
|
||||
),
|
||||
"wrong-sni" => connect_oracle(
|
||||
&oracle,
|
||||
admission_key,
|
||||
tls_key,
|
||||
|manifest| manifest["gateway"]["public_identity"] = "other.test".into(),
|
||||
|_| {},
|
||||
),
|
||||
"wrong-root" => connect_oracle(
|
||||
&oracle,
|
||||
admission_key,
|
||||
tls_key,
|
||||
|_| {},
|
||||
|credential| credential["trust_bundle_pem"] = oracle.ready.bad_trust.clone().into(),
|
||||
),
|
||||
"wrong-alpn" => {
|
||||
drop(oracle);
|
||||
let oracle = Oracle::start("alpn");
|
||||
connect_oracle(
|
||||
&oracle,
|
||||
&oracle.ready.admission_key,
|
||||
&oracle.ready.client_key,
|
||||
|_| {},
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let expected = if case == "wrong-signature" {
|
||||
CoreError::AuthorityRejected
|
||||
} else {
|
||||
CoreError::Tls
|
||||
};
|
||||
assert_eq!(result.err(), Some(expected), "wrong error for {case}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_gateway_and_rust_reject_authority_binding_and_capability_mismatches() {
|
||||
for mode in [
|
||||
"session",
|
||||
"gateway",
|
||||
"audience",
|
||||
"reconnect",
|
||||
"capability",
|
||||
"expiry",
|
||||
] {
|
||||
let oracle = Oracle::start(mode);
|
||||
let result = connect_oracle(
|
||||
&oracle,
|
||||
&oracle.ready.admission_key,
|
||||
&oracle.ready.client_key,
|
||||
|_| {},
|
||||
|_| {},
|
||||
);
|
||||
assert!(result.is_err(), "{mode} mismatch unexpectedly connected");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signer_purposes_are_not_interchangeable() {
|
||||
let oracle = Oracle::start("");
|
||||
let admission_key = test_key(&oracle.ready.admission_key);
|
||||
let tls_key = test_key(&oracle.ready.client_key);
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let admission = AdmissionSigner::new(move |message| {
|
||||
if !message.starts_with(b"versevdi/tunnel-admission/v1") {
|
||||
return Err(CoreError::AuthorityRejected);
|
||||
}
|
||||
signer(Arc::clone(&admission_key))(message)
|
||||
});
|
||||
let tls = TlsEd25519Signer::new(move |message| {
|
||||
if message.starts_with(b"versevdi/tunnel-admission/v1") {
|
||||
return Err(CoreError::Tls);
|
||||
}
|
||||
signer(Arc::clone(&tls_key))(message)
|
||||
});
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(admission, tls),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("purpose-separated signers");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swapping_admission_and_tls_signer_purposes_is_rejected() {
|
||||
let oracle = Oracle::start("");
|
||||
let result = connect_oracle(
|
||||
&oracle,
|
||||
&oracle.ready.client_key,
|
||||
&oracle.ready.admission_key,
|
||||
|_| {},
|
||||
|_| {},
|
||||
);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"one interchangeable key was accepted for both signer purposes"
|
||||
);
|
||||
}
|
||||
|
||||
fn recording_signers(
|
||||
admission_key: Arc<dyn SigningKey>,
|
||||
tls_key: Arc<dyn SigningKey>,
|
||||
transcripts: Arc<Mutex<Vec<Vec<u8>>>>,
|
||||
) -> Signers {
|
||||
Signers::new(
|
||||
AdmissionSigner::new(move |message| {
|
||||
transcripts
|
||||
.lock()
|
||||
.expect("admission transcripts")
|
||||
.push(message.to_vec());
|
||||
signer(Arc::clone(&admission_key))(message)
|
||||
}),
|
||||
TlsEd25519Signer::new(signer(tls_key)),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_nonce_changes_transcript_and_oracle_rejects_identical_replay() {
|
||||
let transcripts = Arc::new(Mutex::new(Vec::new()));
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
for _ in 0..2 {
|
||||
let oracle = Oracle::start("");
|
||||
let manifest =
|
||||
ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::clone(&transcripts),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("fresh nonce admission");
|
||||
runtime.block_on(session.close());
|
||||
assert!(
|
||||
oracle.ready.replay_rejected,
|
||||
"oracle accepted identical replay"
|
||||
);
|
||||
}
|
||||
let transcripts = transcripts.lock().expect("transcripts");
|
||||
assert_eq!(transcripts.len(), 2);
|
||||
assert_ne!(transcripts[0], transcripts[1], "nonce was reused");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overall_deadline_cancels_an_unresponsive_manifest_address() {
|
||||
let oracle = Oracle::start("");
|
||||
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind blackhole");
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
manifest["gateway"]["addresses"] =
|
||||
serde_json::json!([socket.local_addr().expect("blackhole address").to_string()]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let started = Instant::now();
|
||||
let result = runtime.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_millis(150),
|
||||
));
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overall_deadline_includes_synchronous_admission_signing() {
|
||||
let oracle = Oracle::start("");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let admission_key = test_key(&oracle.ready.admission_key);
|
||||
let tls_key = test_key(&oracle.ready.client_key);
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let started = Instant::now();
|
||||
let result = runtime.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(
|
||||
AdmissionSigner::new(move |message| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
signer(Arc::clone(&admission_key))(message)
|
||||
}),
|
||||
TlsEd25519Signer::new(signer(tls_key)),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_millis(25),
|
||||
));
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_cancellation_interrupts_network_wait() {
|
||||
let oracle = Oracle::start("");
|
||||
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind blackhole");
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
manifest["gateway"]["addresses"] =
|
||||
serde_json::json!([socket.local_addr().expect("blackhole address").to_string()]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let cancellation = Cancellation::new();
|
||||
let trigger = cancellation.clone();
|
||||
let canceller = thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
trigger.cancel();
|
||||
});
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let started = Instant::now();
|
||||
let result = runtime.block_on(connect_with_cancellation(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
&cancellation,
|
||||
));
|
||||
canceller.join().expect("canceller");
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blackholed_first_manifest_address_does_not_starve_live_second_address() {
|
||||
let oracle = Oracle::start("");
|
||||
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind blackhole");
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
let live = manifest["gateway"]["addresses"][0].clone();
|
||||
manifest["gateway"]["addresses"] = serde_json::json!([
|
||||
socket.local_addr().expect("blackhole address").to_string(),
|
||||
live,
|
||||
]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("live second manifest address");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_record_first_manifest_address_does_not_starve_live_second_address() {
|
||||
let oracle = Oracle::start("");
|
||||
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind blackhole");
|
||||
let port = socket.local_addr().expect("blackhole address").port();
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
let live = manifest["gateway"]["addresses"][0].clone();
|
||||
manifest["gateway"]["addresses"] = serde_json::json!([format!("localhost:{port}"), live]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("live second address after multi-record first address");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_first_manifest_address_does_not_block_live_second_address() {
|
||||
let oracle = Oracle::start("");
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
|
||||
let live = manifest["gateway"]["addresses"][0].clone();
|
||||
manifest["gateway"]["addresses"] = serde_json::json!(["127.0.0.1:0", live]);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(&serde_json::to_vec(&manifest).expect("encode manifest"))
|
||||
.expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("live address after resolution failure");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retryable_stable_error_advances_to_live_manifest_address() {
|
||||
let oracle = Oracle::start("retryable");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("retryable draining response advanced to live address");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_admission_retry_uses_a_fresh_signed_nonce() {
|
||||
let oracle = Oracle::start("post-retryable");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let transcripts = Arc::new(Mutex::new(Vec::new()));
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::clone(&transcripts),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("post-admission retry used a fresh request");
|
||||
runtime.block_on(session.close());
|
||||
let transcripts = transcripts.lock().expect("transcripts");
|
||||
assert_eq!(transcripts.len(), 2);
|
||||
assert_ne!(transcripts[0], transcripts[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_production_gateway_admissions_remain_bounded() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
for index in 0..10 {
|
||||
let oracle = Oracle::start("reusable");
|
||||
let manifest =
|
||||
ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.unwrap_or_else(|error| panic!("stress admission {index}: {error:?}"));
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,19 @@ fn strict_rc5_dtos_reject_duplicate_trailing_unknown_and_provider_fields() {
|
||||
assert!(TunnelAdmissionRequest::decode(admission_provider.as_bytes()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_public_identity_requires_dns_sni_not_ip_or_uuid() {
|
||||
for invalid_identity in ["127.0.0.1", "::1", "550e8400-e29b-41d4-a716-446655440000"] {
|
||||
let manifest = String::from_utf8(valid_manifest().to_vec())
|
||||
.expect("fixture is UTF-8")
|
||||
.replace("gateway.test\"}", &format!("{invalid_identity}\"}}"));
|
||||
assert!(
|
||||
ConnectionManifest::decode(manifest.as_bytes()).is_err(),
|
||||
"non-DNS SNI accepted: {invalid_identity}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rc5_manifest_credential_and_authority_enforce_bounds_and_bindings() {
|
||||
let manifest = ConnectionManifest::decode(valid_manifest()).expect("valid manifest");
|
||||
@@ -164,6 +177,70 @@ fn rc5_manifest_credential_and_authority_enforce_bounds_and_bindings() {
|
||||
|
||||
let provider_authority = br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"provider_profile":"apollo"}"#;
|
||||
assert!(ClientSessionAuthority::decode(provider_authority).is_err());
|
||||
let provider_route = br#"{"version":"1","session_id":"session","gateway_id":"gateway","audience":"audience","reconnect_sequence":0,"expires_at":"2098-01-01T00:00:00Z","capabilities":{"transport":"quic-tls13","framing":"datagram-v2","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":["h264-opus"]},"stream_host":"provider.invalid"}"#;
|
||||
assert!(ClientSessionAuthority::decode(provider_route).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_authority_directly_rejects_every_manifest_binding_mismatch() {
|
||||
let manifest = ConnectionManifest::decode(valid_manifest()).expect("valid manifest");
|
||||
let offered = CapabilityProfile::new(
|
||||
"quic-tls13",
|
||||
"datagram-v2",
|
||||
"encoded",
|
||||
"encoded",
|
||||
"server",
|
||||
vec!["h264-opus".to_owned()],
|
||||
)
|
||||
.expect("offered capabilities");
|
||||
let valid = serde_json::json!({
|
||||
"version": "1",
|
||||
"session_id": "session",
|
||||
"gateway_id": "gateway",
|
||||
"audience": "audience",
|
||||
"reconnect_sequence": 0,
|
||||
"expires_at": "2098-01-01T00:00:00Z",
|
||||
"capabilities": {
|
||||
"transport": "quic-tls13",
|
||||
"framing": "datagram-v2",
|
||||
"media": "encoded",
|
||||
"audio": "encoded",
|
||||
"source_rate_control": "server",
|
||||
"client_decode": ["h264-opus"],
|
||||
},
|
||||
});
|
||||
for case in [
|
||||
"session",
|
||||
"gateway",
|
||||
"audience",
|
||||
"reconnect",
|
||||
"expired",
|
||||
"beyond-grant",
|
||||
"capability",
|
||||
] {
|
||||
let mut value = valid.clone();
|
||||
match case {
|
||||
"session" => value["session_id"] = "other".into(),
|
||||
"gateway" => value["gateway_id"] = "other".into(),
|
||||
"audience" => value["audience"] = "other".into(),
|
||||
"reconnect" => value["reconnect_sequence"] = 1.into(),
|
||||
"expired" => value["expires_at"] = "2026-08-12T00:00:00Z".into(),
|
||||
"beyond-grant" => value["expires_at"] = "2100-01-01T00:00:00Z".into(),
|
||||
"capability" => {
|
||||
value["capabilities"]["client_decode"] =
|
||||
serde_json::json!(["h264-opus", "hevc-opus"]);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let authority =
|
||||
ClientSessionAuthority::decode(&serde_json::to_vec(&value).expect("encode authority"))
|
||||
.expect("structurally valid authority");
|
||||
assert_eq!(
|
||||
authority.validate_binding(&manifest, &offered, "2026-08-12T00:00:00Z"),
|
||||
Err(versevdi_core::error::CoreError::AuthorityRejected),
|
||||
"binding mismatch accepted: {case}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
@@ -476,6 +477,83 @@ func TestAdmissionQUICMTLSRelayAndCleanup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStableErrorFramePrecedesConnectionTeardown(t *testing.T) {
|
||||
for _, retryable := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("retryable=%t", retryable), func(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
clientTLS.NextProtos = []string{"versevdi-gateway-v1"}
|
||||
fake := NewFakeApollo(FakeApolloConfig{Now: time.Now()})
|
||||
authority := protocol.SessionAuthority{
|
||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||
ExpiresAt: time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), Capabilities: DefaultCapabilities(),
|
||||
ProviderProfile: ProviderProfileApollo, ProviderIdentity: fake.config.Identity.Key(),
|
||||
}
|
||||
server, err := NewServer(ServerConfig{ListenAddress: "127.0.0.1:0", TLSConfig: serverTLS, GatewayID: authority.GatewayID, Admission: &oneTimeAdmission{authority: authority, released: make(chan struct{})}, Provider: fake})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if retryable {
|
||||
server.BeginDrain()
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go func() { _ = server.Serve(ctx) }()
|
||||
connection, err := quic.DialAddr(context.Background(), server.Addr().String(), clientTLS, &quic.Config{EnableDatagrams: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stream, err := connection.OpenStreamSync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gatewayID := authority.GatewayID
|
||||
if !retryable {
|
||||
gatewayID = "wrong-gateway"
|
||||
}
|
||||
request := protocol.TunnelAdmissionRequest{Version: "1", SessionID: authority.SessionID, GatewayID: gatewayID, Audience: authority.Audience, Grant: strings.Repeat("g", 64), ClientNonce: "nonce-0000000001", DeviceSignature: strings.Repeat("s", 86), Capabilities: DefaultCapabilities()}
|
||||
payload, err := protocol.EncodeTunnelAdmissionRequest(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeWire(stream, payload, defaultHelloLimit); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := readWire(stream, defaultHelloLimit)
|
||||
if err != nil {
|
||||
t.Fatalf("stable response lost before connection teardown: %v", err)
|
||||
}
|
||||
stable, err := protocol.DecodeStableError(response)
|
||||
if err != nil || stable.Retryable != retryable {
|
||||
t.Fatalf("stable response = %#v, %v", stable, err)
|
||||
}
|
||||
select {
|
||||
case <-connection.Context().Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("server retained rejected connection")
|
||||
}
|
||||
_ = server.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStableErrorNeverLeaksInternalProviderDetails(t *testing.T) {
|
||||
var wire bytes.Buffer
|
||||
if err := writeStableError(&wire, "provider_unavailable", errors.New("https://provider.invalid/launch?rikey=secret-sentinel"), true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := readWire(&wire, defaultHelloLimit)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stable, err := protocol.DecodeStableError(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(stable.Message, "provider.invalid") || strings.Contains(stable.Message, "secret-sentinel") {
|
||||
t.Fatalf("stable error leaked internal details: %q", stable.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayTelemetrySeparatesQueueProcessingAndPacing(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
session := &fakeSession{
|
||||
|
||||
+50
-20
@@ -206,74 +206,84 @@ func (s *Server) handleConnection(parent context.Context, connection *quic.Conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
writeError := func(code string, err error, retryable bool) {
|
||||
if writeStableError(stream, code, err, retryable) == nil && stream.Close() == nil {
|
||||
responseCtx, responseCancel := context.WithTimeout(ctx, time.Second)
|
||||
defer responseCancel()
|
||||
select {
|
||||
case <-connection.Context().Done():
|
||||
case <-responseCtx.Done():
|
||||
}
|
||||
}
|
||||
}
|
||||
requestBytes, err := readWire(stream, defaultHelloLimit)
|
||||
if err != nil {
|
||||
_ = writeStableError(stream, "invalid_hello", err, false)
|
||||
writeError("invalid_hello", err, false)
|
||||
return
|
||||
}
|
||||
request, err := protocol.DecodeTunnelAdmissionRequest(requestBytes)
|
||||
if err != nil {
|
||||
_ = writeStableError(stream, "invalid_hello", err, false)
|
||||
writeError("invalid_hello", err, false)
|
||||
return
|
||||
}
|
||||
if s.Draining() {
|
||||
_ = writeStableError(stream, "gateway_draining", ErrGatewayDraining, true)
|
||||
writeError("gateway_draining", ErrGatewayDraining, true)
|
||||
return
|
||||
}
|
||||
if request.GatewayID != s.config.GatewayID {
|
||||
_ = writeStableError(stream, "wrong_gateway", ErrAdmissionRejected, false)
|
||||
writeError("wrong_gateway", ErrAdmissionRejected, false)
|
||||
return
|
||||
}
|
||||
authority, err := s.config.Admission.Admit(ctx, request)
|
||||
if err != nil {
|
||||
s.metrics.AdmissionRejects.Add(1)
|
||||
_ = writeStableError(stream, stableAdmissionCode(err), err, errors.Is(err, context.DeadlineExceeded))
|
||||
writeError(stableAdmissionCode(err), err, errors.Is(err, context.DeadlineExceeded))
|
||||
return
|
||||
}
|
||||
if s.Draining() {
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
_ = writeStableError(stream, "gateway_draining", ErrGatewayDraining, true)
|
||||
writeError("gateway_draining", ErrGatewayDraining, true)
|
||||
return
|
||||
}
|
||||
if err := s.validateAuthority(authority, request); err != nil {
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
_ = writeStableError(stream, "invalid_authority", err, false)
|
||||
writeError("invalid_authority", err, false)
|
||||
return
|
||||
}
|
||||
work, err := s.config.Admission.ProviderWork(ctx, authority)
|
||||
if err != nil || s.validateProviderWork(work, authority) != nil {
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
_ = writeStableError(stream, "provider_work_unavailable", ErrAdmissionRejected, err != nil)
|
||||
writeError("provider_work_unavailable", ErrAdmissionRejected, err != nil)
|
||||
return
|
||||
}
|
||||
selected, err := IntersectCapabilities(s.config.Capabilities, s.config.ProviderCapabilities, request.Capabilities, authority.Capabilities)
|
||||
if err != nil {
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
s.metrics.AdmissionRejects.Add(1)
|
||||
_ = writeStableError(stream, "no_capability_overlap", err, false)
|
||||
writeError("no_capability_overlap", err, false)
|
||||
return
|
||||
}
|
||||
selected, err = selectApolloPolicyCapabilities(work.StreamPolicy, selected)
|
||||
if err != nil {
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
s.metrics.AdmissionRejects.Add(1)
|
||||
_ = writeStableError(stream, "no_capability_overlap", err, false)
|
||||
writeError("no_capability_overlap", err, false)
|
||||
return
|
||||
}
|
||||
clipboard, err := newClipboardGate(work.ClipboardPolicy, time.Now)
|
||||
if err != nil {
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
_ = writeStableError(stream, "provider_work_unavailable", ErrAdmissionRejected, false)
|
||||
writeError("provider_work_unavailable", ErrAdmissionRejected, false)
|
||||
return
|
||||
}
|
||||
if (work.ClipboardPolicy.ClientToProviderEnabled || work.ClipboardPolicy.ProviderToClientEnabled) && s.config.ClipboardAuditReporter == nil {
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
_ = writeStableError(stream, "clipboard_audit_unavailable", ErrAdmissionRejected, true)
|
||||
writeError("clipboard_audit_unavailable", ErrAdmissionRejected, true)
|
||||
return
|
||||
}
|
||||
if err := s.reportProviderState(ctx, protocol.ProviderState{Version: "1", SessionID: request.SessionID, State: ProviderStateStarting, CleanupPending: false, Channels: []string{"video", "audio", "input", "feedback"}}); err != nil {
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
_ = writeStableError(stream, "provider_state_unavailable", err, true)
|
||||
writeError("provider_state_unavailable", err, true)
|
||||
return
|
||||
}
|
||||
providerSession, err := s.config.Provider.Start(ctx, LaunchRequest{SessionID: request.SessionID, Capabilities: selected, ProviderProfile: authority.ProviderProfile, ProviderIdentity: work.ProviderIdentity, ProviderWork: work})
|
||||
@@ -281,14 +291,14 @@ func (s *Server) handleConnection(parent context.Context, connection *quic.Conn)
|
||||
s.metrics.ProviderErrors.Add(1)
|
||||
_ = s.reportProviderState(context.Background(), protocol.ProviderState{Version: "1", SessionID: request.SessionID, State: ProviderStateFailed, CleanupPending: false, Channels: []string{"video", "audio", "input", "feedback"}})
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
_ = writeStableError(stream, stableProviderCode(err), err, errors.Is(err, context.DeadlineExceeded))
|
||||
writeError(stableProviderCode(err), err, errors.Is(err, context.DeadlineExceeded))
|
||||
return
|
||||
}
|
||||
if err := s.reportProviderState(ctx, providerSession.State()); err != nil {
|
||||
_ = providerSession.ReleaseAll(context.Background())
|
||||
_ = providerSession.Terminate(context.Background())
|
||||
_ = s.config.Admission.Release(context.Background(), authority)
|
||||
_ = writeStableError(stream, "provider_state_unavailable", err, true)
|
||||
writeError("provider_state_unavailable", err, true)
|
||||
return
|
||||
}
|
||||
clientAuthority := protocol.ClientSessionAuthority{
|
||||
@@ -943,11 +953,8 @@ func (s *gatewaySession) cleanup() {
|
||||
})
|
||||
}
|
||||
|
||||
func writeStableError(writer io.Writer, code string, err error, retryable bool) error {
|
||||
message := err.Error()
|
||||
if len(message) > 256 {
|
||||
message = message[:256]
|
||||
}
|
||||
func writeStableError(writer io.Writer, code string, _ error, retryable bool) error {
|
||||
message := stableErrorMessage(code)
|
||||
payload, encodeErr := protocol.EncodeStableError(protocol.StableError{Version: "1", Code: code, Message: message, Retryable: retryable})
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
@@ -955,6 +962,29 @@ func writeStableError(writer io.Writer, code string, err error, retryable bool)
|
||||
return writeWire(writer, payload, defaultHelloLimit)
|
||||
}
|
||||
|
||||
func stableErrorMessage(code string) string {
|
||||
switch code {
|
||||
case "invalid_hello":
|
||||
return "invalid client hello"
|
||||
case "gateway_draining":
|
||||
return "gateway is draining"
|
||||
case "wrong_gateway":
|
||||
return "gateway does not match admission request"
|
||||
case "admission_rejected", "expired_grant":
|
||||
return "admission rejected"
|
||||
case "invalid_authority":
|
||||
return "invalid session authority"
|
||||
case "no_capability_overlap":
|
||||
return "no compatible capability"
|
||||
case "clipboard_audit_unavailable":
|
||||
return "clipboard audit unavailable"
|
||||
case "provider_work_unavailable", "provider_identity_rejected", "provider_malformed", "provider_timeout", "provider_unavailable", "provider_state_unavailable":
|
||||
return "provider unavailable"
|
||||
default:
|
||||
return "request failed"
|
||||
}
|
||||
}
|
||||
|
||||
func stableAdmissionCode(err error) string {
|
||||
if errors.Is(err, ErrGatewayDraining) {
|
||||
return "gateway_draining"
|
||||
|
||||
Reference in New Issue
Block a user