feat(core): add QUIC TLS admission transport

This commit is contained in:
sechmachine
2026-08-12 19:08:32 +07:00
parent 7947ebcc75
commit 111092becb
11 changed files with 2304 additions and 38 deletions
+595 -1
View File
@@ -2,18 +2,181 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" 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]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.3" version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.107" version = "1.0.107"
@@ -23,6 +186,62 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
@@ -32,6 +251,108 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "serde" name = "serde"
version = "1.0.229" version = "1.0.229"
@@ -59,7 +380,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 3.0.3",
] ]
[[package]] [[package]]
@@ -75,6 +396,45 @@ dependencies = [
"zmij", "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]] [[package]]
name = "syn" name = "syn"
version = "3.0.3" version = "3.0.3"
@@ -86,20 +446,254 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "versevdi-core" name = "versevdi-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64",
"getrandom 0.4.3",
"quinn",
"rustls",
"rustls-pemfile",
"serde", "serde",
"serde_json", "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]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.23" version = "1.0.23"
+6
View File
@@ -9,8 +9,14 @@ publish = false
crate-type = ["staticlib", "rlib"] crate-type = ["staticlib", "rlib"]
[dependencies] [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 = { version = "=1.0.229", features = ["derive"] }
serde_json = "=1.0.151" serde_json = "=1.0.151"
tokio = { version = "=1.51.4", features = ["rt-multi-thread", "sync", "time", "net"] }
[[test]] [[test]]
name = "protocol_fixtures" name = "protocol_fixtures"
+6
View File
@@ -5,6 +5,9 @@ use std::fmt;
pub enum CoreError { pub enum CoreError {
InvalidArgument, InvalidArgument,
AuthorityRejected, AuthorityRejected,
Tls,
Transport,
Protocol,
Expired, Expired,
QueueFull, QueueFull,
Cancelled, Cancelled,
@@ -33,6 +36,9 @@ impl CoreError {
match self { match self {
Self::InvalidArgument => "invalid_argument", Self::InvalidArgument => "invalid_argument",
Self::AuthorityRejected => "authority_rejected", Self::AuthorityRejected => "authority_rejected",
Self::Tls => "tls",
Self::Transport => "transport",
Self::Protocol => "protocol",
Self::Expired => "expired", Self::Expired => "expired",
Self::QueueFull => "queue_full", Self::QueueFull => "queue_full",
Self::Cancelled => "cancelled", Self::Cancelled => "cancelled",
+3
View File
@@ -16,7 +16,10 @@ pub mod error;
pub mod input; pub mod input;
pub mod media; pub mod media;
pub mod session; pub mod session;
pub mod transport;
pub mod wire; pub mod wire;
mod tls;
#[allow(unsafe_code)] #[allow(unsafe_code)]
mod abi; mod abi;
+114
View File
@@ -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)
}
+422
View File
@@ -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
View File
@@ -1,5 +1,5 @@
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use crate::error::{CoreError, Result}; 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 { fn valid_dns_name(value: &str) -> bool {
bounded(value, 1, 253) bounded(value, 1, 253)
&& value.parse::<std::net::IpAddr>().is_err()
&& !uuid_shaped(value)
&& value.split('.').all(|label| { && value.split('.').all(|label| {
bounded(label, 1, 63) bounded(label, 1, 63)
&& !label.starts_with('-') && !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)] #[serde(deny_unknown_fields)]
pub struct CapabilityProfile { pub struct CapabilityProfile {
transport: String, transport: String,
@@ -432,6 +442,35 @@ impl ConnectionManifest {
} }
Ok(()) 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)] #[derive(Clone, Debug, Deserialize)]
@@ -477,9 +516,17 @@ impl NativeTunnelCredential {
} }
Ok(()) 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)] #[serde(deny_unknown_fields)]
pub struct TunnelAdmissionRequest { pub struct TunnelAdmissionRequest {
version: String, version: String,
@@ -501,23 +548,25 @@ impl TunnelAdmissionRequest {
/// Returns `invalid_argument` for malformed, unknown, or out-of-bound data. /// Returns `invalid_argument` for malformed, unknown, or out-of-bound data.
pub fn decode(bytes: &[u8]) -> Result<Self> { pub fn decode(bytes: &[u8]) -> Result<Self> {
let request: Self = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?; let request: Self = decode_strict(bytes, MAX_ADMISSION_JSON_BYTES)?;
if request.version != "1" request.validate()?;
|| !bounded(&request.session_id, 1, 128) Ok(request)
|| !bounded(&request.gateway_id, 1, 128) }
|| !bounded(&request.audience, 1, 256)
|| !bounded(&request.grant, 43, 256) fn validate(&self) -> Result<()> {
|| !bounded(&request.client_nonce, 16, 128) if self.version != "1"
|| request.device_signature.len() != 86 || !bounded(&self.session_id, 1, 128)
|| !matches!( || !bounded(&self.gateway_id, 1, 128)
raw_base64url_decoded_len(&request.client_nonce), || !bounded(&self.audience, 1, 256)
Some(12..=96) || !bounded(&self.grant, 43, 256)
) || !bounded(&self.client_nonce, 16, 128)
|| raw_base64url_decoded_len(&request.device_signature) != Some(64) || 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); return Err(CoreError::InvalidArgument);
} }
request.capabilities.validate()?; self.capabilities.validate()?;
Ok(request) Ok(())
} }
#[must_use] #[must_use]
@@ -561,6 +610,20 @@ pub struct ClientSessionAuthority {
capabilities: CapabilityProfile, 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 { impl ClientSessionAuthority {
/// Strictly decodes and validates a provider-free RC5 client authority. /// Strictly decodes and validates a provider-free RC5 client authority.
/// ///
@@ -608,4 +671,71 @@ impl ClientSessionAuthority {
} }
Ok(()) 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)
);
}
}
} }
+806
View File
@@ -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());
}
}
+77
View File
@@ -134,6 +134,19 @@ fn strict_rc5_dtos_reject_duplicate_trailing_unknown_and_provider_fields() {
assert!(TunnelAdmissionRequest::decode(admission_provider.as_bytes()).is_err()); 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] #[test]
fn rc5_manifest_credential_and_authority_enforce_bounds_and_bindings() { fn rc5_manifest_credential_and_authority_enforce_bounds_and_bindings() {
let manifest = ConnectionManifest::decode(valid_manifest()).expect("valid manifest"); 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"}"#; 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()); 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] #[test]
+78
View File
@@ -14,6 +14,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"math/big" "math/big"
"net" "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) { func TestGatewayTelemetrySeparatesQueueProcessingAndPacing(t *testing.T) {
serverTLS, clientTLS := testTLS(t) serverTLS, clientTLS := testTLS(t)
session := &fakeSession{ session := &fakeSession{
+50 -20
View File
@@ -206,74 +206,84 @@ func (s *Server) handleConnection(parent context.Context, connection *quic.Conn)
if err != nil { if err != nil {
return 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) requestBytes, err := readWire(stream, defaultHelloLimit)
if err != nil { if err != nil {
_ = writeStableError(stream, "invalid_hello", err, false) writeError("invalid_hello", err, false)
return return
} }
request, err := protocol.DecodeTunnelAdmissionRequest(requestBytes) request, err := protocol.DecodeTunnelAdmissionRequest(requestBytes)
if err != nil { if err != nil {
_ = writeStableError(stream, "invalid_hello", err, false) writeError("invalid_hello", err, false)
return return
} }
if s.Draining() { if s.Draining() {
_ = writeStableError(stream, "gateway_draining", ErrGatewayDraining, true) writeError("gateway_draining", ErrGatewayDraining, true)
return return
} }
if request.GatewayID != s.config.GatewayID { if request.GatewayID != s.config.GatewayID {
_ = writeStableError(stream, "wrong_gateway", ErrAdmissionRejected, false) writeError("wrong_gateway", ErrAdmissionRejected, false)
return return
} }
authority, err := s.config.Admission.Admit(ctx, request) authority, err := s.config.Admission.Admit(ctx, request)
if err != nil { if err != nil {
s.metrics.AdmissionRejects.Add(1) 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 return
} }
if s.Draining() { if s.Draining() {
_ = s.config.Admission.Release(context.Background(), authority) _ = s.config.Admission.Release(context.Background(), authority)
_ = writeStableError(stream, "gateway_draining", ErrGatewayDraining, true) writeError("gateway_draining", ErrGatewayDraining, true)
return return
} }
if err := s.validateAuthority(authority, request); err != nil { if err := s.validateAuthority(authority, request); err != nil {
_ = s.config.Admission.Release(context.Background(), authority) _ = s.config.Admission.Release(context.Background(), authority)
_ = writeStableError(stream, "invalid_authority", err, false) writeError("invalid_authority", err, false)
return return
} }
work, err := s.config.Admission.ProviderWork(ctx, authority) work, err := s.config.Admission.ProviderWork(ctx, authority)
if err != nil || s.validateProviderWork(work, authority) != nil { if err != nil || s.validateProviderWork(work, authority) != nil {
_ = s.config.Admission.Release(context.Background(), authority) _ = s.config.Admission.Release(context.Background(), authority)
_ = writeStableError(stream, "provider_work_unavailable", ErrAdmissionRejected, err != nil) writeError("provider_work_unavailable", ErrAdmissionRejected, err != nil)
return return
} }
selected, err := IntersectCapabilities(s.config.Capabilities, s.config.ProviderCapabilities, request.Capabilities, authority.Capabilities) selected, err := IntersectCapabilities(s.config.Capabilities, s.config.ProviderCapabilities, request.Capabilities, authority.Capabilities)
if err != nil { if err != nil {
_ = s.config.Admission.Release(context.Background(), authority) _ = s.config.Admission.Release(context.Background(), authority)
s.metrics.AdmissionRejects.Add(1) s.metrics.AdmissionRejects.Add(1)
_ = writeStableError(stream, "no_capability_overlap", err, false) writeError("no_capability_overlap", err, false)
return return
} }
selected, err = selectApolloPolicyCapabilities(work.StreamPolicy, selected) selected, err = selectApolloPolicyCapabilities(work.StreamPolicy, selected)
if err != nil { if err != nil {
_ = s.config.Admission.Release(context.Background(), authority) _ = s.config.Admission.Release(context.Background(), authority)
s.metrics.AdmissionRejects.Add(1) s.metrics.AdmissionRejects.Add(1)
_ = writeStableError(stream, "no_capability_overlap", err, false) writeError("no_capability_overlap", err, false)
return return
} }
clipboard, err := newClipboardGate(work.ClipboardPolicy, time.Now) clipboard, err := newClipboardGate(work.ClipboardPolicy, time.Now)
if err != nil { if err != nil {
_ = s.config.Admission.Release(context.Background(), authority) _ = s.config.Admission.Release(context.Background(), authority)
_ = writeStableError(stream, "provider_work_unavailable", ErrAdmissionRejected, false) writeError("provider_work_unavailable", ErrAdmissionRejected, false)
return return
} }
if (work.ClipboardPolicy.ClientToProviderEnabled || work.ClipboardPolicy.ProviderToClientEnabled) && s.config.ClipboardAuditReporter == nil { if (work.ClipboardPolicy.ClientToProviderEnabled || work.ClipboardPolicy.ProviderToClientEnabled) && s.config.ClipboardAuditReporter == nil {
_ = s.config.Admission.Release(context.Background(), authority) _ = s.config.Admission.Release(context.Background(), authority)
_ = writeStableError(stream, "clipboard_audit_unavailable", ErrAdmissionRejected, true) writeError("clipboard_audit_unavailable", ErrAdmissionRejected, true)
return 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 { 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) _ = s.config.Admission.Release(context.Background(), authority)
_ = writeStableError(stream, "provider_state_unavailable", err, true) writeError("provider_state_unavailable", err, true)
return return
} }
providerSession, err := s.config.Provider.Start(ctx, LaunchRequest{SessionID: request.SessionID, Capabilities: selected, ProviderProfile: authority.ProviderProfile, ProviderIdentity: work.ProviderIdentity, ProviderWork: work}) 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.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.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) _ = 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 return
} }
if err := s.reportProviderState(ctx, providerSession.State()); err != nil { if err := s.reportProviderState(ctx, providerSession.State()); err != nil {
_ = providerSession.ReleaseAll(context.Background()) _ = providerSession.ReleaseAll(context.Background())
_ = providerSession.Terminate(context.Background()) _ = providerSession.Terminate(context.Background())
_ = s.config.Admission.Release(context.Background(), authority) _ = s.config.Admission.Release(context.Background(), authority)
_ = writeStableError(stream, "provider_state_unavailable", err, true) writeError("provider_state_unavailable", err, true)
return return
} }
clientAuthority := protocol.ClientSessionAuthority{ clientAuthority := protocol.ClientSessionAuthority{
@@ -943,11 +953,8 @@ func (s *gatewaySession) cleanup() {
}) })
} }
func writeStableError(writer io.Writer, code string, err error, retryable bool) error { func writeStableError(writer io.Writer, code string, _ error, retryable bool) error {
message := err.Error() message := stableErrorMessage(code)
if len(message) > 256 {
message = message[:256]
}
payload, encodeErr := protocol.EncodeStableError(protocol.StableError{Version: "1", Code: code, Message: message, Retryable: retryable}) payload, encodeErr := protocol.EncodeStableError(protocol.StableError{Version: "1", Code: code, Message: message, Retryable: retryable})
if encodeErr != nil { if encodeErr != nil {
return encodeErr return encodeErr
@@ -955,6 +962,29 @@ func writeStableError(writer io.Writer, code string, err error, retryable bool)
return writeWire(writer, payload, defaultHelloLimit) 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 { func stableAdmissionCode(err error) string {
if errors.Is(err, ErrGatewayDraining) { if errors.Is(err, ErrGatewayDraining) {
return "gateway_draining" return "gateway_draining"