diff --git a/Cargo.lock b/Cargo.lock index 25eedc4b6d..71f925ea4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,13 +68,13 @@ dependencies = [ [[package]] name = "aes" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" dependencies = [ "cipher 0.5.2", "cpubits", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -98,7 +98,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" dependencies = [ "aead 0.6.1", - "aes 0.9.1", + "aes 0.9.3", "cipher 0.5.2", "ctr 0.10.1", "ghash 0.6.0", @@ -111,7 +111,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ac571010bd60765c56085a4f1d412012a9be2663b1a2f2b19b49318653fd0d" dependencies = [ - "aes 0.9.1", + "aes 0.9.3", "const-oid 0.10.2", ] @@ -130,9 +130,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -153,7 +153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "libc", ] @@ -175,7 +175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", - "bitflags 2.13.1", + "bitflags 2.13.2", "cc", "jni 0.22.4", "libc", @@ -184,7 +184,7 @@ dependencies = [ "ndk-context", "ndk-sys", "num_enum", - "thiserror 2.0.19", + "thiserror 2.0.21", ] [[package]] @@ -195,9 +195,9 @@ checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -309,7 +309,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros 4.1.0", - "thiserror 2.0.19", + "thiserror 2.0.21", "time", ] @@ -321,8 +321,8 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", - "synstructure", + "syn 2.0.119", + "synstructure 0.13.2", ] [[package]] @@ -333,7 +333,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -363,18 +363,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] @@ -400,9 +400,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "zeroize", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -433,6 +433,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -491,9 +497,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ "arbitrary", ] @@ -581,13 +587,13 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -610,9 +616,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bytesize" -version = "2.4.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e" +checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" [[package]] name = "calloop" @@ -620,7 +626,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "log", "polling", "rustix 0.38.44", @@ -657,9 +663,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" dependencies = [ "find-msvc-tools", "jobserver", @@ -675,24 +681,24 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -765,9 +771,9 @@ checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" [[package]] name = "clap" -version = "4.6.5" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -775,9 +781,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -787,21 +793,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cmake" @@ -826,9 +832,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -930,7 +936,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -985,18 +991,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -1044,18 +1050,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1063,18 +1069,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crossterm" @@ -1082,13 +1088,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crossterm_winapi", "derive_more", "document-features", "mio", "parking_lot", - "rustix 1.1.4", + "rustix 1.1.5", "signal-hook", "signal-hook-mio", "winapi", @@ -1170,11 +1176,11 @@ dependencies = [ [[package]] name = "cryptoki" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff765b99fc49f3116c9a908484486a2b92fd73c48da45c3a69716471c6cc56c6" +checksum = "625cf4599c43d69a16996ce2573fc80745a0b0300c962f0bfa11d55b6cab2b2d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cryptoki-sys", "libloading", "log", @@ -1240,7 +1246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c906a87e53a36ff795d72e06e8162a83c5436e3ea89e942a9cb9fc083f0a384f" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto", @@ -1257,7 +1263,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1268,9 +1274,9 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "der" @@ -1284,9 +1290,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" dependencies = [ "const-oid 0.10.2", "der_derive", @@ -1317,7 +1323,7 @@ checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1334,7 +1340,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1356,7 +1362,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1405,7 +1411,7 @@ dependencies = [ "diplomat_core", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1425,7 +1431,7 @@ dependencies = [ "serde", "smallvec", "strck_ident", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1440,19 +1446,19 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", ] [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -1497,7 +1503,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "drm-ffi", "drm-fourcc", @@ -1512,7 +1518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" dependencies = [ "drm-sys", - "rustix 1.1.4", + "rustix 1.1.5", ] [[package]] @@ -1555,7 +1561,7 @@ version = "0.17.0-rc.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c72d1455753a703ad4b90ed2a759f2bc4562024a303176439cf6e593b5ade4" dependencies = [ - "der 0.8.1", + "der 0.8.2", "digest 0.11.3", "elliptic-curve", "rfc6979", @@ -1589,9 +1595,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "elliptic-curve" @@ -1638,7 +1644,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1669,9 +1675,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fdeflate" @@ -1702,12 +1708,12 @@ dependencies = [ "embed-resource", "ironrdp", "ironrdp-cliprdr-native", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc-pipe-proxy", "ironrdp-rdcleanpath", "ironrdp-vmconnect", "sspi", - "thiserror 2.0.19", + "thiserror 2.0.21", "tracing", "tracing-subscriber", ] @@ -1720,9 +1726,9 @@ checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "flagset" @@ -1732,13 +1738,14 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.9.1", + "zlib-rs", ] [[package]] @@ -1768,13 +1775,13 @@ dependencies = [ [[package]] name = "foreign-types-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -1812,9 +1819,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -1827,9 +1834,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -1837,15 +1844,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1854,32 +1861,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" @@ -1889,9 +1896,9 @@ checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -1929,7 +1936,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.4", + "rustix 1.1.5", "windows-link", ] @@ -1990,14 +1997,14 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "polyval 0.7.1", + "polyval 0.7.3", ] [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "gloo-net" @@ -2013,7 +2020,7 @@ dependencies = [ "http", "js-sys", "pin-project", - "thiserror 2.0.19", + "thiserror 2.0.21", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2055,9 +2062,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -2119,9 +2126,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -2145,9 +2152,9 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.4", + "rand 0.9.5", "ring", - "thiserror 2.0.19", + "thiserror 2.0.21", "tinyvec", "tokio", "tracing", @@ -2167,10 +2174,10 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.4", + "rand 0.9.5", "resolv-conf", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.21", "tokio", "tracing", ] @@ -2204,9 +2211,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2214,9 +2221,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -2224,9 +2231,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -2250,9 +2257,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "subtle", "typenum", @@ -2261,9 +2268,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -2283,9 +2290,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.9" +version = "0.27.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" dependencies = [ "http", "hyper", @@ -2315,16 +2322,17 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "futures-channel", "futures-util", "http", "http-body", + "httparse", "hyper", "ipnet", "libc", @@ -2364,9 +2372,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -2378,9 +2386,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -2391,9 +2399,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2405,16 +2413,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2425,15 +2434,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -2480,9 +2489,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown", @@ -2513,7 +2522,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crossterm", "dyn-clone", "fuzzy-matcher", @@ -2536,9 +2545,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "iron-remote-desktop" @@ -2554,7 +2563,7 @@ dependencies = [ [[package]] name = "ironrdp" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "async-trait", @@ -2565,7 +2574,7 @@ dependencies = [ "ironrdp-cliprdr", "ironrdp-cliprdr-native", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-echo", @@ -2582,7 +2591,7 @@ dependencies = [ "ironrdp-vmconnect", "opus2", "pico-args", - "rand 0.9.4", + "rand 0.9.5", "sspi", "tokio", "tokio-rustls", @@ -2593,14 +2602,14 @@ dependencies = [ [[package]] name = "ironrdp-acceptor" -version = "0.10.0" +version = "0.11.0" dependencies = [ "ironrdp-async", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-pdu", "ironrdp-svc", - "rand 0.9.4", + "rand 0.9.5", "tracing", ] @@ -2609,7 +2618,7 @@ name = "ironrdp-activex" version = "0.1.0" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "embed-resource", "ironrdp-cfg", "ironrdp-client", @@ -2617,7 +2626,7 @@ dependencies = [ "ironrdp-cliprdr-format", "ironrdp-cliprdr-native", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-daemon", "ironrdp-input", "ironrdp-pdu", @@ -2641,7 +2650,7 @@ dependencies = [ [[package]] name = "ironrdp-agent" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "bytes", @@ -2664,10 +2673,10 @@ dependencies = [ [[package]] name = "ironrdp-ainput" -version = "0.8.0" +version = "0.9.0" dependencies = [ - "bitflags 2.13.1", - "ironrdp-core 0.2.1", + "bitflags 2.13.2", + "ironrdp-core 0.3.0", "ironrdp-dvc", "num-derive 0.5.1", "num-traits", @@ -2675,11 +2684,11 @@ dependencies = [ [[package]] name = "ironrdp-async" -version = "0.10.0" +version = "0.11.0" dependencies = [ "bytes", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-pdu", "tracing", "web-time", @@ -2697,18 +2706,18 @@ dependencies = [ [[package]] name = "ironrdp-blocking" -version = "0.10.0" +version = "0.11.0" dependencies = [ "bytes", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-pdu", "tracing", ] [[package]] name = "ironrdp-bulk" -version = "0.1.1" +version = "0.2.0" dependencies = [ "criterion", ] @@ -2719,7 +2728,7 @@ version = "0.0.0" dependencies = [ "aes-gcm 0.10.3", "hmac 0.12.1", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-egfx", "ironrdp-graphics", @@ -2729,20 +2738,20 @@ dependencies = [ "pcap-parser", "png", "sha2 0.10.9", - "thiserror 2.0.19", + "thiserror 2.0.21", "zeroize", ] [[package]] name = "ironrdp-cfg" -version = "0.1.0" +version = "0.2.0" dependencies = [ "ironrdp-propertyset", ] [[package]] name = "ironrdp-client" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "futures-util", @@ -2750,7 +2759,7 @@ dependencies = [ "ironrdp-cliprdr", "ironrdp-cliprdr-native", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-dvc-com-plugin", @@ -2788,10 +2797,10 @@ dependencies = [ [[package]] name = "ironrdp-cliprdr" -version = "0.7.0" +version = "0.8.0" dependencies = [ - "bitflags 2.13.1", - "ironrdp-core 0.2.1", + "bitflags 2.13.2", + "ironrdp-core 0.3.0", "ironrdp-pdu", "ironrdp-svc", "tracing", @@ -2800,34 +2809,34 @@ dependencies = [ [[package]] name = "ironrdp-cliprdr-format" -version = "0.2.0" +version = "0.3.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "png", ] [[package]] name = "ironrdp-cliprdr-native" -version = "0.7.0" +version = "0.7.1" dependencies = [ "ironrdp-cliprdr", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "tracing", "windows", ] [[package]] name = "ironrdp-connector" -version = "0.10.0" +version = "0.11.0" dependencies = [ - "ironrdp-core 0.2.1", - "ironrdp-error 0.2.0", + "ironrdp-core 0.3.0", + "ironrdp-error 0.2.1", "ironrdp-pdu", "ironrdp-svc", "picky", "picky-asn1-der", "picky-asn1-x509", - "rand 0.9.4", + "rand 0.9.5", "sspi", "tracing", "url", @@ -2844,9 +2853,9 @@ dependencies = [ [[package]] name = "ironrdp-core" -version = "0.2.1" +version = "0.3.0" dependencies = [ - "ironrdp-error 0.2.0", + "ironrdp-error 0.2.1", ] [[package]] @@ -2880,9 +2889,9 @@ dependencies = [ [[package]] name = "ironrdp-displaycontrol" -version = "0.8.0" +version = "0.9.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -2891,9 +2900,9 @@ dependencies = [ [[package]] name = "ironrdp-dvc" -version = "0.8.0" +version = "0.9.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-pdu", "ironrdp-svc", "tracing", @@ -2901,9 +2910,9 @@ dependencies = [ [[package]] name = "ironrdp-dvc-com-plugin" -version = "0.1.3" +version = "0.1.4" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -2914,10 +2923,10 @@ dependencies = [ [[package]] name = "ironrdp-dvc-pipe-proxy" -version = "0.5.0" +version = "0.5.1" dependencies = [ "async-trait", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -2927,9 +2936,9 @@ dependencies = [ [[package]] name = "ironrdp-echo" -version = "0.4.0" +version = "0.4.1" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "tracing", @@ -2937,12 +2946,12 @@ dependencies = [ [[package]] name = "ironrdp-egfx" -version = "0.3.0" +version = "0.4.0" dependencies = [ "arbitrary", "bit_field", - "bitflags 2.13.1", - "ironrdp-core 0.2.1", + "bitflags 2.13.2", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-graphics", "ironrdp-pdu", @@ -2959,11 +2968,11 @@ checksum = "4a9d7794e854eef2f13fdf79c8502bcc567a75a15fd0522885f37739386a4cef" [[package]] name = "ironrdp-error" -version = "0.2.0" +version = "0.2.1" [[package]] name = "ironrdp-futures" -version = "0.8.0" +version = "0.8.1" dependencies = [ "futures-util", "ironrdp-async", @@ -2977,7 +2986,7 @@ dependencies = [ "ironrdp-bulk", "ironrdp-cliprdr", "ironrdp-cliprdr-format", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-egfx", @@ -2994,16 +3003,16 @@ dependencies = [ [[package]] name = "ironrdp-graphics" -version = "0.9.0" +version = "0.10.0" dependencies = [ "bit_field", - "bitflags 2.13.1", + "bitflags 2.13.2", "bitvec", "bmp", "bytemuck", "byteorder", "expect-test", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-pdu", "num-derive 0.5.1", "num-traits", @@ -3013,7 +3022,7 @@ dependencies = [ [[package]] name = "ironrdp-input" -version = "0.7.0" +version = "0.7.1" dependencies = [ "bitvec", "ironrdp-pdu", @@ -3022,16 +3031,16 @@ dependencies = [ [[package]] name = "ironrdp-mstsgu" -version = "0.0.1" +version = "0.0.2" dependencies = [ - "base64", - "bitflags 2.13.1", + "base64 0.22.1", + "bitflags 2.13.2", "futures-util", "http-body-util", "hyper", "hyper-util", - "ironrdp-core 0.2.1", - "ironrdp-error 0.2.0", + "ironrdp-core 0.3.0", + "ironrdp-error 0.2.1", "ironrdp-tls", "log", "picky", @@ -3048,24 +3057,24 @@ dependencies = [ [[package]] name = "ironrdp-nscodec" -version = "0.2.0" +version = "0.2.1" dependencies = [ "ironrdp-graphics", ] [[package]] name = "ironrdp-pdu" -version = "0.9.0" +version = "0.10.0" dependencies = [ "arbitrary", "bit_field", - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "der-parser", "expect-test", "hmac 0.12.1", - "ironrdp-core 0.2.1", - "ironrdp-error 0.2.0", + "ironrdp-core 0.3.0", + "ironrdp-error 0.2.1", "md-5 0.10.6", "num-bigint 0.4.8", "num-derive 0.5.1", @@ -3089,25 +3098,25 @@ version = "0.1.0" name = "ironrdp-rail" version = "0.1.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-svc", ] [[package]] name = "ironrdp-rdcleanpath" -version = "0.2.2" +version = "0.2.3" dependencies = [ - "der 0.8.1", + "der 0.8.2", ] [[package]] name = "ironrdp-rdpdr" -version = "0.7.0" +version = "0.8.0" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "getrandom 0.3.4", - "ironrdp-core 0.2.1", - "ironrdp-error 0.2.0", + "ironrdp-core 0.3.0", + "ironrdp-error 0.2.1", "ironrdp-pdu", "ironrdp-svc", "tracing", @@ -3115,9 +3124,9 @@ dependencies = [ [[package]] name = "ironrdp-rdpdr-native" -version = "0.7.0" +version = "0.7.1" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-pdu", "ironrdp-rdpdr", "ironrdp-svc", @@ -3130,7 +3139,7 @@ dependencies = [ name = "ironrdp-rdpeai" version = "0.1.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-rdpsnd", @@ -3142,7 +3151,7 @@ dependencies = [ name = "ironrdp-rdpecam" version = "0.0.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -3153,8 +3162,8 @@ dependencies = [ name = "ironrdp-rdpei" version = "0.1.0" dependencies = [ - "bitflags 2.13.1", - "ironrdp-core 0.2.1", + "bitflags 2.13.2", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -3165,7 +3174,7 @@ dependencies = [ name = "ironrdp-rdpel" version = "0.0.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -3177,8 +3186,8 @@ name = "ironrdp-rdpemt" version = "0.1.0" dependencies = [ "arbitrary", - "ironrdp-core 0.2.1", - "ironrdp-error 0.2.0", + "ironrdp-core 0.3.0", + "ironrdp-error 0.2.1", "subtle", "tracing", ] @@ -3188,9 +3197,9 @@ name = "ironrdp-rdpeudp" version = "0.1.0" dependencies = [ "arbitrary", - "bitflags 2.13.1", - "ironrdp-core 0.2.1", - "ironrdp-error 0.2.0", + "bitflags 2.13.2", + "ironrdp-core 0.3.0", + "ironrdp-error 0.2.1", ] [[package]] @@ -3199,8 +3208,8 @@ version = "0.1.0" dependencies = [ "bytes", "ironrdp-async", - "ironrdp-core 0.2.1", - "ironrdp-error 0.2.0", + "ironrdp-core 0.3.0", + "ironrdp-error 0.2.1", "ironrdp-pdu", "ironrdp-rdpemt", "ironrdp-rdpeudp", @@ -3215,7 +3224,7 @@ dependencies = [ name = "ironrdp-rdpeusb" version = "0.1.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-str", @@ -3226,7 +3235,7 @@ dependencies = [ name = "ironrdp-rdpewa" version = "0.1.0" dependencies = [ - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "ironrdp-svc", @@ -3251,10 +3260,10 @@ dependencies = [ [[package]] name = "ironrdp-rdpsnd" -version = "0.9.0" +version = "0.10.0" dependencies = [ - "bitflags 2.13.1", - "ironrdp-core 0.2.1", + "bitflags 2.13.2", + "ironrdp-core 0.3.0", "ironrdp-pdu", "ironrdp-svc", "tracing", @@ -3263,12 +3272,12 @@ dependencies = [ [[package]] name = "ironrdp-rdpsnd-native" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "bytemuck", "cpal", - "ironrdp-error 0.2.0", + "ironrdp-error 0.2.1", "ironrdp-rdpeai", "ironrdp-rdpsnd", "opus2", @@ -3282,7 +3291,7 @@ name = "ironrdp-rpc" version = "0.1.0" dependencies = [ "anyhow", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-input", "ironrdp-pdu", "ironrdp-propertyset", @@ -3296,7 +3305,7 @@ dependencies = [ [[package]] name = "ironrdp-server" -version = "0.13.0" +version = "0.14.0" dependencies = [ "async-trait", "bytes", @@ -3305,12 +3314,12 @@ dependencies = [ "ironrdp-async", "ironrdp-cliprdr", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-echo", "ironrdp-egfx", - "ironrdp-error 0.2.0", + "ironrdp-error 0.2.1", "ironrdp-graphics", "ironrdp-nscodec", "ironrdp-pdu", @@ -3326,7 +3335,7 @@ dependencies = [ "ironrdp-tokio", "ironrdp-usb", "qoicoubeh", - "rand 0.9.4", + "rand 0.9.5", "rayon", "rustls-pemfile", "tokio", @@ -3339,14 +3348,14 @@ dependencies = [ [[package]] name = "ironrdp-session" -version = "0.11.0" +version = "0.12.0" dependencies = [ "ironrdp-bulk", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-egfx", - "ironrdp-error 0.2.0", + "ironrdp-error 0.2.1", "ironrdp-graphics", "ironrdp-pdu", "ironrdp-rdpei", @@ -3363,18 +3372,18 @@ version = "0.0.0" [[package]] name = "ironrdp-str" -version = "0.1.1" +version = "0.2.0" dependencies = [ "bytemuck", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", ] [[package]] name = "ironrdp-svc" -version = "0.8.0" +version = "0.9.0" dependencies = [ - "bitflags 2.13.1", - "ironrdp-core 0.2.1", + "bitflags 2.13.2", + "ironrdp-core 0.3.0", "ironrdp-pdu", ] @@ -3394,12 +3403,12 @@ dependencies = [ "ironrdp-cliprdr", "ironrdp-cliprdr-format", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-echo", "ironrdp-egfx", - "ironrdp-error 0.2.0", + "ironrdp-error 0.2.1", "ironrdp-fuzzing", "ironrdp-graphics", "ironrdp-input", @@ -3444,7 +3453,7 @@ dependencies = [ "ironrdp-bulk", "ironrdp-cfg", "ironrdp-client", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-daemon", "ironrdp-dvc", "ironrdp-dvc-pipe-proxy", @@ -3479,7 +3488,7 @@ dependencies = [ [[package]] name = "ironrdp-tls" -version = "0.2.2" +version = "0.2.3" dependencies = [ "rustls-native-certs", "tokio", @@ -3490,7 +3499,7 @@ dependencies = [ [[package]] name = "ironrdp-tokio" -version = "0.10.0" +version = "0.10.1" dependencies = [ "ironrdp-async", "ironrdp-connector", @@ -3505,7 +3514,7 @@ version = "0.1.0" [[package]] name = "ironrdp-viewer" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", @@ -3537,7 +3546,7 @@ version = "0.1.0" dependencies = [ "ironrdp-async", "ironrdp-connector", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-dvc", "ironrdp-pdu", "sha2 0.10.9", @@ -3551,7 +3560,7 @@ name = "ironrdp-web" version = "0.0.0" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "chrono", "futures-channel", "futures-util", @@ -3563,7 +3572,7 @@ dependencies = [ "iron-remote-desktop", "ironrdp", "ironrdp-cliprdr-format", - "ironrdp-core 0.2.1", + "ironrdp-core 0.3.0", "ironrdp-futures", "ironrdp-pdu", "ironrdp-propertyset", @@ -3654,7 +3663,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror 2.0.21", "walkdir", "windows-link", ] @@ -3669,7 +3678,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3697,7 +3706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3712,9 +3721,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -3723,12 +3732,12 @@ dependencies = [ [[package]] name = "keccak" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -3766,14 +3775,14 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", "plain", - "redox_syscall 0.9.0", + "redox_syscall 0.9.4", ] [[package]] @@ -3807,9 +3816,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -3828,15 +3837,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru-slab" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "mach2" @@ -3916,11 +3925,21 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", @@ -3987,7 +4006,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "jni-sys 0.3.1", "log", "ndk-sys", @@ -4017,7 +4036,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -4049,7 +4068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8e4f1efbc5536f3e43f40e089abc7562b3e0bc48dfee57444558d45c31dee20" dependencies = [ "now-proto-pdu", - "thiserror 2.0.19", + "thiserror 2.0.21", "tokio", ] @@ -4059,7 +4078,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1fbbab671b284dfd3e1a81a84da2cd1f4d77e7da675498e59e55de69c713545" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "ironrdp-core 0.1.5", "ironrdp-error 0.1.3", "uuid", @@ -4109,7 +4128,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4120,14 +4139,14 @@ checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -4160,7 +4179,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4194,7 +4213,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4210,7 +4229,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", "objc2 0.6.4", "objc2-core-audio", @@ -4235,7 +4254,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -4272,7 +4291,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", ] @@ -4282,7 +4301,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4294,7 +4313,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.6.2", "dispatch2", "libc", @@ -4307,7 +4326,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -4350,7 +4369,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "dispatch", "libc", @@ -4363,7 +4382,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -4376,7 +4395,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", "objc2-core-foundation", ] @@ -4399,7 +4418,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4411,7 +4430,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4424,7 +4443,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -4455,7 +4474,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -4487,7 +4506,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -4542,19 +4561,19 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openh264" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b2b561d2103303e233779545da757ecd35bad82188d619a6d8901f3007e1ff" +checksum = "fcc9071a0b5f9c501ddd01066c2600d922cc799dc450a52975222bcda7755a08" dependencies = [ "openh264-sys2", - "wide 1.5.0", + "wide 1.7.1", ] [[package]] name = "openh264-sys2" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a8867e48183bbd9147380227448c065fe456eb30b0ebc68929809c36c30985" +checksum = "6ada29a4cadc13d4d326737af87c13e224210d7d99fe47594e9f3f9b0102fd70" dependencies = [ "cc", "libloading", @@ -4569,7 +4588,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "foreign-types 0.3.2", "libc", @@ -4585,7 +4604,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4737,11 +4756,11 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.6" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" dependencies = [ - "base64", + "base64 0.23.1", "serde_core", ] @@ -4766,10 +4785,10 @@ version = "7.0.0-rc.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ae9cd78eb1d61be4790713d28368cf71c844218fcd91542768805423f02666" dependencies = [ - "aes 0.9.1", + "aes 0.9.3", "aes-gcm 0.11.0-rc.4", "aes-kw", - "base64", + "base64 0.22.1", "cbc", "crypto-bigint", "crypto-common 0.2.2", @@ -4793,7 +4812,7 @@ dependencies = [ "picky-asn1-x509", "pkcs1 0.8.0-rc.4", "primeorder", - "rand 0.10.2", + "rand 0.10.3", "rand_core 0.10.1", "rc2", "rsa", @@ -4805,7 +4824,7 @@ dependencies = [ "sha1 0.11.0", "sha2 0.11.0", "sha3", - "thiserror 2.0.19", + "thiserror 2.0.21", "x25519-dalek", "zeroize", ] @@ -4840,7 +4859,7 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "859d4117bd1b1dc5646359ee7243c50c5000c0920ea2d1fb120335a2f4c684b8" dependencies = [ - "base64", + "base64 0.22.1", "crypto-bigint", "oid", "picky-asn1", @@ -4856,7 +4875,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d188f3192356068dbdba54bddbca6fd0f7a09565d3861eeb8efe1ab77ae8e97" dependencies = [ - "aes 0.9.1", + "aes 0.9.3", "block-padding", "byteorder", "cbc", @@ -4870,11 +4889,11 @@ dependencies = [ "picky-asn1", "picky-asn1-der", "picky-asn1-x509", - "rand 0.10.2", + "rand 0.10.3", "rand_core 0.10.1", "serde", "sha1 0.11.0", - "thiserror 2.0.19", + "thiserror 2.0.21", "uuid", ] @@ -4901,7 +4920,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4933,7 +4952,7 @@ version = "0.8.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" dependencies = [ - "der 0.8.1", + "der 0.8.2", "spki 0.8.0", ] @@ -4953,15 +4972,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.1", + "der 0.8.2", "spki 0.8.0", ] [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plain" @@ -5003,11 +5022,11 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -5020,7 +5039,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.4", + "rustix 1.1.5", "windows-sys 0.61.2", ] @@ -5038,26 +5057,26 @@ dependencies = [ [[package]] name = "polyval" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ "cpubits", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "universal-hash 0.6.1", ] [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] @@ -5068,14 +5087,14 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" dependencies = [ - "rand 0.8.6", + "rand 0.8.8", ] [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -5148,9 +5167,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -5163,9 +5182,9 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec 0.8.0", - "bitflags 2.13.1", + "bitflags 2.13.2", "num-traits", - "rand 0.9.4", + "rand 0.9.5", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -5197,18 +5216,18 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" dependencies = [ "bytes", "cfg_aliases", @@ -5218,7 +5237,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.19", + "thiserror 2.0.21", "tokio", "tracing", "web-time", @@ -5226,21 +5245,21 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand 0.10.2", + "rand 0.10.3", "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.19", + "thiserror 2.0.21", "tinyvec", "tracing", "web-time", @@ -5248,9 +5267,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" dependencies = [ "cfg_aliases", "libc", @@ -5262,9 +5281,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -5289,9 +5308,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5300,9 +5319,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -5310,9 +5329,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -5418,9 +5437,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.9" +version = "0.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" dependencies = [ "pem", "ring", @@ -5445,23 +5464,23 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] name = "redox_syscall" -version = "0.9.0" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -5471,9 +5490,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -5498,7 +5517,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -5586,9 +5605,9 @@ dependencies = [ [[package]] name = "ringbuf" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a158e09ede21a14b172ca6cdd6208386c6ae2cb6acef58d774368ef8c450dfa7" +checksum = "0b123165531e325df9079f56c036f2da4103a5d5abfef9e2fda4b9c34d1ef7aa" dependencies = [ "crossbeam-utils", "portable-atomic", @@ -5638,7 +5657,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", "unicode-ident", ] @@ -5719,7 +5738,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5728,11 +5747,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.12.1", @@ -5741,9 +5760,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "aws-lc-rs", "log", @@ -5778,9 +5797,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -5788,9 +5807,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -5833,9 +5852,9 @@ dependencies = [ [[package]] name = "safe_arch" -version = "1.0.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed" +checksum = "42c6efa15875e6ecb39ca61fb0b0c1a40b84fac5a5ffe71eef7d1000c8eb3f5f" dependencies = [ "bytemuck", ] @@ -5891,7 +5910,7 @@ checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", "ctutils", - "der 0.8.1", + "der 0.8.2", "hybrid-array", "subtle", "zeroize", @@ -5912,7 +5931,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5937,9 +5956,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -5957,22 +5976,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -6037,7 +6056,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -6059,7 +6078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -6132,15 +6151,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -6160,9 +6179,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" [[package]] name = "smithay-client-toolkit" @@ -6170,7 +6189,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "calloop", "calloop-wayland-source", "cursor-icon", @@ -6200,9 +6219,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -6228,7 +6247,7 @@ dependencies = [ "objc2-quartz-core 0.3.2", "raw-window-handle", "redox_syscall 0.5.18", - "rustix 1.1.4", + "rustix 1.1.5", "tiny-xlib", "tracing", "wasm-bindgen", @@ -6242,9 +6261,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] @@ -6266,7 +6285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.1", + "der 0.8.2", ] [[package]] @@ -6283,7 +6302,7 @@ checksum = "5c3bd2a45e7e24fd72eba100ced3fd847e05fe4657d7f067eb06d1a894f0d4e5" dependencies = [ "async-dnssd", "async-recursion", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytemuck", "byteorder", "cfg-if", @@ -6313,7 +6332,7 @@ dependencies = [ "pkcs1 0.8.0-rc.4", "portpicker", "primeorder", - "rand 0.10.2", + "rand 0.10.3", "rand_core 0.10.1", "reqwest", "rsa", @@ -6390,9 +6409,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -6401,9 +6420,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -6427,7 +6446,18 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", ] [[package]] @@ -6436,7 +6466,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6472,7 +6502,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", + "rustix 1.1.5", "windows-sys 0.61.2", ] @@ -6487,11 +6517,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.21", ] [[package]] @@ -6502,34 +6532,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "js-sys", @@ -6602,9 +6632,9 @@ checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -6622,18 +6652,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tls_codec" @@ -6653,14 +6674,14 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -6675,13 +6696,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] [[package]] @@ -6696,9 +6717,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", @@ -6750,9 +6771,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ "indexmap", "serde_core", @@ -6774,9 +6795,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ "indexmap", "toml_datetime", @@ -6786,18 +6807,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -6820,7 +6841,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -6864,7 +6885,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6944,11 +6965,11 @@ dependencies = [ "httparse", "log", "native-tls", - "rand 0.9.4", + "rand 0.9.5", "rustls", "rustls-pki-types", "sha1 0.10.7", - "thiserror 2.0.19", + "thiserror 2.0.21", ] [[package]] @@ -6965,9 +6986,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-segmentation" @@ -7033,9 +7054,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.4" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -7069,7 +7090,7 @@ checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -7155,9 +7176,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -7168,19 +7189,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ "js-sys", + "tokio", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7188,35 +7210,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] [[package]] name = "wayland-backend" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.4", + "rustix 1.1.5", "scoped-tls", "smallvec", "wayland-sys", @@ -7224,12 +7246,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.14" +version = "0.31.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" dependencies = [ - "bitflags 2.13.1", - "rustix 1.1.4", + "bitflags 2.13.2", + "rustix 1.1.5", "wayland-backend", "wayland-scanner", ] @@ -7240,7 +7262,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cursor-icon", "wayland-backend", ] @@ -7251,7 +7273,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ - "rustix 1.1.4", + "rustix 1.1.5", "wayland-client", "xcursor", ] @@ -7262,7 +7284,7 @@ version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7274,7 +7296,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7287,7 +7309,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7296,9 +7318,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", "quick-xml", @@ -7319,9 +7341,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -7339,18 +7361,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" dependencies = [ "libc", "libredox", @@ -7371,12 +7393,12 @@ dependencies = [ [[package]] name = "wide" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfdfe6a32973f2d1b268b8895845a8a96cac2f0191e72c27cc929036060dbf89" +checksum = "d920ac99c3c8edce110cb8d07dbb324d6d026011dce85b1e9355b70f0adacc4f" dependencies = [ "bytemuck", - "safe_arch 1.0.0", + "safe_arch 1.2.0", ] [[package]] @@ -7469,7 +7491,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -7480,7 +7502,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -7703,7 +7725,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.13.1", + "bitflags 2.13.2", "block2 0.5.1", "bytemuck", "calloop", @@ -7748,9 +7770,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -7771,7 +7793,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12dafb3c1468d0a3f5440e21e51614b53d1fdc62c9f82cc861c447906d09c69a" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crypto-bigint", "flate2", "iso7816", @@ -7796,9 +7818,9 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wyz" @@ -7831,7 +7853,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix 1.1.4", + "rustix 1.1.5", "x11rb-protocol", ] @@ -7859,7 +7881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b" dependencies = [ "const-oid 0.10.2", - "der 0.8.1", + "der 0.8.2", "spki 0.8.0", "tls_codec", ] @@ -7878,15 +7900,15 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros 4.1.0", - "thiserror 2.0.19", + "thiserror 2.0.21", "time", ] [[package]] name = "xcursor" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" [[package]] name = "xkbcommon-dl" @@ -7894,7 +7916,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "dlib", "log", "once_cell", @@ -7961,43 +7983,43 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] name = "yuv" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d85a782d94ee43f078bcfd6fa82d4e6a5b2d1cfbbad168e4df5a9f7b39ef48c" +checksum = "a295923535c00a4e72a50a0a1130097f93160b39892dd4d2d006b1e2b90d6bf9" dependencies = [ "num-traits", ] [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -8011,14 +8033,14 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] @@ -8038,14 +8060,14 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -8054,9 +8076,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -8065,35 +8087,41 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.6", ] +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd-safe" -version = "7.2.4" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", diff --git a/crates/ironrdp-acceptor/CHANGELOG.md b/crates/ironrdp-acceptor/CHANGELOG.md index cd78fe14bf..96800b2807 100644 --- a/crates/ironrdp-acceptor/CHANGELOG.md +++ b/crates/ironrdp-acceptor/CHANGELOG.md @@ -6,6 +6,374 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.10.0...ironrdp-acceptor-v0.11.0)] - 2026-09-30 + +### Security + +- Validate auto-reconnect cookies ([#1509](https://github.com/Devolutions/IronRDP/issues/1509)) ([44f675e244](https://github.com/Devolutions/IronRDP/commit/44f675e244ee76b5311756668ffbbe28e98c7175)) + + ## Summary + - parse and carry `ARC_CS_PRIVATE_PACKET` data through the acceptor + - validate returning Enhanced RDP Security cookies with HMAC-MD5 before + reconnecting + - rotate reconnect randoms per connection and hourly, with runtime + cookie updates + - restrict cookie authentication to TLS/Hybrid and document the behavior + + ## Testing + - `cargo test -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server` + - `cargo clippy -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server + --all-targets -- -D warnings` + +- Send periodic Heartbeat PDUs on the message channel ([#1842](https://github.com/Devolutions/IronRDP/issues/1842)) ([8f57691a9a](https://github.com/Devolutions/IronRDP/commit/8f57691a9a6e497388dc2824bffe93eeed7bb698)) + + The server never sends Server Heartbeat PDUs (MS-RDPBCGR 2.2.16.1), even + though the client half of the feature is in place: HeartbeatPdu + encode/decode landed with #1814 and clients decode and ignore them. This + adds the send half, opt-in. + +- Add server-side UDP multitransport bootstrapping ([#1951](https://github.com/Devolutions/IronRDP/issues/1951)) ([c36883027d](https://github.com/Devolutions/IronRDP/commit/c36883027de1975d22c0dbc7943a17b3f73bc5d0)) + + ## Summary + + - Add a `MultitransportBootstrapping` state to the acceptor sequence, + entered right after licensing. + - Advertise UDP multitransport support in the GCC Server + MultiTransportChannelData block via a new `set_multitransport_offer()` + config (previously always `None`, so a server could never enable it), + gated on the client having populated its own Client + MultiTransportChannelData block (MS-RDPBCGR 2.2.1.4). + - When the client reciprocates the reliable-UDP flag, send the Initiate + Multitransport Request (MS-RDPBCGR 2.2.15.1) on the MCS message channel, + then move straight on to capability negotiation. + - `multitransport_request()` surfaces the sent request so the caller can + establish the sideband UDP transport (RDPEUDP2 + TLS + RDPEMT) in + parallel, without the acceptor waiting for the client's response first. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + 9 tests in `ironrdp-testsuite-core/tests/server/acceptor.rs` driving the + full handshake through a real MCS channel join sequence: + offered-and-reciprocated (request sent on the message channel, response + tolerated before Confirm Active), disabled by default, + client-does-not-reciprocate, a late response tolerated during + ConnectionFinalization (not just before Confirm Active), non-response + message-channel traffic (Auto-Detect Response, Heartbeat) not + misclassified as a multitransport response, a response with trailing + bytes not consumed, security cookie and request ID taken from the + injected RNG, and an offer changed after Basic Settings Exchange + (request and Soft-Sync) not affecting the negotiation already + advertised. + + ## Notes + + The acceptor does not wait for the client's Initiate Multitransport + Response before continuing: MS-RDPBCGR 3.2.5.15.1 only obliges the + client to send one when Soft-Sync is negotiated or the sideband attempt + failed, so blocking on it would stall the handshake on the plain + successful path. A response can legitimately arrive either before the + mandatory Confirm Active or after it, during ConnectionFinalization: + both `CapabilitiesWaitConfirm` and `ConnectionFinalization` recognize it + (by channel and a successful strict decode of the payload, so other + message-channel traffic like Auto-Detect Response or Heartbeat correctly + falls through instead) and drop it rather than erroring or desyncing the + finalization sequence. + + Only reliable UDP (`TRANSPORT_TYPE_UDP_FECR`) is requested; lossy UDP is + accepted in the offered flags for advertisement but never requested. + Server-side wiring (`accept_finalize_with_multitransport` and + `ironrdp-server` integration) is a follow-up. + + ## One PR is stacked on this + + #1953 (`accept_finalize_with_multitransport`, an async driver consuming + this PR's API) is stacked on this branch. Its diff is filed against + `master` and is cumulative with this one; see its own body for the + incremental compare. + +- Wire UDP multitransport into ironrdp-server ([#1954](https://github.com/Devolutions/IronRDP/issues/1954)) ([73dfa30e38](https://github.com/Devolutions/IronRDP/commit/73dfa30e3831e51cf8aa58f93e27b1b00102a7ec)) + + - Add `RdpServerBuilder::with_udp_transport(udp_bind_addr)`: opt-in, + `None` by default, no behavior change unless called. + - When set (and the security mode is `Tls` or `Hybrid`, matching the + reference client's Enhanced-Security-only gate), the acceptor offers UDP + multitransport, and `accept_finalize` uses + `accept_finalize_with_multitransport` with a callback that binds a fresh + UDP socket per connection, reuses the connection's own TLS certificate + (`TlsAcceptor::config()`) for the sideband transport, and calls + `accept_udp()`. + - Once established, the transport is used to migrate EGFX graphics + traffic off TCP: `request_reliable_udp` is called opportunistically the + first time EGFX has data to send (its dynamic channel id is only known + once the client opens it). From the request on, every server message on + that channel goes over the tunnel, starting with the batch that + triggered it: the request carries SOFT_SYNC_TCP_FLUSHED and the server + MUST keep using the named tunnel immediately after sending it + (MS-RDPEDYC 2.2.5.1, 3.3.5.3.1). DRDYNVC replies for a tunneled channel + go over the tunnel too, whichever path produced them. A new + `client_loop` select arm feeds incoming tunnel payloads into + `DrdynvcServer::process_tunnel()`; payloads that arrive before the + client's Soft-Sync Response are held and processed once it does + (3.3.5.3.2), rather than dropped. + - The UDP accept runs as an ordinary `tokio::spawn` task, so enabling + UDP adds no runtime requirement for the caller. + - A failure before EGFX has moved (bind, handshake, TLS, or the tunnel + closing) leaves the session on TCP, matching the reference client's + posture. Once EGFX is on the tunnel, the tunnel closing ends the + connection: Soft-Sync cannot move a channel back to TCP (MS-RDPEDYC + 2.2.5.1), and the tunnel lasts as long as the connection (MS-RDPEMT + 1.3.3). + - A client that answers the Initiate Multitransport Request with E_ABORT + (MS-RDPBCGR 2.2.15.2) has given up on the sideband transport, so the + pending UDP accept is stopped as soon as that response arrives, whether + during finalization or later on the message channel, instead of holding + its socket until the 15 s accept timeout. Windows clients send it about + 2.7 s after connecting. + - When the UDP bind address has an unspecified IP, each connection's + socket binds to the local address that client reached over TCP instead. + A socket bound to the unspecified address replies from whichever address + the routing table picks, and on a host with several IPv6 addresses that + is not always the one the client sent to: mstsc dropped the replies and + gave up with E_ABORT. `run()` records the address itself; embedders + driving `run_connection_with` pass it with the new + `RdpServer::set_connection_local_addr`. + - A successful Initiate Multitransport Response that arrives after + finalization now enables EGFX migration for the rest of the session, + provided Soft-Sync was negotiated. mstsc finishes its UDP bootstrap + after the TCP finalization (0.87 s later in my test), so migration was + previously decided before its response existed and the session never + left TCP even with the sideband transport up. + - The EGFX channel is found whichever way it was registered: an embedder + that takes a frame handle from its `GfxServerFactory` registers it as + `GfxDvcBridge`, which the migration lookup did not recognise, so it + never sent the Soft-Sync Request. The Soft-Sync Request, the client's + response (with the tunnels and channels it accepted) and the switch of + EGFX onto UDP are now logged at debug level. + +### Features + +- Expose client multitransport flags on AcceptorResult ([#1453](https://github.com/Devolutions/IronRDP/issues/1453)) ([f0fc215555](https://github.com/Devolutions/IronRDP/commit/f0fc215555394a89510ff85c7b8a93b20e878074)) + + ## What + + The acceptor already parses the client's GCC `MultiTransportChannelData` + block (MS-RDPBCGR §2.2.1.3.8) into `ClientGccBlocks` during + `BasicSettingsWaitInitial` and then discards it, keeping only the + early-capability flags, core desktop size, and keyboard layout. This + surfaces the client's multitransport (MS-RDPEMT) capability flags on + `AcceptorResult`. + + ## Why + + A server implementing UDP multitransport needs to know whether the + client advertised support (`SOFT_SYNC_TCP_TO_UDP`, + `TRANSPORT_TYPE_UDP_FEC{R,L}`) before deciding whether to send a Server + Initiate Multitransport Request. Today that information is parsed and + thrown away, so there's no way for a downstream server to see it. + + ## Shape + + Purely additive, mirroring the existing `keyboard_layout` ([#1397](https://github.com/Devolutions/IronRDP/issues/1397)) and + desktop-size ([#1373](https://github.com/Devolutions/IronRDP/issues/1373)) surfacing of GCC client data the acceptor already + parses: + + - new private `multitransport_flags: gcc::MultiTransportFlags` field on + `Acceptor`, captured from `gcc_blocks.multi_transport_channel`; + - new `pub multitransport_flags: gcc::MultiTransportFlags` field on + `AcceptorResult`; + - empty when the client sends no multitransport block; + - carried across a deactivation-reactivation like the sibling fields. + + No behavior change — the acceptor just stops discarding a block it + already decodes. + + `cargo clippy -p ironrdp-acceptor --all-targets` and `cargo fmt --check` + are clean. + +- [**breaking**] Clamp honored client desktop size to an operator maximum ([#1404](https://github.com/Devolutions/IronRDP/issues/1404)) ([d3747a05b2](https://github.com/Devolutions/IronRDP/commit/d3747a05b202ba2d87ac19698354ae7e487850a2)) + + Follow-up to #1373 (the resource-hardening angle you flagged in review — + thanks for the go-ahead 🙂). + + ## Problem + + `#1373` gated honor-client-desktop-size behind a bare `bool`. With it + on, the acceptor adopts the client-requested desktop size bounded only + by the protocol range `[200, 8192]`. But the desktop size is a + client-controlled `u16`, and the server still builds its + framebuffer/encoder from the negotiated size — so a client could request + e.g. `8192x8192` and drive the server's allocation off an untrusted + number (~256 MiB per frame buffer). Mild, and only on an opt-in + default-off path, but it's a resource-exhaustion vector driven purely by + a number the client picks. + + Your review comment: *"[200, 8192] is a protocol ceiling, not a resource + guard … tracked the 'clamp/range policy rather than a bare bool' idea as + a future follow-up (an operator-set max size)."* This is that PR. + + ## Change + + Replace the `bool` with `Option` carrying an **operator-set + maximum**: + + - `None` (default) — disabled; always enforce the server-provided size + (unchanged behavior). + - `Some(max)` — honor the client's request, **clamped per dimension to + `max`**. The client can ask for a smaller desktop, never a larger one. + + The acceptor clamps the requested `width`/`height` to `max` *before* the + existing `validate_desktop_size` protocol-range check, so the negotiated + size can never exceed what the operator is willing to render — set `max` + to the host display's native resolution (or whatever ceiling the server + can afford). + +- Support runtime-defined static virtual channels ([#1517](https://github.com/Devolutions/IronRDP/issues/1517)) ([8b4c483ba0](https://github.com/Devolutions/IronRDP/commit/8b4c483ba0c900a8de0b2718347754f56dd363ba)) + + ## Summary + - add keyed runtime-defined static-channel registration, lookup, and + negotiated ID attachment + - enforce the static-channel limit and reject malformed SVC fragment + sequences + - wire generic connector, acceptor, and session name-based dispatch + support + + ## Testing + - `cargo test -p ironrdp-testsuite-core --test integration_tests_core + svc::` + - `cargo clippy -p ironrdp-testsuite-core --test integration_tests_core + -- -D warnings` + + --------- + +- Negotiate monitor topology ([#1675](https://github.com/Devolutions/IronRDP/issues/1675)) ([063efcdc30](https://github.com/Devolutions/IronRDP/commit/063efcdc3088d8f44e423cc322077d40bf9aadf2)) + + Negotiate the client monitor layout from UseMultimon and expose the + confirmed remote topology through the ActiveX compatibility interface. + + Advertise Monitor Layout PDU support whenever Extended Client Data is + negotiated, and forward layouts from activation, active sessions, and + reactivation so advertised support does not terminate sessions. + + Keep fallback reporting truthful when servers do not honor the request, + while preserving single-monitor resize behavior and blocking + multi-monitor resizing. + + Do not send Client Monitor Extended Data; per-monitor DPI and + orientation remain unavailable. + +- [**breaking**] Pass frame arrival time into Sequence::step ([#1530](https://github.com/Devolutions/IronRDP/issues/1530)) ([6a499faece](https://github.com/Devolutions/IronRDP/commit/6a499faece8911e50a715a3fb08d4fd8e7d7dc87)) + + ## Summary + + - Connect-time bandwidth measurement needs to know when bytes arrived, + and nothing in the sans-I/O layer could tell it. #1465, now merged, + answers the server's Bandwidth Measure Stop with a nominal interval for + exactly that reason: the connector has no way to observe the real one. + - Introduce `MonotonicInstant`, a millisecond counter with an arbitrary + epoch, and make `Option` a required parameter of + `Sequence::step`. The I/O drivers already know when a read completed, so + `Framed` records the arrival time of each read and hands it to the state + machine. A driver with no clock passes `None`. + - With arrival times available, measure for real: a Bandwidth Measure + Start opens a window, Payload messages accumulate their byte counts, and + Stop reports the elapsed time between its own arrival and the Start's. + + #1465 has merged, so this applies directly to master and carries no + merge-order dependency. That PR was the FreeRDP unblock on its own; this + is the design change behind it, split out at @CBenoit's suggestion in + review. + + ## Why the clock lives in the driver + + Two reasons, both of which rule out having the sequence read a clock + itself. + +- [**breaking**] Expose per-connection keyboard metadata via ConnectionHandler ([#1691](https://github.com/Devolutions/IronRDP/issues/1691)) ([393869b30b](https://github.com/Devolutions/IronRDP/commit/393869b30b1078da7204c6bf20e8a5472e419070)) + + ## Summary + + AcceptorResult already carried keyboard_layout (the client's GCC Client + Core Data keyboardLayout, MS-RDPBCGR 2.2.1.3.2), but ironrdp-server's + client_accepted never read it, and the only extension point that could + plausibly expose it, ConnectionHandler::on_accept/on_disconnected, only + fires from RdpServer::run's own accept loop. An embedder with its own + accept loop calling run_connection or run_connection_with directly never + sees these hooks at all. + + Added keyboard_type and ime_file_name to Acceptor and AcceptorResult, + captured from the same Client Core Data alongside keyboard_layout. + + Added a new ConnectionInfo struct and a default-no-op + ConnectionHandler::on_connection_info(&ConnectionInfo) method, fired + from client_accepted itself, right after credential and auto-reconnect + validation succeed. This is reachable from every code path that + completes connection setup, not only run's accept loop, so it is usable + by embedders that never call run. + + Kept the hook synchronous. It only hands the embedder a small Clone-able + struct; an embedder that needs to do blocking work in response can spawn + its own task, the same way the existing on_accept/on_disconnected hooks + already work. + + Open question: AcceptorResult is a public struct without non_exhaustive, + so the two new fields are a real breaking change for any consumer + destructuring it exhaustively, same class as the keyboardType change in + #1689. AcceptorResult's attributes are unchanged here since marking it + non_exhaustive is a broader decision than this PR's two fields. + + ## Validation + + cargo xtask check fmt/lints/tests/typos/locks all pass. + + ## Review round and rebase, 2026-08-19 + + #1689 (KeyboardType) merged. This branch was still carrying a stale + pre-merge copy of that commit, so the diff was cumulative against + master. Rebased onto current master, which dropped the redundant + duplicate commit (its content was already upstream) and left this PR's + own single commit. + + Four review findings from the bot review, all addressed: + + - `on_connection_info` fired on every Deactivation-Reactivation resize, + not just the initial connection, since `accept_finalize` loops back into + `client_accepted` with `result.reactivation` set. Gated the call on + `!result.reactivation`, matching the existing gate on the static-channel + start block just below it. + - `get_result()` took `ime_file_name` via `mem::take`, emptying it out + of the acceptor before `new_deactivation_reactivation` copied the same + acceptor's field into the next result, so every reactivation after the + first reported an empty IME name. Changed to a clone, matching how the + Copy-type sibling fields on the same lines already survive. + - No regression test covered the permissive zero/unrecognized-value + keyboardType decode in the Input capability set. Added + `keyboard_type_zero_decodes_to_none` and + `keyboard_type_unrecognized_value_round_trips` (0x51). + - A fourth finding asked for the same coverage on Client Core Data's own + keyboardType field; that test already exists on master, added to #1689 + in response to its own review. The rebase above inherits it directly, so + no new code was needed there. + +- Add accept_finalize_with_multitransport driver ([#1953](https://github.com/Devolutions/IronRDP/issues/1953)) ([22006ce1f9](https://github.com/Devolutions/IronRDP/commit/22006ce1f9dae8a052ae8b6131fb5f7f2fce663e)) + + - Add `accept_finalize_with_multitransport`, an async driver mirroring + the client side's `connect_finalize_with_multitransport`. + - It drives the acceptor sequence to completion exactly like + `accept_finalize` already does, and awaits an app-supplied handler once, + synchronously, the moment the acceptor sends an Initiate Multitransport + Request, so the caller can establish the sideband UDP transport + (RDPEUDP2 + TLS + RDPEMT). + - Unlike the client-side callback, the handler reports nothing back into + the sequence: the acceptor has already sent the request and moved on by + the time it runs. + - `accept_finalize` becomes a thin wrapper around this with a no-op + handler, matching the client side's + `connect_finalize`/`connect_finalize_with_multitransport` relationship. + + + ## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-acceptor-v0.9.0...ironrdp-acceptor-v0.10.0)] - 2026-07-10 ### Features diff --git a/crates/ironrdp-acceptor/Cargo.toml b/crates/ironrdp-acceptor/Cargo.toml index 6f08e9661e..31e1e36166 100644 --- a/crates/ironrdp-acceptor/Cargo.toml +++ b/crates/ironrdp-acceptor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-acceptor" -version = "0.10.0" +version = "0.11.0" readme = "README.md" description = "State machines to drive an RDP connection acceptance sequence" edition.workspace = true @@ -17,11 +17,11 @@ doctest = false test = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public -ironrdp-async = { path = "../ironrdp-async", version = "0.10" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public +ironrdp-async = { path = "../ironrdp-async", version = "0.11" } # public rand = "0.9" tracing = { version = "0.1", features = ["log"] } diff --git a/crates/ironrdp-activex/Cargo.toml b/crates/ironrdp-activex/Cargo.toml index 9e4a49cb0e..02e7a5169e 100644 --- a/crates/ironrdp-activex/Cargo.toml +++ b/crates/ironrdp-activex/Cargo.toml @@ -21,24 +21,24 @@ doctest = false [target.'cfg(windows)'.dependencies] anyhow = "1" base64 = "0.22" -ironrdp-client = { path = "../ironrdp-client", version = "0.1", features = ["clipboard", "dvc-com-plugin", "gateway", "location", "rdpdr", "rustls", "smartcard", "sound", "webauthn"] } -ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.7" } -ironrdp-cliprdr-format = { path = "../ironrdp-cliprdr-format", version = "0.2.0" } +ironrdp-client = { path = "../ironrdp-client", version = "0.2", features = ["clipboard", "dvc-com-plugin", "gateway", "location", "rdpdr", "rustls", "smartcard", "sound", "webauthn"] } +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8" } +ironrdp-cliprdr-format = { path = "../ironrdp-cliprdr-format", version = "0.3.0" } ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.7" } -ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.1" } -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } +ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" } +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } ironrdp-daemon = { path = "../ironrdp-daemon", version = "0.1" } ironrdp-input = { path = "../ironrdp-input", version = "0.7" } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1" } ironrdp-rdpfile = { path = "../ironrdp-rdpfile", version = "0.1.0" } ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } ironrdp-rail = { path = "../ironrdp-rail", version = "0.1" } ironrdp-rdpdr-native = { path = "../ironrdp-rdpdr-native", version = "0.7" } ironrdp-rpc = { path = "../ironrdp-rpc", version = "0.1" } -ironrdp-session = { path = "../ironrdp-session", version = "0.11" } -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } +ironrdp-session = { path = "../ironrdp-session", version = "0.12" } +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" } png = "0.18" sha2 = "0.10" diff --git a/crates/ironrdp-agent/CHANGELOG.md b/crates/ironrdp-agent/CHANGELOG.md index 46674e619c..09c045611e 100644 --- a/crates/ironrdp-agent/CHANGELOG.md +++ b/crates/ironrdp-agent/CHANGELOG.md @@ -6,6 +6,485 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-agent-v0.1.0...ironrdp-agent-v0.2.0)] - 2026-09-30 + +### Security + +- Restrict Windows named pipe to the current user ([#1482](https://github.com/Devolutions/IronRDP/issues/1482)) ([26821836b7](https://github.com/Devolutions/IronRDP/commit/26821836b78e0670f4a0a41eb7021f6e1c256be4)) + + ## Summary + + The `ironrdp-agent` Windows named-pipe listener inherited the pipe + namespace's default ACL, so **any local user** could connect to a + running daemon and drive the session — inject input, capture + screenshots, read logs, and trigger NOW remote execution. The Unix + listener already locks its socket to `0o600` (owner-only); this mirrors + that stance on Windows. + + ## The fix + + Build a **protected DACL** (`D:P(A;;GA;;;{user-sid})`) granting + `GENERIC_ALL` only to the current user's SID, and apply it to **every** + `CreateNamedPipeW` instance (both the first in `bind` and the + replacement minted in `accept`) via tokio's + `create_with_security_attributes_raw`. The descriptor is cached on the + `Listener` and reused across instances (the kernel copies it on each + `CreateNamedPipeW`, so it only needs to outlive each synchronous call). + + Implementation follows the established windows-FFI style used by sibling + crates (`ironrdp-cliprdr-native`): the `windows` crate (0.62, matching + siblings), RAII guards (`OwnedTokenHandle`, `OwnedSecurityDescriptor`) + for `CloseHandle`/`LocalFree` cleanup, one unsafe op per block with `// + SAFETY:` comments, `.cast::<>()` + `try_from` instead of `as` casts, and + `tracing::warn!` on cleanup failure. + + ## Why this matters + + On shared Windows hosts the `\\.\pipe\` namespace is reachable by every + local user by default. This is the last line of defense against a + different local user taking over a running agent session. It closes the + asymmetry with the Unix path, which already documents the `/tmp` + fallback as world-writable and explicitly sets `0o600`. + + ## Test coverage + + `transport.rs` previously had **zero tests**. Added a Windows-gated + `#[cfg(test)]` smoke test + (`security_descriptor_for_current_user_is_non_null`) that exercises the + full token → SID → SDDL → security-descriptor pipeline for the current + process and asserts a non-null descriptor is produced — the happy path + `Listener::bind` depends on. (Asserting the ACL denies a *different* + user requires a second token context and is out of scope.) + + ## Verification + + All `cargo xtask` CI-equivalent checks pass on a clean tree: + + - `cargo xtask check fmt -v` → All good + - `cargo xtask check lints -v` (workspace clippy `--all-targets -D + warnings`) → All good (ironrdp-agent produces zero warnings) + - `cargo test -p ironrdp-agent` → 12 passed; 0 failed (incl. the new + test) + - `cargo xtask check locks -v` → All good + + ## Files + + - `crates/ironrdp-agent/Cargo.toml` — add `windows = "0.62"` + (cfg(windows), + Win32_Foundation/Security/Security_Authorization/System_Threading) + - `crates/ironrdp-agent/src/transport.rs` — + `OwnedTokenHandle`/`OwnedSecurityDescriptor` RAII guards, + `create_server_instance` helper, `Listener` caches + reuses the SD; new + smoke test + - `Cargo.lock` — +1 line registering `windows` as an ironrdp-agent dep + (no new versions; crates already present via siblings) + + ## Release + + This is a `fix(agent):` conventional commit, picked up by release-plz + into the next release cycle (alongside the existing `feat(agent)` + NOW-integration commit) to propose the `0.2.0` bump once merged to + `master`. CHANGELOG is auto-generated — no manual edit needed. + +- Connect to Windows Sandbox named pipes ([#1580](https://github.com/Devolutions/IronRDP/issues/1580)) ([39b020343d](https://github.com/Devolutions/IronRDP/commit/39b020343d962962bfbefc89939be64d5c716196)) + + Windows Sandbox's default attach path is a local named pipe carrying + plain TPKT/X.224 with PROTOCOL_RDP and ENCRYPTION_LEVEL_NONE, not + TCP:3389 or VMConnect. Allow the connector and client to complete that + sequence only via an explicit opt-in (`enable_standard_rdp_security`; + NamedPipe enables it), and teach ironrdp-agent to resolve pipe path and + guest credentials from WindowsSandboxServer after `wsb start`. + + Adds Transport::NamedPipe, ironrdp_named_pipe/ironrdp_sandbox_id + properties, sandbox list/config/stop CLI helpers via an in-process + h2/gRPC client on the per-user `\\.\pipe\wsandbox\{guid}` pipe (no .NET + helper), and connect --sandbox-id / --sandbox-pipe. Sandbox-derived + properties are the merge base; explicit .rdp/--prop/flags override them + while NamedPipe TLS/CredSSP stay forced off. Local :2179+PCB remains + unsupported. + +### Features + +- Integrate NOW client ([#1451](https://github.com/Devolutions/IronRDP/issues/1451)) ([405770e2a6](https://github.com/Devolutions/IronRDP/commit/405770e2a6232f506e10f88ab6cb413d6bec7c5b)) + + ## Summary + - Integrate the released registry `now-client = "0.1.0"` into + `ironrdp-agent`; no Git, path, patch, or vendored dependency is used. + - Add a per-session `Devolutions::Now::Agent` DVC endpoint with + 30-second initial readiness and 10-second reconnect deadlines. + - Add durable NOW operations, IPC streaming/retention, supported CLI + forms (`run`, `powershell`, `pwsh`, `exec process`, `exec batch`), safe + PowerShell defaults, raw output forwarding, and remote exit propagation. + - Add IPC/retention coverage and an adapter regression proving immediate + Run frames are quarantined before a following Process execution. + - Do not modify Gateway or `ironrdp-dvc-pipe-proxy`. + + ## Validation + - `cargo test -p ironrdp-agent` + - `cargo test -p ironrdp-testsuite-extra --test integration_tests_extra + agent` + - `cargo clippy -p ironrdp-agent --all-targets -- -D warnings` + - `cargo xtask check fmt -v` + - `cargo xtask check tests -v` + - `cargo xtask check locks -v` + + `cargo xtask check lints -v` is blocked by a pre-existing `ironrdp-str` + `manual_is_multiple_of` lint under the available Rust 1.90 toolchain. + `cargo xtask check typos -v` cannot run because `typos-cli` is not + installed. Authorized-VM end-to-end validation remains pending access to + the designated RDP/NOW endpoint. + + --------- + +- Expose generic session configuration and lifecycle APIs ([#1522](https://github.com/Devolutions/IronRDP/issues/1522)) ([57b1366650](https://github.com/Devolutions/IronRDP/commit/57b13666506dc40c15b4c4702d35150beee99133)) + + ## Summary + - expose generic client configuration for connection metadata, + compression, shell/work directory, audio, and runtime static-channel + factories + - add bounded input delivery with independent close cancellation, host + clipboard plumbing, lifecycle events, and Display Control resize + readiness/fallback handling + - update agent, viewer, web, FFI, examples, and tests for the generic + APIs + + ## Stack dependencies + This PR is stacked on `copilot/tls-validation-policy` (`b2bbcece`), + which already includes the merged runtime static-channel support from + `master`. It intentionally contains no TLS implementation/policy, + ActiveX/COM, SVC implementation, decompression, or bitmap-recovery + changes. + + ## Validation + - `cargo fmt --check --all` + - `cargo xtask check tests --no-run -v` + - `cargo xtask check lints -v` + - `cargo test -p ironrdp-client --lib --features rustls` + - `cargo check -p ironrdp-agent -p ironrdp-viewer -p ironrdp-web -p ffi` + + --------- + +- Configure static RDPDR drives ([#1617](https://github.com/Devolutions/IronRDP/issues/1617)) ([b06cd71e0f](https://github.com/Devolutions/IronRDP/commit/b06cd71e0f2845893db2123c8280f4a80e1b5a36)) + + Allow an agent daemon to opt in to fixed Windows filesystem drives. + + Validate names, roots, and duplicate definitions before listening, then + attach the native RDPDR backend factory to each client. + +- Add authorized RDPDR harness ([#1620](https://github.com/Devolutions/IronRDP/issues/1620)) ([24eb1f62f9](https://github.com/Devolutions/IronRDP/commit/24eb1f62f9aadf36fb6b3b588ecda390fcad2b38)) + + Add an opt-in Windows harness that verifies \\tsclient direct PowerShell + and Explorer copy paths for an explicitly authorized endpoint. + + Keep TLS certificate and hostname validation strict by default, but + permit a daemon-start-only bypass for the authorized test endpoint. Add + bounded all-or-nothing Unicode text input for remote commands without + expanding ActiveX. + +- Add RAIL audit commands ([#1646](https://github.com/Devolutions/IronRDP/issues/1646)) ([77759dca03](https://github.com/Devolutions/IronRDP/commit/77759dca032eb829b5be54a3c44d9be92252cf41)) + + Expose bounded client-validated RAIL events and RemoteApp launch + requests through the daemon IPC so headless agents can verify sessions. + + Preserve cursors across resize reconnects, wake waiting readers for + locally queued launches, and redact launch data in logs. + + Report terminal local Execute failures without disrupting an otherwise + valid RDP session. + +- Send MS-RDPEI touch over RPC ([#1651](https://github.com/Devolutions/IronRDP/issues/1651)) ([254fa5565d](https://github.com/Devolutions/IronRDP/commit/254fa5565dc82b9f9deadf037ecd044cdea537e3)) + + Expose Request::Touch so ironrdp-agent can inject RDPEI contact frames + through the daemon and ActiveX backends for live testing. + + CLI touch/touch-tap map to legal contact flag sets, and ActiveX rejects + illegal combinations before queueing. + +- Expose MS-RDPEI pen and multi-touch ([#1652](https://github.com/Devolutions/IronRDP/issues/1652)) ([51f8738ea1](https://github.com/Devolutions/IronRDP/commit/51f8738ea156daeea292c63bba09d9137791af57)) + + Extend the agent RPC path beyond single-contact touch so multi-contact + frames, pen frames, and dismiss-hovering can be driven end-to-end + against a real host. + + Wire Pen/Dismiss through rpc, daemon, CLI, ActiveX control RPC, client + input loop, and session encode helpers. Add pen contact flag legality + checks and round-trip coverage. + +- Enable Windows smartcard in product surfaces ([#1672](https://github.com/Devolutions/IronRDP/issues/1672)) ([1561a4c327](https://github.com/Devolutions/IronRDP/commit/1561a4c327512a1a3a6c618d4b06dc388fab132f)) + + Wire daemon, agent, viewer, and ActiveX to the WinSCard RDPDR backend so + products can announce a smartcard device only with a matching factory. + + Expose `WindowsRdpdrBackendFactory::with_smartcard`, daemon/agent + `--smartcard` and `ironrdp_smartcard` connect overrides, viewer + `--smartcard`, and ActiveX `RedirectSmartCards`. Smartcard-only RDPDR is + valid on Windows; non-Windows paths refuse or hard-disable enablement. + +- Start Sandbox VMs over gRPC ([#1671](https://github.com/Devolutions/IronRDP/issues/1671)) ([f3fdb5d565](https://github.com/Devolutions/IronRDP/commit/f3fdb5d5653166bbdd4d3f8aaec26846e2d2d2ec)) + + Create Windows Sandbox instances through the SandboxCore named pipe. + Accept an optional GUID and configuration file. + Wait up to two minutes for provisioning before reporting failure. + Print the created ID and report single-instance policy failures. + +- Forward TCP through an RD Gateway tunnel ([#1714](https://github.com/Devolutions/IronRDP/issues/1714)) ([ac7faecb33](https://github.com/Devolutions/IronRDP/commit/ac7faecb338bb9a7f2f4a097ee8a2d85f944ccfb)) + + Forward TCP through an RD Gateway without an RDP session. + + MS-TSGU is not RDP-specific. ironrdp-agent gw-forward exposes an SSH + -L-style local port forward or a SOCKS5 CONNECT proxy, opening one + WebSocket gateway tunnel per inbound connection with + GwClient::connect_with_port. + + Master GwClient is WebSocket and HTTP Basic only, so this slice does not + add an RPC transport. + + Local-to-gateway copies are capped at 8182 bytes so each write fits in + the 8192-byte MS-TSGU data packet. SOCKS5 carries explicit RFC 1928 + reply codes, rejects a non-zero reserved byte, and does not send a + CONNECT reply after a method-negotiation failure. --socks5 and --target + are exclusive, and --target is parsed with TargetAddr so IPv6 requires + an explicit port and is passed to the gateway unbracketed. + GwClient::poll_shutdown is a no-op on master, so a local half-close is + not forwarded to the target. + +- Add smart-card gateway authentication ([#1741](https://github.com/Devolutions/IronRDP/issues/1741)) ([761dae12b0](https://github.com/Devolutions/IronRDP/commit/761dae12b0128be0f9bae52ca59eae8a0ba0b02f)) + + Add an opt-in Kerberos PKINIT path for HTTP Negotiate gateway + authentication while retaining the password Negotiate, NTLM, and Basic + flows. + + The public credentials type accepts an application-supplied UPN, redacts + credentials, and rejects unsupported smart-card feature or challenge + combinations without exposing them. + +- Expose Hyper-V VMConnect ([#1770](https://github.com/Devolutions/IronRDP/issues/1770)) ([e1e2cafea7](https://github.com/Devolutions/IronRDP/commit/e1e2cafea73290e116b348dab3a3864cfe597657)) + + Expose Hyper-V VMConnect through the agent CLI and daemon build. Add + VMConnect mode and current-user options, default an omitted host to + localhost, and enable the client VMConnect feature in the daemon. + +- Add clipboard-get and clipboard-set operations ([#1863](https://github.com/Devolutions/IronRDP/issues/1863)) ([3955003dad](https://github.com/Devolutions/IronRDP/commit/3955003dad36f02e6632f88be9d5cd488da9c4c1)) + + Resolves the TODO in ironrdp-rpc's Request enum for clipboard support: + read the remote clipboard text and set it, so an LLM (or any daemon + caller) can copy and paste to and from the session. + + The backend is deliberately text only. A headless daemon has no host + clipboard, so it holds the last text pushed by clipboard-set as its + local content and the last text received from the remote as its remote + content, both behind a lock shared with the daemon's IPC handlers. + client_capabilities() advertises no file transfer flags, matching that + scope. + + The clipboard Cargo feature on ironrdp-daemon's ironrdp-client + dependency was previously off, so the CLIPRDR channel fell back to + StubClipboard on non-Windows and did nothing with remote clipboard + content. This enables it and supplies a real backend factory. + + Verified live against a running RDP server: clipboard-set text landed in + the server's own OS clipboard, and a clipboard change made directly on + the server side was correctly fetched by clipboard-get. + +- Add clipboard image support ([#1877](https://github.com/Devolutions/IronRDP/issues/1877)) ([a59b3b687d](https://github.com/Devolutions/IronRDP/commit/a59b3b687d8ec81cfb79bd6e91e7d3341c866b6b)) + + Adds PNG clipboard image support to the agent daemon and CLI using CLIPRDR DIB conversions. + +- Add clipboard HTML support ([#1878](https://github.com/Devolutions/IronRDP/issues/1878)) ([7442a63af0](https://github.com/Devolutions/IronRDP/commit/7442a63af048e5cebdf13685166b306964c56436)) + + #1877 (clipboard image support) merged: This PR now rebases cleanly onto + master and its own diff is HTML-only. It still depends on the + ClipboardContent enum #1877 introduced. + + Extends the daemon clipboard-get/clipboard-set operations to HTML + fragments: + clipboard-get-html prints the last HTML fragment received from the + remote + clipboard, clipboard-set-html sets one and advertises it to the remote + as + the registered HTML Format. + + Stores the fragment as plain text (not yet CF_HTML-wrapped) and converts + at + the point of use via ironrdp-cliprdr-format's existing + plain_html_to_cf_html/cf_html_to_plain_html, so no new markup handling + is + needed here, mirroring the image PR's use of the crate's existing bitmap + conversions. + + HTML Format is a registered, not fixed-ID, clipboard format: The + offering + side picks a private-range ID (0xC000 here) and pairs it with the name; + the + receiver learns the ID-to-name mapping from the FormatList. When + receiving, + matches the remote's offer by name and uses whatever ID it assigned, not + the + locally-chosen one, since those only apply to formats this backend + itself + offers. + + Extends the remote-copy priority order to image, then HTML, then text + (richest representation first), and the PendingPaste tracking added in + #1877 with an Html variant so a returned FormatDataResponse is decoded + correctly regardless of which of the three was requested. A remote HTML + fragment over the 256 KiB bound is dropped and logged rather than + stored, + mirroring the oversized-image drop #1877's review round added, for the + same + reason: An unbounded stored value would otherwise fail encoding once + handed + back over the RPC transport. + + New Request::ClipboardGetHtml/ClipboardSetHtml and + Payload::ClipboardHtml + wire variants, bounded at 256 KiB in the CLI parser and on wire decode. + Extends ironrdp-activex's exhaustive match on Request with the same + treatment as the other clipboard operations. Extends the wire round-trip + and Debug-redaction test coverage added in #1877 to the two new + variants. + + cargo xtask check fmt/lints/tests/typos/locks all pass. Not yet verified + against a live remote client, same as #1877. + +- Add clipboard file transfer ([#1899](https://github.com/Devolutions/IronRDP/issues/1899)) ([ed0e12b559](https://github.com/Devolutions/IronRDP/commit/ed0e12b559e155e3f3dfd35b5171c26c3ae2f22f)) + + Extends the daemon clipboard-get/clipboard-set operations added in #1877 + to + files: clipboard-set-files offers one or more local files to the remote + via + the CLIPRDR file-list mechanism (FileGroupDescriptorW), + clipboard-list-files + lists what the remote currently offers (metadata only, matching the + delayed-render model MS-RDPECLIP itself uses), and clipboard-get-file + fetches one file's full contents by its position in that list. + + Rebased onto master after #1878 (HTML support) merged. The two were + built in + parallel and both extend the ClipboardContent enum from #1877, so this + PR now + carries both: A remote copy is requested files over image over HTML over + text, + and the file-transfer IPC tags sit after HTML's (Payload 16 and 17, + Request 35 + to 37) rather than reusing the numbers HTML took. + + Extends ClipboardContent with a Files variant (wire-shaped metadata, + symmetric for what is offered locally and what the remote last + advertised); + a parallel local_file_paths list on ClipboardState carries the on-disk + paths + a local offer maps to, since FileDescriptor itself carries no filesystem + path and file transfer is a structurally different CLIPRDR mechanism + from + the FormatDataRequest path text/image/HTML share. + + Advertises STREAM_FILECLIP_ENABLED and CAN_LOCK_CLIPDATA in + client_capabilities. Cliprdr::initiate_file_copy and + request_file_contents + both hard-error when file transfer was not negotiated, and that error is + session-fatal by the time it reaches ironrdp-client's dispatcher, so the + daemon tracks the negotiated capabilities from + on_process_negotiated_capabilities and checks them before sending either + message, failing cleanly at the IPC layer instead. + + Cliprdr already owns the whole clipboard-lock lifecycle for file + transfer + (snapshotting the local file list on an incoming LockData, auto-locking + when + a remote file list arrives, timeout-driven expiry); this backend's + on_lock/on_unlock stay informational. on_outgoing_locks_expired is not: + It + aborts an in-progress clipboard-get-file fetch bound to the expiring + lock + rather than let it continue issuing requests against a remote clipboard + that + has since changed underneath it. on_remote_copy clears an in-progress + fetch + the same way when the remote clipboard changes again mid-fetch, so a + clipboard-get-file caller left waiting is told the fetch was interrupted + instead of silently blocked until its own timeout. + + clipboard-get-file drives ironrdp_cliprdr::chunked_fetch::ChunkedFetch + to + completion inside the daemon, bounded at MAX_CLIPBOARD_FILE_BYTES + (derived + from the RPC transport's own frame limit, the same reasoning + MAX_CLIPBOARD_IMAGE_BYTES already uses) before issuing any wire request, + not + just before framing the IPC response. The IPC handler and the CLIPRDR + backend's on_file_contents_response coordinate through a per-session + Notify, the same wait-and-recheck shape rail_wait already uses for RAIL + evidence, including re-checking state once more after a timeout races + against a same-instant completion rather than assuming a timeout means + nothing arrived. + + Serving a remote's request for a file we offered + (on_file_contents_request) + reads from the local path clipboard-set-files recorded, seeking to the + requested range and capping a single RANGE response at 4 MiB regardless + of + what the peer's own cbRequested asks for: MS-RDPECLIP defines + cbRequested as + an upper bound on what a responder may return, not a guarantee, and + ChunkedFetch on the requesting side already handles a shorter-than-asked + response by issuing another RANGE request for the remainder. This read + happens synchronously on the session's own current-thread runtime + (Cliprdr + callbacks are not async), bounded per response by the same 4 MiB cap: A + slow + local read stalls that session briefly rather than the whole daemon, but + it + is a real tradeoff against a fully async read path, disclosed rather + than + silently accepted. + + Directory entries are rejected, not silently mishandled: + clipboard-set-files + refuses a directory path outright rather than support recursive folder + copy + (out of scope here), and a remote directory entry (real Windows Explorer + folder copies advertise one per subfolder, mixed in with the files) is + shown + as such in clipboard-list-files and refused by clipboard-get-file rather + than attempt a byte fetch against it. + + New Request::ClipboardSetFiles/ClipboardListFiles/ClipboardGetFile and + Payload::ClipboardFileList/ClipboardFile wire variants, plus the shared + ClipboardFileEntry metadata type, bounded at MAX_CLIPBOARD_FILE_BYTES + and + MAX_CLIPBOARD_FILE_LIST_ENTRIES on wire decode. Extends + ironrdp-activex's + exhaustive match on Request with the same treatment as the other + clipboard + operations. Extends the wire round-trip and Debug-redaction test + coverage + added in #1877 to the three new variants. + + cargo xtask check fmt/lints/tests/typos/locks all pass. Not yet verified + against a live remote client, same as #1877/#1878. + +### Bug Fixes + +- Preserve RAIL audit correlation ([#1655](https://github.com/Devolutions/IronRDP/issues/1655)) ([fe0e86d814](https://github.com/Devolutions/IronRDP/commit/fe0e86d8144364c9d8bcd0597be2372d5bbdf9cf)) + + Retain RAIL audit history after a session terminates while clearing + Execute requests that can no longer receive a result, including + connection failures. + Render daemon RAIL errors in the selected machine format and exit + nonzero. + Keep bounded RAIL event encoding symmetric with decoding. + +### Please Sort + +- Add agentic RDP CLI and localhost CI workflow ([#1289](https://github.com/Devolutions/IronRDP/issues/1289)) ([ffedb69ca8](https://github.com/Devolutions/IronRDP/commit/ffedb69ca8d4f2dec5a0649fa2cc4758ee74d13f)) + +- Extract reusable RDP daemon support ([#1543](https://github.com/Devolutions/IronRDP/issues/1543)) ([dc4692538e](https://github.com/Devolutions/IronRDP/commit/dc4692538e67ca089969879b7c62b69558e128e6)) + +- Add ActiveX RPC backend for ironrdp-agent ([#1544](https://github.com/Devolutions/IronRDP/issues/1544)) ([c31e43f755](https://github.com/Devolutions/IronRDP/commit/c31e43f755dbf24a302c86ca1ef8ba186d51216e)) + + + ## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-agent-v0.1.0)] - 2026-07-10 Initial release. diff --git a/crates/ironrdp-agent/Cargo.toml b/crates/ironrdp-agent/Cargo.toml index 2df2d2df40..7921b647c0 100644 --- a/crates/ironrdp-agent/Cargo.toml +++ b/crates/ironrdp-agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-agent" -version = "0.1.0" +version = "0.2.0" readme = "README.md" description = "CLI-driven, daemon-backed agentic RDP client suitable for LLM consumption" edition.workspace = true @@ -22,7 +22,7 @@ test = false [dependencies] # Configuration model and codecs ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } -ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.1" } +ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" } ironrdp-rdpfile = { path = "../ironrdp-rdpfile", version = "0.1" } ironrdp-input = { path = "../ironrdp-input", version = "0.7" } ironrdp-daemon = { path = "../ironrdp-daemon", version = "0.1" } @@ -38,7 +38,7 @@ clap = { version = "4.6", features = ["derive", "cargo", "env"] } anyhow = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" -ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.1", features = ["rustls"] } +ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.2", features = ["rustls"] } [target.'cfg(windows)'.dependencies] # Windows Sandbox control plane: gRPC/HTTP2 over a per-user named pipe. diff --git a/crates/ironrdp-ainput/CHANGELOG.md b/crates/ironrdp-ainput/CHANGELOG.md index dd5cc20410..282bd744ae 100644 --- a/crates/ironrdp-ainput/CHANGELOG.md +++ b/crates/ironrdp-ainput/CHANGELOG.md @@ -6,6 +6,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.8.0...ironrdp-ainput-v0.9.0)] - 2026-09-30 + +### Features + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + + + ## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-ainput-v0.7.0...ironrdp-ainput-v0.8.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-ainput/Cargo.toml b/crates/ironrdp-ainput/Cargo.toml index cc72eb6239..8526c417c0 100644 --- a/crates/ironrdp-ainput/Cargo.toml +++ b/crates/ironrdp-ainput/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-ainput" -version = "0.8.0" +version = "0.9.0" readme = "README.md" description = "AInput dynamic channel implementation" edition.workspace = true @@ -17,8 +17,8 @@ doctest = false test = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public bitflags = "2.11" num-derive.workspace = true # TODO: remove num-traits.workspace = true # TODO: remove diff --git a/crates/ironrdp-async/CHANGELOG.md b/crates/ironrdp-async/CHANGELOG.md index 34a854a384..b4045cd4be 100644 --- a/crates/ironrdp-async/CHANGELOG.md +++ b/crates/ironrdp-async/CHANGELOG.md @@ -6,6 +6,222 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.10.0...ironrdp-async-v0.11.0)] - 2026-09-30 + +### Security + +- [**breaking**] Implement multitransport bootstrapping handshake ([#1098](https://github.com/Devolutions/IronRDP/issues/1098)) ([e45fbfe0f5](https://github.com/Devolutions/IronRDP/commit/e45fbfe0f597011706e77fc174ca14e5e9d435b9)) + + ## Summary + + Makes the `MultitransportBootstrapping` state functional instead of a + no-op + pass-through. After licensing the server may send 0, 1, or 2 Initiate + Multitransport Request PDUs before capabilities exchange. Each one is + surfaced + to the application, which establishes UDP transport (RDPEUDP2 + TLS + + RDPEMT) + or declines, and the connector reports the outcome back to the server. + + ## API + + Mirrors the existing `should_perform_X()` pause-point pattern used by + TLS + upgrade and CredSSP, but uses `complete_X()` / `skip_X()` rather than + `mark_X_as_done()` because completion carries result data: + + - `should_perform_multitransport()`: true while a request awaits an + outcome + - `multitransport_request()`: the request awaiting an outcome, or `None` + - `complete_multitransport(result, output)`: report the outcome, resume + - `skip_multitransport(output)`: decline, resume + + `complete_multitransport` accepts a `MultitransportResult` (a `Success` + / + `Failure(hresult)` enum) rather than a caller-built response PDU. The + connector + builds the response internally from the stored request ID. + + Requests are surfaced one at a time rather than as a batch. There is no + end + marker for the set, and MS-RDPBCGR 3.2.5.15.1 requires the client to act + on a + request as soon as it decodes one, so waiting to learn how many are + coming is + not an option the protocol offers. `should_perform_multitransport()` can + therefore come round twice; the caller answers reliable and lossy + separately. + + ## Approach + + **Routing.** Requests arrive on the negotiated MCS message channel + (2.2.15.1) and the Demand Active on the I/O channel, so the channel + decides + which is which. The message channel also carries NetworkAutoDetect since + #1348, + so a decode still confirms what arrived there, but the I/O channel is + never + speculatively decoded as multitransport. A PDU on neither channel is an + error. + + For the decode to be a sound confirmation the request decoder must + reject a + Demand Active, so this PR also tightens `MultitransportRequestPdu` to + require + the exact `SEC_TRANSPORT_REQ` security-header flag. + + **Yielding.** Each request is surfaced the moment it decodes. Responding + returns the connector to `MultitransportBootstrapping` to read whatever + comes + next, which may be a second request or the Demand Active. Nothing is + buffered + and nothing is replayed: when the request is surfaced the Demand Active + has not + arrived yet. + + **Soft-Sync.** The Initiate Multitransport Response is the Soft-Sync + signalling path (2.2.15.2), permitted only when both peers advertised + `SOFTSYNC_TCP_TO_UDP` in their GCC `MultiTransportChannelData`. The + server's + block is retained from the GCC exchange and checked against the client's + configured flags. One rule covers both paths: + + - Soft-Sync negotiated: always respond, `S_OK` or `E_ABORT`, including + on + `skip_multitransport()`, which 3.2.5.15.1 requires. Both the async and + blocking drivers skip automatically, so without this every default + client + leaves a compliant server waiting. + - Not negotiated: never respond. The outcome is reported in band on the + new + transport, and putting anything on the main channel would be the + violation. + + The response goes on the message channel per 2.2.15.2 and 3.2.5.15.2. If + Soft-Sync was negotiated but no message channel exists the connector + errors + rather than falling back to the I/O channel, and that check runs before + the + pending state is taken, so the caller is left with a connector it can + still + inspect or decline from. + + ## Wire behaviour + + On the wire TCP and UDP negotiation happen in parallel: the UDP + transport is + established alongside the ongoing TCP handshake, and its completion + signals the + dynamic-channel layer that subsequent channels may migrate to UDP. The + connector's API yield point here is a Rust affordance, not a + spec-mandated TCP + pause. Thanks to @hardening for the correction. + + ## Tests + + Connector state-machine tests in `ironrdp-testsuite-core` drive the + public API + with the shared `SERVER_DEMAND_ACTIVE` fixture: + + - a request is surfaced on arrival, without waiting for a following PDU + (regression test for the stall); + - responding returns to bootstrapping so a second request is read + normally; + - a third request is rejected per the 2.2.15.1 cap; + - a Demand Active on the I/O channel ends bootstrapping; + - the response targets the message channel, decoded back off the wire; + - a `Failure` result is carried through; + - `skip` sends `E_ABORT` under Soft-Sync, and nothing without it; + - `complete` emits nothing without Soft-Sync but still resumes; + - a failed response leaves the connector in `MultitransportPending`, + still able + to report or decline, rather than `Consumed`; + - `complete` / `skip` outside `MultitransportPending` error; + - a Demand Active's user data does not decode as a + `MultitransportRequestPdu` + (regression test for the decoder tightening above). + +### Features + +- Hyper-V vmconnect support ([#1503](https://github.com/Devolutions/IronRDP/issues/1503)) ([a7cc067d50](https://github.com/Devolutions/IronRDP/commit/a7cc067d5069cbbcb13bae3e0561c0611da3bcf6)) + + Adds Hyper-V VMConnect's direct ordering: PCB → TLS → CredSSP → X.224. + + Enhanced Session is the default (`GUID;EnhancedMode=1`), with + `--vmconnect-basic` for the synthetic console. Kept this separate in + `ironrdp-vmconnect`; no SPN changes. + + Tested against the nested Hyper-V lab: + - Enhanced: `HYBRID_EX`, rendered 1280×720 + - Basic: `HYBRID`, rendered 1280×720 + - `cargo xtask check fmt/lints/tests -v` + + --------- + +- Support Hyper-V connection ordering ([#1505](https://github.com/Devolutions/IronRDP/issues/1505)) ([5c1816244e](https://github.com/Devolutions/IronRDP/commit/5c1816244e83187a04249e9d9c240d096cb78f55)) + + Hyper-V over RDCleanPath needs PCB → TLS on the proxy, then CredSSP → + X.224 on the client. Ordinary RDCleanPath stays X.224-first. + + Still VERSION_1 with the same DER fields. An explicit VMConnect request + carries a Unicode PCB payload in `preconnection_blob` with no X.224; the + proxy encodes the binary PCB. Generic PCB requests keep their existing + X.224-first behavior. + + Gateway reference implementation: + [Devolutions/devolutions-gateway#1372](https://github.com/Devolutions/devolutions-gateway/pull/1372) + + Checked locally: Rust builds, formatting, Svelte typecheck, and .NET + build. Real nested Hyper-V E2E through Gateway: Native rendered 18 + frames, Avalonia connected and rendered its first frame, and Web + rendered a non-empty 1280×720 canvas. + + --------- + +- [**breaking**] Pass frame arrival time into Sequence::step ([#1530](https://github.com/Devolutions/IronRDP/issues/1530)) ([6a499faece](https://github.com/Devolutions/IronRDP/commit/6a499faece8911e50a715a3fb08d4fd8e7d7dc87)) + + ## Summary + + - Connect-time bandwidth measurement needs to know when bytes arrived, + and nothing in the sans-I/O layer could tell it. #1465, now merged, + answers the server's Bandwidth Measure Stop with a nominal interval for + exactly that reason: the connector has no way to observe the real one. + - Introduce `MonotonicInstant`, a millisecond counter with an arbitrary + epoch, and make `Option` a required parameter of + `Sequence::step`. The I/O drivers already know when a read completed, so + `Framed` records the arrival time of each read and hands it to the state + machine. A driver with no clock passes `None`. + - With arrival times available, measure for real: a Bandwidth Measure + Start opens a window, Payload messages accumulate their byte counts, and + Stop reports the elapsed time between its own arrival and the Start's. + + #1465 has merged, so this applies directly to master and carries no + merge-order dependency. That PR was the FreeRDP unblock on its own; this + is the design change behind it, split out at @CBenoit's suggestion in + review. + + ## Why the clock lives in the driver + + Two reasons, both of which rule out having the sequence read a clock + itself. + +- Delegate multitransport setup ([#1858](https://github.com/Devolutions/IronRDP/issues/1858)) ([7036fb8c7e](https://github.com/Devolutions/IronRDP/commit/7036fb8c7ef32f71b456745902e14d34008c7add)) + + Let applications establish negotiated multitransport channels while the + async connector retains protocol sequencing and response ownership. + + Expose Soft-Sync state to the setup callback, centralize response + construction, and report callback failures with E_ABORT when possible. + The existing finalizer still declines multitransport and uses TCP. + +### Bug Fixes + +- Reject a zero-length unmatched PDU in read_by_hint ([#1556](https://github.com/Devolutions/IronRDP/issues/1556)) ([2d2c37fc21](https://github.com/Devolutions/IronRDP/commit/2d2c37fc21bd7f089fbbca41831abba14fa2b72b)) + + ## Problem + + + ## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-async-v0.9.0...ironrdp-async-v0.10.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-async/Cargo.toml b/crates/ironrdp-async/Cargo.toml index 5423515e3e..812b68e305 100644 --- a/crates/ironrdp-async/Cargo.toml +++ b/crates/ironrdp-async/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-async" -version = "0.10.0" +version = "0.11.0" readme = "README.md" description = "Provides `Future`s wrapping the IronRDP state machines conveniently" edition.workspace = true @@ -17,9 +17,9 @@ doctest = false test = false [dependencies] -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public tracing = { version = "0.1", features = ["log"] } bytes = "1" # public web-time = "1.1" diff --git a/crates/ironrdp-blocking/CHANGELOG.md b/crates/ironrdp-blocking/CHANGELOG.md index ced129c3f4..a83480a308 100644 --- a/crates/ironrdp-blocking/CHANGELOG.md +++ b/crates/ironrdp-blocking/CHANGELOG.md @@ -6,6 +6,172 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.10.0...ironrdp-blocking-v0.11.0)] - 2026-09-30 + +### Security + +- [**breaking**] Implement multitransport bootstrapping handshake ([#1098](https://github.com/Devolutions/IronRDP/issues/1098)) ([e45fbfe0f5](https://github.com/Devolutions/IronRDP/commit/e45fbfe0f597011706e77fc174ca14e5e9d435b9)) + + ## Summary + + Makes the `MultitransportBootstrapping` state functional instead of a + no-op + pass-through. After licensing the server may send 0, 1, or 2 Initiate + Multitransport Request PDUs before capabilities exchange. Each one is + surfaced + to the application, which establishes UDP transport (RDPEUDP2 + TLS + + RDPEMT) + or declines, and the connector reports the outcome back to the server. + + ## API + + Mirrors the existing `should_perform_X()` pause-point pattern used by + TLS + upgrade and CredSSP, but uses `complete_X()` / `skip_X()` rather than + `mark_X_as_done()` because completion carries result data: + + - `should_perform_multitransport()`: true while a request awaits an + outcome + - `multitransport_request()`: the request awaiting an outcome, or `None` + - `complete_multitransport(result, output)`: report the outcome, resume + - `skip_multitransport(output)`: decline, resume + + `complete_multitransport` accepts a `MultitransportResult` (a `Success` + / + `Failure(hresult)` enum) rather than a caller-built response PDU. The + connector + builds the response internally from the stored request ID. + + Requests are surfaced one at a time rather than as a batch. There is no + end + marker for the set, and MS-RDPBCGR 3.2.5.15.1 requires the client to act + on a + request as soon as it decodes one, so waiting to learn how many are + coming is + not an option the protocol offers. `should_perform_multitransport()` can + therefore come round twice; the caller answers reliable and lossy + separately. + + ## Approach + + **Routing.** Requests arrive on the negotiated MCS message channel + (2.2.15.1) and the Demand Active on the I/O channel, so the channel + decides + which is which. The message channel also carries NetworkAutoDetect since + #1348, + so a decode still confirms what arrived there, but the I/O channel is + never + speculatively decoded as multitransport. A PDU on neither channel is an + error. + + For the decode to be a sound confirmation the request decoder must + reject a + Demand Active, so this PR also tightens `MultitransportRequestPdu` to + require + the exact `SEC_TRANSPORT_REQ` security-header flag. + + **Yielding.** Each request is surfaced the moment it decodes. Responding + returns the connector to `MultitransportBootstrapping` to read whatever + comes + next, which may be a second request or the Demand Active. Nothing is + buffered + and nothing is replayed: when the request is surfaced the Demand Active + has not + arrived yet. + + **Soft-Sync.** The Initiate Multitransport Response is the Soft-Sync + signalling path (2.2.15.2), permitted only when both peers advertised + `SOFTSYNC_TCP_TO_UDP` in their GCC `MultiTransportChannelData`. The + server's + block is retained from the GCC exchange and checked against the client's + configured flags. One rule covers both paths: + + - Soft-Sync negotiated: always respond, `S_OK` or `E_ABORT`, including + on + `skip_multitransport()`, which 3.2.5.15.1 requires. Both the async and + blocking drivers skip automatically, so without this every default + client + leaves a compliant server waiting. + - Not negotiated: never respond. The outcome is reported in band on the + new + transport, and putting anything on the main channel would be the + violation. + + The response goes on the message channel per 2.2.15.2 and 3.2.5.15.2. If + Soft-Sync was negotiated but no message channel exists the connector + errors + rather than falling back to the I/O channel, and that check runs before + the + pending state is taken, so the caller is left with a connector it can + still + inspect or decline from. + + ## Wire behaviour + + On the wire TCP and UDP negotiation happen in parallel: the UDP + transport is + established alongside the ongoing TCP handshake, and its completion + signals the + dynamic-channel layer that subsequent channels may migrate to UDP. The + connector's API yield point here is a Rust affordance, not a + spec-mandated TCP + pause. Thanks to @hardening for the correction. + + ## Tests + + Connector state-machine tests in `ironrdp-testsuite-core` drive the + public API + with the shared `SERVER_DEMAND_ACTIVE` fixture: + + - a request is surfaced on arrival, without waiting for a following PDU + (regression test for the stall); + - responding returns to bootstrapping so a second request is read + normally; + - a third request is rejected per the 2.2.15.1 cap; + - a Demand Active on the I/O channel ends bootstrapping; + - the response targets the message channel, decoded back off the wire; + - a `Failure` result is carried through; + - `skip` sends `E_ABORT` under Soft-Sync, and nothing without it; + - `complete` emits nothing without Soft-Sync but still resumes; + - a failed response leaves the connector in `MultitransportPending`, + still able + to report or decline, rather than `Consumed`; + - `complete` / `skip` outside `MultitransportPending` error; + - a Demand Active's user data does not decode as a + `MultitransportRequestPdu` + (regression test for the decoder tightening above). + +### Features + +- [**breaking**] Pass frame arrival time into Sequence::step ([#1530](https://github.com/Devolutions/IronRDP/issues/1530)) ([6a499faece](https://github.com/Devolutions/IronRDP/commit/6a499faece8911e50a715a3fb08d4fd8e7d7dc87)) + + ## Summary + + - Connect-time bandwidth measurement needs to know when bytes arrived, + and nothing in the sans-I/O layer could tell it. #1465, now merged, + answers the server's Bandwidth Measure Stop with a nominal interval for + exactly that reason: the connector has no way to observe the real one. + - Introduce `MonotonicInstant`, a millisecond counter with an arbitrary + epoch, and make `Option` a required parameter of + `Sequence::step`. The I/O drivers already know when a read completed, so + `Framed` records the arrival time of each read and hands it to the state + machine. A driver with no clock passes `None`. + - With arrival times available, measure for real: a Bandwidth Measure + Start opens a window, Payload messages accumulate their byte counts, and + Stop reports the elapsed time between its own arrival and the Start's. + + #1465 has merged, so this applies directly to master and carries no + merge-order dependency. That PR was the FreeRDP unblock on its own; this + is the design change behind it, split out at @CBenoit's suggestion in + review. + + ## Why the clock lives in the driver + + Two reasons, both of which rule out having the sequence read a clock + itself. + + + ## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-blocking-v0.9.0...ironrdp-blocking-v0.10.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-blocking/Cargo.toml b/crates/ironrdp-blocking/Cargo.toml index 8ce213293c..b19108df81 100644 --- a/crates/ironrdp-blocking/Cargo.toml +++ b/crates/ironrdp-blocking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-blocking" -version = "0.10.0" +version = "0.11.0" readme = "README.md" description = "Blocking I/O abstraction wrapping the IronRDP state machines conveniently" edition.workspace = true @@ -17,9 +17,9 @@ doctest = false test = false [dependencies] -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public tracing = { version = "0.1", features = ["log"] } bytes = "1" # public diff --git a/crates/ironrdp-bulk/CHANGELOG.md b/crates/ironrdp-bulk/CHANGELOG.md index cfbe2a4854..972930f560 100644 --- a/crates/ironrdp-bulk/CHANGELOG.md +++ b/crates/ironrdp-bulk/CHANGELOG.md @@ -6,6 +6,127 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-bulk-v0.1.1...ironrdp-bulk-v0.2.0)] - 2026-09-30 + +### Bug Fixes + +- [**breaking**] Always own a bulk decompressor for FastPath updates ([#1255](https://github.com/Devolutions/IronRDP/issues/1255)) ([0dc0194418](https://github.com/Devolutions/IronRDP/commit/0dc0194418375d504a8041b75ba250dc8eeb21ad)) + + ## Summary + + - A compressed FastPath update is dropped whenever the client did not + negotiate compression, because the decompressor is only built when a + compression type was negotiated. Servers send compressed updates + regardless, for example on a full-frame redraw after a resize, and the + session then fails. Closes #1193. + - The negotiated type is the wrong thing to condition on. It describes + what the client would send, and nothing in `ironrdp-session`, + `ironrdp-client`, `ironrdp-web` or the FFI ever compresses outbound. On + the receive path `BulkCompressor` holds a context per algorithm and + `decompress` selects one per update from the packet's own type bits, so + a decompressor built with any type decodes all of them. + - The `Processor` now owns the decompressor and builds it on the first + update that needs one. `ProcessorBuilder` has no corresponding field, so + there is no `None` a consumer can pass and no path that drops a + compressed update. + - On demand rather than at construction because `ironrdp-web` hardcodes + `compression_type: None` in `build_config` and so never negotiates + compression. Constructing eagerly would charge every web session for a + full set of algorithm contexts, and the two XCRUSH history buffers alone + are 2 MB each, for a decompressor most of those sessions never use. That + consumer is also the one most exposed to this bug, for the same reason. + - `BulkCompressor::new` is now infallible. Its only failure path was a + self-check over NCRUSH's static Huffman tables, a compile-time + invariant, now a `debug_assert`. + + ## Relationship to #1474 + + #1474 is kept, not reverted. `ActiveStage::reactivate` is adopted as the + reactivation entry point at all four call sites it introduced: native + client, web, FFI and the e2e test. + + What this PR removes is the `compression_type` retained on `ActiveStage` + and the `make_bulk_decompressor` helper, because an on-demand + decompressor makes both unnecessary. `reactivate` keeps its behaviour + and loses only the compression plumbing. + + #1474 closed the reactivation instance of #1193, where a rebuild passed + `None` and silently disabled decompression for the rest of the session. + The general case is still open on master: when compression was never + negotiated the retained type is `None`, `make_bulk_decompressor` returns + `None`, and every compressed update takes the drop path in + `fast_path.rs` for the lifetime of the session. Conditioning on the + negotiated type gates the ability to receive on what was negotiated to + send, and nothing sends. + + The evidence that removing the field is safe is #1474's own test. + `test_reactivation_processes_compressed_fastpath_updates` passes + unchanged with `compression_type` gone from the builder: the rebuilt + processor decompresses because every processor can, not because a type + was carried across the rebuild. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + The gated regression test is + `testsuite-core/tests/session/fast_path.rs`, which renders the same + bitmap update plain and bulk-compressed through fresh processors and + asserts identical framebuffers. #1474's + `test_reactivation_processes_compressed_fastpath_updates` in + `testsuite-extra` passes unchanged. + + There is also an inline test in `fast_path.rs` pinning the allocation + invariant, that no contexts are built until an update needs them. Note + that `ironrdp-session` sets `[lib] test = false`, so inline tests in + this crate are not run by `cargo test --workspace`; it runs under `cargo + test -p ironrdp-session --lib`. + + ## Notes + + - This addresses the four points from the 2026-06-24 review. Point 4, + that the `Option` is misleading, is the shape of this change: it is gone + from the public API, and the private one that remains carries no + implication that a consumer could choose not to decompress. Point 1, + whether a cold `Rdp61` context decodes `RDP40` and `RDP50` updates + correctly, is a non-issue: `decompress` selects the algorithm per update + through `CompressionType::from_flags` against per-algorithm receive + contexts, so the construction-time type never constrains the receive + path. Point 3, silent degradation if the constructor fails, is removed + by making `new` infallible. Point 2 is the tests above. + - Breaking across two crates, hence the `fix(bulk,session)!` scope: + `ProcessorBuilder` loses `bulk_decompressor`, `ActiveStageBuilder` loses + `compression_type`, and `ironrdp_bulk::BulkCompressor::new` returns + `Self`. + - Incidental: `ironrdp-session` no longer exposes any `ironrdp_bulk` + type in its public API, so that dependency's lack of a `# public` marker + in `Cargo.toml` is now correct. + +- Share bulk decompression across output paths ([#1518](https://github.com/Devolutions/IronRDP/issues/1518)) ([6151e21bf5](https://github.com/Devolutions/IronRDP/commit/6151e21bf58b7297e9b4abc2167aa36fc2ba77e4)) + + Bulk compression state is stream-wide, but Fast-Path and slow-path + outputs previously used separate or missing decompression paths. This + could corrupt history-dependent server updates or leave negotiated + slow-path compression undecodable. + + This change owns the negotiated bulk decompressor in `ActiveStage` and + passes it to both X.224 and Fast-Path processing. It retains Share Data + compression metadata through the PDU context, resets decompression + history on reactivation, and initializes consumers from the connection's + negotiated compression type. + + Fast-Path now decompresses each fragment before reassembly so + compression flags apply at packet boundaries. Failures expose bounded + protocol metadata without retaining remote payloads or decoder details. + + Tests cover Share Data metadata propagation, slow-path decompression + behavior, fragmented Fast-Path reassembly and bounded errors, and + compressed Fast-Path updates after reactivation. + + --------- + + + ## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-bulk-v0.1.0...ironrdp-bulk-v0.1.1)] - 2026-05-27 ### Bug Fixes diff --git a/crates/ironrdp-bulk/Cargo.toml b/crates/ironrdp-bulk/Cargo.toml index 200919bcce..78e0233da8 100644 --- a/crates/ironrdp-bulk/Cargo.toml +++ b/crates/ironrdp-bulk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-bulk" -version = "0.1.1" +version = "0.2.0" description = "Bulk compression algorithms (MPPC, XCRUSH, NCRUSH) for IronRDP" edition.workspace = true rust-version = "1.94" diff --git a/crates/ironrdp-capture-replay/Cargo.toml b/crates/ironrdp-capture-replay/Cargo.toml index 49206498a5..fdf05cbd74 100644 --- a/crates/ironrdp-capture-replay/Cargo.toml +++ b/crates/ironrdp-capture-replay/Cargo.toml @@ -15,13 +15,13 @@ categories.workspace = true [dependencies] aes-gcm = "0.10" hmac = "0.12" -ironrdp-core = { version = "0.2.1", path = "../ironrdp-core" } -ironrdp-dvc = { version = "0.8.0", path = "../ironrdp-dvc" } # public -ironrdp-egfx = { version = "0.3.0", path = "../ironrdp-egfx" } -ironrdp-graphics = { version = "0.9.0", path = "../ironrdp-graphics" } -ironrdp-pdu = { version = "0.9.0", path = "../ironrdp-pdu" } # public -ironrdp-session = { version = "0.11.0", path = "../ironrdp-session" } -ironrdp-svc = { version = "0.8.0", path = "../ironrdp-svc" } +ironrdp-core = { version = "0.3.0", path = "../ironrdp-core" } +ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public +ironrdp-egfx = { version = "0.4.0", path = "../ironrdp-egfx" } +ironrdp-graphics = { version = "0.10.0", path = "../ironrdp-graphics" } +ironrdp-pdu = { version = "0.10.0", path = "../ironrdp-pdu" } # public +ironrdp-session = { version = "0.12.0", path = "../ironrdp-session" } +ironrdp-svc = { version = "0.9.0", path = "../ironrdp-svc" } pcap-parser = "0.17" png = "0.18" sha2 = "0.10" diff --git a/crates/ironrdp-cfg/CHANGELOG.md b/crates/ironrdp-cfg/CHANGELOG.md index 62378530ec..e18fe7cdb5 100644 --- a/crates/ironrdp-cfg/CHANGELOG.md +++ b/crates/ironrdp-cfg/CHANGELOG.md @@ -6,6 +6,80 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cfg-v0.1.0...ironrdp-cfg-v0.2.0)] - 2026-09-30 + +### Security + +- Connect to Windows Sandbox named pipes ([#1580](https://github.com/Devolutions/IronRDP/issues/1580)) ([39b020343d](https://github.com/Devolutions/IronRDP/commit/39b020343d962962bfbefc89939be64d5c716196)) + + Windows Sandbox's default attach path is a local named pipe carrying + plain TPKT/X.224 with PROTOCOL_RDP and ENCRYPTION_LEVEL_NONE, not + TCP:3389 or VMConnect. Allow the connector and client to complete that + sequence only via an explicit opt-in (`enable_standard_rdp_security`; + NamedPipe enables it), and teach ironrdp-agent to resolve pipe path and + guest credentials from WindowsSandboxServer after `wsb start`. + + Adds Transport::NamedPipe, ironrdp_named_pipe/ironrdp_sandbox_id + properties, sandbox list/config/stop CLI helpers via an in-process + h2/gRPC client on the per-user `\\.\pipe\wsandbox\{guid}` pipe (no .NET + helper), and connect --sandbox-id / --sandbox-pipe. Sandbox-derived + properties are the merge base; explicit .rdp/--prop/flags override them + while NamedPipe TLS/CredSSP stay forced off. Local :2179+PCB remains + unsupported. + +### Features + +- Add RemoteApp channel support ([#1637](https://github.com/Devolutions/IronRDP/issues/1637)) ([ab48c6cb8c](https://github.com/Devolutions/IronRDP/commit/ab48c6cb8c017504f8a92799aeb91b821c50a13a)) + + Configure and negotiate RAIL connections, then route its static channel + through the portable client with bounded request queues and server + control events. + +- Wire MS-RDPEAI capture into Windows client and ActiveX ([#1642](https://github.com/Devolutions/IronRDP/issues/1642)) ([205fe038cc](https://github.com/Devolutions/IronRDP/commit/205fe038cc693598adf803fe181526b789b2ec3d)) + + Add the client MS-RDPEAI capture path on top of hardened RDPSND + playback: connector CFG + static channel wiring, CPAL PCM capture + backend, ironrdp-client --audio-capture, and ActiveX + AudioCaptureRedirectionMode. + + PCM capture only accepts encode formats that match the Open capture + stream, rejects non-16-bit capture (Data PDU size contract), and gates + the capture backend behind ironrdp-rdpsnd-native/capture. + + Depends on #1648 (playback). + +- Add native MS-RDPEWA WebAuthn redirection ([#1644](https://github.com/Devolutions/IronRDP/issues/1644)) ([66da78bc4e](https://github.com/Devolutions/IronRDP/commit/66da78bc4e6b37a7780dbf9f333234be63d96afb)) + + Implement the RDPEWA dynamic channel with a Windows WebAuthn backend and + wire RedirectWebAuthn for ActiveX, the optional client feature, and the + viewer CLI. + + Prefer System32\webauthn.dll via the DVC COM plugin for MSTSC parity. + The pure-Rust backend forwards ceremonies through a webauthn.dll IWTS + oneshot so hash-only hosts that omit clientDataJSON still work; public + WebAuthN* remains a fallback when JSON is present. Recreate + WebAuthN_Channel opens through shared COM/listener factories because + Windows opens and closes the channel around each RPC. + + Side effects: + - New crates ironrdp-rdpewa and ironrdp-rdpewa-native + - Config key redirectwebauthn; ActiveX ExtendedSettings property + - ironrdp-daemon webauthn feature for ironrdp-agent + - IRONRDP_WEBAUTHN_FORCE_NATIVE debug switch + - Viewer --webauthn/--no-webauthn flags; .rdp redirectwebauthn default + - ActiveX docs note no AdvancedSettings slot and no IPersist persistence + +- Try a direct connection before the RD Gateway ([#1713](https://github.com/Devolutions/IronRDP/issues/1713)) ([a67dbf47e4](https://github.com/Devolutions/IronRDP/commit/a67dbf47e429a73b29e2b36966b6dc5b870625cd)) + +- Wire local VMConnect features ([#1768](https://github.com/Devolutions/IronRDP/issues/1768)) ([cf9bdef764](https://github.com/Devolutions/IronRDP/commit/cf9bdef764da6b9aff33b436662441e1503dc93a)) + + Wire VMConnect modes through the client configuration and RDP connector. + Support current-user host authentication without a password, preserve + guest credential boundaries, and attach Hyper-V framebuffer redirection + only for eligible local Windows sessions. + + + ## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-cfg-v0.1.0)] - 2026-07-10 Initial release. diff --git a/crates/ironrdp-cfg/Cargo.toml b/crates/ironrdp-cfg/Cargo.toml index 488b9f9714..14b6d52b3b 100644 --- a/crates/ironrdp-cfg/Cargo.toml +++ b/crates/ironrdp-cfg/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-cfg" -version = "0.1.0" +version = "0.2.0" readme = "README.md" description = "IronRDP utilities for ironrdp-cfgstore" edition.workspace = true diff --git a/crates/ironrdp-client/CHANGELOG.md b/crates/ironrdp-client/CHANGELOG.md index cdbc79f701..1c4cfde881 100644 --- a/crates/ironrdp-client/CHANGELOG.md +++ b/crates/ironrdp-client/CHANGELOG.md @@ -6,6 +6,1039 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-client-v0.1.0...ironrdp-client-v0.2.0)] - 2026-09-30 + +### Security + +- [**breaking**] Support session resume via the auto-reconnect cookie ([#1501](https://github.com/Devolutions/IronRDP/issues/1501)) ([74b3365c1f](https://github.com/Devolutions/IronRDP/commit/74b3365c1f98c0da6feed7507779c67e1b8e6d08)) + + > **Rebased onto post-#1522 master.** #1509 landed the server half of + #1508 while this was open, including the `ClientAutoReconnect` + structure. This PR no longer declares it; it extends it, and picks up + the parts #1509 did not build. + + ## What + + The client half of automatic reconnection. The session layer surfaces + the Server Auto-Reconnect Cookie, `ironrdp-pdu` derives and verifies the + client's response to it, and the connector sends that response when + resuming a session. + + ## Why + + A client whose connection drops ungracefully can reattach to its session + instead of making the user log on again, provided it returns the cookie + the server issued during logon ([MS-RDPBCGR] 1.3.1.5). + + #1509 built the server side of that: it validates a returning + `ARC_CS_PRIVATE_PACKET` and rotates the random. Nothing answers it. + `ironrdp-session` decodes the cookie and drops it, `ironrdp-connector` + has no way to send one back, and `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` still sits in + `ironrdp-client`. So `ironrdp-client` cannot resume a session against + `ironrdp-server`, and the validation #1509 added has no in-tree + counterpart to exercise it. + + The wire encoding was already there. `ExtendedClientOptionalInfo` + carries, encodes and decodes a 28-byte `autoReconnectCookie` and its + builder already had a `reconnect_cookie` step; `ServerAutoReconnect` + already decoded; #1509 added `ClientAutoReconnect` and its decode. + Nothing connected them. + + ## The three parts + + **Receive.** `SaveSessionInfo` now also surfaces the cookie, as + `ProcessorOutput::AutoReconnectCookie` and + `ActiveStageOutput::AutoReconnectCookie`. #1522 added a `SaveSessionInfo + { logon_complete }` output on that same handler; the two coexist rather + than compete, since both are read off one PDU and neither supersedes the + other. The handler emits the logon notification unconditionally and + appends the cookie when one is present, and a test pins that surfacing + the cookie does not suppress the notification. #1509's server replaces + the cookie whenever a client connects and again hourly ([MS-RDPBCGR] + 3.3.6.2), so this can arrive more than once in a session and the + consumer keeps the most recent. + + **Derive.** `ClientAutoReconnect::from_server_cookie` implements + [MS-RDPBCGR] 5.5: + + > The auto-reconnect random is used to key the HMAC function + ([RFC2104]), which uses MD5 as the iterative hash function. The security + verifier is derived by applying the HMAC to the client random received + in Step 3. + > + > `SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom)` + > + > When Enhanced RDP Security is in effect the client random value is not + generated (section 5.3.2). In this case, for the purpose of generating + the security verifier, the client random is assumed to be an array of 32 + zero bytes. + + IronRDP implements no Standard RDP Security path (there is no Security + Exchange PDU), so the zero-client-random case is the only one that + arises. As 5.5 notes, that makes the verifier constant for a given + cookie, so it proves possession of the cookie and nothing more; session + security comes from the outer TLS/CredSSP handshake. + + @clintcan independently confirmed this construction against real + **mstsc** while validating #1509 + ([comment](https://github.com/Devolutions/IronRDP/pull/1509#issuecomment-5151200681)): + a Windows client's `ARC_CS_PRIVATE_PACKET` verifies against + `HMAC-MD5(random_bits, [0u8; 32])`. That is the same derivation + implemented here, so the two halves interoperate with Microsoft's client + and not only with each other. + + **Send.** `ClientConnector::with_auto_reconnect_cookie` takes the cookie + last received and makes the connector put the derived Client + Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. + Absent, that PDU is byte-for-byte what it was. + + Unlike the server packet, this structure has no enclosing logon-info + field header, so it encodes to exactly the 28 bytes the cookie field + expects. `to_bytes` writes that layout directly rather than going + through `Encode`, so filling a fixed-size field has no error path a + caller must handle; a test pins the two to agree. + + ## One derivation, not two + + Putting `from_server_cookie` in `ironrdp-pdu` would leave the workspace + with two implementations of 5.5, since #1509 added a private HMAC to + `ironrdp-server`. So `ClientAutoReconnect` also gains `verify`, and the + server routes through it. + + `verify` keeps the constant-time comparison the server had. The verifier + is the whole credential, so a comparison returning early on the first + differing byte would let a peer recover it a byte at a time from the + timing; the session identifier is not secret and is compared normally. + `ironrdp-server` keeps the policy around the check, which cookies are + live and whether the security protocol permits auto-reconnect, and drops + its `hmac` and `md-5` dependencies. `hmac` moves to `ironrdp-pdu` as + `default-features = false`; the crate's full feature powerset still + checks clean, including `--no-default-features`. + + I would rather not have reached into `ironrdp-server` in a + `pdu,session,connector` change, but the alternative was shipping the + duplicate and filing a follow-up to remove it, which is a worse trade + for reviewer time. + + ## Tests that were not running + + That move also rehomes the known-answer tests @clintcan contributed on + #1509. They went in as an inline `#[cfg(test)]` module in + `crates/ironrdp-server/src/server.rs`, and that crate sets `[lib] test = + false`, so they have never executed in CI. They now live in + `ironrdp-testsuite-core` against the public API, where CI runs them: his + HMAC-MD5 reference vector is kept as a second vector alongside a + differently-keyed one, plus the cases for a tampered verifier and a + mismatched logon ID. + + Worth flagging separately: `ironrdp-server` is not alone. + `ironrdp-agent`, `ironrdp-session` and `ironrdp-web` also set `[lib] + test = false` and between them carry 16 files of inline `#[cfg(test)]` + modules that CI never runs. That is out of scope here, but I am happy to + open an issue if it would be useful. + + ## Breaking changes + + `ActiveStageOutput` and `x224::ProcessorOutput` gain a variant, and + `ClientConnector` gains a public field, so exhaustive matches and struct + literals need updating. + + Confirmed with `cargo-semver-checks` against the merge-base: those three + are the only findings this branch introduces. The others it reports on + `master` today (`ShareDataPdu::Compressed` and the `ShareDataCtx` fields + from #1518, `ProcessorBuilder.bulk_decompressor` from #1518, + `ServerEvent::SetAutoReconnectCookie` from #1509) are present on + `master` unchanged. The `ironrdp-pdu` additions are additive. + + ## Scope + + This is the library half. `ironrdp-client`, `ironrdp-web` and the FFI + bindings gain an arm for the new output but none of them reconnect + automatically yet; that is the remaining part of #271, and the existing + `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` in `ironrdp-client` marks where it goes. + + I kept receive, derive and send together deliberately. Split up, none of + them is usable on its own: without the receive half there is no way to + obtain a cookie, and without the send half there is nothing to do with + one. + + ## Tests + + Thirteen, all in `ironrdp-testsuite-core`. + + On the packet and the derivation: the `SecurityVerifier` matches two + independently computed HMAC-MD5 vectors of 32 zero bytes under different + keys, so the tests pin the derivation rather than restating the code; + the logon ID carries over from the server cookie; the encoding matches + the 2.2.4.3 field layout byte for byte with `cbLen` fixed at `0x1C`; + `to_bytes` agrees with `Encode`; it round-trips; and it rejects both a + wrong packet length and an unknown version. + + On verification: a derived answer is accepted, a single flipped byte in + the verifier is rejected, a correct verifier under a different logon ID + is rejected, and an answer derived from a different random is rejected. + + On the surfacing path: a Save Session Info PDU framed the way a server + sends it, through the real x224 processor, yields an + `AutoReconnectCookie` carrying the right logon ID and random bits, + alongside #1522's logon notification rather than in place of it; and one + without a cookie surfaces no cookie. + + ## Verification + + `cargo xtask check fmt/lints/tests/typos/locks` all pass on 1.94.1, + including a `fuzz/` build before the lock check. + + ## Note + + #1496 also touches the `ClientAutoReconnect` declaration. Whichever of + the two lands second needs a one-line rebase on the derive attribute; + happy to take that in either order. + +- Connect to Windows Sandbox named pipes ([#1580](https://github.com/Devolutions/IronRDP/issues/1580)) ([39b020343d](https://github.com/Devolutions/IronRDP/commit/39b020343d962962bfbefc89939be64d5c716196)) + + Windows Sandbox's default attach path is a local named pipe carrying + plain TPKT/X.224 with PROTOCOL_RDP and ENCRYPTION_LEVEL_NONE, not + TCP:3389 or VMConnect. Allow the connector and client to complete that + sequence only via an explicit opt-in (`enable_standard_rdp_security`; + NamedPipe enables it), and teach ironrdp-agent to resolve pipe path and + guest credentials from WindowsSandboxServer after `wsb start`. + + Adds Transport::NamedPipe, ironrdp_named_pipe/ironrdp_sandbox_id + properties, sandbox list/config/stop CLI helpers via an in-process + h2/gRPC client on the per-user `\\.\pipe\wsandbox\{guid}` pipe (no .NET + helper), and connect --sandbox-id / --sandbox-pipe. Sandbox-derived + properties are the merge base; explicit .rdp/--prop/flags override them + while NamedPipe TLS/CredSSP stay forced off. Local :2179+PCB remains + unsupported. + +- Support advanced session settings ([#1781](https://github.com/Devolutions/IronRDP/issues/1781)) ([14ef4fd49f](https://github.com/Devolutions/IronRDP/commit/14ef4fd49fcf950169806866ee67db0c49662cfc)) + + Wire administrative-session GCC data, opaque load-balance routing + tokens, and RDPSND quality selection through the ActiveX settings + objects and client configuration. + + Keep unrelated transport, cache, video, device, and security policy + slots as explicit E_NOTIMPL failures. + + --------- + +- Honor input timing settings ([#1872](https://github.com/Devolutions/IronRDP/issues/1872)) ([ffb921c211](https://github.com/Devolutions/IronRDP/commit/ffb921c211477c6b77cccaf98eb500d627513374)) + + Wire MinInputSendInterval and KeepAliveInterval through the client + configuration and active session loop. + + Delay mouse-only batches without delaying forced input, keep the + software-rendered cursor aligned with the final batched position, and + synthesize current-position keepalives using native defaults and value + handling. + + Keep idle timeout, cache, video, device, and security policy settings + explicitly unsupported until their owning backends exist. + +- Enable reliable UDP transport ([#1869](https://github.com/Devolutions/IronRDP/issues/1869)) ([0024c8284a](https://github.com/Devolutions/IronRDP/commit/0024c8284ae48da3c2dd659fd2776fb6e838d76a)) + + Add opt-in reliable RDP-UDP2 multitransport for direct + enhanced-security connections. + + Negotiate Soft-Sync, reuse primary certificate validation for the + UDP sideband, and route selected dynamic channels without changing + TCP routing for other traffic. Bootstrap failures and pre-migration + loss fall back to TCP; post-migration failures trigger reconnect with + fresh transport state. + + Keep gateway and unsupported carriers or security modes TCP-only. + +### Features + +- Expose the server's Input capability flags on ConnectionResult ([#1488](https://github.com/Devolutions/IronRDP/issues/1488)) ([9bcf13438c](https://github.com/Devolutions/IronRDP/commit/9bcf13438cb068b53a8cb0dc23477c498727d49f)) + + Per [MS-RDPBCGR] 2.2.8.1.2, a client must not send fast-path input + events unless the server advertised `INPUT_FLAG_FASTPATH_INPUT` or + `INPUT_FLAG_FASTPATH_INPUT2` in its Input Capability Set. Today the + connector discards the server's capability sets after Demand Active, so + client code has no way to honour that requirement. + + This PR captures the server's Input capability flags during capabilities + exchange, carries them through the `ConnectionFinalization`/`Finalized` + activation states (so they are refreshed correctly across a + Deactivation-Reactivation Sequence too), and surfaces them as + `ConnectionResult::input_flags`. The session layer can then choose + between fast-path and slow-path input per server. + + ### Motivation / real-world interop + + This is not theoretical: VirtualBox's VRDP server closes the connection + outright on receiving a fast-path input PDU — its `VBox.log` reports + + ``` + VRDP: Network packet length is incorrect 0x0004. Closing connection. + ``` + + (a single fast-path scancode event is a 4-byte packet). VirtualBox never + advertises fast-path input; its Demand Active offers + `InputFlags(SCANCODES)` alone. mstsc and FreeRDP honour the negotiation + and fall back to slow-path `TS_INPUT_PDU`s, which is why they work + against VRDE. + + Haven (Android RDP client built on IronRDP) has been shipping this exact + change as a vendored-connector patch since v5.86.1, with the slow-path + fallback keyed off `ConnectionResult::input_flags`. Verified against a + real VirtualBox 7.2.6 VRDE server: before the gate, the first arrow-key + press killed the session with the log line above; with the gate, + extended input sessions run clean, and a fast-path-capable server on the + same host still takes the fast-path branch. (Discussed in #1158; this is + the third and last piece Haven carries in its connector fork, alongside + #1237 and #1472.) + + ### Changes + + - `connection_activation.rs`: capture `input_flags` from the + `CapabilitySet::Input` in Server Demand Active (empty if absent); add + the field to `ConnectionActivationState::{ConnectionFinalization, + Finalized}`. + - `connection.rs`: add `ConnectionResult::input_flags`, populated from + the `Finalized` state. + - Call sites in `ironrdp-client`, `ironrdp-web`, ffi, and the e2e test + updated for the new variant field (all currently ignore it). + + ### Testing + + - Two new integration tests in + `ironrdp-testsuite-core/tests/session/connection_activation.rs`: the + fixture's Demand Active yields `SCANCODES | MOUSEX | UNICODE | + FASTPATH_INPUT_2` in the `ConnectionFinalization` state, and a Demand + Active with the Input capability stripped yields `InputFlags::empty()`. + - `cargo check --workspace --all-targets`, `cargo clippy --workspace + --all-targets`, and `cargo fmt --check` are clean; the 7 activation + tests pass. + +- Expose generic session configuration and lifecycle APIs ([#1522](https://github.com/Devolutions/IronRDP/issues/1522)) ([57b1366650](https://github.com/Devolutions/IronRDP/commit/57b13666506dc40c15b4c4702d35150beee99133)) + + ## Summary + - expose generic client configuration for connection metadata, + compression, shell/work directory, audio, and runtime static-channel + factories + - add bounded input delivery with independent close cancellation, host + clipboard plumbing, lifecycle events, and Display Control resize + readiness/fallback handling + - update agent, viewer, web, FFI, examples, and tests for the generic + APIs + + ## Stack dependencies + This PR is stacked on `copilot/tls-validation-policy` (`b2bbcece`), + which already includes the merged runtime static-channel support from + `master`. It intentionally contains no TLS implementation/policy, + ActiveX/COM, SVC implementation, decompression, or bitmap-recovery + changes. + + ## Validation + - `cargo fmt --check --all` + - `cargo xtask check tests --no-run -v` + - `cargo xtask check lints -v` + - `cargo test -p ironrdp-client --lib --features rustls` + - `cargo check -p ironrdp-agent -p ironrdp-viewer -p ironrdp-web -p ffi` + + --------- + +- Add IronRDP ActiveX COM server ([#1523](https://github.com/Devolutions/IronRDP/issues/1523)) ([ee58b7c5f2](https://github.com/Devolutions/IronRDP/commit/ee58b7c5f283ef64be93a8483242f49070c6cdc9)) + + ## Summary + - Add the IronRDP ActiveX COM server and native MSTSC host integration. + - Provide bounded native-host diagnostics, credential-bridge support, + and an AxHost test harness. + - Preserve the minimal client, connector, and error integrations needed + by the control. + + ## Validation + - cargo check -p ironrdp-activex + - Focused ironrdp-error and ironrdp-connector tests + - cargo test -p ironrdp-activex --lib --no-run + - cargo fmt --all -- --check + - cargo xtask check locks -v + + --------- + +- Hyper-V vmconnect support ([#1503](https://github.com/Devolutions/IronRDP/issues/1503)) ([a7cc067d50](https://github.com/Devolutions/IronRDP/commit/a7cc067d5069cbbcb13bae3e0561c0611da3bcf6)) + + Adds Hyper-V VMConnect's direct ordering: PCB → TLS → CredSSP → X.224. + + Enhanced Session is the default (`GUID;EnhancedMode=1`), with + `--vmconnect-basic` for the synthetic console. Kept this separate in + `ironrdp-vmconnect`; no SPN changes. + + Tested against the nested Hyper-V lab: + - Enhanced: `HYBRID_EX`, rendered 1280×720 + - Basic: `HYBRID`, rendered 1280×720 + - `cargo xtask check fmt/lints/tests -v` + + --------- + +- Wire RDPDR backends into client connections ([#1600](https://github.com/Devolutions/IronRDP/issues/1600)) ([1fbc9bab0b](https://github.com/Devolutions/IronRDP/commit/1fbc9bab0bc26d8fe0789d5215005d7ea22e2a54)) + + Build a fresh RDPDR backend product for every connection attempt. + + Attach RDPDR only when its product has filesystem devices, advertise + RDPSND for Windows interoperability, and deliver deferred responses. + +- Negotiate static channel chunk sizing ([#1622](https://github.com/Devolutions/IronRDP/issues/1622)) ([4e3903fbbe](https://github.com/Devolutions/IronRDP/commit/4e3903fbbef2904505f35e3437a7106807ac5987)) + + Use the validated server VCChunkSize for outgoing static virtual channel + data and retain 1600-byte chunks when it is absent or invalid. + + Apply refreshed values after reactivation across native, web, and FFI + active stages while preserving channel flags. + +- Support Hyper-V connection ordering ([#1505](https://github.com/Devolutions/IronRDP/issues/1505)) ([5c1816244e](https://github.com/Devolutions/IronRDP/commit/5c1816244e83187a04249e9d9c240d096cb78f55)) + + Hyper-V over RDCleanPath needs PCB → TLS on the proxy, then CredSSP → + X.224 on the client. Ordinary RDCleanPath stays X.224-first. + + Still VERSION_1 with the same DER fields. An explicit VMConnect request + carries a Unicode PCB payload in `preconnection_blob` with no X.224; the + proxy encodes the binary PCB. Generic PCB requests keep their existing + X.224-first behavior. + + Gateway reference implementation: + [Devolutions/devolutions-gateway#1372](https://github.com/Devolutions/devolutions-gateway/pull/1372) + + Checked locally: Rust builds, formatting, Svelte typecheck, and .NET + build. Real nested Hyper-V E2E through Gateway: Native rendered 18 + frames, Avalonia connected and rendered its first frame, and Web + rendered a non-empty 1280×720 canvas. + + --------- + +- Forward negotiated windowing orders ([#1631](https://github.com/Devolutions/IronRDP/issues/1631)) ([0c3fbe78b4](https://github.com/Devolutions/IronRDP/commit/0c3fbe78b4366533b9fcea046b2b53654e003a72)) + + Preserve Window List support during activation. + Forward validated orders through ActiveStage and the raw FFI output. + Desktop and web consumers retain their existing behavior. + +- Add RemoteApp channel support ([#1637](https://github.com/Devolutions/IronRDP/issues/1637)) ([ab48c6cb8c](https://github.com/Devolutions/IronRDP/commit/ab48c6cb8c017504f8a92799aeb91b821c50a13a)) + + Configure and negotiate RAIL connections, then route its static channel + through the portable client with bounded request queues and server + control events. + +- Project RemoteApp windows ([#1641](https://github.com/Devolutions/IronRDP/issues/1641)) ([f5554f40dc](https://github.com/Devolutions/IronRDP/commit/f5554f40dc280d93506ea8352e3992e641d58e96)) + + Project server-authoritative RAIL windows for an enabled ActiveX + RemoteApp session and launch the configured program after RAIL becomes + available. + + Forward validated opaque windowing orders to the worker, maintain their + basic HWND lifecycle, and retain windows until the server removes them. + Leave desktop behavior and unsupported shell features unchanged. + +- Add RAIL audit commands ([#1646](https://github.com/Devolutions/IronRDP/issues/1646)) ([77759dca03](https://github.com/Devolutions/IronRDP/commit/77759dca032eb829b5be54a3c44d9be92252cf41)) + + Expose bounded client-validated RAIL events and RemoteApp launch + requests through the daemon IPC so headless agents can verify sessions. + + Preserve cursors across resize reconnects, wake waiting readers for + locally queued launches, and redact launch data in logs. + + Report terminal local Execute failures without disrupting an otherwise + valid RDP session. + +- Add Input DVC and ActiveX touch ([#1647](https://github.com/Devolutions/IronRDP/issues/1647)) ([a912e19bd2](https://github.com/Devolutions/IronRDP/commit/a912e19bd2bb31f403fd7c35c8efd729a5ab5f6f)) + + Implement MS-RDPEI for multi-touch over the dynamic virtual channel + Microsoft::Windows::RDS::Input, and wire Windows pointer messages in + ActiveX through session encode helpers. + + Introduce ironrdp-rdpei PDUs and processors, register the channel from + the client, encode touch frames from ActiveX WM_POINTER*, and cover the + protocol with unit and integration tests. + +- Harden Windows client playback path ([#1648](https://github.com/Devolutions/IronRDP/issues/1648)) ([2d9a9bf114](https://github.com/Devolutions/IronRDP/commit/2d9a9bf114dcf41a1ddc7343f564bc2e8d1d06db)) + + Keep client format order for wFormatNo, play pre-v8 Wave PDUs, and apply + volume on a broader CPAL PCM offer so ActiveX mode 0 can redirect remote + audio reliably. + + Also fix clippy noise in the RDPSND client suite and keep interleaved + volume L/R phase stable across wave blocks. Volume scaling is a simple + amplitude map, not a logarithmic MS-RDPEA model. + +- Wire MS-RDPEAI capture into Windows client and ActiveX ([#1642](https://github.com/Devolutions/IronRDP/issues/1642)) ([205fe038cc](https://github.com/Devolutions/IronRDP/commit/205fe038cc693598adf803fe181526b789b2ec3d)) + + Add the client MS-RDPEAI capture path on top of hardened RDPSND + playback: connector CFG + static channel wiring, CPAL PCM capture + backend, ironrdp-client --audio-capture, and ActiveX + AudioCaptureRedirectionMode. + + PCM capture only accepts encode formats that match the Open capture + stream, rejects non-16-bit capture (Data PDU size contract), and gates + the capture backend behind ironrdp-rdpsnd-native/capture. + + Depends on #1648 (playback). + +- Add native MS-RDPEWA WebAuthn redirection ([#1644](https://github.com/Devolutions/IronRDP/issues/1644)) ([66da78bc4e](https://github.com/Devolutions/IronRDP/commit/66da78bc4e6b37a7780dbf9f333234be63d96afb)) + + Implement the RDPEWA dynamic channel with a Windows WebAuthn backend and + wire RedirectWebAuthn for ActiveX, the optional client feature, and the + viewer CLI. + + Prefer System32\webauthn.dll via the DVC COM plugin for MSTSC parity. + The pure-Rust backend forwards ceremonies through a webauthn.dll IWTS + oneshot so hash-only hosts that omit clientDataJSON still work; public + WebAuthN* remains a fallback when JSON is present. Recreate + WebAuthN_Channel opens through shared COM/listener factories because + Windows opens and closes the channel around each RPC. + + Side effects: + - New crates ironrdp-rdpewa and ironrdp-rdpewa-native + - Config key redirectwebauthn; ActiveX ExtendedSettings property + - ironrdp-daemon webauthn feature for ironrdp-agent + - IRONRDP_WEBAUTHN_FORCE_NATIVE debug switch + - Viewer --webauthn/--no-webauthn flags; .rdp redirectwebauthn default + - ActiveX docs note no AdvancedSettings slot and no IPersist persistence + +- Expose MS-RDPEI pen and multi-touch ([#1652](https://github.com/Devolutions/IronRDP/issues/1652)) ([51f8738ea1](https://github.com/Devolutions/IronRDP/commit/51f8738ea156daeea292c63bba09d9137791af57)) + + Extend the agent RPC path beyond single-contact touch so multi-contact + frames, pen frames, and dismiss-hovering can be driven end-to-end + against a real host. + + Wire Pen/Dismiss through rpc, daemon, CLI, ActiveX control RPC, client + input loop, and session encode helpers. Add pen contact flag legality + checks and round-trip coverage. + +- Plumb smartcard device into RDPDR backends ([#1656](https://github.com/Devolutions/IronRDP/issues/1656)) ([66831bbbba](https://github.com/Devolutions/IronRDP/commit/66831bbbbabe3bf36bedff769c3e62819f60d46b)) + + Return immediate SvcMessage completions from handle_scard_call so + backends can finish MS-RDPESC IRPs without blocking the channel path. + + Wire WindowsRdpdrBackendFactory::with_smartcard and a minimal + ScardSession stub that answers decoded calls with + SCARD_E_UNSUPPORTED_FEATURE. Allow smartcard-only RDPDR products (no + drives). Full WinSCard work and product CLI/ActiveX enablement remain + follow-ups. + + Depends on #1654. + +- Support automatic reconnect ([#1662](https://github.com/Devolutions/IronRDP/issues/1662)) ([34d7b31dc5](https://github.com/Devolutions/IronRDP/commit/34d7b31dc5039273dc5502d0ca3301f256eb6154)) + + Support automatic reconnect through the ActiveX control with bounded + host-approved retries after transport loss. + + Reuse ARC cookies only for resumed sessions, reject server ARC status + failures, and report success after active-session traffic. + + --------- + +- Negotiate monitor topology ([#1675](https://github.com/Devolutions/IronRDP/issues/1675)) ([063efcdc30](https://github.com/Devolutions/IronRDP/commit/063efcdc3088d8f44e423cc322077d40bf9aadf2)) + + Negotiate the client monitor layout from UseMultimon and expose the + confirmed remote topology through the ActiveX compatibility interface. + + Advertise Monitor Layout PDU support whenever Extended Client Data is + negotiated, and forward layouts from activation, active sessions, and + reactivation so advertised support does not terminate sessions. + + Keep fallback reporting truthful when servers do not honor the request, + while preserving single-monitor resize behavior and blocking + multi-monitor resizing. + + Do not send Client Monitor Extended Data; per-monitor DPI and + orientation remain unavailable. + +- [**breaking**] Pass frame arrival time into Sequence::step ([#1530](https://github.com/Devolutions/IronRDP/issues/1530)) ([6a499faece](https://github.com/Devolutions/IronRDP/commit/6a499faece8911e50a715a3fb08d4fd8e7d7dc87)) + + ## Summary + + - Connect-time bandwidth measurement needs to know when bytes arrived, + and nothing in the sans-I/O layer could tell it. #1465, now merged, + answers the server's Bandwidth Measure Stop with a nominal interval for + exactly that reason: the connector has no way to observe the real one. + - Introduce `MonotonicInstant`, a millisecond counter with an arbitrary + epoch, and make `Option` a required parameter of + `Sequence::step`. The I/O drivers already know when a read completed, so + `Framed` records the arrival time of each read and hands it to the state + machine. A driver with no clock passes `None`. + - With arrival times available, measure for real: a Bandwidth Measure + Start opens a window, Payload messages accumulate their byte counts, and + Stop reports the elapsed time between its own arrival and the Start's. + + #1465 has merged, so this applies directly to master and carries no + merge-order dependency. That PR was the FreeRDP unblock on its own; this + is the design change behind it, split out at @CBenoit's suggestion in + review. + + ## Why the clock lives in the driver + + Two reasons, both of which rule out having the sequence read a clock + itself. + +- Try a direct connection before the RD Gateway ([#1713](https://github.com/Devolutions/IronRDP/issues/1713)) ([a67dbf47e4](https://github.com/Devolutions/IronRDP/commit/a67dbf47e429a73b29e2b36966b6dc5b870625cd)) + +- Resolve gateway credential sources ([#1740](https://github.com/Devolutions/IronRDP/issues/1740)) ([64375e78cc](https://github.com/Devolutions/IronRDP/commit/64375e78ccd796117e2a300a096f9b75ea6e01db)) + + Resolve UseServerCredentials from RDP account fields while preserving + explicit gateway credentials. + + Qualify bare server usernames with the configured domain for gateway + authentication. + + Reject unavailable credential sources instead of silently substituting + credentials. + +- Add smart-card gateway authentication ([#1741](https://github.com/Devolutions/IronRDP/issues/1741)) ([761dae12b0](https://github.com/Devolutions/IronRDP/commit/761dae12b0128be0f9bae52ca59eae8a0ba0b02f)) + + Add an opt-in Kerberos PKINIT path for HTTP Negotiate gateway + authentication while retaining the password Negotiate, NTLM, and Basic + flows. + + The public credentials type accepts an application-supplied UPN, redacts + credentials, and rejects unsupported smart-card feature or challenge + combinations without exposing them. + +- Enforce gateway redirection policy ([#1760](https://github.com/Devolutions/IronRDP/issues/1760)) ([9a3617f46c](https://github.com/Devolutions/IronRDP/commit/9a3617f46c9190f86cf79814a37b77dc84497f70)) + + Honor restrictive gateway redirection flags without enabling new + channels. + +- Apply certificate validation to gateways ([#1775](https://github.com/Devolutions/IronRDP/issues/1775)) ([312d4466e7](https://github.com/Devolutions/IronRDP/commit/312d4466e7cae15d45c73a4ffcb4ae730d5e6a30)) + + Apply the RDP client's certificate-validation policy and callback to + every + gateway HTTPS connection. + + Existing gateway callers retain their compatibility default. + +- Redirect dynamic drives ([#1780](https://github.com/Devolutions/IronRDP/issues/1780)) ([5e05637f06](https://github.com/Devolutions/IronRDP/commit/5e05637f0659d460d6e02b5567bb728f1d04585a)) + + Keep drive capability active for hotplug-only sessions and route ActiveX + drive rescans and selection changes through the live RDPDR channel. + Preserve stable drive IDs, defer removals until server acknowledgement, + and keep generic devices, printers, and ports explicitly unsupported. + + Smartcard redirection remains independent. + +- Add location redirection ([#1778](https://github.com/Devolutions/IronRDP/issues/1778)) ([1cee7a8613](https://github.com/Devolutions/IronRDP/commit/1cee7a86135a0556c01965d0406233bd7df367a9)) + + Implement MS-RDPEL v1 codecs and the location DVC state machine, then + route the ActiveX methods through the bounded client input queue. + + Preserve mstsc-compatible validation and altitude caching while + surfacing inactive sessions, channel readiness, queue pressure, and + encoding failures. Coordinates are caller-supplied only and are never + logged or persisted. + +- Wire local VMConnect features ([#1768](https://github.com/Devolutions/IronRDP/issues/1768)) ([cf9bdef764](https://github.com/Devolutions/IronRDP/commit/cf9bdef764da6b9aff33b436662441e1503dc93a)) + + Wire VMConnect modes through the client configuration and RDP connector. + Support current-user host authentication without a password, preserve + guest credential boundaries, and attach Hyper-V framebuffer redirection + only for eligible local Windows sessions. + +- Register the graphics pipeline dynamic virtual channel ([#1462](https://github.com/Devolutions/IronRDP/issues/1462)) ([f7b07e6e55](https://github.com/Devolutions/IronRDP/commit/f7b07e6e55cda7063bfa3153549f69f40424fe91)) + + ## Summary + + - `ironrdp-client` never registered `GraphicsPipelineClient`, so the + native client did not open the `Microsoft::Windows::RDS::Graphics` + channel and could not use the Graphics Pipeline. + - Registers it as a DVC alongside Display Control and Echo, with a no-op + `GraphicsPipelineHandler` (the compositor and session render do the + work) and no H.264 decoder, so the client advertises only the non-AVC + capability sets it can decode. + + ## Validation + + - `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + - Codec decoders (H.264 via `openh264-libloading`, ClearCodec #1175, + Progressive #1443) and the connector early-cap flag #1237 layer on + separately. + - #1377 and #1460 have merged. This is now stacked on #1461 alone, so + the merge order is #1461 then this, and #1461's commit appears in the + diff until it lands. Its own change is 3 files, +12/-1. + - Part of #1464. Motivated by #1446. + +- [**breaking**] Add drop-policy classification for RdpOutputEvent ([#1815](https://github.com/Devolutions/IronRDP/issues/1815)) ([b0d5ca88a8](https://github.com/Devolutions/IronRDP/commit/b0d5ca88a8c77dc8b49dbde9392d36a989a5a990)) + + ## Summary + +- [**breaking**] Expose SUPPORT_DYN_VC_GFX_PROTOCOL early-cap flag for EGFX clients ([#1237](https://github.com/Devolutions/IronRDP/issues/1237)) ([5bdb67980c](https://github.com/Devolutions/IronRDP/commit/5bdb67980cb98b070076acbff802254316e786de)) + + Currently, `early_capability_flags` in the GCC core data is built from a + fixed set in `connection.rs`. Clients that want to use the Graphics + Pipeline Extension (MS-RDPEGFX) — by attaching a `DvcClientProcessor` + for `Microsoft::Windows::RDS::Graphics` — have no way to set + `SUPPORT_DYN_VC_GFX_PROTOCOL` without forking the connector, and modern + Windows servers won't open the EGFX channel unless the client advertises + support. + + This PR adds an opt-in `Config.support_dyn_vc_gfx_protocol: bool` + (default `false`). When set, the flag is OR'd into + `early_capability_flags` alongside the existing `WANT_32_BPP_SESSION` + conditional. Existing consumers are unaffected; the doc comment includes + a safety note that setting this without an EGFX implementation will + cause Windows to stop sending legacy bitmap updates, leaving the desktop + blank. + + Used downstream by [Haven](https://github.com/GlassHaven/Haven) (an + Android RDP/VNC client) which implements EGFX with ClearCodec + + RemoteFxProgressive decoders; this lets us drop a vendored fork of + `ironrdp-connector` we currently carry just for this one flag. + + Default `false` to preserve current behaviour. `cargo check --workspace` + clean — six other in-tree `Config { … }` builders updated with the + default-false field. + + --------- + +- Redirect default Windows printer ([#1876](https://github.com/Devolutions/IronRDP/issues/1876)) ([eaadd650c9](https://github.com/Devolutions/IronRDP/commit/eaadd650c9e2be98b57274b72095bf41da6ac3dd)) + + Expose the preconnect RedirectPrinters setting and carry printer + metadata through the RDPDR client factory. + + Discover the current Windows default queue, negotiate its driver + metadata, and stream bounded RAW jobs on a module-pinned worker. Ports, + generic PnP/USB devices, multiple printers, and Easy Print/XPS remain + unsupported. + +- Present EGFX dirty regions ([#1874](https://github.com/Devolutions/IronRDP/issues/1874)) ([2b44c62bdb](https://github.com/Devolutions/IronRDP/commit/2b44c62bdbd997f4aa33bdbaf4749ac4ecc85140)) + + Propagate validated ResetGraphics extents into the session framebuffer + and deliver exact packed desktop damage to ActiveX without rebuilding + full image snapshots. + + Coalesce only fully covered regions, preserve sparse updates with a + 64-event and 256 MiB pixel-data budget, and backpressure without + evicting accepted frame or static-channel payloads. Update retained GDI + and RPC surfaces in place. Reuse framebuffer storage with fallible + growth, and retain software cursor shape, position, visibility, and + clipping across graphics resets. Keep V8 non-AVC negotiation and + full-frame output fallback unchanged. Rejected oversized resets still + destroy prior surfaces and are reported as unsupported without + allocating a session framebuffer. + +### Bug Fixes + +- Preserve bulk compression across reactivation ([#1474](https://github.com/Devolutions/IronRDP/issues/1474)) ([8fcffb9e8f](https://github.com/Devolutions/IronRDP/commit/8fcffb9e8f1a2c468321c05a56ec96144316c90a)) + + Any session that reactivates (Deactivate All → re-activate) loses bulk + decompression and dies right after. Windows consoles reactivate right + after logon, and compression is on by default, so this hits pretty + easily. + + The reactivation path rebuilt the FastPath processor with + [`bulk_decompressor: + None`](https://github.com/Devolutions/IronRDP/blob/079b4842/crates/ironrdp-client/src/rdp.rs#L988). + After that every compressed update got parsed as a raw bitmap: + + ``` + Received compressed FastPath data but no decompressor is configured + BitmapData decode NotEnoughBytes: received 1662, expected 17134 + ``` + +- [**breaking**] Always own a bulk decompressor for FastPath updates ([#1255](https://github.com/Devolutions/IronRDP/issues/1255)) ([0dc0194418](https://github.com/Devolutions/IronRDP/commit/0dc0194418375d504a8041b75ba250dc8eeb21ad)) + + ## Summary + + - A compressed FastPath update is dropped whenever the client did not + negotiate compression, because the decompressor is only built when a + compression type was negotiated. Servers send compressed updates + regardless, for example on a full-frame redraw after a resize, and the + session then fails. Closes #1193. + - The negotiated type is the wrong thing to condition on. It describes + what the client would send, and nothing in `ironrdp-session`, + `ironrdp-client`, `ironrdp-web` or the FFI ever compresses outbound. On + the receive path `BulkCompressor` holds a context per algorithm and + `decompress` selects one per update from the packet's own type bits, so + a decompressor built with any type decodes all of them. + - The `Processor` now owns the decompressor and builds it on the first + update that needs one. `ProcessorBuilder` has no corresponding field, so + there is no `None` a consumer can pass and no path that drops a + compressed update. + - On demand rather than at construction because `ironrdp-web` hardcodes + `compression_type: None` in `build_config` and so never negotiates + compression. Constructing eagerly would charge every web session for a + full set of algorithm contexts, and the two XCRUSH history buffers alone + are 2 MB each, for a decompressor most of those sessions never use. That + consumer is also the one most exposed to this bug, for the same reason. + - `BulkCompressor::new` is now infallible. Its only failure path was a + self-check over NCRUSH's static Huffman tables, a compile-time + invariant, now a `debug_assert`. + + ## Relationship to #1474 + + #1474 is kept, not reverted. `ActiveStage::reactivate` is adopted as the + reactivation entry point at all four call sites it introduced: native + client, web, FFI and the e2e test. + + What this PR removes is the `compression_type` retained on `ActiveStage` + and the `make_bulk_decompressor` helper, because an on-demand + decompressor makes both unnecessary. `reactivate` keeps its behaviour + and loses only the compression plumbing. + + #1474 closed the reactivation instance of #1193, where a rebuild passed + `None` and silently disabled decompression for the rest of the session. + The general case is still open on master: when compression was never + negotiated the retained type is `None`, `make_bulk_decompressor` returns + `None`, and every compressed update takes the drop path in + `fast_path.rs` for the lifetime of the session. Conditioning on the + negotiated type gates the ability to receive on what was negotiated to + send, and nothing sends. + + The evidence that removing the field is safe is #1474's own test. + `test_reactivation_processes_compressed_fastpath_updates` passes + unchanged with `compression_type` gone from the builder: the rebuilt + processor decompresses because every processor can, not because a type + was carried across the rebuild. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + The gated regression test is + `testsuite-core/tests/session/fast_path.rs`, which renders the same + bitmap update plain and bulk-compressed through fresh processors and + asserts identical framebuffers. #1474's + `test_reactivation_processes_compressed_fastpath_updates` in + `testsuite-extra` passes unchanged. + + There is also an inline test in `fast_path.rs` pinning the allocation + invariant, that no contexts are built until an update needs them. Note + that `ironrdp-session` sets `[lib] test = false`, so inline tests in + this crate are not run by `cargo test --workspace`; it runs under `cargo + test -p ironrdp-session --lib`. + + ## Notes + + - This addresses the four points from the 2026-06-24 review. Point 4, + that the `Option` is misleading, is the shape of this change: it is gone + from the public API, and the private one that remains carries no + implication that a consumer could choose not to decompress. Point 1, + whether a cold `Rdp61` context decodes `RDP40` and `RDP50` updates + correctly, is a non-issue: `decompress` selects the algorithm per update + through `CompressionType::from_flags` against per-algorithm receive + contexts, so the construction-time type never constrains the receive + path. Point 3, silent degradation if the constructor fails, is removed + by making `new` infallible. Point 2 is the tests above. + - Breaking across two crates, hence the `fix(bulk,session)!` scope: + `ProcessorBuilder` loses `bulk_decompressor`, `ActiveStageBuilder` loses + `compression_type`, and `ironrdp_bulk::BulkCompressor::new` returns + `Self`. + - Incidental: `ironrdp-session` no longer exposes any `ironrdp_bulk` + type in its public API, so that dependency's lack of a `# public` marker + in `Cargo.toml` is now correct. + +- Make certificate validation explicit ([#1520](https://github.com/Devolutions/IronRDP/issues/1520)) ([f1d53c78d3](https://github.com/Devolutions/IronRDP/commit/f1d53c78d390de1c6778773cdc859d59901466f0)) + + IronRDP deployments commonly use self-signed or private-CA certificates. + This keeps the historical permissive behavior for unmodified callers + while making platform-root and server-name validation an explicit + opt-in. + + ## Approach + + - Keep `upgrade` and `ConfigBuilder` defaults compatible with existing + self-signed endpoints, including the prior native-TLS SNI behavior. + - Expose `CertificateValidation::Strict` for callers that require normal + certificate-chain and hostname validation. + - Retain the Rustls callback path for certificate pinning or other + explicit exception decisions; configuring a callback selects strict + validation before invoking it. + - Preserve CredSSP's existing public-key binding and disabled TLS + resumption behavior. + + MS-CSSP section 3.1.5 does not require a common trusted CA root and + permits servers to use self-signed certificates, so strict verification + cannot be introduced as a transparent default. + + ## Validation + + - `cargo xtask check fmt -v` + - `cargo xtask check lints -v` + - Focused Rustls default/strict/callback runtime test + - Focused native-TLS default/strict runtime test + + --------- + +- Share bulk decompression across output paths ([#1518](https://github.com/Devolutions/IronRDP/issues/1518)) ([6151e21bf5](https://github.com/Devolutions/IronRDP/commit/6151e21bf58b7297e9b4abc2167aa36fc2ba77e4)) + + Bulk compression state is stream-wide, but Fast-Path and slow-path + outputs previously used separate or missing decompression paths. This + could corrupt history-dependent server updates or leave negotiated + slow-path compression undecodable. + + This change owns the negotiated bulk decompressor in `ActiveStage` and + passes it to both X.224 and Fast-Path processing. It retains Share Data + compression metadata through the PDU context, resets decompression + history on reactivation, and initializes consumers from the connection's + negotiated compression type. + + Fast-Path now decompresses each fragment before reassembly so + compression flags apply at packet boundaries. Failures expose bounded + protocol metadata without retaining remote payloads or decoder details. + + Tests cover Share Data metadata propagation, slow-path decompression + behavior, fragmented Fast-Path reassembly and bounded errors, and + compressed Fast-Path updates after reactivation. + + --------- + +- Recover from malformed bitmap updates ([#1521](https://github.com/Devolutions/IronRDP/issues/1521)) ([20e2d414e5](https://github.com/Devolutions/IronRDP/commit/20e2d414e5ac060db25a100ed219f417e19f79b2)) + + ## Summary + - safely discard malformed bitmap and pointer updates without + terminating the session + - request at most one capability-gated full redraw per activation + - propagate Refresh Rect and Suppress Output support through the + connector and generic client + - retain FFI compatibility and focused malformed-update regression + coverage + + ## Stack + Depends on `copilot/fix-session-share-bulk-decompression` (`47270c2a`). + + ## Validation + - `cargo fmt --all -- --check` + - `cargo test -p ironrdp-session --lib` + - `cargo test -p ironrdp-error --lib` + - `cargo check -p ironrdp-connector` + - `cargo check -p ironrdp-client --features native-tls` + - `cargo check -p ffi --features ironrdp/native-tls` + + --------- + +- [**breaking**] Replace DVC wrappers with typed accessors ([#1377](https://github.com/Devolutions/IronRDP/issues/1377)) ([d43ecf9a54](https://github.com/Devolutions/IronRDP/commit/d43ecf9a54363d37e0c485a1e9e73da0d47ae540)) + + Follow-up to #1368. This is not urgent; review whenever the DVC API + direction is worth revisiting. + + Rework DVC channel access APIs so callers can recover a typed processor + together with its dynamic channel id, without exposing internal channel + wrapper types. + + - Add typed borrowed DVC accessors carrying both channel id and + processor borrow for `DrdynvcClient`. + - Keep dynamic channel wrapper types private. + - Align client listener/registration APIs on `DvcClientProcessor`. + +- [**breaking**] Accept the full documented range of Client Core Data keyboardType values ([#1689](https://github.com/Devolutions/IronRDP/issues/1689)) ([c74e7c5c94](https://github.com/Devolutions/IronRDP/commit/c74e7c5c9431c87e57a1550b037867d235c1d362)) + + ## Summary + + KeyboardType (TS_UD_CS_CORE's keyboardType field, MS-RDPBCGR 2.2.1.3.2) + was a closed enum covering discriminants 1 through 7, transcribed from + the field's other documentation in 2.2.7.1.6 (TS_INPUT_CAPABILITYSET), + which omits value 8 (Korean keyboard). The table this type actually + decodes against, 2.2.1.3.2, documents 1 through 8. Any client outside + the narrower range, including every genuine Korean keyboard, had its + whole Client Core Data parse hard rejected before the connection + started. + + Converted KeyboardType to a repr(transparent) struct KeyboardType(pub + u32) with named constants for the eight documented values, mirroring + RdpVersion one field above it in the same struct, and made decode + infallible at all three sites that previously disagreed with each other: + gcc::core_data::client hard rejected, rdp::capability_sets::input + silently dropped to None, and ironrdp-activex's COM setter returned + E_INVALIDARG. + + FreeRDP hit the identical gap in its own documentation-only enum until a + two-line fix (FreeRDP#11035). Neither FreeRDP nor xrdp gate connection + admission on this field at all; both decode it as a raw integer. + Windows' own GetKeyboardType additionally documents 0x51 for generic HID + keyboards, which a narrower fix adding only Korean would not survive. + + Marked as breaking since KeyboardType's public shape changes for any + downstream consumer of ironrdp-pdu outside this workspace. Note that + cargo-semver-checks in this repo's PR automation only runs against the + facade ironrdp crate, which will not see a break confined to + ironrdp-pdu's own API, so the automated breaking-change label may not + apply here even though this is a real one. + + ## Validation + + cargo xtask check fmt/lints/tests/typos/locks all pass. Extended + ironrdp-activex's existing inline COM boundary test to cover value 8 + round tripping and a negative value still being rejected. + +- Forward the target host and port to the gateway ([#1710](https://github.com/Devolutions/IronRDP/issues/1710)) ([f43966cade](https://github.com/Devolutions/IronRDP/commit/f43966cadeb460b3bb02625532143d45447ca14a)) + + The channel-create packet (HTTP_CHANNEL_PACKET) hardcoded port 3389, so + non-3389 RDP targets and Hyper-V VMConnect (port 2179) could not be + tunneled through an RD Gateway. + +- Restore Noop RDPDR channel fallback, fixing audio regres… ([#1791](https://github.com/Devolutions/IronRDP/issues/1791)) ([eb75febca7](https://github.com/Devolutions/IronRDP/commit/eb75febca77946943f2250fe56efb98f4b301dec)) + + …sion + + Regression introduced in 1fbc9bab ("feat: wire RDPDR backends into + client connections", #1600). + + Before that commit, the RDPDR static channel was always attached (using + NoopRdpdrBackend when no real backend was needed), regardless of whether + a native RdpdrBackendFactory was available. #1600 changed this so the + channel is only attached when build_rdpdr_channel() returns Some(_), + i.e. when a factory is supplied and produces at least one device. + + On platforms without a native RDPDR factory (currently: everything + except Windows, since ironrdp-rdpdr-native is a target `cfg(windows)` + dependencies entry and attach_windows_rdpdr_backend() is gated + `#[cfg(windows)]`), rdpdr_factory is always None, so the RDPDR channel + is now never attached at all. + + This silently broke RDPSND audio playback on the client side: with the + RDPDR channel entirely absent from the static channel set, the locally + created "Remote Audio" playback device stopped appearing (confirmed via + git bisect, first bad commit 1fbc9bab, reproduced on Linux). The sound + feature itself, the rdpsnd_backend_kind() selection, and the + enable_audio_playback flag in the Client Info PDU are unaffected by + #1600 and behave the same before/after — only the presence of the RDPDR + channel differs. + +- Re-register AUDIO_INPUT listener so microphone can reopen ([#1797](https://github.com/Devolutions/IronRDP/issues/1797)) ([8811eadd17](https://github.com/Devolutions/IronRDP/commit/8811eadd17652082d8d52f3435649c3b7401c89a)) + + The AUDIO_INPUT (MS-RDPEAI) dynamic virtual channel was registered via + `with_dynamic_channel`, which wraps the processor in an `OnceListener`. + `OnceListener::create` hands out its `RdpeaiClient` exactly once: after + the first DVC Close, it keeps returning `None`, so any later Create + Request for AUDIO_INPUT is answered with `NO_LISTENER` and the + microphone silently stops working for the rest of the session. + + Windows opens and closes AUDIO_INPUT repeatedly as apps grab/release the + mic, so a single-shot registration is not sufficient. + + Switch to `with_listener` with a small `RdpeaiListener` factory that + builds a fresh `RdpeaiClient` (and a fresh `RdpeaiCaptureBackend`/cpal + stream) on every Create Request, matching the pattern already used for + the RDPEWA listener. Gate the new type behind `feature = "sound"` and + widen the existing `DvcChannelListener` import's cfg accordingly, since + it is now also needed outside the `windows` + `dvc-com-plugin` path. + + @mamoreau-devolutions + +- [**breaking**] Serve channels and graphics that Windows moves onto a tunnel ([#2008](https://github.com/Devolutions/IronRDP/issues/2008)) ([bfd16a2e55](https://github.com/Devolutions/IronRDP/commit/bfd16a2e5573d4cd6617dd71b2802b4e7b944a59)) + + Once Soft-Sync has moved dynamic channels onto the reliable UDP tunnel, + Windows keeps using the tunnel for more than channel data. Two gaps kept + the graphics pipeline from working there. + +### Documentation + +- Document RD Gateway behavior ([#1820](https://github.com/Devolutions/IronRDP/issues/1820)) ([5c7d30e156](https://github.com/Devolutions/IronRDP/commit/5c7d30e15623adf5d6ecadd1ae6433b5a963457b)) + + Document the client's legacy dual-channel HTTP fallback and + background MS-TSGU reauthentication behavior. + + Clarify that live RPCH and RDG-UDP/DTLS transports remain unavailable. + +### Build + +- Bump the crypto group across 1 directory with 3 updates ([#1449](https://github.com/Devolutions/IronRDP/issues/1449)) ([e1725e8c8a](https://github.com/Devolutions/IronRDP/commit/e1725e8c8a581b83835647b6ee563a5b3f6c7a1b)) + + + ## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-client-v0.1.0)] - 2026-07-10 Initial release. diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index d3b50eb465..258fb99c3c 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-client" -version = "0.1.0" +version = "0.2.0" readme = "README.md" description = "Portable RDP client engine without GPU acceleration" edition.workspace = true @@ -71,16 +71,16 @@ all = [ [dependencies] # Protocols (core features always on) -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public -ironrdp-session = { path = "../ironrdp-session", version = "0.11" } # public -ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public -ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.8" } +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public +ironrdp-session = { path = "../ironrdp-session", version = "0.12" } # public +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10" } # public +ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.9" } ironrdp-echo = { path = "../ironrdp-echo", version = "0.4" } -ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.3" } +ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.4" } ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1" } ironrdp-rdpeudp = { path = "../ironrdp-rdpeudp", version = "0.1", optional = true } ironrdp-rdpeudp-tokio = { path = "../ironrdp-rdpeudp-tokio", version = "0.1", optional = true } @@ -88,21 +88,21 @@ ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" } # public ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.10", features = ["reqwest"] } ironrdp-rdcleanpath = { path = "../ironrdp-rdcleanpath", version = "0.2" } ironrdp-vmconnect = { path = "../ironrdp-vmconnect", version = "0.1", optional = true } -ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.1" } +ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" } ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public ironrdp-rail = { path = "../ironrdp-rail", version = "0.1" } # Optional protocol crates (activated by features above) -ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.7", optional = true } # public -ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.7", optional = true } # public -ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.9", optional = true } +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8", optional = true } # public +ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.8", optional = true } # public +ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.10", optional = true } ironrdp-rdpeai = { path = "../ironrdp-rdpeai", version = "0.1", optional = true } ironrdp-rdpel = { path = "../ironrdp-rdpel", version = "0.0.0", optional = true } # Optional backend crates (activated by features above) ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.7", optional = true } ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.7", optional = true } -ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.1", optional = true } +ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.2", optional = true } ironrdp-dvc-pipe-proxy = { path = "../ironrdp-dvc-pipe-proxy", version = "0.5", optional = true } # Logging diff --git a/crates/ironrdp-cliprdr-format/CHANGELOG.md b/crates/ironrdp-cliprdr-format/CHANGELOG.md index 8af3400cef..a6274f7b24 100644 --- a/crates/ironrdp-cliprdr-format/CHANGELOG.md +++ b/crates/ironrdp-cliprdr-format/CHANGELOG.md @@ -6,6 +6,57 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-format-v0.2.0...ironrdp-cliprdr-format-v0.3.0)] - 2026-09-30 + +### Features + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +- Expand OLE clipboard snapshots ([#1794](https://github.com/Devolutions/IronRDP/issues/1794)) ([c7766c5a56](https://github.com/Devolutions/IronRDP/commit/c7766c5a56ba668a93737f2ca384fc2b63546376)) + + Expose bounded read-only snapshots for text, locale, DIB/DIBV5, and + Windows HTML formats. Validate FORMATETC and payloads, return + independent HGLOBAL media, and keep file, write, and advisory paths + disabled. + +- Add clipboard image support ([#1877](https://github.com/Devolutions/IronRDP/issues/1877)) ([a59b3b687d](https://github.com/Devolutions/IronRDP/commit/a59b3b687d8ec81cfb79bd6e91e7d3341c866b6b)) + + Adds PNG clipboard image support to the agent daemon and CLI using CLIPRDR DIB conversions. + + + ## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-format-v0.1.4...ironrdp-cliprdr-format-v0.2.0)] - 2026-05-27 ### Build diff --git a/crates/ironrdp-cliprdr-format/Cargo.toml b/crates/ironrdp-cliprdr-format/Cargo.toml index 54c44fdd13..e4f2d60594 100644 --- a/crates/ironrdp-cliprdr-format/Cargo.toml +++ b/crates/ironrdp-cliprdr-format/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-cliprdr-format" -version = "0.2.0" +version = "0.3.0" readme = "README.md" description = "CLIPRDR format conversion library" edition.workspace = true @@ -17,7 +17,7 @@ doctest = false test = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["std"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["std"] } # public png = "0.18" [lints] diff --git a/crates/ironrdp-cliprdr-native/CHANGELOG.md b/crates/ironrdp-cliprdr-native/CHANGELOG.md index 5bf69398cf..0dec70fad7 100644 --- a/crates/ironrdp-cliprdr-native/CHANGELOG.md +++ b/crates/ironrdp-cliprdr-native/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.7.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.7.0...ironrdp-cliprdr-native-v0.7.1)] - 2026-09-30 + + + ## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-native-v0.6.0...ironrdp-cliprdr-native-v0.7.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-cliprdr-native/Cargo.toml b/crates/ironrdp-cliprdr-native/Cargo.toml index c4c55328ce..26cf245fd8 100644 --- a/crates/ironrdp-cliprdr-native/Cargo.toml +++ b/crates/ironrdp-cliprdr-native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-cliprdr-native" -version = "0.7.0" +version = "0.7.1" readme = "README.md" description = "Native CLIPRDR static channel backend implementations for IronRDP" edition.workspace = true @@ -17,8 +17,8 @@ doctest = false test = false [dependencies] -ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.7" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } tracing = { version = "0.1", features = ["log"] } [target.'cfg(windows)'.dependencies] diff --git a/crates/ironrdp-cliprdr/CHANGELOG.md b/crates/ironrdp-cliprdr/CHANGELOG.md index c2c748d099..28c821fbe5 100644 --- a/crates/ironrdp-cliprdr/CHANGELOG.md +++ b/crates/ironrdp-cliprdr/CHANGELOG.md @@ -6,6 +6,247 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.7.0...ironrdp-cliprdr-v0.8.0)] - 2026-09-30 + +### Features + +- Add a chunked file-contents fetch primitive ([#1742](https://github.com/Devolutions/IronRDP/issues/1742)) ([58bcbaf572](https://github.com/Devolutions/IronRDP/commit/58bcbaf572d1c75c40daec3921ec217a811c25a4)) + + ## Summary + + - MS-RDPECLIP's file-contents protocol is receiver-driven: fetching a + file means sending a sequence of byte-range FileContentsRequests and + reassembling the FileContentsResponses. Nothing in this crate helps + with that sequencing, so every consumer that wants a whole file ends + up writing its own request/response loop by hand. + - Adds ChunkedFetch: a small state machine that produces the next + request to send and consumes each response, tracking offset and + buffered bytes so the caller doesn't have to. The size phase is + optional: construct with a known size (the common case, since it's + usually already available from an earlier FileGroupDescriptorW + exchange) to skip straight to range requests, or without one to + query it first. + - Every request carries an explicit clip_data_id, the id + CliprdrBackend::on_remote_file_list already hands the caller for the + file's locked snapshot. request_file_contents can auto-fill this from + current_lock_id when left unset, but a fetch spans multiple requests + and a FormatList between them can expire the old lock and install a + new one under the same field, so auto-filling would let a fetch + already in flight silently retarget to whichever lock happens to be + current at send time. + - A caller-provided max_total_size bounds how large a file this type + will try to buffer in memory, checked as soon as the size is known + (either the caller-supplied total_size, or the peer's own SIZE + response), independent of what that size claims to be. + - Two defensive cases beyond the happy path: an empty range response + before the file is complete fails the fetch rather than + re-requesting the same range forever, since the protocol has no + "not ready yet" signal that would legitimately produce one, and a + response longer than what's left is clamped rather than trusted, + since accepting it as-is would grow the buffer past the file's + total size. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Tests live + in ironrdp-testsuite-core since this crate sets [lib] test = false. + + ## Notes + + No public API break: this is a new module with entirely new public + types, no existing signature changes. + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +- Add a clipboard sync loop detector ([#1739](https://github.com/Devolutions/IronRDP/issues/1739)) ([4556adcd98](https://github.com/Devolutions/IronRDP/commit/4556adcd985f491bddfef8099638141dce037ff3)) + + ## Summary + + - An embedder bridging CLIPRDR to a local OS clipboard commonly hits a + feedback loop: content copied on one side syncs to the other, the + other side's own change notification fires for that same content, and + the embedder syncs it back, forever. ironrdp-cliprdr has no visibility + into the local OS clipboard on either side by design (that lives + entirely in the embedder's CliprdrBackend implementation), so it + can't detect this on its own, but nothing in the crate gives an + embedder a building block to break the cycle either. Every consumer + bridging to a real desktop clipboard ends up needing to write the + same hash-and-time-window correlation logic themselves. + - Adds LoopDetector: hashes recent format lists and content by source + (Remote/Local) within a configurable time window, so an embedder can + ask "would syncing this out right now just echo what the other side + just sent" before acting, plus an optional per-source rate limit as + a belt-and-suspenders guard against update storms. + - Every method that needs the time takes an explicit now_ms rather than + reading a clock itself, matching CliprdrBackend::now_ms()/elapsed_ms() + already on this crate for the same wasm/test-determinism reasons. + - Hashing uses DefaultHasher rather than adding a crypto dependency: + nothing here defends against an adversary, it only needs to avoid + mistaking two different clipboard payloads for the same one within + one process's own recent history. + - would_cause_loop takes an explicit source parameter, matching its + siblings would_cause_content_loop/should_skip_sync. An earlier + version of this algorithm I'd written elsewhere hardcoded that + direction on would_cause_loop specifically, which was an + inconsistency with its own siblings rather than an intentional + asymmetry; fixed here. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Tests live in + ironrdp-testsuite-core since this crate sets [lib] test = false. + + ## Notes + + No public API break: this is a new module with entirely new public + types, no existing signature changes. + +### Bug Fixes + +- A clipboard failure must not disconnect the session ([#1980](https://github.com/Devolutions/IronRDP/issues/1980)) ([124406d881](https://github.com/Devolutions/IronRDP/commit/124406d881af377cb339e8c131831f69e3cf7e45)) + + Two halves of the same problem: a rejected clipboard operation is both + invisible to the backend waiting on it and fatal to the session. + + ## `ironrdp-server`: a clipboard error ends `client_loop` + + `dispatch_server_events` propagates every `CliprdrServer` error with + `?`: + + ```rust + ClipboardMessage::SendFileContentsRequest(request) => cliprdr.request_file_contents(request), + ... + } + .map_err_kind("failed to send clipboard event", ServerErrorKind::Pdu)?; + ``` + + That error unwinds through `dispatch_events` → `client_loop` → + `client_accepted` → `accept_finalize`, and the accept loop logs + `"Connection error"`, resets the static channels and calls + `on_disconnected`. **The session is torn down.** + + The things that can trigger it are all ordinary: a file contents request + that became stale because the remote clipboard changed under it, a + capability that was never negotiated, `MAX_PENDING_FILE_REQUESTS` + backpressure, a malformed flag combination. None is a reason to + disconnect a peer whose display, audio and input are fine. + + Three lines above, `ClipboardMessage::Error` already takes the opposite + view: + + ```rust + ClipboardMessage::Error(error) => { + error!(?error, "Handling clipboard event"); + continue; + } + ``` + + This makes the two paths agree. + + ## `ironrdp-cliprdr`: a rejection strands its caller + + `request_file_contents` has ten rejection paths and all of them return a + bare `PduError`. The error carries no stream id, so an embedder that + surfaces it has no way to map it back to the transfer that failed — + whatever is awaiting that stream just hangs until an unrelated timeout + fires. + + The crate already solves this elsewhere. `FormatListResponse::Fail` + does: + + ```rust + // Notify backend for each pending request so it can clean up + // (e.g. reject pending download promises in WASM). + for stream_id in stream_ids { + self.backend.on_file_contents_response(FileContentsResponse::new_error(stream_id)); + } + ``` + + Every rejection in `request_file_contents` now does the same before + returning. **The `Err` is unchanged** — this is strictly additive for + existing callers. + + ## Tests + + - + `crates/ironrdp-testsuite-core/tests/clipboard/file_contents_state_machine.rs` + — `a_rejected_request_fails_its_stream_on_the_backend` (out-of-bounds + index) and + `a_request_rejected_before_ready_fails_its_stream_on_the_backend` + (`require_ready`, the path most likely to strand a caller). Both assert + the error is still returned *and* that the backend saw an error response + for the right stream id. + - `crates/ironrdp-server/src/server.rs` — + `a_refused_clipboard_message_does_not_end_the_client_loop` drives + `dispatch_server_events` with a request the channel refuses and asserts + `RunState::Continue`. + + All 211 existing clipboard tests and the 15 `ironrdp-server` lib tests + pass unchanged. + + 🤖 Generated with [Claude Code](https://claude.com/claude-code) + + --------- + +- Correlate format data responses with requests in order ([#2053](https://github.com/Devolutions/IronRDP/issues/2053)) ([cd5739d129](https://github.com/Devolutions/IronRDP/commit/cd5739d1299aefd22eb38b206cd73222b37bfff4)) + + A `FormatDataResponse` names no format, so `Cliprdr` has to work out + which request each response answers. It tracked only the most recently + sent request (`pending_format_data_request: Option`), + which pairs responses with the wrong request in two cases: + + - **A second paste initiated before the first is answered.** + `initiate_paste` overwrote the slot, so the first response was + correlated with the second request. + - **A FormatList arriving while a request is outstanding.** + `handle_format_list` cleared the slot, although that request's response + was still coming. + + The file-list interception is where this bites. A text response can be + parsed as the file list, or the real file list is handed to the backend + as plain data and `on_remote_file_list` never runs. + + The second case is easy to hit. Firefox and Word announce a single copy + with two FormatLists a few milliseconds apart, so a backend that fetches + on each announcement has a request outstanding when the next one + arrives. We ran into the same misattribution in macrdp's clipboard + backend while live-testing rich-text copy from Windows 11 over mstsc, + then found `Cliprdr` had the same single-slot correlation. + + + ## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cliprdr-v0.6.0...ironrdp-cliprdr-v0.7.0)] - 2026-07-10 ### Features diff --git a/crates/ironrdp-cliprdr/Cargo.toml b/crates/ironrdp-cliprdr/Cargo.toml index a4d4f6ad3d..96c25203b1 100644 --- a/crates/ironrdp-cliprdr/Cargo.toml +++ b/crates/ironrdp-cliprdr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-cliprdr" -version = "0.7.0" +version = "0.8.0" readme = "README.md" description = "CLIPRDR static channel for clipboard implemented as described in MS-RDPECLIP" edition.workspace = true @@ -22,9 +22,9 @@ test = false __test = ["dep:visibility"] [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public tracing = { version = "0.1", features = ["log"] } bitflags = "2.11" visibility = { version = "0.1", optional = true } diff --git a/crates/ironrdp-connector/CHANGELOG.md b/crates/ironrdp-connector/CHANGELOG.md index 65a88959ba..73a5ee0d08 100644 --- a/crates/ironrdp-connector/CHANGELOG.md +++ b/crates/ironrdp-connector/CHANGELOG.md @@ -6,6 +6,893 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.10.0...ironrdp-connector-v0.11.0)] - 2026-09-30 + +### Security + +- [**breaking**] Implement multitransport bootstrapping handshake ([#1098](https://github.com/Devolutions/IronRDP/issues/1098)) ([e45fbfe0f5](https://github.com/Devolutions/IronRDP/commit/e45fbfe0f597011706e77fc174ca14e5e9d435b9)) + + ## Summary + + Makes the `MultitransportBootstrapping` state functional instead of a + no-op + pass-through. After licensing the server may send 0, 1, or 2 Initiate + Multitransport Request PDUs before capabilities exchange. Each one is + surfaced + to the application, which establishes UDP transport (RDPEUDP2 + TLS + + RDPEMT) + or declines, and the connector reports the outcome back to the server. + + ## API + + Mirrors the existing `should_perform_X()` pause-point pattern used by + TLS + upgrade and CredSSP, but uses `complete_X()` / `skip_X()` rather than + `mark_X_as_done()` because completion carries result data: + + - `should_perform_multitransport()`: true while a request awaits an + outcome + - `multitransport_request()`: the request awaiting an outcome, or `None` + - `complete_multitransport(result, output)`: report the outcome, resume + - `skip_multitransport(output)`: decline, resume + + `complete_multitransport` accepts a `MultitransportResult` (a `Success` + / + `Failure(hresult)` enum) rather than a caller-built response PDU. The + connector + builds the response internally from the stored request ID. + + Requests are surfaced one at a time rather than as a batch. There is no + end + marker for the set, and MS-RDPBCGR 3.2.5.15.1 requires the client to act + on a + request as soon as it decodes one, so waiting to learn how many are + coming is + not an option the protocol offers. `should_perform_multitransport()` can + therefore come round twice; the caller answers reliable and lossy + separately. + + ## Approach + + **Routing.** Requests arrive on the negotiated MCS message channel + (2.2.15.1) and the Demand Active on the I/O channel, so the channel + decides + which is which. The message channel also carries NetworkAutoDetect since + #1348, + so a decode still confirms what arrived there, but the I/O channel is + never + speculatively decoded as multitransport. A PDU on neither channel is an + error. + + For the decode to be a sound confirmation the request decoder must + reject a + Demand Active, so this PR also tightens `MultitransportRequestPdu` to + require + the exact `SEC_TRANSPORT_REQ` security-header flag. + + **Yielding.** Each request is surfaced the moment it decodes. Responding + returns the connector to `MultitransportBootstrapping` to read whatever + comes + next, which may be a second request or the Demand Active. Nothing is + buffered + and nothing is replayed: when the request is surfaced the Demand Active + has not + arrived yet. + + **Soft-Sync.** The Initiate Multitransport Response is the Soft-Sync + signalling path (2.2.15.2), permitted only when both peers advertised + `SOFTSYNC_TCP_TO_UDP` in their GCC `MultiTransportChannelData`. The + server's + block is retained from the GCC exchange and checked against the client's + configured flags. One rule covers both paths: + + - Soft-Sync negotiated: always respond, `S_OK` or `E_ABORT`, including + on + `skip_multitransport()`, which 3.2.5.15.1 requires. Both the async and + blocking drivers skip automatically, so without this every default + client + leaves a compliant server waiting. + - Not negotiated: never respond. The outcome is reported in band on the + new + transport, and putting anything on the main channel would be the + violation. + + The response goes on the message channel per 2.2.15.2 and 3.2.5.15.2. If + Soft-Sync was negotiated but no message channel exists the connector + errors + rather than falling back to the I/O channel, and that check runs before + the + pending state is taken, so the caller is left with a connector it can + still + inspect or decline from. + + ## Wire behaviour + + On the wire TCP and UDP negotiation happen in parallel: the UDP + transport is + established alongside the ongoing TCP handshake, and its completion + signals the + dynamic-channel layer that subsequent channels may migrate to UDP. The + connector's API yield point here is a Rust affordance, not a + spec-mandated TCP + pause. Thanks to @hardening for the correction. + + ## Tests + + Connector state-machine tests in `ironrdp-testsuite-core` drive the + public API + with the shared `SERVER_DEMAND_ACTIVE` fixture: + + - a request is surfaced on arrival, without waiting for a following PDU + (regression test for the stall); + - responding returns to bootstrapping so a second request is read + normally; + - a third request is rejected per the 2.2.15.1 cap; + - a Demand Active on the I/O channel ends bootstrapping; + - the response targets the message channel, decoded back off the wire; + - a `Failure` result is carried through; + - `skip` sends `E_ABORT` under Soft-Sync, and nothing without it; + - `complete` emits nothing without Soft-Sync but still resumes; + - a failed response leaves the connector in `MultitransportPending`, + still able + to report or decline, rather than `Consumed`; + - `complete` / `skip` outside `MultitransportPending` error; + - a Demand Active's user data does not decode as a + `MultitransportRequestPdu` + (regression test for the decoder tightening above). + +- [**breaking**] Support session resume via the auto-reconnect cookie ([#1501](https://github.com/Devolutions/IronRDP/issues/1501)) ([74b3365c1f](https://github.com/Devolutions/IronRDP/commit/74b3365c1f98c0da6feed7507779c67e1b8e6d08)) + + > **Rebased onto post-#1522 master.** #1509 landed the server half of + #1508 while this was open, including the `ClientAutoReconnect` + structure. This PR no longer declares it; it extends it, and picks up + the parts #1509 did not build. + + ## What + + The client half of automatic reconnection. The session layer surfaces + the Server Auto-Reconnect Cookie, `ironrdp-pdu` derives and verifies the + client's response to it, and the connector sends that response when + resuming a session. + + ## Why + + A client whose connection drops ungracefully can reattach to its session + instead of making the user log on again, provided it returns the cookie + the server issued during logon ([MS-RDPBCGR] 1.3.1.5). + + #1509 built the server side of that: it validates a returning + `ARC_CS_PRIVATE_PACKET` and rotates the random. Nothing answers it. + `ironrdp-session` decodes the cookie and drops it, `ironrdp-connector` + has no way to send one back, and `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` still sits in + `ironrdp-client`. So `ironrdp-client` cannot resume a session against + `ironrdp-server`, and the validation #1509 added has no in-tree + counterpart to exercise it. + + The wire encoding was already there. `ExtendedClientOptionalInfo` + carries, encodes and decodes a 28-byte `autoReconnectCookie` and its + builder already had a `reconnect_cookie` step; `ServerAutoReconnect` + already decoded; #1509 added `ClientAutoReconnect` and its decode. + Nothing connected them. + + ## The three parts + + **Receive.** `SaveSessionInfo` now also surfaces the cookie, as + `ProcessorOutput::AutoReconnectCookie` and + `ActiveStageOutput::AutoReconnectCookie`. #1522 added a `SaveSessionInfo + { logon_complete }` output on that same handler; the two coexist rather + than compete, since both are read off one PDU and neither supersedes the + other. The handler emits the logon notification unconditionally and + appends the cookie when one is present, and a test pins that surfacing + the cookie does not suppress the notification. #1509's server replaces + the cookie whenever a client connects and again hourly ([MS-RDPBCGR] + 3.3.6.2), so this can arrive more than once in a session and the + consumer keeps the most recent. + + **Derive.** `ClientAutoReconnect::from_server_cookie` implements + [MS-RDPBCGR] 5.5: + + > The auto-reconnect random is used to key the HMAC function + ([RFC2104]), which uses MD5 as the iterative hash function. The security + verifier is derived by applying the HMAC to the client random received + in Step 3. + > + > `SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom)` + > + > When Enhanced RDP Security is in effect the client random value is not + generated (section 5.3.2). In this case, for the purpose of generating + the security verifier, the client random is assumed to be an array of 32 + zero bytes. + + IronRDP implements no Standard RDP Security path (there is no Security + Exchange PDU), so the zero-client-random case is the only one that + arises. As 5.5 notes, that makes the verifier constant for a given + cookie, so it proves possession of the cookie and nothing more; session + security comes from the outer TLS/CredSSP handshake. + + @clintcan independently confirmed this construction against real + **mstsc** while validating #1509 + ([comment](https://github.com/Devolutions/IronRDP/pull/1509#issuecomment-5151200681)): + a Windows client's `ARC_CS_PRIVATE_PACKET` verifies against + `HMAC-MD5(random_bits, [0u8; 32])`. That is the same derivation + implemented here, so the two halves interoperate with Microsoft's client + and not only with each other. + + **Send.** `ClientConnector::with_auto_reconnect_cookie` takes the cookie + last received and makes the connector put the derived Client + Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. + Absent, that PDU is byte-for-byte what it was. + + Unlike the server packet, this structure has no enclosing logon-info + field header, so it encodes to exactly the 28 bytes the cookie field + expects. `to_bytes` writes that layout directly rather than going + through `Encode`, so filling a fixed-size field has no error path a + caller must handle; a test pins the two to agree. + + ## One derivation, not two + + Putting `from_server_cookie` in `ironrdp-pdu` would leave the workspace + with two implementations of 5.5, since #1509 added a private HMAC to + `ironrdp-server`. So `ClientAutoReconnect` also gains `verify`, and the + server routes through it. + + `verify` keeps the constant-time comparison the server had. The verifier + is the whole credential, so a comparison returning early on the first + differing byte would let a peer recover it a byte at a time from the + timing; the session identifier is not secret and is compared normally. + `ironrdp-server` keeps the policy around the check, which cookies are + live and whether the security protocol permits auto-reconnect, and drops + its `hmac` and `md-5` dependencies. `hmac` moves to `ironrdp-pdu` as + `default-features = false`; the crate's full feature powerset still + checks clean, including `--no-default-features`. + + I would rather not have reached into `ironrdp-server` in a + `pdu,session,connector` change, but the alternative was shipping the + duplicate and filing a follow-up to remove it, which is a worse trade + for reviewer time. + + ## Tests that were not running + + That move also rehomes the known-answer tests @clintcan contributed on + #1509. They went in as an inline `#[cfg(test)]` module in + `crates/ironrdp-server/src/server.rs`, and that crate sets `[lib] test = + false`, so they have never executed in CI. They now live in + `ironrdp-testsuite-core` against the public API, where CI runs them: his + HMAC-MD5 reference vector is kept as a second vector alongside a + differently-keyed one, plus the cases for a tampered verifier and a + mismatched logon ID. + + Worth flagging separately: `ironrdp-server` is not alone. + `ironrdp-agent`, `ironrdp-session` and `ironrdp-web` also set `[lib] + test = false` and between them carry 16 files of inline `#[cfg(test)]` + modules that CI never runs. That is out of scope here, but I am happy to + open an issue if it would be useful. + + ## Breaking changes + + `ActiveStageOutput` and `x224::ProcessorOutput` gain a variant, and + `ClientConnector` gains a public field, so exhaustive matches and struct + literals need updating. + + Confirmed with `cargo-semver-checks` against the merge-base: those three + are the only findings this branch introduces. The others it reports on + `master` today (`ShareDataPdu::Compressed` and the `ShareDataCtx` fields + from #1518, `ProcessorBuilder.bulk_decompressor` from #1518, + `ServerEvent::SetAutoReconnectCookie` from #1509) are present on + `master` unchanged. The `ironrdp-pdu` additions are additive. + + ## Scope + + This is the library half. `ironrdp-client`, `ironrdp-web` and the FFI + bindings gain an arm for the new output but none of them reconnect + automatically yet; that is the remaining part of #271, and the existing + `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` in `ironrdp-client` marks where it goes. + + I kept receive, derive and send together deliberately. Split up, none of + them is usable on its own: without the receive half there is no way to + obtain a cookie, and without the send half there is nothing to do with + one. + + ## Tests + + Thirteen, all in `ironrdp-testsuite-core`. + + On the packet and the derivation: the `SecurityVerifier` matches two + independently computed HMAC-MD5 vectors of 32 zero bytes under different + keys, so the tests pin the derivation rather than restating the code; + the logon ID carries over from the server cookie; the encoding matches + the 2.2.4.3 field layout byte for byte with `cbLen` fixed at `0x1C`; + `to_bytes` agrees with `Encode`; it round-trips; and it rejects both a + wrong packet length and an unknown version. + + On verification: a derived answer is accepted, a single flipped byte in + the verifier is rejected, a correct verifier under a different logon ID + is rejected, and an answer derived from a different random is rejected. + + On the surfacing path: a Save Session Info PDU framed the way a server + sends it, through the real x224 processor, yields an + `AutoReconnectCookie` carrying the right logon ID and random bits, + alongside #1522's logon notification rather than in place of it; and one + without a cookie surfaces no cookie. + + ## Verification + + `cargo xtask check fmt/lints/tests/typos/locks` all pass on 1.94.1, + including a `fuzz/` build before the lock check. + + ## Note + + #1496 also touches the `ClientAutoReconnect` declaration. Whichever of + the two lands second needs a one-line rebase on the derive attribute; + happy to take that in either order. + +- Connect to Windows Sandbox named pipes ([#1580](https://github.com/Devolutions/IronRDP/issues/1580)) ([39b020343d](https://github.com/Devolutions/IronRDP/commit/39b020343d962962bfbefc89939be64d5c716196)) + + Windows Sandbox's default attach path is a local named pipe carrying + plain TPKT/X.224 with PROTOCOL_RDP and ENCRYPTION_LEVEL_NONE, not + TCP:3389 or VMConnect. Allow the connector and client to complete that + sequence only via an explicit opt-in (`enable_standard_rdp_security`; + NamedPipe enables it), and teach ironrdp-agent to resolve pipe path and + guest credentials from WindowsSandboxServer after `wsb start`. + + Adds Transport::NamedPipe, ironrdp_named_pipe/ironrdp_sandbox_id + properties, sandbox list/config/stop CLI helpers via an in-process + h2/gRPC client on the per-user `\\.\pipe\wsandbox\{guid}` pipe (no .NET + helper), and connect --sandbox-id / --sandbox-pipe. Sandbox-derived + properties are the merge base; explicit .rdp/--prop/flags override them + while NamedPipe TLS/CredSSP stay forced off. Local :2179+PCB remains + unsupported. + +- Support advanced session settings ([#1781](https://github.com/Devolutions/IronRDP/issues/1781)) ([14ef4fd49f](https://github.com/Devolutions/IronRDP/commit/14ef4fd49fcf950169806866ee67db0c49662cfc)) + + Wire administrative-session GCC data, opaque load-balance routing + tokens, and RDPSND quality selection through the ActiveX settings + objects and client configuration. + + Keep unrelated transport, cache, video, device, and security policy + slots as explicit E_NOTIMPL failures. + + --------- + +### Features + +- Expose the server's Input capability flags on ConnectionResult ([#1488](https://github.com/Devolutions/IronRDP/issues/1488)) ([9bcf13438c](https://github.com/Devolutions/IronRDP/commit/9bcf13438cb068b53a8cb0dc23477c498727d49f)) + + Per [MS-RDPBCGR] 2.2.8.1.2, a client must not send fast-path input + events unless the server advertised `INPUT_FLAG_FASTPATH_INPUT` or + `INPUT_FLAG_FASTPATH_INPUT2` in its Input Capability Set. Today the + connector discards the server's capability sets after Demand Active, so + client code has no way to honour that requirement. + + This PR captures the server's Input capability flags during capabilities + exchange, carries them through the `ConnectionFinalization`/`Finalized` + activation states (so they are refreshed correctly across a + Deactivation-Reactivation Sequence too), and surfaces them as + `ConnectionResult::input_flags`. The session layer can then choose + between fast-path and slow-path input per server. + + ### Motivation / real-world interop + + This is not theoretical: VirtualBox's VRDP server closes the connection + outright on receiving a fast-path input PDU — its `VBox.log` reports + + ``` + VRDP: Network packet length is incorrect 0x0004. Closing connection. + ``` + + (a single fast-path scancode event is a 4-byte packet). VirtualBox never + advertises fast-path input; its Demand Active offers + `InputFlags(SCANCODES)` alone. mstsc and FreeRDP honour the negotiation + and fall back to slow-path `TS_INPUT_PDU`s, which is why they work + against VRDE. + + Haven (Android RDP client built on IronRDP) has been shipping this exact + change as a vendored-connector patch since v5.86.1, with the slow-path + fallback keyed off `ConnectionResult::input_flags`. Verified against a + real VirtualBox 7.2.6 VRDE server: before the gate, the first arrow-key + press killed the session with the log line above; with the gate, + extended input sessions run clean, and a fast-path-capable server on the + same host still takes the fast-path branch. (Discussed in #1158; this is + the third and last piece Haven carries in its connector fork, alongside + #1237 and #1472.) + + ### Changes + + - `connection_activation.rs`: capture `input_flags` from the + `CapabilitySet::Input` in Server Demand Active (empty if absent); add + the field to `ConnectionActivationState::{ConnectionFinalization, + Finalized}`. + - `connection.rs`: add `ConnectionResult::input_flags`, populated from + the `Finalized` state. + - Call sites in `ironrdp-client`, `ironrdp-web`, ffi, and the e2e test + updated for the new variant field (all currently ignore it). + + ### Testing + + - Two new integration tests in + `ironrdp-testsuite-core/tests/session/connection_activation.rs`: the + fixture's Demand Active yields `SCANCODES | MOUSEX | UNICODE | + FASTPATH_INPUT_2` in the `ConnectionFinalization` state, and a Demand + Active with the Input capability stripped yields `InputFlags::empty()`. + - `cargo check --workspace --all-targets`, `cargo clippy --workspace + --all-targets`, and `cargo fmt --check` are clean; the 7 activation + tests pass. + +- Support runtime-defined static virtual channels ([#1517](https://github.com/Devolutions/IronRDP/issues/1517)) ([8b4c483ba0](https://github.com/Devolutions/IronRDP/commit/8b4c483ba0c900a8de0b2718347754f56dd363ba)) + + ## Summary + - add keyed runtime-defined static-channel registration, lookup, and + negotiated ID attachment + - enforce the static-channel limit and reject malformed SVC fragment + sequences + - wire generic connector, acceptor, and session name-based dispatch + support + + ## Testing + - `cargo test -p ironrdp-testsuite-core --test integration_tests_core + svc::` + - `cargo clippy -p ironrdp-testsuite-core --test integration_tests_core + -- -D warnings` + + --------- + +- Expose generic session configuration and lifecycle APIs ([#1522](https://github.com/Devolutions/IronRDP/issues/1522)) ([57b1366650](https://github.com/Devolutions/IronRDP/commit/57b13666506dc40c15b4c4702d35150beee99133)) + + ## Summary + - expose generic client configuration for connection metadata, + compression, shell/work directory, audio, and runtime static-channel + factories + - add bounded input delivery with independent close cancellation, host + clipboard plumbing, lifecycle events, and Display Control resize + readiness/fallback handling + - update agent, viewer, web, FFI, examples, and tests for the generic + APIs + + ## Stack dependencies + This PR is stacked on `copilot/tls-validation-policy` (`b2bbcece`), + which already includes the merged runtime static-channel support from + `master`. It intentionally contains no TLS implementation/policy, + ActiveX/COM, SVC implementation, decompression, or bitmap-recovery + changes. + + ## Validation + - `cargo fmt --check --all` + - `cargo xtask check tests --no-run -v` + - `cargo xtask check lints -v` + - `cargo test -p ironrdp-client --lib --features rustls` + - `cargo check -p ironrdp-agent -p ironrdp-viewer -p ironrdp-web -p ffi` + + --------- + +- Surface ShareDataPdu variant in unexpected-PDU errors ([#1329](https://github.com/Devolutions/IronRDP/issues/1329)) ([df1f7e7faa](https://github.com/Devolutions/IronRDP/commit/df1f7e7faaf068435bfbbe1efcb4a8800ebb3d9f)) + + ## Summary + + - Addresses ask 1 of #1232: when the server sends a + `ShareControlPdu::Data` wrapping an unexpected `ShareDataPdu`, the three + error sites in `headers.rs` and `connection_activation.rs` now drill + into the `Data` wrapper and surface the inner variant name instead of + reporting only `"Data"`. + - For `ServerSetErrorInfo` specifically (the asker's high-value case), + the existing `ErrorInfo::description()` is appended so callers can see + why the server rejected the session without substring matching on the + `Reason` string. + - New `pub fn describe_unexpected_share_control_pdu` in `headers.rs` + centralizes the formatting; `decode_share_data`, `decode_io_channel`, + and `ConnectionActivation::CapabilitiesExchange` all route through it. + - Non-`Data` variants continue to use the outer `as_short_name()`, so + diagnostics for `ServerDeactivateAll` and `ClientConfirmActive` are + preserved verbatim. + + ## Validation + + - Three unit tests in `headers::tests` cover the helper: a non-`Data` + variant (`ServerDeactivateAll`), a `Data` wrapper around a + non-SetErrorInfo inner (`Update(Vec::new())`), and a `Data` wrapper + around `ServerSetErrorInfo` carrying + `ProtocolIndependentCode::ServerDeniedConnection`. + - `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + - Helper is `pub`, not `pub(crate)`: it has to be, since + `ironrdp-connector`'s `connection_activation.rs` calls it cross-crate. + That adds + `ironrdp_pdu::rdp::headers::describe_unexpected_share_control_pdu` to + `ironrdp-pdu`'s public surface. Additive and non-breaking, confirmed by + `cargo semver-checks --baseline-rev `: no update required + for either `ironrdp-pdu` or `ironrdp-connector`. + - Ask 2 from #1232 (an optional structured `ConnectorErrorKind` variant + for "server rejected at capabilities phase") is intentionally deferred. + The asker framed it as optional and the wire-level information is now + available in the `Reason` string. + - `Refs #1232` rather than `Closes` so the issue stays open while you + decide on ask 2. + +- Add IronRDP ActiveX COM server ([#1523](https://github.com/Devolutions/IronRDP/issues/1523)) ([ee58b7c5f2](https://github.com/Devolutions/IronRDP/commit/ee58b7c5f283ef64be93a8483242f49070c6cdc9)) + + ## Summary + - Add the IronRDP ActiveX COM server and native MSTSC host integration. + - Provide bounded native-host diagnostics, credential-bridge support, + and an AxHost test harness. + - Preserve the minimal client, connector, and error integrations needed + by the control. + + ## Validation + - cargo check -p ironrdp-activex + - Focused ironrdp-error and ironrdp-connector tests + - cargo test -p ironrdp-activex --lib --no-run + - cargo fmt --all -- --check + - cargo xtask check locks -v + + --------- + +- Hyper-V vmconnect support ([#1503](https://github.com/Devolutions/IronRDP/issues/1503)) ([a7cc067d50](https://github.com/Devolutions/IronRDP/commit/a7cc067d5069cbbcb13bae3e0561c0611da3bcf6)) + + Adds Hyper-V VMConnect's direct ordering: PCB → TLS → CredSSP → X.224. + + Enhanced Session is the default (`GUID;EnhancedMode=1`), with + `--vmconnect-basic` for the synthetic console. Kept this separate in + `ironrdp-vmconnect`; no SPN changes. + + Tested against the nested Hyper-V lab: + - Enhanced: `HYBRID_EX`, rendered 1280×720 + - Basic: `HYBRID`, rendered 1280×720 + - `cargo xtask check fmt/lints/tests -v` + + --------- + +- Support connection correlation info ([#1582](https://github.com/Devolutions/IronRDP/issues/1582)) ([c4483617ba](https://github.com/Devolutions/IronRDP/commit/c4483617ba05c31182b58c58be66bd41120a076d)) + + Encode the optional 36-byte X.224 RDP_NEG_CORRELATION_INFO block and + reject malformed negotiation records. + +- Negotiate static channel chunk sizing ([#1622](https://github.com/Devolutions/IronRDP/issues/1622)) ([4e3903fbbe](https://github.com/Devolutions/IronRDP/commit/4e3903fbbef2904505f35e3437a7106807ac5987)) + + Use the validated server VCChunkSize for outgoing static virtual channel + data and retain 1600-byte chunks when it is absent or invalid. + + Apply refreshed values after reactivation across native, web, and FFI + active stages while preserving channel flags. + +- Forward negotiated windowing orders ([#1631](https://github.com/Devolutions/IronRDP/issues/1631)) ([0c3fbe78b4](https://github.com/Devolutions/IronRDP/commit/0c3fbe78b4366533b9fcea046b2b53654e003a72)) + + Preserve Window List support during activation. + Forward validated orders through ActiveStage and the raw FFI output. + Desktop and web consumers retain their existing behavior. + +- Add RemoteApp channel support ([#1637](https://github.com/Devolutions/IronRDP/issues/1637)) ([ab48c6cb8c](https://github.com/Devolutions/IronRDP/commit/ab48c6cb8c017504f8a92799aeb91b821c50a13a)) + + Configure and negotiate RAIL connections, then route its static channel + through the portable client with bounded request queues and server + control events. + +- Wire MS-RDPEAI capture into Windows client and ActiveX ([#1642](https://github.com/Devolutions/IronRDP/issues/1642)) ([205fe038cc](https://github.com/Devolutions/IronRDP/commit/205fe038cc693598adf803fe181526b789b2ec3d)) + + Add the client MS-RDPEAI capture path on top of hardened RDPSND + playback: connector CFG + static channel wiring, CPAL PCM capture + backend, ironrdp-client --audio-capture, and ActiveX + AudioCaptureRedirectionMode. + + PCM capture only accepts encode formats that match the Open capture + stream, rejects non-16-bit capture (Data PDU size contract), and gates + the capture backend behind ironrdp-rdpsnd-native/capture. + + Depends on #1648 (playback). + +- Negotiate monitor topology ([#1675](https://github.com/Devolutions/IronRDP/issues/1675)) ([063efcdc30](https://github.com/Devolutions/IronRDP/commit/063efcdc3088d8f44e423cc322077d40bf9aadf2)) + + Negotiate the client monitor layout from UseMultimon and expose the + confirmed remote topology through the ActiveX compatibility interface. + + Advertise Monitor Layout PDU support whenever Extended Client Data is + negotiated, and forward layouts from activation, active sessions, and + reactivation so advertised support does not terminate sessions. + + Keep fallback reporting truthful when servers do not honor the request, + while preserving single-monitor resize behavior and blocking + multi-monitor resizing. + + Do not send Client Monitor Extended Data; per-monitor DPI and + orientation remain unavailable. + +- [**breaking**] Pass frame arrival time into Sequence::step ([#1530](https://github.com/Devolutions/IronRDP/issues/1530)) ([6a499faece](https://github.com/Devolutions/IronRDP/commit/6a499faece8911e50a715a3fb08d4fd8e7d7dc87)) + + ## Summary + + - Connect-time bandwidth measurement needs to know when bytes arrived, + and nothing in the sans-I/O layer could tell it. #1465, now merged, + answers the server's Bandwidth Measure Stop with a nominal interval for + exactly that reason: the connector has no way to observe the real one. + - Introduce `MonotonicInstant`, a millisecond counter with an arbitrary + epoch, and make `Option` a required parameter of + `Sequence::step`. The I/O drivers already know when a read completed, so + `Framed` records the arrival time of each read and hands it to the state + machine. A driver with no clock passes `None`. + - With arrival times available, measure for real: a Bandwidth Measure + Start opens a window, Payload messages accumulate their byte counts, and + Stop reports the elapsed time between its own arrival and the Start's. + + #1465 has merged, so this applies directly to master and carries no + merge-order dependency. That PR was the FreeRDP unblock on its own; this + is the design change behind it, split out at @CBenoit's suggestion in + review. + + ## Why the clock lives in the driver + + Two reasons, both of which rule out having the sequence read a clock + itself. + +- [**breaking**] Expose SUPPORT_DYN_VC_GFX_PROTOCOL early-cap flag for EGFX clients ([#1237](https://github.com/Devolutions/IronRDP/issues/1237)) ([5bdb67980c](https://github.com/Devolutions/IronRDP/commit/5bdb67980cb98b070076acbff802254316e786de)) + + Currently, `early_capability_flags` in the GCC core data is built from a + fixed set in `connection.rs`. Clients that want to use the Graphics + Pipeline Extension (MS-RDPEGFX) — by attaching a `DvcClientProcessor` + for `Microsoft::Windows::RDS::Graphics` — have no way to set + `SUPPORT_DYN_VC_GFX_PROTOCOL` without forking the connector, and modern + Windows servers won't open the EGFX channel unless the client advertises + support. + + This PR adds an opt-in `Config.support_dyn_vc_gfx_protocol: bool` + (default `false`). When set, the flag is OR'd into + `early_capability_flags` alongside the existing `WANT_32_BPP_SESSION` + conditional. Existing consumers are unaffected; the doc comment includes + a safety note that setting this without an EGFX implementation will + cause Windows to stop sending legacy bitmap updates, leaving the desktop + blank. + + Used downstream by [Haven](https://github.com/GlassHaven/Haven) (an + Android RDP/VNC client) which implements EGFX with ClearCodec + + RemoteFxProgressive decoders; this lets us drop a vendored fork of + `ironrdp-connector` we currently carry just for this one flag. + + Default `false` to preserve current behaviour. `cargo check --workspace` + clean — six other in-tree `Config { … }` builders updated with the + default-false field. + + --------- + +- Delegate multitransport setup ([#1858](https://github.com/Devolutions/IronRDP/issues/1858)) ([7036fb8c7e](https://github.com/Devolutions/IronRDP/commit/7036fb8c7ef32f71b456745902e14d34008c7add)) + + Let applications establish negotiated multitransport channels while the + async connector retains protocol sequencing and response ownership. + + Expose Soft-Sync state to the setup callback, centralize response + construction, and report callback failures with E_ABORT when possible. + The existing finalizer still declines multitransport and uses TCP. + +### Bug Fixes + +- Surface server error-info disconnect during reactivation ([#1467](https://github.com/Devolutions/IronRDP/issues/1467)) ([f57d38ff74](https://github.com/Devolutions/IronRDP/commit/f57d38ff74624b99ebcc1369c220b646dd261b71)) + + ## Summary + + After a Deactivate-All, a server may end the session (MS-RDPBCGR + 1.3.1.3) instead of reactivating, sending a Set Error Info PDU that + carries the disconnect reason. `ConnectionActivationSequence`'s + Capabilities Exchange step only recognized `ServerDemandActive` (and + skipped `ServerDeactivateAll`), so the Error Info PDU fell through to a + generic "unexpected Share Control PDU" error and the real reason was + lost. + + - Handle `ServerSetErrorInfo` in Capabilities Exchange the way + `ConnectionFinalizationSequence` already does: return a `reason` error + carrying the error-info description. + - `ERRINFO_NONE` is informational, so it is skipped (stay in + Capabilities Exchange, await Demand Active) rather than treated as + fatal, matching the finalization sequence. + + Found while validating the client against GNOME Remote Desktop ([#1446](https://github.com/Devolutions/IronRDP/issues/1446)): + grd ends the session right after activation when its backend screencast + session cannot be created (for example a locked desktop), and the client + surfaced only an opaque error at that point. + + ## Validation + + `cargo xtask check fmt`, `lints`, `tests`, `typos`, `locks` all pass. + Two new integration tests in `tests/session/connection_activation.rs` + cover the disconnect-reason path and the benign `ERRINFO_NONE` path. + + ## Notes + + No wire-format or public-API change: this only improves the error + surfaced on an existing failure path. + +- Recover from malformed bitmap updates ([#1521](https://github.com/Devolutions/IronRDP/issues/1521)) ([20e2d414e5](https://github.com/Devolutions/IronRDP/commit/20e2d414e5ac060db25a100ed219f417e19f79b2)) + + ## Summary + - safely discard malformed bitmap and pointer updates without + terminating the session + - request at most one capability-gated full redraw per activation + - propagate Refresh Rect and Suppress Output support through the + connector and generic client + - retain FFI compatibility and focused malformed-update regression + coverage + + ## Stack + Depends on `copilot/fix-session-share-bulk-decompression` (`47270c2a`). + + ## Validation + - `cargo fmt --all -- --check` + - `cargo test -p ironrdp-session --lib` + - `cargo test -p ironrdp-error --lib` + - `cargo check -p ironrdp-connector` + - `cargo check -p ironrdp-client --features native-tls` + - `cargo check -p ffi --features ironrdp/native-tls` + + --------- + +- Answer connect-time Bandwidth Measure to unblock FreeRDP servers ([#1465](https://github.com/Devolutions/IronRDP/issues/1465)) ([f736b4e8b9](https://github.com/Devolutions/IronRDP/commit/f736b4e8b901f8889435449c26421dd45ec05bf4)) + + ## Summary + + - The connector answers only the RTT auto-detect request at connect time + and returns nothing for the Bandwidth Measure Stop, on the assumption + that skipping it does not stall the sequence. + - That assumption fails for FreeRDP-based servers: GNOME Remote Desktop + blocks in its `AWAIT_BW_RESULT` state until it receives a Bandwidth + Measure Results reply and never proceeds to licensing, so the connection + hangs right after the Client Info PDU. Windows servers tolerate the + omission, which hid it. + - Reply to a connect-time `BandwidthMeasureStop` with a + `BandwidthMeasureResults` PDU carrying the payload size the server + handed us, over a nominal interval. + + ## Scope + + This is the unblock alone. The reported interval is nominal + (`time_delta_ms: 1`) because the sans-I/O layer has no time source: + nothing reaches the connector that says when the bytes arrived. The + figure is an informational QoS hint and the server proceeds on receipt, + which is what unsticks the connection. + + Measuring it properly needs an arrival time threaded down from the I/O + driver, which is a breaking change to `Sequence::step` and does not + belong in a fix aimed at getting FreeRDP servers connecting. It is + #1530, stacked on this branch: it introduces `MonotonicInstant`, has + `Framed` record when each read completed, and replaces the nominal + figure here with the real Start-to-Stop interval and accumulated byte + count. + + Split out at @CBenoit's suggestion in review. + + ## Validation + + - `cargo xtask check fmt/typos/lints/tests/locks` all pass on the pinned + toolchain. + - Regression test: a connect-time Bandwidth Measure Stop produces a + response frame and the auto-detect phase continues. + - Reproduced and fixed live against gnome-remote-desktop 49: before, the + connector stalled after Client Info with grd in `AWAIT_BW_RESULT`; + after, grd proceeds through licensing and DEMAND_ACTIVE to an active + session. + + ## Notes + + - Found while bringing up the client-side Graphics Pipeline against grd + ([#1446](https://github.com/Devolutions/IronRDP/issues/1446)), but it is an independent connector bug affecting any connection + to a FreeRDP-based server, not specific to EGFX. + - Previously depended on #1511 for a zero-`payloadLength` regression + test. #1511 has merged, and that test moved out with the rest of the + measurement work, so there is no dependency left here. + +- [**breaking**] Accept the full documented range of Client Core Data keyboardType values ([#1689](https://github.com/Devolutions/IronRDP/issues/1689)) ([c74e7c5c94](https://github.com/Devolutions/IronRDP/commit/c74e7c5c9431c87e57a1550b037867d235c1d362)) + + ## Summary + + KeyboardType (TS_UD_CS_CORE's keyboardType field, MS-RDPBCGR 2.2.1.3.2) + was a closed enum covering discriminants 1 through 7, transcribed from + the field's other documentation in 2.2.7.1.6 (TS_INPUT_CAPABILITYSET), + which omits value 8 (Korean keyboard). The table this type actually + decodes against, 2.2.1.3.2, documents 1 through 8. Any client outside + the narrower range, including every genuine Korean keyboard, had its + whole Client Core Data parse hard rejected before the connection + started. + + Converted KeyboardType to a repr(transparent) struct KeyboardType(pub + u32) with named constants for the eight documented values, mirroring + RdpVersion one field above it in the same struct, and made decode + infallible at all three sites that previously disagreed with each other: + gcc::core_data::client hard rejected, rdp::capability_sets::input + silently dropped to None, and ironrdp-activex's COM setter returned + E_INVALIDARG. + + FreeRDP hit the identical gap in its own documentation-only enum until a + two-line fix (FreeRDP#11035). Neither FreeRDP nor xrdp gate connection + admission on this field at all; both decode it as a raw integer. + Windows' own GetKeyboardType additionally documents 0x51 for generic HID + keyboards, which a narrower fix adding only Korean would not survive. + + Marked as breaking since KeyboardType's public shape changes for any + downstream consumer of ironrdp-pdu outside this workspace. Note that + cargo-semver-checks in this repo's PR automation only runs against the + facade ironrdp crate, which will not see a break confined to + ironrdp-pdu's own API, so the automated breaking-change label may not + apply here even though this is a real one. + + ## Validation + + cargo xtask check fmt/lints/tests/typos/locks all pass. Extended + ironrdp-activex's existing inline COM boundary test to cover value 8 + round tripping and a negative value still being rejected. + +- [**breaking**] Count the auto-detect header, and only answer connect-time ([#1559](https://github.com/Devolutions/IronRDP/issues/1559)) ([36a848e085](https://github.com/Devolutions/IronRDP/commit/36a848e08571c76c0c3d1ef9cce899e0c99c9001)) + + ## Summary + + - The Network Characteristics Byte Count store was incremented by the + payload length alone, so every counted Bandwidth Measure message was + reported 8 bytes short. + - [MS-RDPBCGR] 3.2.5.14 says to increment it "by the value specified in + the **payloadLength** field plus the size of the header fields (8 + bytes)", on each Bandwidth Measure Payload and on the 0x002B Stop. + 2.2.14.1.3 pins that 8 by requiring `headerLength` to be 0x08. + - The Start and Stop arms now check the request type rather than + matching every variant. + + ## Those two increments are the whole of the connect-time byte count + + Worth stating, because 3.2.5.14 also contains a rule that counts every + byte received while a window is open, and it's easy to assume it applies + here. + + It doesn't. That rule belongs to the 0x0014 and 0x0114 Starts, which are + the reliable and lossy UDP variants. Connect-time is 0x1014, and its + step list says only to clear the stores and start the timer. So on this + path the Payload and Stop increments are the entire count, and no + framing-layer accumulation is needed. + + ## Why the request-type check is required, not tidying + + 2.2.14.1.4 sets `headerLength` to 0x08 for the 0x002B Stop and 0x06 + otherwise. A fixed 8-byte addend is therefore only correct once the type + is known, which makes the check part of the byte-count fix rather than + separate from it. + + It has two further effects that are worth having. A continuous-detection + Start no longer opens a connect-time window, and a continuous Stop is no + longer answered with a connect-time result on the main channel when + 3.2.5.14 routes those to a multitransport channel under a different + procedure. That procedure also requires a sequence-number correlation on + the 0x0629 Stop which this phase does not track, so ignoring is the + conservative response. + + This overlaps the automated reviewer's fifth finding on #1530, which + suggested exactly this narrowing. I'd offered there to fold it into that + PR or take it separately; the conditional header size settled it, since + the byte count is wrong without it. + + ## This is deliberately not FreeRDP's arithmetic + + FreeRDP counts the whole PDU length at the framing layer, + `bandwidthMeasureByteCount += length` in `libfreerdp/core/rdp.c` after + `rdp_read_header`, for any window including connect-time, and then adds + `payloadLength` again in `libfreerdp/core/autodetect.c` for the Payload + and the Stop. On the connect-time path that counts the payload twice and + adds framing bytes the spec doesn't ask for. + + I went with the spec rather than the reference here. The reported figure + is an informational QoS hint and the server proceeds on receipt either + way, so there's no interop cost to being correct. The divergence is + called out in a comment at the helper so nobody later "fixes" it toward + FreeRDP. + + ## Depends on #1530 + +- Skip surface capabilities without bitmap codecs ([#1860](https://github.com/Devolutions/IronRDP/issues/1860)) ([0d493e5aa0](https://github.com/Devolutions/IronRDP/commit/0d493e5aa070179f0ddc3cd548ebbd6ca5e1f349)) + + Advertise SurfaceCommands, BitmapCodecs, and FrameAcknowledge only when + the client config includes at least one bitmap codec. Legacy servers + such as Windows 7 then stay on basic bitmap updates instead of getting + an empty codec fallback that still enables surface commands. + + Extracted from cxxzhang's work in #1408. CBenoit already reviewed this + gating logic there. This is one of three independent follow-ups from + that PR; concatenated X.224 PDUs and web legacy_graphics are handled + separately. + + + ## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-connector-v0.9.0...ironrdp-connector-v0.10.0)] - 2026-07-10 ### Security diff --git a/crates/ironrdp-connector/Cargo.toml b/crates/ironrdp-connector/Cargo.toml index 5507ea81c1..b3f76a7a75 100644 --- a/crates/ironrdp-connector/Cargo.toml +++ b/crates/ironrdp-connector/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-connector" -version = "0.10.0" +version = "0.11.0" readme = "README.md" description = "State machines to drive an RDP connection sequence" edition.workspace = true @@ -22,10 +22,10 @@ qoi = ["ironrdp-pdu/qoi"] qoiz = ["ironrdp-pdu/qoiz"] [dependencies] -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["std"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["std"] } # public sspi = { version = "0.21", features = ["scard"] } url = "2.5" # public rand = { version = "0.9", features = ["std"] } # TODO: dependency injection? diff --git a/crates/ironrdp-core/CHANGELOG.md b/crates/ironrdp-core/CHANGELOG.md index 33b4c23fad..a9e48623ab 100644 --- a/crates/ironrdp-core/CHANGELOG.md +++ b/crates/ironrdp-core/CHANGELOG.md @@ -6,6 +6,111 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-core-v0.2.1...ironrdp-core-v0.3.0)] - 2026-09-30 + +### Features + +- Add NonEmpty ([#1444](https://github.com/Devolutions/IronRDP/issues/1444)) ([bdcc0ceec3](https://github.com/Devolutions/IronRDP/commit/bdcc0ceec3aaa19441917db02c36cd3be2f58465)) + + Add a `NonEmpty` collection guaranteeing at least one element. The + first element (head) is stored inline, so a single-element `NonEmpty` + performs no heap allocation, and `first()` is infallible while `len()` + returns a `NonZeroUsize`, and callers never branch on an "is it empty?" + case. + +- [**breaking**] Pass frame arrival time into Sequence::step ([#1530](https://github.com/Devolutions/IronRDP/issues/1530)) ([6a499faece](https://github.com/Devolutions/IronRDP/commit/6a499faece8911e50a715a3fb08d4fd8e7d7dc87)) + + ## Summary + + - Connect-time bandwidth measurement needs to know when bytes arrived, + and nothing in the sans-I/O layer could tell it. #1465, now merged, + answers the server's Bandwidth Measure Stop with a nominal interval for + exactly that reason: the connector has no way to observe the real one. + - Introduce `MonotonicInstant`, a millisecond counter with an arbitrary + epoch, and make `Option` a required parameter of + `Sequence::step`. The I/O drivers already know when a read completed, so + `Framed` records the arrival time of each read and hands it to the state + machine. A driver with no clock passes `None`. + - With arrival times available, measure for real: a Bandwidth Measure + Start opens a window, Payload messages accumulate their byte counts, and + Stop reports the elapsed time between its own arrival and the Start's. + + #1465 has merged, so this applies directly to master and carries no + merge-order dependency. That PR was the FreeRDP unblock on its own; this + is the design change behind it, split out at @CBenoit's suggestion in + review. + + ## Why the clock lives in the driver + + Two reasons, both of which rule out having the sequence read a clock + itself. + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +### Bug Fixes + +- Rename {Read,Write}Cursor::rewinded into rewound ([#1529](https://github.com/Devolutions/IronRDP/issues/1529)) ([c85b089b46](https://github.com/Devolutions/IronRDP/commit/c85b089b4617176240b41482be65a77c9ad76a07)) + + + ## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-core-v0.2.0...ironrdp-core-v0.2.1)] - 2026-07-10 ### Features diff --git a/crates/ironrdp-core/Cargo.toml b/crates/ironrdp-core/Cargo.toml index 6d6f8a0a0e..1a37c4c6d4 100644 --- a/crates/ironrdp-core/Cargo.toml +++ b/crates/ironrdp-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-core" -version = "0.2.1" +version = "0.3.0" readme = "README.md" description = "IronRDP common traits and types" edition.workspace = true diff --git a/crates/ironrdp-daemon/Cargo.toml b/crates/ironrdp-daemon/Cargo.toml index 7315dbbb19..61245928c1 100644 --- a/crates/ironrdp-daemon/Cargo.toml +++ b/crates/ironrdp-daemon/Cargo.toml @@ -13,12 +13,12 @@ keywords.workspace = true categories.workspace = true [dependencies] -ironrdp-client = { path = "../ironrdp-client", version = "0.1", features = ["rustls", "dvc-pipe-proxy", "rdpdr", "smartcard", "vmconnect", "gateway", "clipboard"] } # public -ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.1" } -ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.7" } -ironrdp-cliprdr-format = { path = "../ironrdp-cliprdr-format", version = "0.2" } +ironrdp-client = { path = "../ironrdp-client", version = "0.2", features = ["rustls", "dvc-pipe-proxy", "rdpdr", "smartcard", "vmconnect", "gateway", "clipboard"] } # public +ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" } +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8" } +ironrdp-cliprdr-format = { path = "../ironrdp-cliprdr-format", version = "0.3" } ironrdp-input = { path = "../ironrdp-input", version = "0.7" } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public ironrdp-rpc = { path = "../ironrdp-rpc", version = "0.1" } # public ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" } @@ -43,7 +43,7 @@ libc = "0.2" [target.'cfg(windows)'.dependencies] # WebAuthn redirection is Windows-only (webauthn.dll / WebAuthN* APIs). -ironrdp-client = { path = "../ironrdp-client", version = "0.1", features = ["webauthn"] } +ironrdp-client = { path = "../ironrdp-client", version = "0.2", features = ["webauthn"] } ironrdp-rdpdr-native = { path = "../ironrdp-rdpdr-native", version = "0.7" } [lints] diff --git a/crates/ironrdp-displaycontrol/CHANGELOG.md b/crates/ironrdp-displaycontrol/CHANGELOG.md index 0d09e4ba4f..00d703bee8 100644 --- a/crates/ironrdp-displaycontrol/CHANGELOG.md +++ b/crates/ironrdp-displaycontrol/CHANGELOG.md @@ -6,6 +6,133 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-displaycontrol-v0.8.0...ironrdp-displaycontrol-v0.9.0)] - 2026-09-30 + +### Features + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +- Let a server handler override its capabilities ([#1917](https://github.com/Devolutions/IronRDP/issues/1917)) ([db0066072e](https://github.com/Devolutions/IronRDP/commit/db0066072e787ea1268acf2ce46ed5ff0bb578b9)) + + ## Summary + + - `DisplayControlServer::start()` sends + `DisplayControlCapabilities::new(1, 3840, 2400)` unconditionally, so + every server using this crate is capped at one monitor with no way to + advertise more. + - Added a default trait method on `DisplayControlHandler`, + `capabilities()`, that `start()` now calls instead of the hardcoded + literal. + - The default returns the same `(1, 3840, 2400)` values, so any existing + implementation that does not override it keeps its current behavior + unchanged. + - A handler serving more than one monitor can override `capabilities()` + to report the real count. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + - Additive only: one new trait method with a default body, one call-site + change in `start()`. No existing implementor is affected unless it opts + in. + +- Let RdpServerDisplay report its monitor count ([#1918](https://github.com/Devolutions/IronRDP/issues/1918)) ([25331598f3](https://github.com/Devolutions/IronRDP/commit/25331598f37f49573205c54ee45c878d291317f2)) + + ## Summary + + - DisplayControlBackend (ironrdp-server's internal DisplayControlHandler + implementor) never overrode capabilities(), so it fell through to the + single-monitor default regardless of how many monitors the consumer's + RdpServerDisplay actually serves. + - Added a default RdpServerDisplay::monitor_count() method (returns 1, + matching today's behavior), fetched once alongside size() before the + dynamic channels are attached. + - Threaded the count into DisplayControlBackend so its capabilities() + now reports the real value instead of the hardcoded literal. + - A consumer serving more than one monitor can override monitor_count() + to report the real total. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + - Builds on #1917 (DisplayControlHandler::capabilities()), now merged; + rebased onto master. + - Additive only: one new trait method with a default body, one new + field, one call-site change. No existing implementor is affected unless + it opts in. + +### Bug Fixes + +- Decode the full headered DISPLAYCONTROL_CAPS_PDU ([#1442](https://github.com/Devolutions/IronRDP/issues/1442)) ([3b66961a8b](https://github.com/Devolutions/IronRDP/commit/3b66961a8b2ec5bb2d49175c6970e4a480348b3f)) + + ## Summary + + + ## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-displaycontrol-v0.7.0...ironrdp-displaycontrol-v0.8.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-displaycontrol/Cargo.toml b/crates/ironrdp-displaycontrol/Cargo.toml index f6370acc25..3d90a12396 100644 --- a/crates/ironrdp-displaycontrol/Cargo.toml +++ b/crates/ironrdp-displaycontrol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-displaycontrol" -version = "0.8.0" +version = "0.9.0" readme = "README.md" description = "Display control dynamic channel extension implementation" edition.workspace = true @@ -17,10 +17,10 @@ doctest = false test = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-dvc-com-plugin/CHANGELOG.md b/crates/ironrdp-dvc-com-plugin/CHANGELOG.md index 9cca74face..233c4d8a1a 100644 --- a/crates/ironrdp-dvc-com-plugin/CHANGELOG.md +++ b/crates/ironrdp-dvc-com-plugin/CHANGELOG.md @@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.1.4](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-com-plugin-v0.1.3...ironrdp-dvc-com-plugin-v0.1.4)] - 2026-09-30 + +### Features + +- Add native MS-RDPEWA WebAuthn redirection ([#1644](https://github.com/Devolutions/IronRDP/issues/1644)) ([66da78bc4e](https://github.com/Devolutions/IronRDP/commit/66da78bc4e6b37a7780dbf9f333234be63d96afb)) + + Implement the RDPEWA dynamic channel with a Windows WebAuthn backend and + wire RedirectWebAuthn for ActiveX, the optional client feature, and the + viewer CLI. + + Prefer System32\webauthn.dll via the DVC COM plugin for MSTSC parity. + The pure-Rust backend forwards ceremonies through a webauthn.dll IWTS + oneshot so hash-only hosts that omit clientDataJSON still work; public + WebAuthN* remains a fallback when JSON is present. Recreate + WebAuthN_Channel opens through shared COM/listener factories because + Windows opens and closes the channel around each RPC. + + Side effects: + - New crates ironrdp-rdpewa and ironrdp-rdpewa-native + - Config key redirectwebauthn; ActiveX ExtendedSettings property + - ironrdp-daemon webauthn feature for ironrdp-agent + - IRONRDP_WEBAUTHN_FORCE_NATIVE debug switch + - Viewer --webauthn/--no-webauthn flags; .rdp redirectwebauthn default + - ActiveX docs note no AdvancedSettings slot and no IPersist persistence + + + ## [[0.1.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-com-plugin-v0.1.2...ironrdp-dvc-com-plugin-v0.1.3)] - 2026-07-10 ### Bug Fixes diff --git a/crates/ironrdp-dvc-com-plugin/Cargo.toml b/crates/ironrdp-dvc-com-plugin/Cargo.toml index aca7e69c18..ab48f8dec6 100644 --- a/crates/ironrdp-dvc-com-plugin/Cargo.toml +++ b/crates/ironrdp-dvc-com-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-dvc-com-plugin" -version = "0.1.3" +version = "0.1.4" readme = "README.md" description = "DVC COM client plugin loader for IronRDP (Windows)" edition.workspace = true @@ -19,10 +19,10 @@ test = false [dependencies] [target.'cfg(windows)'.dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } tracing = { version = "0.1", features = ["log"] } windows = { version = "0.62", features = [ "Win32_Foundation", diff --git a/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md b/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md index 6aacd9649f..2de221cc41 100644 --- a/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md +++ b/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.5.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-pipe-proxy-v0.5.0...ironrdp-dvc-pipe-proxy-v0.5.1)] - 2026-09-30 + +### Bug Fixes + +- Handle pre-connected Windows clients ([#1447](https://github.com/Devolutions/IronRDP/issues/1447)) ([079b48422b](https://github.com/Devolutions/IronRDP/commit/079b48422b0b78d37beb76994950a2a07c442a94)) + + + ## [[0.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-pipe-proxy-v0.4.1...ironrdp-dvc-pipe-proxy-v0.5.0)] - 2026-07-10 ### Bug Fixes diff --git a/crates/ironrdp-dvc-pipe-proxy/Cargo.toml b/crates/ironrdp-dvc-pipe-proxy/Cargo.toml index a87a76f71e..41d04e76aa 100644 --- a/crates/ironrdp-dvc-pipe-proxy/Cargo.toml +++ b/crates/ironrdp-dvc-pipe-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-dvc-pipe-proxy" -version = "0.5.0" +version = "0.5.1" readme = "README.md" description = "DVC named pipe proxy for IronRDP" edition.workspace = true @@ -17,10 +17,10 @@ doctest = false test = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public (PduResult type) -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public (SvcMessage type) +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public (PduResult type) +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public (SvcMessage type) tracing = { version = "0.1", features = ["log"] } tokio = { version = "1", features = ["net", "rt", "sync", "macros", "io-util", "fs"]} diff --git a/crates/ironrdp-dvc/CHANGELOG.md b/crates/ironrdp-dvc/CHANGELOG.md index 854052722b..a5dd7c1229 100644 --- a/crates/ironrdp-dvc/CHANGELOG.md +++ b/crates/ironrdp-dvc/CHANGELOG.md @@ -6,6 +6,262 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.8.0...ironrdp-dvc-v0.9.0)] - 2026-09-30 + +### Security + +- Wire UDP multitransport into ironrdp-server ([#1954](https://github.com/Devolutions/IronRDP/issues/1954)) ([73dfa30e38](https://github.com/Devolutions/IronRDP/commit/73dfa30e3831e51cf8aa58f93e27b1b00102a7ec)) + + - Add `RdpServerBuilder::with_udp_transport(udp_bind_addr)`: opt-in, + `None` by default, no behavior change unless called. + - When set (and the security mode is `Tls` or `Hybrid`, matching the + reference client's Enhanced-Security-only gate), the acceptor offers UDP + multitransport, and `accept_finalize` uses + `accept_finalize_with_multitransport` with a callback that binds a fresh + UDP socket per connection, reuses the connection's own TLS certificate + (`TlsAcceptor::config()`) for the sideband transport, and calls + `accept_udp()`. + - Once established, the transport is used to migrate EGFX graphics + traffic off TCP: `request_reliable_udp` is called opportunistically the + first time EGFX has data to send (its dynamic channel id is only known + once the client opens it). From the request on, every server message on + that channel goes over the tunnel, starting with the batch that + triggered it: the request carries SOFT_SYNC_TCP_FLUSHED and the server + MUST keep using the named tunnel immediately after sending it + (MS-RDPEDYC 2.2.5.1, 3.3.5.3.1). DRDYNVC replies for a tunneled channel + go over the tunnel too, whichever path produced them. A new + `client_loop` select arm feeds incoming tunnel payloads into + `DrdynvcServer::process_tunnel()`; payloads that arrive before the + client's Soft-Sync Response are held and processed once it does + (3.3.5.3.2), rather than dropped. + - The UDP accept runs as an ordinary `tokio::spawn` task, so enabling + UDP adds no runtime requirement for the caller. + - A failure before EGFX has moved (bind, handshake, TLS, or the tunnel + closing) leaves the session on TCP, matching the reference client's + posture. Once EGFX is on the tunnel, the tunnel closing ends the + connection: Soft-Sync cannot move a channel back to TCP (MS-RDPEDYC + 2.2.5.1), and the tunnel lasts as long as the connection (MS-RDPEMT + 1.3.3). + - A client that answers the Initiate Multitransport Request with E_ABORT + (MS-RDPBCGR 2.2.15.2) has given up on the sideband transport, so the + pending UDP accept is stopped as soon as that response arrives, whether + during finalization or later on the message channel, instead of holding + its socket until the 15 s accept timeout. Windows clients send it about + 2.7 s after connecting. + - When the UDP bind address has an unspecified IP, each connection's + socket binds to the local address that client reached over TCP instead. + A socket bound to the unspecified address replies from whichever address + the routing table picks, and on a host with several IPv6 addresses that + is not always the one the client sent to: mstsc dropped the replies and + gave up with E_ABORT. `run()` records the address itself; embedders + driving `run_connection_with` pass it with the new + `RdpServer::set_connection_local_addr`. + - A successful Initiate Multitransport Response that arrives after + finalization now enables EGFX migration for the rest of the session, + provided Soft-Sync was negotiated. mstsc finishes its UDP bootstrap + after the TCP finalization (0.87 s later in my test), so migration was + previously decided before its response existed and the session never + left TCP even with the sideband transport up. + - The EGFX channel is found whichever way it was registered: an embedder + that takes a frame handle from its `GfxServerFactory` registers it as + `GfxDvcBridge`, which the migration lookup did not recognise, so it + never sent the Soft-Sync Request. The Soft-Sync Request, the client's + response (with the tunnels and channels it accepted) and the switch of + EGFX onto UDP are now logged at debug level. + +### Features + +- Expose generic session configuration and lifecycle APIs ([#1522](https://github.com/Devolutions/IronRDP/issues/1522)) ([57b1366650](https://github.com/Devolutions/IronRDP/commit/57b13666506dc40c15b4c4702d35150beee99133)) + + ## Summary + - expose generic client configuration for connection metadata, + compression, shell/work directory, audio, and runtime static-channel + factories + - add bounded input delivery with independent close cancellation, host + clipboard plumbing, lifecycle events, and Display Control resize + readiness/fallback handling + - update agent, viewer, web, FFI, examples, and tests for the generic + APIs + + ## Stack dependencies + This PR is stacked on `copilot/tls-validation-policy` (`b2bbcece`), + which already includes the merged runtime static-channel support from + `master`. It intentionally contains no TLS implementation/policy, + ActiveX/COM, SVC implementation, decompression, or bitmap-recovery + changes. + + ## Validation + - `cargo fmt --check --all` + - `cargo xtask check tests --no-run -v` + - `cargo xtask check lints -v` + - `cargo test -p ironrdp-client --lib --features rustls` + - `cargo check -p ironrdp-agent -p ironrdp-viewer -p ironrdp-web -p ffi` + + --------- + +- [**breaking**] Add Soft-Sync PDU support ([#1584](https://github.com/Devolutions/IronRDP/issues/1584)) ([bd630842ba](https://github.com/Devolutions/IronRDP/commit/bd630842bacc49cc129e613610482023a0f760db)) + + Add Soft-Sync codecs and DVC dispatch for tunnel assignments. + + Keep decoding forward compatible and bound peer-controlled allocations. + Reject exchanges before their required multitransport endpoint is ready. + +- Create channels with assigned IDs ([#1416](https://github.com/Devolutions/IronRDP/issues/1416)) ([41293c2442](https://github.com/Devolutions/IronRDP/commit/41293c2442dfb2da6b61ca05a0c842706d048fc1)) + + Reserve the channel ID before constructing its processor so the processor + and its dependencies can use the ID during initialization. + + Add a fallible builder API that preserves construction errors. + +- Attach recorded dynamic channels ([#1664](https://github.com/Devolutions/IronRDP/issues/1664)) ([93780feeec](https://github.com/Devolutions/IronRDP/commit/93780feeec1f09e13c8dd4691d5c5da20fae9310)) + + Attach known channel IDs for offline replay. + + Reject duplicate IDs and failed startup atomically. + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +- Add location redirection ([#1778](https://github.com/Devolutions/IronRDP/issues/1778)) ([1cee7a8613](https://github.com/Devolutions/IronRDP/commit/1cee7a86135a0556c01965d0406233bd7df367a9)) + + Implement MS-RDPEL v1 codecs and the location DVC state machine, then + route the ActiveX methods through the bounded client input queue. + + Preserve mstsc-compatible validation and altitude caching while + surfacing inactive sessions, channel readiness, queue pressure, and + encoding failures. Coordinates are caller-supplied only and are never + logged or persisted. + +- Route channels over Soft-Sync tunnels ([#1826](https://github.com/Devolutions/IronRDP/issues/1826)) ([a989229409](https://github.com/Devolutions/IronRDP/commit/a9892294095d32808ee24dc8dad74dce46cb0bb4)) + + Add DvcMessageBatch to carry a dynamic channel ID alongside its + encoded SVC messages, and validate that a Soft-Sync-selected tunnel + matches the channel before forwarding tunneled DRDYNVC data. + +### Bug Fixes + +- [**breaking**] Replace DVC wrappers with typed accessors ([#1377](https://github.com/Devolutions/IronRDP/issues/1377)) ([d43ecf9a54](https://github.com/Devolutions/IronRDP/commit/d43ecf9a54363d37e0c485a1e9e73da0d47ae540)) + + Follow-up to #1368. This is not urgent; review whenever the DVC API + direction is worth revisiting. + + Rework DVC channel access APIs so callers can recover a typed processor + together with its dynamic channel id, without exposing internal channel + wrapper types. + + - Add typed borrowed DVC accessors carrying both channel id and + processor borrow for `DrdynvcClient`. + - Keep dynamic channel wrapper types private. + - Align client listener/registration APIs on `DvcClientProcessor`. + +- Log channel name on DVC creation failure ([#1765](https://github.com/Devolutions/IronRDP/issues/1765)) ([0a4147e5b1](https://github.com/Devolutions/IronRDP/commit/0a4147e5b1e2d4bd35b8cb9c401d61fb06960396)) + + ## Summary + + - When a client rejects a DVC create request, `DrdynvcServer::process()` + only logged the raw PDU (channel_id and status code). The channel's name + is already in scope one line earlier, from building the original create + request, but the failure branch never surfaces it. + - I found this while debugging a real server log: a channel_id 0 + creation failure with no way to tell which channel it was without + reading the source and tracing registration order by hand. + - Added the channel name to the failure log line using structured + tracing fields, and raised the level from implicit debug to warn, since + a channel creation failure is a real, actionable event most deployments + would want visible at default verbosity rather than something requiring + debug or trace logging to notice. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + Five-line diff, `crates/ironrdp-dvc/src/server.rs` only. No behavior + change beyond the added log line. + +- Switch a Soft-Sync tunnel that also lists declined channels ([#2007](https://github.com/Devolutions/IronRDP/issues/2007)) ([3b3a17b958](https://github.com/Devolutions/IronRDP/commit/3b3a17b958e1edae1a00cd3a3a97b0103d932792)) + + Windows lists every dynamic channel it intends to move in its Soft-Sync + request, including the ones the client declined with NO_LISTENER. + Against a Windows 11 host the request lists channels 2, 6, 7, 8, 9, 10, + 11 and 12 (CoreInput, MouseCursor, Graphics, Video, Geometry, ...), and + only channel 7, the graphics pipeline, is open. + + `process_soft_sync_request` dropped a whole channel list as soon as one + ID in it was not open. The tunnel was then never switched, and the + channels the client had opened stayed on TCP while the server was + already sending them on the tunnel (MS-RDPEDYC 3.2.5.3.1). + + Unopened channels are now skipped one by one, and the tunnel is switched + for the rest. + + ## Testing + + - New `dvc::client::soft_sync_skips_channels_the_client_did_not_open` in + `ironrdp-testsuite-core`. + - Live, against a Windows 11 host over RDP-UDP version 2, with the + viewer built from a branch that also carries the tunnel and client PRs + of this series: the Soft-Sync request above now switches the tunnel, and + the graphics pipeline moves onto it. + + ## Checks + + - `cargo fmt --all -- --check` + - `cargo clippy --workspace --all-targets --features helper,__bench + --locked -- -D warnings` + - `cargo test --locked -p ironrdp-testsuite-core -p + ironrdp-testsuite-extra`, plus the lib tests of the crates touched here + - `cargo test --workspace --locked` on a branch that merges this PR with + the other Windows interop PRs from this series + - `typos` on the changed files + + ## Series + + These PRs port the Windows interop fixes and Linux backends from a + downstream IronRDP fork, so the fork can be retired. Each one is based + on `master` and can be reviewed and merged on its own. I also checked + that all of them merge cleanly together in this order. + +- [**breaking**] Serve channels and graphics that Windows moves onto a tunnel ([#2008](https://github.com/Devolutions/IronRDP/issues/2008)) ([bfd16a2e55](https://github.com/Devolutions/IronRDP/commit/bfd16a2e5573d4cd6617dd71b2802b4e7b944a59)) + + Once Soft-Sync has moved dynamic channels onto the reliable UDP tunnel, + Windows keeps using the tunnel for more than channel data. Two gaps kept + the graphics pipeline from working there. + + + ## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.7.0...ironrdp-dvc-v0.8.0)] - 2026-07-10 ### Features diff --git a/crates/ironrdp-dvc/Cargo.toml b/crates/ironrdp-dvc/Cargo.toml index 071bd0dc11..a50390581d 100644 --- a/crates/ironrdp-dvc/Cargo.toml +++ b/crates/ironrdp-dvc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-dvc" -version = "0.8.0" +version = "0.9.0" readme = "README.md" description = "DRDYNVC static channel implementation and traits to implement dynamic virtual channels" edition.workspace = true @@ -21,9 +21,9 @@ default = [] std = [] [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["alloc"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["alloc"] } # public tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-echo/CHANGELOG.md b/crates/ironrdp-echo/CHANGELOG.md index 1b4f37fe26..441d156bdd 100644 --- a/crates/ironrdp-echo/CHANGELOG.md +++ b/crates/ironrdp-echo/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-echo-v0.4.0...ironrdp-echo-v0.4.1)] - 2026-09-30 + + + ## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-echo-v0.3.0...ironrdp-echo-v0.4.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-echo/Cargo.toml b/crates/ironrdp-echo/Cargo.toml index b7e7f525d1..ed2cc0860e 100644 --- a/crates/ironrdp-echo/Cargo.toml +++ b/crates/ironrdp-echo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-echo" -version = "0.4.0" +version = "0.4.1" readme = "README.md" description = "Virtual channel echo extension implementation" edition.workspace = true @@ -17,9 +17,9 @@ doctest = false test = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-egfx/CHANGELOG.md b/crates/ironrdp-egfx/CHANGELOG.md index fb6d836b38..74c4805fdd 100644 --- a/crates/ironrdp-egfx/CHANGELOG.md +++ b/crates/ironrdp-egfx/CHANGELOG.md @@ -5,6 +5,532 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-egfx-v0.3.0...ironrdp-egfx-v0.4.0)] - 2026-09-30 + +### Features + +- Add ClearCodec client-side decode dispatch ([#1175](https://github.com/Devolutions/IronRDP/issues/1175)) ([714dce4662](https://github.com/Devolutions/IronRDP/commit/714dce46627e299c57d82f4f6a5c18067a95bffa)) + + Follow-up to #1174. Supersedes #1195 (the standalone server-helper PR; + its 46-line `send_clearcodec_frame()` is included here). + + Wires ClearCodec into the EGFX client's WireToSurface1 codec dispatch, + matching the existing AVC420 and Uncompressed decode patterns. + +- Composite client surface commands into pixel buffers ([#1460](https://github.com/Devolutions/IronRDP/issues/1460)) ([9cd36952ca](https://github.com/Devolutions/IronRDP/commit/9cd36952ca18196cf72c85dc42a79d5d1f5620d5)) + +- Decode Planar bitmaps in the client ([#1507](https://github.com/Devolutions/IronRDP/issues/1507)) ([66c8a81be0](https://github.com/Devolutions/IronRDP/commit/66c8a81be0a9f966e3cf4935ca2a0274d10b063f)) + + Wires `RDPGFX_CODECID_PLANAR` (0x000A) into the EGFX client's + `WireToSurface1` dispatch, alongside the existing ClearCodec and + Uncompressed paths. + +- Add egfx_avc420_decode oracle and target ([#1326](https://github.com/Devolutions/IronRDP/issues/1326)) ([cafbef1c9f](https://github.com/Devolutions/IronRDP/commit/cafbef1c9faba78acb3e33a39556eb4c4c78c6d4)) + + This change adds an assertion-or-panic fuzz oracle for the AVC + length-prefix to Annex-B conversion that runs inside + `OpenH264Decoder::decode` before any OpenH264 entry point. The same + change refactors the conversion from a private method on + `OpenH264Decoder` into two public free functions in + `ironrdp_egfx::pdu::avc`. The first function, `avc_to_annex_b`, + returns a fresh `Vec` and is symmetric with the existing + `annex_b_to_avc`. The second function, `avc_to_annex_b_into`, writes + into a caller-provided buffer and preserves the per-frame buffer-reuse + optimization that `OpenH264Decoder` relied on. + + The conversion is now available unconditionally to any consumer of + `ironrdp-egfx`. The previous `#[cfg(feature = "openh264")]` gating + went with the location, not the bytes-to-bytes logic, so lifting the + function out of `decode.rs` removed the gate too. + + The new oracle exercises two input distributions on every fuzz call: + + - Direct path: the oracle calls `avc_to_annex_b(data)` on the raw fuzz + input. This exercises the wrapper on arbitrary byte distributions, + including inputs that do not parse as `Avc420BitmapStream`. + - Decode-chain path: the oracle tries + `Avc420BitmapStream::decode(data)`; + on success it calls `avc_to_annex_b(stream.data)`. This exercises the + wrapper on the realistic post-decode payload distribution. + + The oracle catches panics in the wrapper, OOM allocation from + attacker-controlled NAL length encoding, and contract violations on the + produced Annex-B byte stream. The oracle does NOT catch OpenH264 + internal bugs (OSS-Fuzz coverage), the YUV-to-RGBA conversion path + downstream of OpenH264 (separate workstream), or AVC444 luma plus + chroma split (sibling target). + + Smoke fuzz ran 10,922,695 iterations in 31 seconds at ~352K exec/s + sustained with zero crashes. Coverage settled at 158 lines, 456 + features, 69 corpus entries. + + The new target auto-discovers into CI via the `cargo xtask fuzz list` + dynamic fan-out mechanism. The new `check_egfx_avc420_decode` + regression-replay test in + `crates/ironrdp-testsuite-core/tests/fuzz_regression.rs` runs against + the seed corpus and passes. + +- Decode RFX Progressive tiles ([#1673](https://github.com/Devolutions/IronRDP/issues/1673)) ([f21f1979f4](https://github.com/Devolutions/IronRDP/commit/f21f1979f4ef7ce8756a4022d866c7f7fc150b0b)) + + Keep Progressive tile state scoped to its surface and codec context. + + Reject context-less updates that have no state for their surface, and + expose targeted context and surface cleanup for consumers. + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +- Render the EGFX graphics pipeline output ([#1461](https://github.com/Devolutions/IronRDP/issues/1461)) ([d414622231](https://github.com/Devolutions/IronRDP/commit/d414622231ed3a944d8896118f409edead1d3df3)) + + ## Summary + + - The `ironrdp-egfx` compositor exposes changed output regions via + `drain_output()`, but nothing consumed them, so an EGFX session decoded + frames and dropped them. + - Drains the compositor from `ActiveStage::process`: composites each + completed-frame `OutputUpdate` into the `DecodedImage` and emits + `ActiveStageOutput::GraphicsUpdate`, so every session consumer renders + EGFX with no new code. No-op when the graphics DVC is not registered. + - Adds a by-type mutable DVC accessor `DrdynvcClient::get_dvc_mut`, + mirrored on the session (the x224 processor and `ActiveStage`), matching + the existing `get_dvc`. + - All regions drained in one pass are composited first by + `composite_graphics_updates`, then surfaced as a single `GraphicsUpdate` + covering their union. A consumer may redraw whatever region an update + names, and `ironrdp-client` rebuilds the whole framebuffer for each one, + so emitting per region would copy the desktop once per rectangle. A + single `RDPGFX_SOLIDFILL_PDU` or `RDPGFX_CACHE_TO_SURFACE_PDU` can name + up to `u16::MAX` of them. + + ## Validation + + - `cargo xtask check fmt/lints/tests/typos/locks` all pass, including + the dependency guard. + - Four tests cover the coalescing: two disjoint deltas collapsing to the + rectangle spanning both, 64 deltas yielding exactly one region, a single + delta passing through unwidened, and an empty drain surfacing nothing. + Checked against a reverted coalescing, where the first two fail. + - The loop lives in `composite_graphics_updates` rather than inline in + `ActiveStage::process` so it can be tested without standing up an x224 + processor. It takes `(ExclusiveRectangle, Vec)` rather than + `OutputUpdate`, since that type is `#[non_exhaustive]` and cannot be + constructed from `ironrdp-session`; this keeps the change inside the + crate instead of widening an already-merged API for testability. + + ## Notes + + - `ironrdp-session` already depends on `ironrdp-displaycontrol` and + `ironrdp-graphics`; `ironrdp-egfx` is consumed the same way. The guard + keeping `ironrdp-connector` and `sspi` out of `ironrdp-session` still + passes. + - Reuses `apply_rgba32` (previously gated behind the `qoi` feature); + converts the compositor's exclusive-rectangle regions to the session's + inclusive-rectangle convention. + - #1377 and #1460 have both merged, so this now sits directly on master + and the diff is its own change alone: 6 files, +160/-6. + - #1462 is stacked on this one, so the merge order is this then #1462. + - Part of #1464. Motivated by #1446. + +- Clip Progressive updates to REGION and decode Windows streams ([#1443](https://github.com/Devolutions/IronRDP/issues/1443)) ([11b10531de](https://github.com/Devolutions/IronRDP/commit/11b10531de491c67f5c8d6bb6972f6951bbeecc8)) + + ## Problem + + #1673 landed the core `WireToSurface2` → `ProgressiveDecoder` dispatch + (originally proposed here), and #1696/#1698 extended the decoder past + this PR's scope. What remains splits in two. + + **REGION handling.** Master decodes REGION blocks wherever they appear, + including outside `FRAME_BEGIN`/`FRAME_END` — [CBenoit's + review](https://github.com/Devolutions/IronRDP/pull/1443#issuecomment-5164007525) + asked for those to be ignored, and that part is still open. Decoded + tiles are also blitted whole (64x64) with the REGION rectangles ignored, + so pixels outside the damage region the server reported are written to + the surface anyway. + + **Progressive from a Windows host does not decode at all.** Three + separate defects, each hit in order while replaying a live session + against Windows (details in the comment below). + + ## Changes + + REGION handling: + + - `decode_bitmap` processes REGION blocks only inside the first + `FRAME_BEGIN`/`FRAME_END` pair and ignores blocks outside it. + - Each `DecodedTile` carries `update_rectangles`: the tile clipped to + the REGION rectangles and surface bounds. The client emits one + `BitmapUpdate` per rectangle, keeping the whole-tile fast path. + - `begin_frame`/`end_frame` (driven by StartFrame/EndFrame) let REGION + blocks split across several `WireToSurface2` payloads of one frame + reference shared tiles. + - Clipping work is budgeted per payload so a hostile REGION cannot drive + quadratic `union_rectangle`/`intersect_rectangle` scans. + + Windows compatibility: + + - `quality == 0xFF` selects full quality per [MS-RDPEGFX] 2.2.4.2.1.5.2 + instead of indexing `quantProgVals`; Windows sends it with an empty + table, so indexing rejected every tile. + - `ResetGraphics` no longer drops codec contexts. It only resizes the + Graphics Output Buffer, and Windows never repeats SYNC + CONTEXT after + one. + - A new codec context id that arrives without SYNC + CONTEXT inherits + the band layout retained for its surface, released when the surface is + deleted. Deleting a context still drops its tiles. + + ## Testing + + `decoder_ignores_regions_outside_frame`, + `decoder_bounds_region_clipping_work`, + `full_quality_tiles_bypass_the_progressive_quant_table`, + `progressive_context_survives_graphics_reset`, + `wire_to_surface2_clips_compositor_output_to_region`, plus the reworked + context-deletion assertions. + + `ironrdp-graphics` 235 and `ironrdp-egfx` 47 pass; `cargo clippy + --all-targets -- -D warnings` and `cargo fmt` clean. + + --------- + +- Add diagnostic logging for EGFX flow control and dispatch timing ([#1834](https://github.com/Devolutions/IronRDP/issues/1834)) ([8d1e91eb32](https://github.com/Devolutions/IronRDP/commit/8d1e91eb3256e0ba007e909512bbaf84cb76d67b)) + + I ran into a case where I needed to debug slow frame acknowledgement and + dispatch stalls in ironrdp-server and ironrdp-egfx, and found the + relevant signals were either missing or buried at TRACE level where + nobody enables them in normal operation. This PR adds targeted + diagnostic logging without changing any behavior. + + - FrameTracker now edge-triggers a debug log when backpressure or + ack_suspended change state, instead of logging every call (which would + flood at 30+/sec) or nothing at all. + - FrameAcknowledge is now logged at DEBUG with latency, queue_depth, and + in-flight count. An ack for an unknown frame_id (protocol violation or + stale ack per MS-RDPEGFX 2.2.4.3) is now a warning instead of silent. + - drain_output (ZGFX compression) now tracks per-batch compress time and + ratio, logging at INFO when a batch exceeds a 10ms budget and DEBUG + otherwise, since it runs under both the state lock and the writer lock + and can block inbound PDU processing. + - Incoming EGFX DVC PDUs are logged at DEBUG with their kind, so the + client-to-server side of the channel is visible without enabling TRACE. + - dispatch_pdu and dispatch_events now separately time lock-acquisition + wait and handler dispatch, warning when either exceeds 50ms so the two + causes (lock contention vs. handler/runtime stall) can be told apart + instead of both surfacing as one generic slow-dispatch symptom. + + No public API changes, no behavioral changes, logging only. + + Note on CI: the workspace-wide test compile is currently broken on + master independent of this PR, ironrdp-daemon's consume_output() call + site passes a raw Receiver where OutputEventReceiver is expected. I + confirmed this reproduces on a clean, unmodified checkout of master. + This PR only touches ironrdp-server and ironrdp-egfx. + +- Handle mid-session CapsAdvertise as decoder-recovery ([#1833](https://github.com/Devolutions/IronRDP/issues/1833)) ([b4ba2c2237](https://github.com/Devolutions/IronRDP/commit/b4ba2c2237a2269681256e82fc1797d22d6fe0e7)) + + Real-world clients (mstsc on Windows 11 under load, macOS Microsoft + Remote Desktop) re-emit RDPGFX_CAPSADVERTISE mid-session as a + decoder-recovery sequence when their decoder loses sync. The previous + handler treated every CapsAdvertise as initial setup, leaving + reset_graphics_sent=true so the application's follow-up create_surface + wouldn't auto-emit ResetGraphics, breaking recovery. + + I detect the re-advertise via state == Ready at entry, then silently + clear surfaces + frames and re-arm reset_graphics_sent. I don't emit a + DeleteSurface PDU for this: the client has already cleared its surface + state on its end, and treats a stray DeleteSurface as a protocol + violation, closing the connection within milliseconds. + + Surfaces gains reset_for_reinit(), distinct from clear(): it also resets + next_surface_id to 0 so the client's subsequent CreateSurface yields ID + 0, matching its fresh-state expectation. + + MS-RDPEGFX does not document this recovery flow explicitly, but the + empirical pattern is CapsAdvertise -> CacheImportOffer -> the server is + expected to emit CapsConfirm + ResetGraphics + CreateSurface(id=0) + + MapSurfaceToOutput + IDR. + + I validated this against mstsc on Windows 11 and openSUSE Tumbleweed + (KDE Plasma 6.6.4). + +- Add H264Encoder trait and an openh264 reference implementation ([#1852](https://github.com/Devolutions/IronRDP/issues/1852)) ([9609c9e93b](https://github.com/Devolutions/IronRDP/commit/9609c9e93b7e33d64e9daab3326dd4caa0407532)) + + ironrdp-egfx has had the decode half of an H.264 abstraction since the + openh264 decoder landed: an H264Decoder trait plus a feature-gated + reference implementation. The encode half never existed, so every server + producing AVC420 frames brings its own encoder stack and its own answer + to the format questions. This adds the symmetric twin. + + encode.rs mirrors decode.rs piece for piece: an EncodeFrame + borrowed-RGBA input (the same pixel layout DecodedFrame produces on the + other side), an H264Encoder trait with defaulted request_key_frame and + reset hooks, an EncoderError shaped like DecoderError, and an + OpenH264Encoder reference implementation behind the existing openh264 / + openh264-bundled / openh264-libloading features with the same two + construction paths and the same patent-posture notes as the decoder. + + The output contract is documented deliberately: an ITU-T H.264 Annex B + bitstream with in-band SPS/PPS, which is what RFX_AVC420_BITMAP_STREAM + carries per [MS-RDPEGFX] 2.2.4.4 and what send_avc420_frame forwards + unmodified. OpenH264 emits Annex B natively, so the reference + implementation does no conversion. The ecosystem precedent for the trait + shape is FreeRDP's H264_CONTEXT_SUBSYSTEM vtable, which registers + OpenH264, Media Foundation, FFmpeg and MediaCodec backends behind one + seam; downstream hardware encoders (VA-API, NVENC, VideoToolbox) get the + same injection point here. + + No server rewiring is included: send_avc420_frame keeps taking + pre-encoded bytes, and nothing changes for existing callers. The trait + is the integration seam only. + +- Present EGFX dirty regions ([#1874](https://github.com/Devolutions/IronRDP/issues/1874)) ([2b44c62bdb](https://github.com/Devolutions/IronRDP/commit/2b44c62bdbd997f4aa33bdbaf4749ac4ecc85140)) + + Propagate validated ResetGraphics extents into the session framebuffer + and deliver exact packed desktop damage to ActiveX without rebuilding + full image snapshots. + + Coalesce only fully covered regions, preserve sparse updates with a + 64-event and 256 MiB pixel-data budget, and backpressure without + evicting accepted frame or static-channel payloads. Update retained GDI + and RPC surfaces in place. Reuse framebuffer storage with fallible + growth, and retain software cursor shape, position, visibility, and + clipping across graphics resets. Keep V8 non-AVC negotiation and + full-frame output fallback unchanged. Rejected oversized resets still + destroy prior surfaces and are reported as unsupported without + allocating a session framebuffer. + +### Bug Fixes + +- Preserve AVC_DISABLED during negotiation ([#1490](https://github.com/Devolutions/IronRDP/issues/1490)) ([c5bd574c8a](https://github.com/Devolutions/IronRDP/commit/c5bd574c8ac7e4ba92c31348187a196d3e84ae70)) + +- Add spec-compliant Planar frame sender ([#1498](https://github.com/Devolutions/IronRDP/issues/1498)) ([409b256b41](https://github.com/Devolutions/IronRDP/commit/409b256b4168b3a63d97998da311fa9e761bef3e)) + + ## Summary + +- Bound the compositor's dirty-region metadata ([#1510](https://github.com/Devolutions/IronRDP/issues/1510)) ([81f7392422](https://github.com/Devolutions/IronRDP/commit/81f73924221ea994d7f5f32bc9113caa13551ae6)) + + ## Summary + + - The compositor charges materialized pixel buffers against + `MAX_COMPOSITOR_BYTES` but not the `DirtyRegion` entries that produce + them, so `frame` grows outside the budget. + - The repeat filter is O(1) and compares only against the previous + entry, so it collapses a rectangle repeated 65,535 times but not two + rectangles alternating. The frame stays open until the peer sends + `EndFrame`, and the peer chooses when that happens. + - `record_dirty` now charges one entry before pushing, and `EndFrame` + releases the whole set before materializing so the pixel copies can + spend what the metadata was holding. + + ## Cost to the peer + + `RDPGFX_POINT16` (2.2.1.1) is four bytes on the wire; `RDPGFX_RECT16` + (2.2.1.2) is eight. A `DirtyRegion` is ten bytes resident. Three + commands loop over these arrays and record one dirty region each: + + | PDU | Array element | Wire | Resident | Ratio | + |---|---|---|---|---| + | `RDPGFX_SOLIDFILL_PDU` (2.2.2.4) | `fillRects`, RECT16 | 8 | 10 | + 1.25x | + | `RDPGFX_SURFACE_TO_SURFACE_PDU` (2.2.2.5) | `destPts`, POINT16 | 4 | + 10 | 2.5x | + | `RDPGFX_CACHE_TO_SURFACE_PDU` (2.2.2.7) | `destPts`, POINT16 | 4 | 10 + | 2.5x | + + The ratios are modest. The point is that there was no ceiling at all, so + a sustained stream grows the queue until the client is out of memory + regardless of ratio. + + ## Validation + + - New test alternates two rectangles past the budget and asserts the + frame stops growing. Verified against a reverted fix: without the charge + it reaches 128 entries, with it 64. + - `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + - The charge counts logical entries, not `Vec` capacity. Since `Vec` + grows by doubling, resident bytes for `frame` can reach roughly twice + the charged figure. This matches the existing accounting, which charges + `data.len()` for pixel buffers rather than capacity; flagging it rather + than diverging from the established model. + - Refusing the charge drops the dirty region, so a starved client can + show stale pixels. That is the contract `materialize` already follows + for refused allocations, and `charge` logs the allocated and budget + figures. + - Reported by Copilot on #1462. Follows #1460, which introduced the + budget and the deferred materialization. + +- Advertise only capability sets the client can decode ([#1564](https://github.com/Devolutions/IronRDP/issues/1564)) ([b7657bcb67](https://github.com/Devolutions/IronRDP/commit/b7657bcb670b080c8cd19a9dff0078387cb8c478)) + + ## Summary + +- Compose scaled output surfaces ([#1699](https://github.com/Devolutions/IronRDP/issues/1699)) ([96e388eef2](https://github.com/Devolutions/IronRDP/commit/96e388eef27c121b5590da7a1a3fdfb1d47d11e9)) + + Implement client-side composition for MapSurfaceToScaledOutput. + + Track scaled mappings in the EGFX compositor, materialize scaled dirty + output at EndFrame, and preserve bounded output clipping and + frame-atomic updates. + + Add client and compositor coverage for dispatch, nearest-neighbor + pixels, dirty bounds, persistent output, and oversized wire-scale + factors. + +- Declare the complete capability ladder in the default server preferences ([#1854](https://github.com/Devolutions/IronRDP/issues/1854)) ([8cdd788ce7](https://github.com/Devolutions/IronRDP/commit/8cdd788ce7643488ef06df743038815789fd122b)) + + The default preferred_capabilities ladder in GraphicsPipelineHandler + declares four of the eleven capability versions this build can + interpret: V10.7, V10, V8.1 and V8. Negotiation matches exact versions + (discriminant equality, highest server priority first), so a client that + advertises only a mid-tier V10 variant falls through every rung: it + negotiates nothing at the V10 level and either drops to V8-class + capabilities or fails the channel, despite both sides fully supporting, + say, V10.6. Mid-tier-only advertisements exist in the wild; the Windows + App family is the known case. + + This completes the default ladder to all eleven versions in priority + order, with the flags each version actually defines: SMALL_CACHE where + the version has it, V10.3's flag set left empty (it defines no + SMALL_CACHE, and an empty set means AVC enabled), V10.1 as the unit + variant, and the existing V8.1/V8 rungs unchanged. + CodecCapabilities::from_capability_set already maps every one of these + versions correctly, so a newly negotiable mid-tier version derives the + right AVC availability with no other change. + + The handler documentation now explains why the complete ladder matters, + and the negotiation function itself is untouched. + +- [**breaking**] Use exclusive AVC region bounds ([#1788](https://github.com/Devolutions/IronRDP/issues/1788)) ([e0727394c0](https://github.com/Devolutions/IronRDP/commit/e0727394c0c5ef172a13617594e1b2db90e006f7)) + + MS-RDPEGFX 2.2.1.2 defines `RDPGFX_RECT16` with exclusive `right` and + `bottom`, and 2.2.4.4.1 gives `regionRects` in `RFX_AVC420_METABLOCK` + that same type. `Avc420Region` documented its edges as inclusive, + `full_frame()` built `width - 1` / `height - 1`, and `to_rectangle()` + handed those to an `InclusiveRectangle` that encoded them unchanged, so + every `regionRects` entry went out a pixel short on the right and bottom + edge. `GraphicsPipelineServer::compute_dest_rect()`, used for the + `WireToSurface1` destination, does add that pixel back: the two call + sites disagreed about whether the conversion had already happened. + + `Avc420Region` is exclusive throughout now and `to_rectangle` produces + an `ExclusiveRectangle`, so the asymmetric adjustment disappears. + `fix(egfx): bound both AVC444 streams` then computes the destination + rectangle from both streams rather than from the luma one, so a chroma + region larger than luma is no longer cut short. `feat(egfx): add + explicit AVC444v2 frame sender` separates the AVC444v2 path from AVC444; + both fixes build on it. + + What the defect costs: with `ironrdp-server` on master, Windows App on + macOS drops the connection right after the encoder starts, the server + reporting `peer closed connection without sending TLS close_notify`. + Pinning the session resolution, so that no deactivation-reactivation + happens at all, changes nothing. With these three commits the same + server build and the same client run normally. FreeRDP-based clients + accept the frames either way, which is why this shows only against the + Microsoft client. That is not proof this field is the trigger - + 2.2.4.4.1 also calls the metablock informational - only that the field + is wrong on the wire and that these commits make that client work. + + `avc.rs` lost its inline test module to #1736 while these sat in a fork, + so the `full_frame` expectation is updated in + `testsuite-core/tests/egfx/avc.rs` instead. + + Please rebase-merge rather than squash. The three commits are + @MuNeNiCK's with his authorship intact, and the breaking change is + confined to the middle one; squashing would collapse both. + +- Decode AVC420 with full-range BT.709 ([#1923](https://github.com/Devolutions/IronRDP/issues/1923)) ([9542e017da](https://github.com/Devolutions/IronRDP/commit/9542e017da4481ba4919b944f4e0742f23a83742)) + +- Do not track frames a suspended client cannot acknowledge ([#1912](https://github.com/Devolutions/IronRDP/issues/1912)) ([6ceb5387b5](https://github.com/Devolutions/IronRDP/commit/6ceb5387b546290b5d49c60e126ad07c69fe70ba)) + +- Draw AVC420 partial updates from regionRects ([#2042](https://github.com/Devolutions/IronRDP/issues/2042)) ([#2052](https://github.com/Devolutions/IronRDP/issues/2052)) ([9f07fd4b20](https://github.com/Devolutions/IronRDP/commit/9f07fd4b2047ef6147454bace54f97bc45d2e9fb)) + + AVC420 partial frame updates paint the wrong pixels. On the client + decode path, `decode_avc420` decoded the `Avc420BitmapStream` but only + ever used `stream.data` — the `regionRects` in `stream.rectangles` (the + `RFX_AVC420_METABLOCK` from [MS-RDPEGFX] 2.2.4.4) were never read. + `crop_decoded_frame` then copied a single block from the decoded frame's + origin `(0,0)` and blitted it across the whole destination rectangle. + + So whenever a frame's changed regions did not happen to sit at the + top-left corner, each region was filled with pixels lifted from `(0,0)`, + and the space between regions got overwritten. That is the "horizontal + lines and unfilled rectangle outlines" people see in GFX / H.264 + (AVC420) mode once the server starts sending partial updates instead of + full frames. + + The fix follows the spec: `regionRects` are the sub-regions that + actually changed, each one takes its pixels from the *same* `(x, y)` in + the decoded frame, and the PDU's destination rectangle is just their + bounding box — not a copy source. `decode_avc420` now walks + `stream.rectangles`, clips each rect to `surface ∩ frame`, copies `(x, + y) → (x, y)` through a small new `copy_frame_region` helper, and emits + one surface update per rectangle. A stream that carries no `regionRects` + keeps the old single bounding-box path, so nothing changes for that + case. + + The change is deliberately narrow — only the client decode path in + `crates/ironrdp-egfx/src/client.rs`. AVC444, the server encode path, and + the NAL-format work in #1986 are all left alone. + + For the test, a deterministic stand-in decoder stamps every pixel with + its own coordinate (`R = x`, `G = y`), so a test can tell which source + pixel actually landed where. It sends an AVC420 update with two disjoint + regions, neither at the origin, whose bounding box is the destination + rectangle, and checks that each region is drawn from its own + coordinates. Before the fix it fails with a single origin-cropped update + where two were expected; after the fix it gets one update per region, + with region B at `(32, 32)` carrying its own pixels. `cargo test -p + ironrdp-egfx` passes (54 tests) and `cargo clippy -p ironrdp-egfx + --all-targets` is clean. + + This one is a co-fix. The root-cause analysis and the region-rects + harness/recipe (`c06_avc420_region_rects.rs`) came from the issue + author, @se-wo; this PR implements that recipe on the client decode path + and turns the harness into an in-tree unit test. Glad to fold in + @se-wo's original harness verbatim or adjust the attribution however + maintainers prefer. + +### Performance + +- Allocate the ClearCodec decoder lazily on first use ([#1738](https://github.com/Devolutions/IronRDP/issues/1738)) ([01075338e5](https://github.com/Devolutions/IronRDP/commit/01075338e5c23087b55e7673b84b18d7081b01ba)) + + ## Summary + + + ## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-egfx-v0.2.0...ironrdp-egfx-v0.3.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-egfx/Cargo.toml b/crates/ironrdp-egfx/Cargo.toml index 25006b8495..43132f9a4b 100644 --- a/crates/ironrdp-egfx/Cargo.toml +++ b/crates/ironrdp-egfx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-egfx" -version = "0.3.0" +version = "0.4.0" readme = "README.md" description = "Graphics pipeline dynamic channel extension implementation" edition.workspace = true @@ -19,10 +19,10 @@ doctest = false arbitrary = { version = "1", features = ["derive"], optional = true } bit_field = "0.10" bitflags = "2.11" -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public -ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public openh264 = { version = "0.9", optional = true, default-features = false } tracing = { version = "0.1", features = ["log"] } yuv = { version = "0.8", default-features = false, optional = true } diff --git a/crates/ironrdp-error/CHANGELOG.md b/crates/ironrdp-error/CHANGELOG.md index 4614e94933..4343f5820d 100644 --- a/crates/ironrdp-error/CHANGELOG.md +++ b/crates/ironrdp-error/CHANGELOG.md @@ -6,6 +6,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-error-v0.2.0...ironrdp-error-v0.2.1)] - 2026-09-30 + +### Features + +- Add ergonomic cross-error mapping ([#1481](https://github.com/Devolutions/IronRDP/issues/1481)) ([71d63c75f5](https://github.com/Devolutions/IronRDP/commit/71d63c75f58228fa0960c3b52327e9694272e976)) + +- Add IronRDP ActiveX COM server ([#1523](https://github.com/Devolutions/IronRDP/issues/1523)) ([ee58b7c5f2](https://github.com/Devolutions/IronRDP/commit/ee58b7c5f283ef64be93a8483242f49070c6cdc9)) + + ## Summary + - Add the IronRDP ActiveX COM server and native MSTSC host integration. + - Provide bounded native-host diagnostics, credential-bridge support, + and an AxHost test harness. + - Preserve the minimal client, connector, and error integrations needed + by the control. + + ## Validation + - cargo check -p ironrdp-activex + - Focused ironrdp-error and ironrdp-connector tests + - cargo test -p ironrdp-activex --lib --no-run + - cargo fmt --all -- --check + - cargo xtask check locks -v + + --------- + +### Bug Fixes + +- Make source locations opt-in ([#1480](https://github.com/Devolutions/IronRDP/issues/1480)) ([f84cd01450](https://github.com/Devolutions/IronRDP/commit/f84cd01450e18d12838b225859878b311802b805)) + + Default error display omits locations; alternate formatting and reports + with explicit location opt-in preserve diagnostic context. + + + ## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-error-v0.1.3...ironrdp-error-v0.2.0)] - 2026-05-27 ### Features diff --git a/crates/ironrdp-error/Cargo.toml b/crates/ironrdp-error/Cargo.toml index 037fadff72..47edc939a1 100644 --- a/crates/ironrdp-error/Cargo.toml +++ b/crates/ironrdp-error/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-error" -version = "0.2.0" +version = "0.2.1" readme = "README.md" description = "IronPDU generic error definition" edition.workspace = true diff --git a/crates/ironrdp-futures/CHANGELOG.md b/crates/ironrdp-futures/CHANGELOG.md index 1ffd5aa90b..9b9d883f8a 100644 --- a/crates/ironrdp-futures/CHANGELOG.md +++ b/crates/ironrdp-futures/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.8.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-futures-v0.8.0...ironrdp-futures-v0.8.1)] - 2026-09-30 + + + ## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-futures-v0.7.0...ironrdp-futures-v0.8.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-futures/Cargo.toml b/crates/ironrdp-futures/Cargo.toml index 37838f2acd..4d7aa6c83f 100644 --- a/crates/ironrdp-futures/Cargo.toml +++ b/crates/ironrdp-futures/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-futures" -version = "0.8.0" +version = "0.8.1" readme = "README.md" description = "`Framed*` traits implementation above futures’s traits" edition.workspace = true @@ -18,7 +18,7 @@ test = false [dependencies] futures-util = { version = "0.3", features = ["io"] } # public -ironrdp-async = { path = "../ironrdp-async", version = "0.10" } # public +ironrdp-async = { path = "../ironrdp-async", version = "0.11" } # public [lints] workspace = true diff --git a/crates/ironrdp-graphics/CHANGELOG.md b/crates/ironrdp-graphics/CHANGELOG.md index 8dba87b85b..128e37101f 100644 --- a/crates/ironrdp-graphics/CHANGELOG.md +++ b/crates/ironrdp-graphics/CHANGELOG.md @@ -6,6 +6,586 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.9.0...ironrdp-graphics-v0.10.0)] - 2026-09-30 + +### Features + +- Add ClearCodec client-side decode dispatch ([#1175](https://github.com/Devolutions/IronRDP/issues/1175)) ([714dce4662](https://github.com/Devolutions/IronRDP/commit/714dce46627e299c57d82f4f6a5c18067a95bffa)) + + Follow-up to #1174. Supersedes #1195 (the standalone server-helper PR; + its 46-line `send_clearcodec_frame()` is included here). + + Wires ClearCodec into the EGFX client's WireToSurface1 codec dispatch, + matching the existing AVC420 and Uncompressed decode patterns. + +- Decode RFX Progressive tiles ([#1673](https://github.com/Devolutions/IronRDP/issues/1673)) ([f21f1979f4](https://github.com/Devolutions/IronRDP/commit/f21f1979f4ef7ce8756a4022d866c7f7fc150b0b)) + + Keep Progressive tile state scoped to its surface and codec context. + + Reject context-less updates that have no state for their surface, and + expose targeted context and surface cleanup for consumers. + +- Decode ClearCodec NSCodec ([#1728](https://github.com/Devolutions/IronRDP/issues/1728)) ([2cac77e444](https://github.com/Devolutions/IronRDP/commit/2cac77e444a24f2bdcc96af18a551caf21c22a7a)) + + Decode NSCodec layer-three regions into their ClearCodec destinations. + + Validate NSCodec plane lengths and RLE output before converting and + blitting BGRA pixels. + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + +- Clip Progressive updates to REGION and decode Windows streams ([#1443](https://github.com/Devolutions/IronRDP/issues/1443)) ([11b10531de](https://github.com/Devolutions/IronRDP/commit/11b10531de491c67f5c8d6bb6972f6951bbeecc8)) + + ## Problem + + #1673 landed the core `WireToSurface2` → `ProgressiveDecoder` dispatch + (originally proposed here), and #1696/#1698 extended the decoder past + this PR's scope. What remains splits in two. + + **REGION handling.** Master decodes REGION blocks wherever they appear, + including outside `FRAME_BEGIN`/`FRAME_END` — [CBenoit's + review](https://github.com/Devolutions/IronRDP/pull/1443#issuecomment-5164007525) + asked for those to be ignored, and that part is still open. Decoded + tiles are also blitted whole (64x64) with the REGION rectangles ignored, + so pixels outside the damage region the server reported are written to + the surface anyway. + + **Progressive from a Windows host does not decode at all.** Three + separate defects, each hit in order while replaying a live session + against Windows (details in the comment below). + + ## Changes + + REGION handling: + + - `decode_bitmap` processes REGION blocks only inside the first + `FRAME_BEGIN`/`FRAME_END` pair and ignores blocks outside it. + - Each `DecodedTile` carries `update_rectangles`: the tile clipped to + the REGION rectangles and surface bounds. The client emits one + `BitmapUpdate` per rectangle, keeping the whole-tile fast path. + - `begin_frame`/`end_frame` (driven by StartFrame/EndFrame) let REGION + blocks split across several `WireToSurface2` payloads of one frame + reference shared tiles. + - Clipping work is budgeted per payload so a hostile REGION cannot drive + quadratic `union_rectangle`/`intersect_rectangle` scans. + + Windows compatibility: + + - `quality == 0xFF` selects full quality per [MS-RDPEGFX] 2.2.4.2.1.5.2 + instead of indexing `quantProgVals`; Windows sends it with an empty + table, so indexing rejected every tile. + - `ResetGraphics` no longer drops codec contexts. It only resizes the + Graphics Output Buffer, and Windows never repeats SYNC + CONTEXT after + one. + - A new codec context id that arrives without SYNC + CONTEXT inherits + the band layout retained for its surface, released when the surface is + deleted. Deleting a context still drops its tiles. + + ## Testing + + `decoder_ignores_regions_outside_frame`, + `decoder_bounds_region_clipping_work`, + `full_quality_tiles_bypass_the_progressive_quant_table`, + `progressive_context_survives_graphics_reset`, + `wire_to_surface2_clips_compositor_output_to_region`, plus the reworked + context-deletion assertions. + + `ironrdp-graphics` 235 and `ironrdp-egfx` 47 pass; `cargo clippy + --all-targets -- -D warnings` and `cargo fmt` clean. + + --------- + +- [**breaking**] Add egfx_zgfx_decompress oracle and harden the ZGFX decoder against attacker-controlled input ([#1333](https://github.com/Devolutions/IronRDP/issues/1333)) ([42320260a2](https://github.com/Devolutions/IronRDP/commit/42320260a2cdfb32eeab646af3d565e88bd3b655)) + + ## Summary + + - Implements target 3 of #1316 (egfx fuzz-coverage umbrella): + `egfx_zgfx_decompress` oracle and target. + - Target shape mirrors PR #1285's `bulk_*` pattern: panic plus sanitizer + oracle on `Decompressor::decompress`, fresh decompressor per iteration + so history state does not leak between fuzz inputs. + - Ships alongside a complete input-validation audit of the ZGFX decoder, + following PR #1271's precedent for "target plus the bugs it surfaces in + one PR." + + ## Hardening + + Eight input-validation gaps closed in `ironrdp-graphics` (all class-(c) + per #1314: reachable via attacker-controlled wire inputs). Each one was + surfaced by a rigorous smoke-fuzz iteration: + + 1. `Bits::try_split_to` (new in `utils.rs`) provides the checked + counterpart to `split_to` that every fix below builds on. + 2. `SegmentedDataPdu::from_buffer` multipart segment-size: + `split_at_checked` plus a defensive `Vec::with_capacity` cap mirroring + PR #1271. + 3. `decompress_segment` trailing-unused-bits subtraction: `checked_mul` + + `checked_sub`. + 4. `decompress_segment` token loop: checked prefix indexing + checked + `split_to(8)` for the NullLiteral value. + 5. `handle_match`: checked distance-bits split, `checked_add` for + `distance_base + value`, plus `distance > HISTORY_SIZE` bound check. + 6. `read_unencoded_bytes`: checked splits for the 15-bit length, + pad-to-boundary, and `length * 8` payload. + 7. `read_encoded_bytes`: checked splits + `usize::BITS` bound on + `load_be::()` + `checked_shl` replacing `pow` + `checked_add` for + `base + value`. + 8. `FixedCircularBuffer::read_with_offset`: defense-in-depth `offset > + buffer.len()` bound check below the caller-side guard. + + Plus memory-budget enforcement, which a match-copy chain makes + necessary: it can expand a few bytes of input into unbounded output, and + an unbounded run reached a 1.5 GB peak. + + 9. New `MAX_DECOMPRESSED_PER_SEGMENT = 64 MiB` ceiling on a single + segment's decompressed output, enforced per-token in + `decompress_segment` and threaded through the match-copy paths so it is + checked before any allocation. **This is an implementation resource + limit, not a wire requirement**, and the code says so, but MS-RDPEGFX + 3.1.9.1.2 ("RDP 8.0 compressor limits") does supply a number: a + compliant compressor MUST NOT produce any single segment past 65,535 + uncompressed bytes. This crate's production compressor path honors that: + `wrapper.rs`'s `wrap_compressed` panics above 65,535, and + `compress_and_wrap_egfx`, the only production caller, falls back to + uncompressed rather than risk exceeding it. The + `compress_high_entropy_round_trips_and_bounds_table` test that an + earlier revision of this PR body cited as evidence real traffic exceeds + 65,535 does not show that: it calls `Compressor::compress` directly and + feeds the result straight to `decompress_segment`, bypassing the wrapper + every real sender uses. 64 MiB is still used here, not 65,535, because + this ceiling exists to catch non-conforming or hostile input, not to + police conforming input: a decoder that hard-rejects at exactly the + compressor-side limit has no margin for a peer implementation with a + minor, benign spec deviation. Same footing as the compositor's + total-byte budget in #1460. + 10. Multipart running-total check against the declared + `uncompressedSize`, surfacing early detection of segments that + collectively exceed the wire-declared bound. Unlike item 9 this one *is* + spec-grounded: 2.2.5.1 defines `uncompressedSize` as the size of + `segmentArray` once reassembled and decompressed, and 3.1.9.1.2.1 states + it MUST equal the total number of decompressed bytes across all + segments. + + The sender side is unchanged and already agreed with the spec: + `ZGFX_SEGMENTED_MAXSIZE = 65535` in `wrapper.rs` splits into multipart + above that, which is exactly the encoder-side behaviour 3.1.9.1.2.1 + describes. + + Seven new `ZgfxError` variants total with matching Display and + `Error::source` impl entries. + + ## Breaking change + + `ZgfxError` is a public exhaustive enum, so the seven added variants + (`InvalidTrailingBitCount`, `SegmentSizeExceedsBuffer`, + `IncompleteBitStream`, `MatchDistanceOutOfRange`, + `LengthTokenSizeTooLarge`, `SegmentDecompressedSizeExceedsLimit`, + `MultipartTotalExceedsDeclared`) break exhaustive matches downstream. + Confirmed by `cargo semver-checks --baseline-rev `: seven + `enum_variant_added` failures on `ironrdp-graphics`. Title carries `!` + accordingly. + + ## Validation + + - `cargo xtask check fmt/lints/tests/typos/locks` all pass. + - `check_egfx_zgfx_decompress` regression-replay passes against both + shipped crash artifacts. + - 46 existing ZGFX unit tests continue to pass against the hardened + code. + - Final 15-minute rigorous fuzz: 2,942,961 iterations in 901 seconds + (~3,266 exec/s sustained), peak RSS 84 MB, zero panics, zero sanitizer + reports, zero OOMs. + + Iterative audit progression on a 15-minute libFuzzer + ASan budget per + round: + - Round 1 (no fixes): crash at iter ~30 (trailing-bit underflow) + - Round 2 (after fixes 1-2): crash at iter ~28 (bit-budget) + - Round 3 (after F1-F8 audit): OOM at iter ~9877 (1.5 GB peak) + - Round 4 (after F1-F10 complete): clean + + ## Notes + + - Crash artifacts for the two header-parse bugs added to + `crates/ironrdp-testsuite-core/test_data/fuzz_regression/egfx_zgfx_decompress/`. + The later-round crashes are not added as separate regression entries + because the libFuzzer corpus accumulated 161 representative inputs and + the unit-tested error paths cover the cases directly. + - The MS-RDPEGFX 3.1.9.1.2 ("RDP 8.0 compressor limits") per-segment + bound is a semantic change: inputs that previously decoded to more than + 65,535 bytes per segment now return `Err`. Such inputs were never + spec-conformant. Codebase precedent (PR #1097 et seq.) is to add + `ZgfxError` variants without a `!` marker on the commit; following that + convention. + - Memory-budget design discussion lives on #1120 + (`issuecomment-4558356535`) rather than in this PR body so future + readers find the rationale on the canonical fuzz-umbrella thread. + +### Bug Fixes + +- Correct progressive base quantization scale ([#1499](https://github.com/Devolutions/IronRDP/issues/1499)) ([ccfe5bb8b3](https://github.com/Devolutions/IronRDP/commit/ccfe5bb8b3b1ddf447776e056d27cd14e2399ab7)) + + ## Summary + +- Decode indexed pointers and foreground RLE runs ([#1519](https://github.com/Devolutions/IronRDP/issues/1519)) ([ad19280762](https://github.com/Devolutions/IronRDP/commit/ad192807620bcc3a0467eaeb07173a79cb1da257)) + + ## Summary + + 4bpp and 8bpp New/Large pointer shapes previously could not use the + active session palette, and malformed or unsupported pointer data could + terminate the session. This decodes indexed XOR masks with the current + palette and falls back to the default cursor while evicting stale cached + data when decoding fails. + + It also corrects RLE foreground runs so only set-foreground variants + consume a foreground pixel. + + Palette updates now follow the RDP wire format (type, padding, 256 + packed RGB triplets) and are applied from both fast- and slow-path + updates, ensuring indexed pointer decoding uses the negotiated palette. + + ## Tests + + - `cargo test -p ironrdp-graphics -p ironrdp-session` + - `cargo test -p ironrdp-session palette` + - `cargo clippy -p ironrdp-graphics -p ironrdp-session --all-targets -- + -D warnings` + + --------- + +- Rename {Read,Write}Cursor::rewinded into rewound ([#1529](https://github.com/Devolutions/IronRDP/issues/1529)) ([c85b089b46](https://github.com/Devolutions/IronRDP/commit/c85b089b4617176240b41482be65a77c9ad76a07)) + +- Correct three RLGR encoder bugs per MS-RDPRFX spec ([#1179](https://github.com/Devolutions/IronRDP/issues/1179)) ([f26d06da6f](https://github.com/Devolutions/IronRDP/commit/f26d06da6f16bfb7fa57f8d4f67658b33c01bc01)) + + ## Summary + + Fixes three bugs in the RLGR entropy encoder + (`ironrdp-graphics::rlgr::encode`) identified by cross-referencing the + MS-RDPRFX §3.1.8.1.7.3 pseudocode and the FreeRDP reference + implementation (`rfx_rlgr.c`). + + - **RL mode trailing value**: the encoder skipped emitting sign bit + GR + code when input was exhausted after a zero run. The decoder + unconditionally reads these bits, so the encoder must always emit them. + Restructured to match FreeRDP's `GetNextInput` pattern. + - **RLGR3 exhausted second value**: used `unwrap_or(1)` as default + `twoMs` when the second value in a GR-mode pair was unavailable. + FreeRDP's `GetNextInput` returns 0 when exhausted, and `Get2MagSign(0) = + 0`, so the correct default is 0. + - **RLGR1 GR-mode kp update**: used `UP_GR` (4, the RL-mode constant) + instead of `UQ_GR` (3, the GR-mode constant) when updating `kp` for zero + symbols. Both the spec pseudocode and FreeRDP use `UQ_GR` here. + + Each fix is in a separate commit with full rationale and spec/FreeRDP + references. + + ### Spec errata note + + The MS-RDPRFX §4.2.4.1 reference hex dump appears to have been generated + with `UQ_GR=4` and `twoMs2=1` defaults — inconsistent with the normative + pseudocode in §3.1.8.1.7.3. Our encoder follows the pseudocode (which is + authoritative over example data) and matches FreeRDP. The Y test vector + is updated accordingly (2 bytes differ from the spec hex dump at indices + 939–940). Encoder→decoder roundtrip is verified correct. + + ## Test plan + + - [x] All 12 existing RLGR unit tests pass (encode + decode, small + vectors + full 4096-coefficient Y/Cb/Cr datasets) + - [x] Encoder→decoder roundtrip verified for Y, Cb, Cr components + - [x] `cargo clippy -p ironrdp-graphics` clean + - [x] `cargo xtask check lints -v` passes + + 🤖 Generated with [Claude Code](https://claude.com/claude-code) + + --------- + +- [**breaking**] Do not panic when the RLGR output buffer is too small ([#1558](https://github.com/Devolutions/IronRDP/issues/1558)) ([71903c5509](https://github.com/Devolutions/IronRDP/commit/71903c550929dbbd1b1b881cc403a6a693b6e233)) + + ## Summary + + - `BitStream::output_bit` and `output_bits` indexed the output + `BitSlice` with no capacity check, so a tile whose entropy coding didn't + fit the caller's buffer panicked inside a function that already returns + `Result`. + - The RLGR decoder a few hundred lines down has been bounds-checked all + along: `try_split_bits!` breaks when bits run short, the loop guards on + `!bits.is_empty() && !output.is_empty()`, and run-length fills clamp + with `min(run, output.len())`. Only the encoder was unguarded. + - The writers now reserve before writing and record an overflow rather + than indexing past the end. `encode` turns that into + `RlgrError::OutputTooSmall`. + + ## Why it's reachable + + RLGR gives no compression guarantee, but callers size the output as + though it did. `ironrdp-server` splits a 12288-byte tile buffer into + three 4096-byte components, which is 2:1 against 4096 `i16` + coefficients. That holds comfortably at the default quantization table + and stops holding as the table gets lighter, so this is reachable from + configuration rather than from malformed input. + + The tile is still walked to completion after an overflow, so the error + reports the size the tile would have needed. A caller sizing on a ratio + needs that number to correct the ratio; it's the reason the variant + carries data. + + ## Retrying at the size the encoder asks for + + Detection alone was not a fix, which @mamoreau-devolutions was right to + push back on. `alloc_data` gave every component exactly 4096 bytes, and + the loop in `encoder/mod.rs` that grows a buffer on `NotEnoughBytes` + grows the whole-frame one, so it could never have helped here. A tile + that overflowed went from panicking to failing the update. + + The per-component reserve is a parameter now, and the tile-set encode + retries at exactly the size the encoder reports, growing monotonically + and stopping at twice a component's raw `i16` size. + + I did not take the fixed upper bound offered as an alternative. RLGR is + adaptive and unary-dominated in its worst case, so a bound loose enough + to be provable is megabytes per component, and anything small enough to + allocate is a guess. Since the encoder can say exactly what it needs, + retrying at that seemed better than inventing a constant. + + The retry lives in `RfxEncoder::encode` because `rfx::Tile` borrows out + of the buffer, so the buffer has to be sized before any borrow is taken. + Widening `Tile` would reshape a wire PDU type, and a per-tile fallback + needs the overflow buffers to outlive a `par_chunks_mut` borrow. + + `OutputTooSmall` is deliberately not mapped onto `NotEnoughBytes` to + reuse that existing loop, since the loop grows a different buffer and + would look like a fix without being one. + + ## Breaking change + + `RlgrError` gains an `OutputTooSmall` variant and isn't + `#[non_exhaustive]`, so exhaustive matches need updating. + + `ironrdp-graphics` is marked `# public` in `ironrdp-server`, + `ironrdp-client`, `ironrdp-egfx`, `ironrdp-session`, and + `ironrdp-nscodec` (under its `encoder` feature), so the bump cascades to + those. + + I considered reusing + `RlgrError::Io(io::Error::from(ErrorKind::WriteZero))` to avoid the + break. It discards both numbers the caller needs to act on, which + defeats the point. Marking the enum `#[non_exhaustive]` is a separate + breaking change and a wider policy question, so it isn't bundled here. + + ## Validation + + - `cargo xtask check fmt/lints/tests/typos/locks` all pass on the pinned + toolchain, `fuzz/` built before the lock check. + - Two tests added in + `crates/ironrdp-testsuite-core/tests/graphics/rlgr.rs`: an undersized + buffer reports `OutputTooSmall` with `needed > available` for both RLGR1 + and RLGR3, and a buffer of exactly the required size still succeeds. The + twelve existing `graphics::rlgr` tests are unchanged and still pass. + - Two more in `crates/ironrdp-testsuite-core/tests/server/rfx.rs` for + the retry: the reported size must be sufficient rather than merely + indicative, so encoding at a 64-byte reserve and retrying at exactly the + size reported has to succeed; and the default reserve still handles a + full-entropy tile, so the retry stays a fallback. Under-reporting the + size by one byte fails the first of those. + - Those two started life inline in `encoder/rfx.rs`, where they never + ran: `ironrdp-server` sets `[lib] test = false`, so `cargo test + --workspace` builds no unittest binary for it. They reach the encoder + through the crate's existing `__bench` feature, which now also exports + `rfx_enc_at` next to the two helpers already there, so no type had to be + widened. + - Driving the server pipeline (BGRA, `to_64x64_ycbcr_tile`, + `dwt::encode`, `quantization::encode`, `rlgr::encode` into 4096 bytes) + on a random-noise tile, a quantization table of all ones panics on + master and now returns `encoded tile needs 6018 bytes, output buffer is + 4096`. + - Spec-legal tables are unaffected, and that is measured rather than + assumed. Binary-searching the minimum per-component reserve for a set of + adversarial 64x64 tiles at `Quant::default()` gives a worst case of 2857 + bytes of the 4096 available (per-channel independent noise at full + swing, RLGR3); full-range noise needs 2379 under RLGR1. Taking that + worst pattern across the whole legal range from [MS-RDPRFX] 2.2.2.1.5, + RLGR1 needs 3768 bytes at quant 6, 2740 at 8, and 917 at 15. Nothing in + spec overflows, which is why this PR fixes the panic and stops there. + + ## Notes + + Found while looking at #1557, where an all-ones quantization table + panicked the encoder. That table is out of spec ([MS-RDPRFX] 2.2.2.1.5 + requires 6 to 15), and the configurability that issue asks for is + separate work. This change is only about not panicking. + + Touches `BitStream` and the tail of `encode`, deliberately staying clear + of the body of `encode` where #1370 and #1179 both have hunks. It should + rebase cleanly whichever of those lands first. + +- Don't emit a value after a run that ends the input ([#1569](https://github.com/Devolutions/IronRDP/issues/1569)) ([d9d2896c8c](https://github.com/Devolutions/IronRDP/commit/d9d2896c8cbc06cf1c7b82f45b19482d0633dcb2)) + + RL mode codes the following value's magnitude minus one, so it cannot + express + zero. #1179 began coding a zero there when the run consumed the rest of + the + input, and the decoder reconstructs magnitude as the coded value plus + one, so + every input ending in a zero run gained a trailing 1. + + That is what fails + `progressive_fractional_base_quantization_reconstructs_rgb` + from #1499 on master: quantized coefficients end in a zero run, so the + phantom + value lands in HH1 of each component. + + The trailing zeros are the run and nothing follows them. #1179's other + two + fixes are untouched. + + 2000 randomized round trips per entropy mode, 0 failures. Workspace + suite green. + +- Decode Progressive SRL refinements ([#1696](https://github.com/Devolutions/IronRDP/issues/1696)) ([7824bc7503](https://github.com/Devolutions/IronRDP/commit/7824bc75034846e0b69e955aa671cd25527c8d1e)) + + Preserve SRL and raw-bit state across Progressive DWT bands. + + Reject malformed SRL refinement streams without decoding invented zero + data or partially updating tiles. + +- Retain Progressive difference tiles ([#1698](https://github.com/Devolutions/IronRDP/issues/1698)) ([69e323ae47](https://github.com/Devolutions/IronRDP/commit/69e323ae473264e13b2bb3f70a356cd967debea8)) + + Retain quantized DWT coefficients per Progressive tile so difference + updates compose with their matching surface reference while progressive + codec-context state remains isolated. + + Reject difference tiles that lack a retained reference instead of + decoding them against zeros. + + Keep retained surface references across codec grid replacement and + ResetGraphics; only deleting the surface releases them. + +- Widen the Progressive YCbCr conversion to i64 ([#1703](https://github.com/Devolutions/IronRDP/issues/1703)) ([fb0c041350](https://github.com/Devolutions/IronRDP/commit/fb0c041350a5922a4c5d61fc43ac904300044fa6)) + +- Cap RLE decoder dimensions to prevent OOM from adversarial input ([#1962](https://github.com/Devolutions/IronRDP/issues/1962)) ([9b151c4c2e](https://github.com/Devolutions/IronRDP/commit/9b151c4c2e47c6014e1e8e55909d4180aa8bdb99)) + + ## Summary + + - `ironrdp_graphics::rle::decompress_helper` allocated its output buffer + as `dst.resize(row_delta * height, 0)` with no cap on `width`/`height`, + both of which come directly from `TS_BITMAP_DATA`'s wire-decoded, + unvalidated fields (MS-RDPBCGR 2.2.9.1.1.3.1.2.2, both 16-bit unsigned + integers). Worst case: 65535 * 65535 * 3 bytes (24 bpp) is roughly 12.6 + GB from a few attacker-controlled bytes. + - Added a per-axis `MAX_DECODE_DIM = 8192` cap, checked before the + allocation, matching the existing cap already used by + `ironrdp-graphics`'s ClearCodec decoder and MS-RDPBCGR's own documented + maximum desktop width for current Windows RDP server versions (section + 3.3.5.3.3, note 46). + - New `RleError::DimensionsTooLarge` variant carries the offending + width/height for diagnostics. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Added three + regression tests: width over the limit, height over the limit, and a + boundary test confirming `MAX_DECODE_DIM` itself is still accepted (not + an off-by-one). + + ## Notes + + Filed as part of the audit tracked in #1315. The existing + `fuzz/fuzz_targets/rle_decompression.rs` target's `BitmapInput` + generator currently caps width/height at `u8` (max 255), well under both + the old and new limits, so it would not have found this on its own; + widening that generator to the wire's actual `u16` range is a natural + follow-up but is out of scope here since it also feeds three other + oracles (`rdp6_encode_bitmap_stream`, + `rdp6_decode_bitmap_stream_to_rgb24`, and one more) that have not been + audited for the same class of issue. + +### Performance + +- Portable SIMD inverse DWT (wide + SWAR) ([#1383](https://github.com/Devolutions/IronRDP/issues/1383)) ([629154026d](https://github.com/Devolutions/IronRDP/commit/629154026de0eaaf16b93352b4cecbae49a87511)) + + ## Summary + + On the WASM web client, frame **decode** dominates (~93% of frame time + on a 1080p RemoteFX replay), and within decode the **RFX inverse DWT was + ~48%** (the YCbCr→RGBA convert is already SIMD via `yuv`; the + entropy/RLE stages are inherently sequential). This vectorizes the + inverse DWT with the portable [`wide`](https://crates.io/crates/wide) + crate (`i16x8`), so the same code lowers to **wasm `simd128`, x86 + SSE/AVX, and ARM NEON** — desktop and browser both benefit. + + The encode path is unchanged. + + ## How it stays bit-exact (no `unsafe`, no `cfg` split) + + The lifting steps need i32 intermediates only for the averages. + Overflow-free SWAR identities let the whole kernel stay in `i16` lanes + (no widen/narrow): + + - `ceil_avg(a,b) = (a|b) - ((a^b)>>1)` ≡ `(a + b + 1) >> 1` + - `floor_avg(a,b) = (a&b) + ((a^b)>>1)` ≡ `(a + b) >> 1` + + and `(2x+1)>>1 == x` / `(x+x)>>1 == x` simplify the first/last rows. + Every other op is wrapping `i16` arithmetic, identical to the old + `i32`-intermediate-then-`as i16` truncation. + + ## Performance + + 1080p RemoteFX replay, headless Chromium, wasm release `+simd128`, + 8-pass median: + + | inverse DWT | decode (ms) | + |---|--:| + | scalar (baseline) | ~1598 | + | **portable `wide` SIMD** | **~985** | + + → inverse DWT ~2×, **~39% off the decode stage**. (Absolute ms carry + ~±15% machine-load noise; the ratio is stable. Per-frame this is a + throughput win — decode was already within real-time budget.) + + ## Correctness + + Verified bit-exact three ways: + - the replay-bench **framebuffer CRC32** is unchanged, + - the existing **native DWT tests** pass (so it's exact on x86 too, not + just wasm), + - an **exhaustive** check of the SWAR identities over all `i16 × i16` + pairs (0 mismatches). + + ## Notes + + - `wide` is a single-user dep in `ironrdp-graphics`; chosen over + `std::simd` (still nightly-only) and over per-arch intrinsics (one + portable kernel vs three). + - Reproducible bench branches: `bench/draw-*` (renderer) and the DWT + measurements were taken on the replay-bench harness branch (the capture + corpus is gitignored). + + + ## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.8.1...ironrdp-graphics-v0.9.0)] - 2026-07-10 ### Bug Fixes diff --git a/crates/ironrdp-graphics/Cargo.toml b/crates/ironrdp-graphics/Cargo.toml index fa850ded27..a65b2ec3a4 100644 --- a/crates/ironrdp-graphics/Cargo.toml +++ b/crates/ironrdp-graphics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-graphics" -version = "0.9.0" +version = "0.10.0" readme = "README.md" description = "RDP image processing primitives" edition.workspace = true @@ -20,8 +20,8 @@ doctest = false bit_field = "0.10" bitflags = "2.11" bitvec = "1.0" -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["std"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["std"] } # public byteorder = "1.5" # TODO: remove num-derive.workspace = true # TODO: remove num-traits.workspace = true # TODO: remove diff --git a/crates/ironrdp-input/CHANGELOG.md b/crates/ironrdp-input/CHANGELOG.md index 001665e2da..d3a4bc5d98 100644 --- a/crates/ironrdp-input/CHANGELOG.md +++ b/crates/ironrdp-input/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.7.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-input-v0.7.0...ironrdp-input-v0.7.1)] - 2026-09-30 + +### Bug Fixes + +- Clamp wheel rotation units to the wire's 9-bit range ([#1818](https://github.com/Devolutions/IronRDP/issues/1818)) ([2685d85cc4](https://github.com/Devolutions/IronRDP/commit/2685d85cc4f95e2ee3294e4edda4d0ff41f9ff80)) + + + ## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-input-v0.6.0...ironrdp-input-v0.7.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-input/Cargo.toml b/crates/ironrdp-input/Cargo.toml index 94f398a6d6..70a61ba434 100644 --- a/crates/ironrdp-input/Cargo.toml +++ b/crates/ironrdp-input/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-input" -version = "0.7.0" +version = "0.7.1" readme = "README.md" description = "Utilities to manage and build RDP input packets" edition.workspace = true @@ -17,7 +17,7 @@ doctest = false test = false [dependencies] -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public bitvec = "1.0" smallvec = "1.15" diff --git a/crates/ironrdp-mstsgu/CHANGELOG.md b/crates/ironrdp-mstsgu/CHANGELOG.md index 349e0970fd..974f5f74d1 100644 --- a/crates/ironrdp-mstsgu/CHANGELOG.md +++ b/crates/ironrdp-mstsgu/CHANGELOG.md @@ -7,6 +7,320 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [[0.0.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-mstsgu-v0.0.1...ironrdp-mstsgu-v0.0.2)] - 2026-09-30 + +### Security + +- Add RPC integrity framing ([#1839](https://github.com/Devolutions/IronRDP/issues/1839)) ([db6436eead](https://github.com/Devolutions/IronRDP/commit/db6436eead9aa6a028d3e6a7317919b3f61dec84)) + + Provide structural packet-integrity DCE/RPC framing with explicit + caller-owned signed regions and verifier validation. + + Reject unsupported authentication levels and clear terminal response + state after failed decoding. Verifier construction, sequence ownership, + and RPCH transport remain with a future security context. + +### Features + +- Add Negotiate and NTLM HTTP gateway auth ([#1715](https://github.com/Devolutions/IronRDP/issues/1715)) ([476b897129](https://github.com/Devolutions/IronRDP/commit/476b897129634c7f39d5679a80265258d506c1d3)) + + Corporate RD Gateways typically challenge with Negotiate or NTLM rather + than Basic, so the WebSocket-only client could not complete the upgrade. + + The first upgrade request omits Authorization. On 401, WWW-Authenticate + is parsed as an HTTP challenge list and Negotiate is preferred (Kerberos + then NTLM inside SPNEGO via sspi network_client), then NTLM, then HTTP + Basic. SSPI and KDC resolution run on spawn_blocking. A 101 may complete + SSPI from a final GSS token when present. connect and connect_with_port + signatures are unchanged. + + sspi 0.21 is an implementation-only dependency. HTTP auth tests live in + tests/http_auth.rs so [lib] test = false can stay. xtask runs that + target with native-tls. RPCH, UDP, reauth, Detect, extended-auth packet + exchange, and TLS channel bindings remain out of scope. + +- Add RDG-UDP PDU codecs ([#1718](https://github.com/Devolutions/IronRDP/issues/1718)) ([5c6a28d2a5](https://github.com/Devolutions/IronRDP/commit/5c6a28d2a5e8ad38010998bbe2ce14f9a9f0687e)) + + Encode and decode MS-TSGU UDP framing (CONNECT_PKT, DATA_PKT, DISC_PKT, + and UDP_CORRELATION_INFO) plus HTTP_CHANNEL_RESPONSE UDP offer metadata. + + A live DTLS side channel is not opened; these helpers only cover the + packet layouts in [MS-TSGU] 2.2.5.4 and 2.2.11. + +- Decode HTTP service, reauth, and channel-close packets ([#1723](https://github.com/Devolutions/IronRDP/issues/1723)) ([e583e3a1bb](https://github.com/Devolutions/IronRDP/commit/e583e3a1bb59b76624b1c25d7155890b29b0f14f)) + + PktTy already named ChannelClose, ServiceMessage, and ReauthMessage + without decoding those payloads. + + Add encode/decode for HTTP_SERVICE_MESSAGE, HTTP_REAUTH_MESSAGE, and + HTTP_CLOSE_PACKET, plus common MS-TSGU HRESULT display names. The work + loop logs service and reauth packets. A server channel-close is answered + with PKT_TYPE_CLOSE_CHANNEL_RESPONSE, then the work task ends. + Mid-session reauthentication is not performed. + +- Add DCE/RPC fragment codecs ([#1725](https://github.com/Devolutions/IronRDP/issues/1725)) ([438b177645](https://github.com/Devolutions/IronRDP/commit/438b1776459e9cb813f0fad034472e1db3627b85)) + + Master ironrdp-mstsgu is WebSocket + HTTP Negotiate/NTLM/Basic and has + no DCE/RPC types. Add the common-header / fragmentation foundation used + by a later RPC-over-HTTP transport: PDU errors, syntax version, fragment + sizes, common header, stream framer, response reassembly, and fault + parse. + + This does not add TsProxy NDR, RTS, NTLM packet integrity, or a live + RPCH client. + +- Extract PacketIo from the WebSocket gateway client ([#1717](https://github.com/Devolutions/IronRDP/issues/1717)) ([01aa50bedf](https://github.com/Devolutions/IronRDP/commit/01aa50bedf60b73ed1c681989f399e81b4469b49)) + + Move the HTTPS WebSocket transport, auth upgrade loop, and byte + read/write helpers out of lib.rs so handshake/tunnel/channel + sequencing stays separate from I/O. + + GwClient write-side shutdown now closes the outbound sender and + inbound receiver, then waits for the worker so a local EOF can + finish even if inbound delivery is blocked. Local shutdown still + does not send HTTP_CLOSE_PACKET; inbound channel-close handling + from master is preserved. + + This does not include dual-HTTP, RPCH, proxies, or certificate + policy APIs. + +- Add RPC-over-HTTP request framing ([#1727](https://github.com/Devolutions/IronRDP/issues/1727)) ([77689c3439](https://github.com/Devolutions/IronRDP/commit/77689c343999a155ab34b1dba5fdbf7c87925897)) + + Add internal codecs for raw RPCH HTTP request and response framing. + + They validate bounded request and response heads without enabling a live + RPC-over-HTTP gateway transport. + +- Decode tunnel authorization policy fields ([#1730](https://github.com/Devolutions/IronRDP/issues/1730)) ([6d4e78b1af](https://github.com/Devolutions/IronRDP/commit/6d4e78b1afe8a7895f1abd4cfa92f7d232ff7a6b)) + + Decode optional gateway redirection flags, idle timeout, and SoH + response. + Expose them through GwClient without enforcing the reported policy. + + --------- + +- Fall back to dual HTTP gateway transport ([#1752](https://github.com/Devolutions/IronRDP/issues/1752)) ([6e56bd73e4](https://github.com/Devolutions/IronRDP/commit/6e56bd73e45e50a7ef3484187309238ee0747c3a)) + + Support authenticated dual HTTP fallback after an RDG_OUT_DATA 200. + Retain WebSocket on 101 and replay gateway cookies across setup. + +- Add smart-card gateway authentication ([#1741](https://github.com/Devolutions/IronRDP/issues/1741)) ([761dae12b0](https://github.com/Devolutions/IronRDP/commit/761dae12b0128be0f9bae52ca59eae8a0ba0b02f)) + + Add an opt-in Kerberos PKINIT path for HTTP Negotiate gateway + authentication while retaining the password Negotiate, NTLM, and Basic + flows. + + The public credentials type accepts an application-supplied UPN, redacts + credentials, and rejects unsupported smart-card feature or challenge + combinations without exposing them. + +- Encode reauth tunnel context ([#1759](https://github.com/Devolutions/IronRDP/issues/1759)) ([18e259cd10](https://github.com/Devolutions/IronRDP/commit/18e259cd108b1debe6598b93f760af5d527ad194)) + + Encode an optional reauth tunnel context in tunnel-create requests while + clearing its presence bit when no context is supplied. + Requests without a context retain their existing wire format. + +- Add TsProxy control stub codecs ([#1758](https://github.com/Devolutions/IronRDP/issues/1758)) ([44a3bbb674](https://github.com/Devolutions/IronRDP/commit/44a3bbb674468c3a4aaecd49a6dab494d0d0f004)) + + Stage bounded NDR32 codecs for the initial TsProxy control sequence. + Keep the codecs internal and transport-free. + +- Handle gateway consent messages ([#1762](https://github.com/Devolutions/IronRDP/issues/1762)) ([d219ddbe16](https://github.com/Devolutions/IronRDP/commit/d219ddbe1664cf6d60d96a4bbb40d760d09e16c8)) + + Decode gateway consent messages as UTF-16LE during tunnel creation. + + Accept consent by default, and let callbacks decline it before tunnel + authorization and channel setup. + +- Add extended-auth packet codecs ([#1761](https://github.com/Devolutions/IronRDP/issues/1761)) ([a14c150e75](https://github.com/Devolutions/IronRDP/commit/a14c150e75ef386657e9a34680bae114f41cf99e)) + + Preserve advertised handshake extended-auth flags, including unknown + bits, and encode/decode extended-auth packet blobs with exact lengths. + + Add wire vectors for flag combinations, packet round trips, malformed + blobs, and u16 blob boundaries. + +- Preserve gateway HRESULT errors ([#1774](https://github.com/Devolutions/IronRDP/issues/1774)) ([d4369f86db](https://github.com/Devolutions/IronRDP/commit/d4369f86db31a62e4d4457c68e7a9c1acbb391ee)) + + Preserve nonzero gateway control HRESULTs for caller diagnostics. + + Known codes retain established labels; unknown codes remain lossless. + +- Apply certificate validation to gateways ([#1775](https://github.com/Devolutions/IronRDP/issues/1775)) ([312d4466e7](https://github.com/Devolutions/IronRDP/commit/312d4466e7cae15d45c73a4ffcb4ae730d5e6a30)) + + Apply the RDP client's certificate-validation policy and callback to + every + gateway HTTPS connection. + + Existing gateway callers retain their compatibility default. + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +- Support outbound gateway proxies ([#1776](https://github.com/Devolutions/IronRDP/issues/1776)) ([4222f49d07](https://github.com/Devolutions/IronRDP/commit/4222f49d07410c52b43d085f504e24b1acbb0bbd)) + + Route all MS-TSGU HTTPS legs through configured HTTP CONNECT or SOCKS5 + proxies while preserving gateway TLS validation and authentication. + + Honor HTTPS_PROXY/https_proxy and NO_PROXY/no_proxy with bounded CONNECT + response handling and credential redaction. + +- Authenticate gateway sessions ([#1802](https://github.com/Devolutions/IronRDP/issues/1802)) ([8649c7c69d](https://github.com/Devolutions/IronRDP/commit/8649c7c69db5279c2ab61116179c1cc3994aed81)) + + Negotiate MS-TSGU SSPI NTLM authentication and reauthenticate the + original session on a short-lived transport without interrupting + application data. + + Reject unsupported smart-card and pluggable exchanges explicitly. + +- Add RPCH v2 setup state ([#1825](https://github.com/Devolutions/IronRDP/issues/1825)) ([4b7ef885f4](https://github.com/Devolutions/IronRDP/commit/4b7ef885f4604105d4fccaf2259afb410577f2c5)) + + Add codec-only RPCH v2 CONN setup, client ping scheduling, and flow + control. + + Keep client ping scheduling distinct from the proxy-facing CONN/B1 + ClientKeepalive setting and acknowledge reclaimed half windows. + + HTTP transport and NTLM signing remain out of scope. + +- Add RPCH v2 channel recycling ([#1831](https://github.com/Devolutions/IronRDP/issues/1831)) ([35152e662d](https://github.com/Devolutions/IronRDP/commit/35152e662d67d522d09b4bcbbfb70198daafed67)) + + Add wire codecs and ordering validation for v2 R1 recycling. + + Retain canonical recycle state and reopen channels only after the final + RTS action is sent. Keep transport execution separate, and use the typed + output test channel required for workspace test compilation. + +- Add unprotected RPC call framing ([#1829](https://github.com/Devolutions/IronRDP/issues/1829)) ([14e5999b7f](https://github.com/Devolutions/IronRDP/commit/14e5999b7f7c23ceebe41b15a75fb72c77c36e44)) + + Add unprotected bind negotiation and request fragmentation. + Use context-aware response and fault decoding with existing reassembly. + Keep authentication, signing, and TsProxy payloads out of this layer. + +- Add TsProxy RPC stub codecs ([#1828](https://github.com/Devolutions/IronRDP/issues/1828)) ([bc568e47ec](https://github.com/Devolutions/IronRDP/commit/bc568e47ec3a32e36d1b5922a2c849bd6b302104)) + + Add internal NDR32 and raw-stub codecs for TsProxy control and channel + calls without exposing transport or client APIs. + +- Add NTLM RPC association setup ([#1838](https://github.com/Devolutions/IronRDP/issues/1838)) ([2b40f47be0](https://github.com/Devolutions/IronRDP/commit/2b40f47be0a4dcb8d916edee1aaf90db31a0ebe6)) + + Add DCE/RPC NTLM association setup for RD Gateway, including + header-signing negotiation. + Keep its SSPI context independent from HTTP authentication contexts. + + This excludes live RPC-over-HTTP transport and protected RPC traffic. + +- Stage RPCH session engine ([#1856](https://github.com/Devolutions/IronRDP/issues/1856)) ([910eb7cad1](https://github.com/Devolutions/IronRDP/commit/910eb7cad15438987da64549392e5ea9f88e6195)) + + Compile the RPCH harness as an internal stream-based engine. + + Keep live RPC-over-HTTP wiring and signed requests unavailable. + +### Bug Fixes + +- Report EOF and stop polling a completed work task ([#1709](https://github.com/Devolutions/IronRDP/issues/1709)) ([8bd50100f5](https://github.com/Devolutions/IronRDP/commit/8bd50100f51d8fe2195e0bd4f2fd3d4f1f4bdfb6)) + + GwClient polled its background work JoinHandle on every read and write, + which tokio panics on once the task completes, and it never reported + end-of-stream when the gateway stream closed. A caller that holds a read + across the connection lifetime (for example a bidirectional relay) hit + `JoinHandle polled after completion` or hung forever instead of seeing + EOF. + + Track work completion with a flag so the handle is polled at most once, + and surface UnexpectedEof once the task has ended and no data remains. + +- Forward the target host and port to the gateway ([#1710](https://github.com/Devolutions/IronRDP/issues/1710)) ([f43966cade](https://github.com/Devolutions/IronRDP/commit/f43966cadeb460b3bb02625532143d45447ca14a)) + + The channel-create packet (HTTP_CHANNEL_PACKET) hardcoded port 3389, so + non-3389 RDP targets and Hyper-V VMConnect (port 2179) could not be + tunneled through an RD Gateway. + +- Support RPCH IN authentication probes ([#1821](https://github.com/Devolutions/IronRDP/issues/1821)) ([1155896fad](https://github.com/Devolutions/IronRDP/commit/1155896fadc5788eef2cc92f0b26c6f4ead0973a)) + + Allow raw RPCH IN framing to authenticate with a zero-length probe + before committing the channel-lifetime request body. + + Require a declared 401 response body no larger than 16 KiB before + draining it, so retries occur only on a reusable connection. The + internal request wrapper exposes only the remaining-length and flush + operations needed by this framing contract. + + Update daemon error-status tests to create the bounded output-event + receiver that `consume_output` requires. + +- Default gateway port to HTTPS ([#1823](https://github.com/Devolutions/IronRDP/issues/1823)) ([29c96f221a](https://github.com/Devolutions/IronRDP/commit/29c96f221afeb6e57b18a87d70c9544243d2ff2d)) + + Accept host-only gateway endpoints as HTTPS authorities and connect to + port 443. Explicit ports and bracketed IPv6 literals retain their + normalized endpoints. + +- Accept advertised extended auth ([#1841](https://github.com/Devolutions/IronRDP/issues/1841)) ([c4533fee9c](https://github.com/Devolutions/IronRDP/commit/c4533fee9c45ce01ee456556857faca9e5e9c698)) + + Treat handshake ExtendedAuth bits as capabilities after HTTP + authentication. + + Require SSPI_NTLM and run its exchange when selected at transport setup. + + + ### Added - HTTP multi-leg authentication for the WebSocket upgrade: Negotiate SPNEGO (Kerberos with NTLM fallback via KDC discovery / `SSPI_KDC_URL`), pure NTLM when only NTLM is advertised, and Basic fallback. diff --git a/crates/ironrdp-mstsgu/Cargo.toml b/crates/ironrdp-mstsgu/Cargo.toml index 6eba63e274..4822c3126d 100644 --- a/crates/ironrdp-mstsgu/Cargo.toml +++ b/crates/ironrdp-mstsgu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-mstsgu" -version = "0.0.1" +version = "0.0.2" readme = "README.md" description = "Terminal Services Gateway Server Protocol" edition.workspace = true @@ -93,9 +93,9 @@ futures-util = "0.3" http-body-util = { version = "0.1.4", features = ["channel"] } hyper-util = { version = "0.1", features = ["tokio"] } hyper = { version = "1.9", features = ["client", "http1"] } -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["std"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["std"] } # public ironrdp-error = { path = "../ironrdp-error", version = "0.2" } -ironrdp-tls = { path = "../ironrdp-tls", version = "0.2.2" } # public +ironrdp-tls = { path = "../ironrdp-tls", version = "0.2.3" } # public log = "0.4" sspi = { version = "0.21", features = ["network_client"] } tokio-tungstenite = { version = "0.29" } diff --git a/crates/ironrdp-nscodec/CHANGELOG.md b/crates/ironrdp-nscodec/CHANGELOG.md new file mode 100644 index 0000000000..53ade5d587 --- /dev/null +++ b/crates/ironrdp-nscodec/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-nscodec-v0.2.0...ironrdp-nscodec-v0.2.1)] - 2026-09-30 + + diff --git a/crates/ironrdp-nscodec/Cargo.toml b/crates/ironrdp-nscodec/Cargo.toml index 37df3818ca..61223804f5 100644 --- a/crates/ironrdp-nscodec/Cargo.toml +++ b/crates/ironrdp-nscodec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-nscodec" -version = "0.2.0" +version = "0.2.1" readme = "README.md" description = "NSCodec ([MS-RDPNSC]) implementation for IronRDP" edition.workspace = true @@ -23,7 +23,7 @@ default = [] encoder = ["dep:ironrdp-graphics"] [dependencies] -ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9", optional = true } # public when `encoder` is on +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10", optional = true } # public when `encoder` is on [lints] workspace = true diff --git a/crates/ironrdp-pdu/CHANGELOG.md b/crates/ironrdp-pdu/CHANGELOG.md index 5a240de371..2fbab12568 100644 --- a/crates/ironrdp-pdu/CHANGELOG.md +++ b/crates/ironrdp-pdu/CHANGELOG.md @@ -6,6 +6,1316 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.9.0...ironrdp-pdu-v0.10.0)] - 2026-09-30 + +### Security + +- Tolerate unknown security header flags in BasicSecurityHeader ([#1458](https://github.com/Devolutions/IronRDP/issues/1458)) ([a4acab488b](https://github.com/Devolutions/IronRDP/commit/a4acab488b0d549854ebe0e0e922fe7252e84c98)) + + ## Summary + + Use `from_bits_truncate()` instead of `from_bits()` when decoding + `BasicSecurityHeader` flags. Some servers (e.g., Windows Server 2019 + with RDS licensing / RD Connection Broker) send security header flag + combinations that include bits not defined in the current bitflags enum. + The strict `from_bits()` rejected these as invalid, causing connection + failure during the `UpgradeLicense` license exchange phase. + + This matches FreeRDP behavior which masks for known flags without + rejecting the PDU when unrecognized bits are present. + + ## What was tested + + - Existing unit tests pass (`cargo xtask check tests -v`) + - Lints pass (`cargo xtask check lints -v`) + +- [**breaking**] Implement multitransport bootstrapping handshake ([#1098](https://github.com/Devolutions/IronRDP/issues/1098)) ([e45fbfe0f5](https://github.com/Devolutions/IronRDP/commit/e45fbfe0f597011706e77fc174ca14e5e9d435b9)) + + ## Summary + + Makes the `MultitransportBootstrapping` state functional instead of a + no-op + pass-through. After licensing the server may send 0, 1, or 2 Initiate + Multitransport Request PDUs before capabilities exchange. Each one is + surfaced + to the application, which establishes UDP transport (RDPEUDP2 + TLS + + RDPEMT) + or declines, and the connector reports the outcome back to the server. + + ## API + + Mirrors the existing `should_perform_X()` pause-point pattern used by + TLS + upgrade and CredSSP, but uses `complete_X()` / `skip_X()` rather than + `mark_X_as_done()` because completion carries result data: + + - `should_perform_multitransport()`: true while a request awaits an + outcome + - `multitransport_request()`: the request awaiting an outcome, or `None` + - `complete_multitransport(result, output)`: report the outcome, resume + - `skip_multitransport(output)`: decline, resume + + `complete_multitransport` accepts a `MultitransportResult` (a `Success` + / + `Failure(hresult)` enum) rather than a caller-built response PDU. The + connector + builds the response internally from the stored request ID. + + Requests are surfaced one at a time rather than as a batch. There is no + end + marker for the set, and MS-RDPBCGR 3.2.5.15.1 requires the client to act + on a + request as soon as it decodes one, so waiting to learn how many are + coming is + not an option the protocol offers. `should_perform_multitransport()` can + therefore come round twice; the caller answers reliable and lossy + separately. + + ## Approach + + **Routing.** Requests arrive on the negotiated MCS message channel + (2.2.15.1) and the Demand Active on the I/O channel, so the channel + decides + which is which. The message channel also carries NetworkAutoDetect since + #1348, + so a decode still confirms what arrived there, but the I/O channel is + never + speculatively decoded as multitransport. A PDU on neither channel is an + error. + + For the decode to be a sound confirmation the request decoder must + reject a + Demand Active, so this PR also tightens `MultitransportRequestPdu` to + require + the exact `SEC_TRANSPORT_REQ` security-header flag. + + **Yielding.** Each request is surfaced the moment it decodes. Responding + returns the connector to `MultitransportBootstrapping` to read whatever + comes + next, which may be a second request or the Demand Active. Nothing is + buffered + and nothing is replayed: when the request is surfaced the Demand Active + has not + arrived yet. + + **Soft-Sync.** The Initiate Multitransport Response is the Soft-Sync + signalling path (2.2.15.2), permitted only when both peers advertised + `SOFTSYNC_TCP_TO_UDP` in their GCC `MultiTransportChannelData`. The + server's + block is retained from the GCC exchange and checked against the client's + configured flags. One rule covers both paths: + + - Soft-Sync negotiated: always respond, `S_OK` or `E_ABORT`, including + on + `skip_multitransport()`, which 3.2.5.15.1 requires. Both the async and + blocking drivers skip automatically, so without this every default + client + leaves a compliant server waiting. + - Not negotiated: never respond. The outcome is reported in band on the + new + transport, and putting anything on the main channel would be the + violation. + + The response goes on the message channel per 2.2.15.2 and 3.2.5.15.2. If + Soft-Sync was negotiated but no message channel exists the connector + errors + rather than falling back to the I/O channel, and that check runs before + the + pending state is taken, so the caller is left with a connector it can + still + inspect or decline from. + + ## Wire behaviour + + On the wire TCP and UDP negotiation happen in parallel: the UDP + transport is + established alongside the ongoing TCP handshake, and its completion + signals the + dynamic-channel layer that subsequent channels may migrate to UDP. The + connector's API yield point here is a Rust affordance, not a + spec-mandated TCP + pause. Thanks to @hardening for the correction. + + ## Tests + + Connector state-machine tests in `ironrdp-testsuite-core` drive the + public API + with the shared `SERVER_DEMAND_ACTIVE` fixture: + + - a request is surfaced on arrival, without waiting for a following PDU + (regression test for the stall); + - responding returns to bootstrapping so a second request is read + normally; + - a third request is rejected per the 2.2.15.1 cap; + - a Demand Active on the I/O channel ends bootstrapping; + - the response targets the message channel, decoded back off the wire; + - a `Failure` result is carried through; + - `skip` sends `E_ABORT` under Soft-Sync, and nothing without it; + - `complete` emits nothing without Soft-Sync but still resumes; + - a failed response leaves the connector in `MultitransportPending`, + still able + to report or decline, rather than `Consumed`; + - `complete` / `skip` outside `MultitransportPending` error; + - a Demand Active's user data does not decode as a + `MultitransportRequestPdu` + (regression test for the decoder tightening above). + +- Validate auto-reconnect cookies ([#1509](https://github.com/Devolutions/IronRDP/issues/1509)) ([44f675e244](https://github.com/Devolutions/IronRDP/commit/44f675e244ee76b5311756668ffbbe28e98c7175)) + + ## Summary + - parse and carry `ARC_CS_PRIVATE_PACKET` data through the acceptor + - validate returning Enhanced RDP Security cookies with HMAC-MD5 before + reconnecting + - rotate reconnect randoms per connection and hourly, with runtime + cookie updates + - restrict cookie authentication to TLS/Hybrid and document the behavior + + ## Testing + - `cargo test -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server` + - `cargo clippy -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server + --all-targets -- -D warnings` + +- [**breaking**] Support session resume via the auto-reconnect cookie ([#1501](https://github.com/Devolutions/IronRDP/issues/1501)) ([74b3365c1f](https://github.com/Devolutions/IronRDP/commit/74b3365c1f98c0da6feed7507779c67e1b8e6d08)) + + > **Rebased onto post-#1522 master.** #1509 landed the server half of + #1508 while this was open, including the `ClientAutoReconnect` + structure. This PR no longer declares it; it extends it, and picks up + the parts #1509 did not build. + + ## What + + The client half of automatic reconnection. The session layer surfaces + the Server Auto-Reconnect Cookie, `ironrdp-pdu` derives and verifies the + client's response to it, and the connector sends that response when + resuming a session. + + ## Why + + A client whose connection drops ungracefully can reattach to its session + instead of making the user log on again, provided it returns the cookie + the server issued during logon ([MS-RDPBCGR] 1.3.1.5). + + #1509 built the server side of that: it validates a returning + `ARC_CS_PRIVATE_PACKET` and rotates the random. Nothing answers it. + `ironrdp-session` decodes the cookie and drops it, `ironrdp-connector` + has no way to send one back, and `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` still sits in + `ironrdp-client`. So `ironrdp-client` cannot resume a session against + `ironrdp-server`, and the validation #1509 added has no in-tree + counterpart to exercise it. + + The wire encoding was already there. `ExtendedClientOptionalInfo` + carries, encodes and decodes a 28-byte `autoReconnectCookie` and its + builder already had a `reconnect_cookie` step; `ServerAutoReconnect` + already decoded; #1509 added `ClientAutoReconnect` and its decode. + Nothing connected them. + + ## The three parts + + **Receive.** `SaveSessionInfo` now also surfaces the cookie, as + `ProcessorOutput::AutoReconnectCookie` and + `ActiveStageOutput::AutoReconnectCookie`. #1522 added a `SaveSessionInfo + { logon_complete }` output on that same handler; the two coexist rather + than compete, since both are read off one PDU and neither supersedes the + other. The handler emits the logon notification unconditionally and + appends the cookie when one is present, and a test pins that surfacing + the cookie does not suppress the notification. #1509's server replaces + the cookie whenever a client connects and again hourly ([MS-RDPBCGR] + 3.3.6.2), so this can arrive more than once in a session and the + consumer keeps the most recent. + + **Derive.** `ClientAutoReconnect::from_server_cookie` implements + [MS-RDPBCGR] 5.5: + + > The auto-reconnect random is used to key the HMAC function + ([RFC2104]), which uses MD5 as the iterative hash function. The security + verifier is derived by applying the HMAC to the client random received + in Step 3. + > + > `SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom)` + > + > When Enhanced RDP Security is in effect the client random value is not + generated (section 5.3.2). In this case, for the purpose of generating + the security verifier, the client random is assumed to be an array of 32 + zero bytes. + + IronRDP implements no Standard RDP Security path (there is no Security + Exchange PDU), so the zero-client-random case is the only one that + arises. As 5.5 notes, that makes the verifier constant for a given + cookie, so it proves possession of the cookie and nothing more; session + security comes from the outer TLS/CredSSP handshake. + + @clintcan independently confirmed this construction against real + **mstsc** while validating #1509 + ([comment](https://github.com/Devolutions/IronRDP/pull/1509#issuecomment-5151200681)): + a Windows client's `ARC_CS_PRIVATE_PACKET` verifies against + `HMAC-MD5(random_bits, [0u8; 32])`. That is the same derivation + implemented here, so the two halves interoperate with Microsoft's client + and not only with each other. + + **Send.** `ClientConnector::with_auto_reconnect_cookie` takes the cookie + last received and makes the connector put the derived Client + Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. + Absent, that PDU is byte-for-byte what it was. + + Unlike the server packet, this structure has no enclosing logon-info + field header, so it encodes to exactly the 28 bytes the cookie field + expects. `to_bytes` writes that layout directly rather than going + through `Encode`, so filling a fixed-size field has no error path a + caller must handle; a test pins the two to agree. + + ## One derivation, not two + + Putting `from_server_cookie` in `ironrdp-pdu` would leave the workspace + with two implementations of 5.5, since #1509 added a private HMAC to + `ironrdp-server`. So `ClientAutoReconnect` also gains `verify`, and the + server routes through it. + + `verify` keeps the constant-time comparison the server had. The verifier + is the whole credential, so a comparison returning early on the first + differing byte would let a peer recover it a byte at a time from the + timing; the session identifier is not secret and is compared normally. + `ironrdp-server` keeps the policy around the check, which cookies are + live and whether the security protocol permits auto-reconnect, and drops + its `hmac` and `md-5` dependencies. `hmac` moves to `ironrdp-pdu` as + `default-features = false`; the crate's full feature powerset still + checks clean, including `--no-default-features`. + + I would rather not have reached into `ironrdp-server` in a + `pdu,session,connector` change, but the alternative was shipping the + duplicate and filing a follow-up to remove it, which is a worse trade + for reviewer time. + + ## Tests that were not running + + That move also rehomes the known-answer tests @clintcan contributed on + #1509. They went in as an inline `#[cfg(test)]` module in + `crates/ironrdp-server/src/server.rs`, and that crate sets `[lib] test = + false`, so they have never executed in CI. They now live in + `ironrdp-testsuite-core` against the public API, where CI runs them: his + HMAC-MD5 reference vector is kept as a second vector alongside a + differently-keyed one, plus the cases for a tampered verifier and a + mismatched logon ID. + + Worth flagging separately: `ironrdp-server` is not alone. + `ironrdp-agent`, `ironrdp-session` and `ironrdp-web` also set `[lib] + test = false` and between them carry 16 files of inline `#[cfg(test)]` + modules that CI never runs. That is out of scope here, but I am happy to + open an issue if it would be useful. + + ## Breaking changes + + `ActiveStageOutput` and `x224::ProcessorOutput` gain a variant, and + `ClientConnector` gains a public field, so exhaustive matches and struct + literals need updating. + + Confirmed with `cargo-semver-checks` against the merge-base: those three + are the only findings this branch introduces. The others it reports on + `master` today (`ShareDataPdu::Compressed` and the `ShareDataCtx` fields + from #1518, `ProcessorBuilder.bulk_decompressor` from #1518, + `ServerEvent::SetAutoReconnectCookie` from #1509) are present on + `master` unchanged. The `ironrdp-pdu` additions are additive. + + ## Scope + + This is the library half. `ironrdp-client`, `ironrdp-web` and the FFI + bindings gain an arm for the new output but none of them reconnect + automatically yet; that is the remaining part of #271, and the existing + `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` in `ironrdp-client` marks where it goes. + + I kept receive, derive and send together deliberately. Split up, none of + them is usable on its own: without the receive half there is no way to + obtain a cookie, and without the send half there is nothing to do with + one. + + ## Tests + + Thirteen, all in `ironrdp-testsuite-core`. + + On the packet and the derivation: the `SecurityVerifier` matches two + independently computed HMAC-MD5 vectors of 32 zero bytes under different + keys, so the tests pin the derivation rather than restating the code; + the logon ID carries over from the server cookie; the encoding matches + the 2.2.4.3 field layout byte for byte with `cbLen` fixed at `0x1C`; + `to_bytes` agrees with `Encode`; it round-trips; and it rejects both a + wrong packet length and an unknown version. + + On verification: a derived answer is accepted, a single flipped byte in + the verifier is rejected, a correct verifier under a different logon ID + is rejected, and an answer derived from a different random is rejected. + + On the surfacing path: a Save Session Info PDU framed the way a server + sends it, through the real x224 processor, yields an + `AutoReconnectCookie` carrying the right logon ID and random bits, + alongside #1522's logon notification rather than in place of it; and one + without a cookie surfaces no cookie. + + ## Verification + + `cargo xtask check fmt/lints/tests/typos/locks` all pass on 1.94.1, + including a `fuzz/` build before the lock check. + + ## Note + + #1496 also touches the `ClientAutoReconnect` declaration. Whichever of + the two lands second needs a one-line rebase on the derive attribute; + happy to take that in either order. + +- Support advanced session settings ([#1781](https://github.com/Devolutions/IronRDP/issues/1781)) ([14ef4fd49f](https://github.com/Devolutions/IronRDP/commit/14ef4fd49fcf950169806866ee67db0c49662cfc)) + + Wire administrative-session GCC data, opaque load-balance routing + tokens, and RDPSND quality selection through the ActiveX settings + objects and client configuration. + + Keep unrelated transport, cache, video, device, and security policy + slots as explicit E_NOTIMPL failures. + + --------- + +- Decode the Server Heartbeat PDU on the message channel ([#1814](https://github.com/Devolutions/IronRDP/issues/1814)) ([4275b3d7fd](https://github.com/Devolutions/IronRDP/commit/4275b3d7fdf33c0aedeeb6bc9f58bfec83d0893d)) + + ## Summary + + Adds `HeartbeatPdu` (MS-RDPBCGR 2.2.16.1), decoded on the MCS message + channel that #1347/#1348 wired up for Auto-Detect. The Heartbeat PDU is + server-to-client only and defines no client response; the client is free + to ignore `count1`/`count2`. + + Fixes a real gap this exposed: the message-channel demux in + `ironrdp-session` unconditionally decoded incoming traffic as an + Auto-Detect Request PDU. A Heartbeat PDU arriving on the same channel + would fail that decode and error the session. The demux now peeks the + security-header flags first (masking + `SEC_RESET_SEQNO`/`SEC_IGNORE_SEQNO`, which MS-RDPBCGR 2.2.8.1.1.2.1 + says MUST be ignored, the same way + `MultitransportRequestPdu`/`MultitransportResponsePdu` already do) and + dispatches by the masked value. Anything it doesn't recognize is logged + and ignored rather than treated as session-fatal, matching the + forward-safe posture the connect-time demux in `ironrdp-connector` + already has. + + Multitransport Bootstrapping, the other optional feature the message + channel carries, is already fully implemented ([#1098](https://github.com/Devolutions/IronRDP/issues/1098)); this PR is the + Heartbeat half only. + + ## Validation + + `cargo xtask check fmt/typos/tests/locks` and `wasm check` all pass. + `lints` has one pre-existing failure on master unrelated to this diff + (`clippy::result_large_err` on `ErrorInfoDisconnectHandle::disconnect`, + fix filed as #1812); this PR's own diff is clean under the same lints + run. + + New tests: `HeartbeatPdu` encode/decode round-trip plus the same + seqno-flag and cross-PDU-discriminator coverage + `MultitransportRequestPdu` has, in `ironrdp-pdu`; a `Processor::process` + integration test in `ironrdp-testsuite-core` covering the full + message-channel path (Heartbeat produces no output, Auto-Detect still + works alongside it, and an unrecognized PDU is ignored rather than + erroring). + +- Retain unknown ClientInfoFlags bits instead of refusing the PDU ([#1843](https://github.com/Devolutions/IronRDP/issues/1843)) ([ff48d8a8e9](https://github.com/Devolutions/IronRDP/commit/ff48d8a8e93408f10c511dbaf916d38921fc6fcf)) + +- Retain unknown GCC flag bits instead of failing the settings exchange ([#1844](https://github.com/Devolutions/IronRDP/issues/1844)) ([e8a75797e7](https://github.com/Devolutions/IronRDP/commit/e8a75797e7ce3be1682e9a5d1810bc0102662440)) + + Three GCC settings-block decoders reject the whole Basic Settings + Exchange when a flags field carries a bit outside the defined list: + RedirectionFlags in Client Cluster Data, the per-monitor flags in Client + Monitor Data, and transportFlags in the Multitransport Channel Data. + [MS-RDPBCGR] 3.3.5.3.3's processing rules mandate nothing for any of + these three fields (its checks cover the Client Network Data bounds, the + color depth fields, the serverSelectedProtocol echo, and the encryption + method flags), so the strictness is this library's own, and it fails the + connection at GCC, before any channel is joined. + + The exposure is real in each case. The transportFlags list has grown + before (TRANSPORTTYPE_UDP_PREFERRED and SOFTSYNC_TCP_TO_UDP are later + additions), so an older IronRDP would have refused every client that + advertised them. The monitor flags list defines only TS_MONITOR_PRIMARY, + so any vendor or future bit refuses a multimon client outright. The + redirection flags sit next to a version mask that already anticipates + growth. + + All three sites switch to from_bits_retain, the crate-wide policy since + #1144: unknown bits are kept rather than dropped, so re-encoding a + decoded block reproduces the wire value. The cluster version mask + handling is untouched, and the separately parsed RedirectionVersion + stays strict since it is a discriminant, not a bit set. + + This continues the interop line of #1489 (unknown GCC user-data blocks), + #1458 (unknown security header flags), #1837 (undefined channel option + bits) and #1843 (unknown ClientInfoFlags bits). + +- Retain unknown client encryptionMethods bits in the GCC security data ([#1845](https://github.com/Devolutions/IronRDP/issues/1845)) ([f87040f2c3](https://github.com/Devolutions/IronRDP/commit/f87040f2c33f9273690f91e85ac23f3ce0e7f1a7)) + +- Recognize the Initiate Multitransport Response on the message channel ([#1964](https://github.com/Devolutions/IronRDP/issues/1964)) ([6aebe6d0b1](https://github.com/Devolutions/IronRDP/commit/6aebe6d0b11c4aa3c99252bd986e138e43756a6d)) + + ## Summary + + handle_message_channel_data unconditionally decoded every PDU received + on the MCS message channel as an AutoDetectRspPdu, per a comment + claiming the channel "currently carries only the auto-detect response." + That is no longer accurate: once a server sends an Initiate + Multitransport Request, the client answers on this same channel with an + Initiate Multitransport Response (MS-RDPBCGR 2.2.15.2) to report whether + it could establish the sideband transport. + + MultitransportResponsePdu already has full Encode/Decode support in + ironrdp-pdu (with success()/abort() constructors), but has zero + consumers anywhere in ironrdp-server. In practice every such response + from a real client failed to decode as AutoDetectRspPdu (its + securityHeader carries SEC_TRANSPORT_RSP, not SEC_AUTODETECT_RSP) and + was dropped with an "Unhandled MCS message channel PDU" warning: + legitimate protocol traffic logged as an error. + + Reproduced against a real Windows client (mstsc) that offered UDP + multitransport but could not establish it: the raw bytes it sent decode + exactly as MultitransportResponsePdu { security_header: TRANSPORT_RSP, + request_id, hr_response: E_ABORT }. + + ## Changes + + - Added `ironrdp_pdu::rdp::message_channel::ClientMessageChannelPdu`, an + enum with Encode/Decode covering the two PDUs a client sends on this + channel. Decode peeks at the Basic Security Header flags and dispatches: + SEC_TRANSPORT_RSP to MultitransportResponsePdu, anything else to + AutoDetectRspPdu, so a malformed PDU is reported against the type its + header names. + - handle_message_channel_data decodes that type and has a Multitransport + arm that logs the response at debug level (ordinary protocol traffic, + not an error), with the same wording the acceptor uses for this PDU, + instead of falling through to the warn!. + - Six tests in ironrdp-testsuite-core (tests/pdu/message_channel.rs): + recognition of both PDUs, a round trip, a truncated transport response + reported as one, a payload without SEC_TRANSPORT_RSP handed to the + auto-detect decoder, and too-short input. + + ## Test plan + + cargo xtask check fmt/lints/tests/typos/locks all pass. + +### Features + +- Add ClearCodec client-side decode dispatch ([#1175](https://github.com/Devolutions/IronRDP/issues/1175)) ([714dce4662](https://github.com/Devolutions/IronRDP/commit/714dce46627e299c57d82f4f6a5c18067a95bffa)) + + Follow-up to #1174. Supersedes #1195 (the standalone server-helper PR; + its 46-line `send_clearcodec_frame()` is included here). + + Wires ClearCodec into the EGFX client's WireToSurface1 codec dispatch, + matching the existing AVC420 and Uncompressed decode patterns. + +- Surface ShareDataPdu variant in unexpected-PDU errors ([#1329](https://github.com/Devolutions/IronRDP/issues/1329)) ([df1f7e7faa](https://github.com/Devolutions/IronRDP/commit/df1f7e7faaf068435bfbbe1efcb4a8800ebb3d9f)) + + ## Summary + + - Addresses ask 1 of #1232: when the server sends a + `ShareControlPdu::Data` wrapping an unexpected `ShareDataPdu`, the three + error sites in `headers.rs` and `connection_activation.rs` now drill + into the `Data` wrapper and surface the inner variant name instead of + reporting only `"Data"`. + - For `ServerSetErrorInfo` specifically (the asker's high-value case), + the existing `ErrorInfo::description()` is appended so callers can see + why the server rejected the session without substring matching on the + `Reason` string. + - New `pub fn describe_unexpected_share_control_pdu` in `headers.rs` + centralizes the formatting; `decode_share_data`, `decode_io_channel`, + and `ConnectionActivation::CapabilitiesExchange` all route through it. + - Non-`Data` variants continue to use the outer `as_short_name()`, so + diagnostics for `ServerDeactivateAll` and `ClientConfirmActive` are + preserved verbatim. + + ## Validation + + - Three unit tests in `headers::tests` cover the helper: a non-`Data` + variant (`ServerDeactivateAll`), a `Data` wrapper around a + non-SetErrorInfo inner (`Update(Vec::new())`), and a `Data` wrapper + around `ServerSetErrorInfo` carrying + `ProtocolIndependentCode::ServerDeniedConnection`. + - `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + - Helper is `pub`, not `pub(crate)`: it has to be, since + `ironrdp-connector`'s `connection_activation.rs` calls it cross-crate. + That adds + `ironrdp_pdu::rdp::headers::describe_unexpected_share_control_pdu` to + `ironrdp-pdu`'s public surface. Additive and non-breaking, confirmed by + `cargo semver-checks --baseline-rev `: no update required + for either `ironrdp-pdu` or `ironrdp-connector`. + - Ask 2 from #1232 (an optional structured `ConnectorErrorKind` variant + for "server rejected at capabilities phase") is intentionally deferred. + The asker framed it as optional and the wire-level information is now + available in the `Reason` string. + - `Refs #1232` rather than `Closes` so the issue stays open while you + decide on ask 2. + +- Support connection correlation info ([#1582](https://github.com/Devolutions/IronRDP/issues/1582)) ([c4483617ba](https://github.com/Devolutions/IronRDP/commit/c4483617ba05c31182b58c58be66bd41120a076d)) + + Encode the optional 36-byte X.224 RDP_NEG_CORRELATION_INFO block and + reject malformed negotiation records. + +- Support Hyper-V connection ordering ([#1505](https://github.com/Devolutions/IronRDP/issues/1505)) ([5c1816244e](https://github.com/Devolutions/IronRDP/commit/5c1816244e83187a04249e9d9c240d096cb78f55)) + + Hyper-V over RDCleanPath needs PCB → TLS on the proxy, then CredSSP → + X.224 on the client. Ordinary RDCleanPath stays X.224-first. + + Still VERSION_1 with the same DER fields. An explicit VMConnect request + carries a Unicode PCB payload in `preconnection_blob` with no X.224; the + proxy encodes the binary PCB. Generic PCB requests keep their existing + X.224-first behavior. + + Gateway reference implementation: + [Devolutions/devolutions-gateway#1372](https://github.com/Devolutions/devolutions-gateway/pull/1372) + + Checked locally: Rust builds, formatting, Svelte typecheck, and .NET + build. Real nested Hyper-V E2E through Gateway: Native rendered 18 + frames, Avalonia connected and rendered its first frame, and Web + rendered a non-empty 1280×720 canvas. + + --------- + +- Forward negotiated windowing orders ([#1631](https://github.com/Devolutions/IronRDP/issues/1631)) ([0c3fbe78b4](https://github.com/Devolutions/IronRDP/commit/0c3fbe78b4366533b9fcea046b2b53654e003a72)) + + Preserve Window List support during activation. + Forward validated orders through ActiveStage and the raw FFI output. + Desktop and web consumers retain their existing behavior. + +- Add RemoteApp protocol primitives ([#1636](https://github.com/Devolutions/IronRDP/issues/1636)) ([0161906731](https://github.com/Devolutions/IronRDP/commit/0161906731757356953cdb389a2cd6a42863deb2)) + + Add portable RAIL wire types and a typed Remote Programs capability set. + + Validate the RAIL crate's bare `no_std` and allocation-backed + configurations in the workspace feature matrix. + + Keep connection setup and windowing behavior outside this protocol + layer. + +- Negotiate monitor topology ([#1675](https://github.com/Devolutions/IronRDP/issues/1675)) ([063efcdc30](https://github.com/Devolutions/IronRDP/commit/063efcdc3088d8f44e423cc322077d40bf9aadf2)) + + Negotiate the client monitor layout from UseMultimon and expose the + confirmed remote topology through the ActiveX compatibility interface. + + Advertise Monitor Layout PDU support whenever Extended Client Data is + negotiated, and forward layouts from activation, active sessions, and + reactivation so advertised support does not terminate sessions. + + Keep fallback reporting truthful when servers do not honor the request, + while preserving single-monitor resize behavior and blocking + multi-monitor resizing. + + Do not send Client Monitor Extended Data; per-monitor DPI and + orientation remain unavailable. + +- Measure and report network characteristics ([#1470](https://github.com/Devolutions/IronRDP/issues/1470)) ([224e8db7ce](https://github.com/Devolutions/IronRDP/commit/224e8db7cec2dad39032aac097f550a6600e742c)) + + ## Summary + + - The server measures round-trip time and bandwidth from the continuous + auto-detect exchange and reports both to the client in a Network + Characteristics Result on the MCS message channel ([MS-RDPBCGR] + 2.2.14.1.5). + - Nothing is sent until both are known. A result carrying RTT alone + reports part + of the picture as though it were the whole, which is what krdp and grd + avoid by + returning early when they have no bandwidth figure. + - The result is paced at one per second on its own clock rather than + once per + probe, and is withheld unless a client response has arrived since the + last one. + A client that stops answering stops producing results instead of leaving + the + last window values advertised indefinitely. + - `baseRTT` is the lowest RTT seen over the session, per 2.2.14.1.5's + "lowest + detected round-trip time". `averageRTT` is the window average, so the + difference between them is queueing delay. + + ## Validation + + - `cargo xtask check fmt/lints/tests/typos/locks` and `cargo xtask wasm + check` + all pass. + - `cargo semver-checks -p ironrdp-server --baseline-rev master`: no + update + required. + - Tests live in `ironrdp-testsuite-core`. `ironrdp-server` sets + `[lib] test = false`, so an inline module would compile and never run. + Each new + test was checked by planting the corresponding regression and confirming + it + fails. + + ## Notes + + - Supersedes #1471, which added bandwidth as a follow-up. With the gate + above, + this PR alone could only ever emit the form it now declines to send, so + the two + are one change. + - #1487 has merged, so the earlier dependency note no longer applies. + +- Make the RemoteFX quantization table configurable ([#1685](https://github.com/Devolutions/IronRDP/issues/1685)) ([925e7c0f7c](https://github.com/Devolutions/IronRDP/commit/925e7c0f7cb7ae937a92ec93d4cb758289594cc0)) + + Add Quant::try_new(), a validating constructor that rejects any subband + value outside the 6..=15 range, and + RdpServerBuilder::with_remotefx_quant(quant: Quant), wiring it through + RdpServerOptions to the same capability-negotiation call site #1684 + touched. + + Per [MS-RDPRFX] 2.2.2.1.5, each of the 10 TS_RFX_CODEC_QUANT values is a + 4-bit field, and the legal range is 6 to 15. #1557 reports the quant + table is hardcoded to Quant::default(); this gives callers a validated + way to set it instead. + + Builds on #1684, which added the storage this PR wires up. Default + behavior is unchanged: with_remotefx_quant is opt-in, and a server that + doesn't call it still gets Quant::default(), the same values Windows RDP + servers send. + +- [**breaking**] Expose per-connection keyboard metadata via ConnectionHandler ([#1691](https://github.com/Devolutions/IronRDP/issues/1691)) ([393869b30b](https://github.com/Devolutions/IronRDP/commit/393869b30b1078da7204c6bf20e8a5472e419070)) + + ## Summary + + AcceptorResult already carried keyboard_layout (the client's GCC Client + Core Data keyboardLayout, MS-RDPBCGR 2.2.1.3.2), but ironrdp-server's + client_accepted never read it, and the only extension point that could + plausibly expose it, ConnectionHandler::on_accept/on_disconnected, only + fires from RdpServer::run's own accept loop. An embedder with its own + accept loop calling run_connection or run_connection_with directly never + sees these hooks at all. + + Added keyboard_type and ime_file_name to Acceptor and AcceptorResult, + captured from the same Client Core Data alongside keyboard_layout. + + Added a new ConnectionInfo struct and a default-no-op + ConnectionHandler::on_connection_info(&ConnectionInfo) method, fired + from client_accepted itself, right after credential and auto-reconnect + validation succeed. This is reachable from every code path that + completes connection setup, not only run's accept loop, so it is usable + by embedders that never call run. + + Kept the hook synchronous. It only hands the embedder a small Clone-able + struct; an embedder that needs to do blocking work in response can spawn + its own task, the same way the existing on_accept/on_disconnected hooks + already work. + + Open question: AcceptorResult is a public struct without non_exhaustive, + so the two new fields are a real breaking change for any consumer + destructuring it exhaustively, same class as the keyboardType change in + #1689. AcceptorResult's attributes are unchanged here since marking it + non_exhaustive is a broader decision than this PR's two fields. + + ## Validation + + cargo xtask check fmt/lints/tests/typos/locks all pass. + + ## Review round and rebase, 2026-08-19 + + #1689 (KeyboardType) merged. This branch was still carrying a stale + pre-merge copy of that commit, so the diff was cumulative against + master. Rebased onto current master, which dropped the redundant + duplicate commit (its content was already upstream) and left this PR's + own single commit. + + Four review findings from the bot review, all addressed: + + - `on_connection_info` fired on every Deactivation-Reactivation resize, + not just the initial connection, since `accept_finalize` loops back into + `client_accepted` with `result.reactivation` set. Gated the call on + `!result.reactivation`, matching the existing gate on the static-channel + start block just below it. + - `get_result()` took `ime_file_name` via `mem::take`, emptying it out + of the acceptor before `new_deactivation_reactivation` copied the same + acceptor's field into the next result, so every reactivation after the + first reported an empty IME name. Changed to a clone, matching how the + Copy-type sibling fields on the same lines already survive. + - No regression test covered the permissive zero/unrecognized-value + keyboardType decode in the Input capability set. Added + `keyboard_type_zero_decodes_to_none` and + `keyboard_type_unrecognized_value_round_trips` (0x51). + - A fourth finding asked for the same coverage on Client Core Data's own + keyboardType field; that test already exists on master, added to #1689 + in response to its own review. The rebase above inherits it directly, so + no new code was needed there. + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +### Bug Fixes + +- Tolerate unknown GCC user-data blocks instead of failing ([#1489](https://github.com/Devolutions/IronRDP/issues/1489)) ([629a8024f4](https://github.com/Devolutions/IronRDP/commit/629a8024f4832ed04247ef56597604bbb4b85017)) + +- Scope Font Map leniency ([#1506](https://github.com/Devolutions/IronRDP/issues/1506)) ([e496b7b8ea](https://github.com/Devolutions/IronRDP/commit/e496b7b8eaf60688fc0d507961713c6aabd0e05e)) + +- Key auto-detect optional fields off requestType, not the Option ([#1491](https://github.com/Devolutions/IronRDP/issues/1491)) ([f9cc62fa2c](https://github.com/Devolutions/IronRDP/commit/f9cc62fa2ccb4f211838dd0961c19cdf3b79a38e)) + + ## Summary + + MS-RDPBCGR decides which optional fields an auto-detect message carries + by its `requestType`. Two of these message types encoded them by + inspecting which `Option`s happened to be set instead, so the encoder + and the decoder disagreed about the wire. + + This started as a fix for `BandwidthMeasureStop` alone. Review found the + connect-time fallback was still non-compliant, and checking whether the + same shape appeared elsewhere in the file turned up + `NetworkCharacteristicsResult` with the identical defect and a worse + consequence, so both are fixed here rather than one now and one later. + + ## The two failure modes + + **Connect-time stop with no payload: encodes to bytes the decoder + rejects.** + + ```rust + AutoDetectRequest::BandwidthMeasureStop { + sequence_number: 7, + request_type: BW_STOP_CONNECT_TIME, + payload: None, + } + ``` + + encodes to ten bytes with `headerLength` 0x06 and no length field. + Decoding those bytes + fails with `not enough bytes provided to decode: received 0 bytes, + expected 2 bytes`, + because the decoder reads `payloadLength` back for every + `BW_STOP_CONNECT_TIME`. + + **UDP stop with a payload: silently loses it.** + + ```rust + AutoDetectRequest::BandwidthMeasureStop { + sequence_number: 3, + request_type: BW_STOP_RELIABLE_UDP, + payload: Some(vec![0xAA; 16]), + } + ``` + + encodes the length and the sixteen bytes, which the decoder never reads + back for + `BW_STOP_RELIABLE_UDP` or `BW_STOP_LOSSY_UDP`. + `AutoDetectReqPdu::decode` does not check + for trailing bytes, so the payload is dropped in transit without an + error rather than + refused. + + The second is the worse of the two: a round trip that loses data and + reports success. + + ## Network Characteristics Result (2.2.14.1.5) + + The same defect, found by sweeping the file rather than reported. + + `0x0840` carries baseRTT and averageRTT, `0x0880` carries bandwidth and + averageRTT, `0x08C0` carries all three, and the decoder already read + them on that basis. Encoding from the `Option`s meant: + + - a `0x0840` result with no `base_rtt_ms` wrote a body the decoder + cannot read; + - a `0x0840` result carrying `bandwidth_kbps` instead wrote that + bandwidth into the slot the decoder reads as baseRTT, so the value came + back **silently corrupted** rather than rejected; + - `headerLength` was always derived from `requestType`, so it could + contradict the body it described. + + Encode and `size()` now consult `requestType` through a shared helper, a + value the type does not carry is dropped rather than written, and a + missing one that the type requires is an error. + + ## Fix + + `Encode` and `size()` now key off `requestType`, matching the decoder. + That makes the + wire form canonical: + + - an absent payload on a connect-time stop encodes as a zero length and + reads back as an + empty one; + - a payload on a UDP stop never reaches the wire. + + Decoding and re-encoding reproduces the same bytes in every case. The + types can still + express states the protocol cannot, so value-level identity is not + achievable, but byte + stability is, and that is what a decoder consuming real traffic depends + on. + + No public signatures change; this is a behaviour fix inside the existing + `Encode` impl. + + ## Tests + + New `pdu/autodetect.rs` in `ironrdp-testsuite-core`: + + - a connect-time stop with no payload round-trips, and `size()` agrees + with `encode()`; + - a UDP stop carrying a payload encodes header-only and comes back with + `payload: None`, + for both the reliable and lossy request types; + - a connect-time stop preserves a real payload; + - decode-then-encode reproduces identical bytes for both shapes. + + Three of the four fail against the current encoder and pass with the + fix. The fourth is + a control that passed before and after. + + Note the existing `pdu_round_trip` fuzz oracle would not have caught + this even with + auto-detect added to it, since it discards the result of the re-decode + (`let _ =`). That + is deliberate in the oracle's design and out of scope here, but it is + why this went + unnoticed. + + ## How this was found + + Writing a test for the connect-time bandwidth measurement in #1465, + which needs to answer + a connect-time stop. The `None` case was constructed to check the reply + path stayed + lenient and turned out not to be constructible on the wire at all. + + ## Verification + + - `cargo xtask check fmt -v` green + - `cargo xtask check lints -v` green + - `cargo xtask check tests -v` green + - `cargo xtask check typos -v` green + - `cargo xtask check locks -v` green + +- Harden framing and empty output handling ([#1515](https://github.com/Devolutions/IronRDP/issues/1515)) ([33506e6139](https://github.com/Devolutions/IronRDP/commit/33506e613923dae504f46451231dcee15a6320a2)) + + Reject Fast-Path and TPKT frames whose declared length is smaller than + their header or minimum packet size. Also tolerate the zero-length + `totalLength` variation used by empty Update and Pointer output PDUs, + while continuing to reject zero-length non-output data PDUs. + + Adds regression coverage for malformed frame lengths and empty output + compatibility. + +- Share bulk decompression across output paths ([#1518](https://github.com/Devolutions/IronRDP/issues/1518)) ([6151e21bf5](https://github.com/Devolutions/IronRDP/commit/6151e21bf58b7297e9b4abc2167aa36fc2ba77e4)) + + Bulk compression state is stream-wide, but Fast-Path and slow-path + outputs previously used separate or missing decompression paths. This + could corrupt history-dependent server updates or leave negotiated + slow-path compression undecodable. + + This change owns the negotiated bulk decompressor in `ActiveStage` and + passes it to both X.224 and Fast-Path processing. It retains Share Data + compression metadata through the PDU context, resets decompression + history on reactivation, and initializes consumers from the connection's + negotiated compression type. + + Fast-Path now decompresses each fragment before reassembly so + compression flags apply at packet boundaries. Failures expose bounded + protocol metadata without retaining remote payloads or decoder details. + + Tests cover Share Data metadata propagation, slow-path decompression + behavior, fragmented Fast-Path reassembly and bounded errors, and + compressed Fast-Path updates after reactivation. + + --------- + +- Accept a zero-length connect-time bandwidth payload on decode ([#1511](https://github.com/Devolutions/IronRDP/issues/1511)) ([dffad79d61](https://github.com/Devolutions/IronRDP/commit/dffad79d6143f4d7e9b589768ad55fc98a04740c)) + + ## What + + A connect-time Bandwidth Measure Stop with `payloadLength` of zero now + decodes, as a present-but-empty payload. `Encode` still refuses to emit + one. + + ## Why + + [MS-RDPBCGR] 2.2.14.1.4 says of `payloadLength`: "It MUST be present + (and have a value greater than zero) if the value of the **requestType** + field is set to 0x002B." #1491 read that as a rule for both directions + and made encode and decode refuse a zero. The encode half is right. The + decode half is not. + + The two directions answer different questions. Encoding asks what we are + permitted to put on the wire, and a zero length has no conforming + encoding, so refusing is correct. Decoding asks whether we can act on + what a peer already sent. Here we can: `sequenceNumber` and + `requestType` arrive intact, and those fully determine the Bandwidth + Measure Results reply the PDU is asking for. The payload is random + measurement filler per the same section, and its length is the only + thing the reply reports about it. + + Rejecting therefore discards a PDU we could have answered without + gaining any protection. FreeRDP-based servers, including + gnome-remote-desktop, block in `AWAIT_BW_RESULT` until the results + arrive, so a server that sends a zero length stalls the whole connection + rather than getting a diagnostic. + + The fix #1491 was actually titled for, keying the optional fields off + `requestType` instead of the `Option`, is untouched. Only the added + zero-length rejection moves, and only on the receive side. + + ## Tests + + `connect_time_stop_with_a_zero_payload_length_is_rejected` becomes + `..._is_accepted` and now asserts the decoded value: `payload: + Some(vec![])`, present and empty rather than absent, since the wire + carried a length field. + + Added `a_decoded_zero_length_stop_does_not_re_encode`, so the asymmetry + is stated as a test rather than only as a comment. + + `connect_time_stop_without_a_payload_is_refused` is unchanged and still + covers the encode side. + + ## Note + + This does not break the `pdu_round_trip` oracle in #1492: a failing + `encode` after a successful `decode` is already tolerated there, and the + re-decode assertion only fires on a successful encode. + + ## Verification + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. `cargo + semver-checks` reports no update required for `ironrdp-pdu`. + + ## Unblocks #1465 + + #1465 carries a regression test for a zero-`payloadLength` Stop, which + cannot pass until this lands: #1491 made `AutoDetectRequest`'s decoder + reject `payloadLength = 0`, so such a PDU is refused before it reaches + the connector. Its + `connect_time_bandwidth_answers_a_stop_carrying_an_empty_payload` is red + today and that red is this dependency, not a defect there. + + Verified against current `master`: #1465 alone fails that one case out + of 1032; #1465 with this applied passes all of them. Merging this first + turns #1465 green with no change on its side. + + ## Rebased + + Rebased onto `master` on 2026-08-02 so the checks run against the + current tree rather than the state before that day's merges. No + conflicts, no content change. + +- Keep auto-reconnect credential material out of Debug output ([#1496](https://github.com/Devolutions/IronRDP/issues/1496)) ([d0948faa18](https://github.com/Devolutions/IronRDP/commit/d0948faa187da673e9ded44e9e22cfbefc2c7f62)) + +- Batch Fast-Path input events ([#1630](https://github.com/Devolutions/IronRDP/issues/1630)) ([3818b48037](https://github.com/Devolutions/IronRDP/commit/3818b480375ec9411d5319d8e7af161f1d662cbf)) + + Keep outgoing Fast-Path input frames within the 255-event protocol limit + and preserve their order across FFI. + +- Parse ClearCodec V-Bar offsets ([#1694](https://github.com/Devolutions/IronRDP/issues/1694)) ([c1db8608af](https://github.com/Devolutions/IronRDP/commit/c1db8608af35e3c1318a50fa812f3259ff69d800)) + + Decode short V-Bar cache-miss offsets from their specified bit + positions. + +- [**breaking**] Accept the full documented range of Client Core Data keyboardType values ([#1689](https://github.com/Devolutions/IronRDP/issues/1689)) ([c74e7c5c94](https://github.com/Devolutions/IronRDP/commit/c74e7c5c9431c87e57a1550b037867d235c1d362)) + + ## Summary + + KeyboardType (TS_UD_CS_CORE's keyboardType field, MS-RDPBCGR 2.2.1.3.2) + was a closed enum covering discriminants 1 through 7, transcribed from + the field's other documentation in 2.2.7.1.6 (TS_INPUT_CAPABILITYSET), + which omits value 8 (Korean keyboard). The table this type actually + decodes against, 2.2.1.3.2, documents 1 through 8. Any client outside + the narrower range, including every genuine Korean keyboard, had its + whole Client Core Data parse hard rejected before the connection + started. + + Converted KeyboardType to a repr(transparent) struct KeyboardType(pub + u32) with named constants for the eight documented values, mirroring + RdpVersion one field above it in the same struct, and made decode + infallible at all three sites that previously disagreed with each other: + gcc::core_data::client hard rejected, rdp::capability_sets::input + silently dropped to None, and ironrdp-activex's COM setter returned + E_INVALIDARG. + + FreeRDP hit the identical gap in its own documentation-only enum until a + two-line fix (FreeRDP#11035). Neither FreeRDP nor xrdp gate connection + admission on this field at all; both decode it as a raw integer. + Windows' own GetKeyboardType additionally documents 0x51 for generic HID + keyboards, which a narrower fix adding only Korean would not survive. + + Marked as breaking since KeyboardType's public shape changes for any + downstream consumer of ironrdp-pdu outside this workspace. Note that + cargo-semver-checks in this repo's PR automation only runs against the + facade ironrdp crate, which will not see a break confined to + ironrdp-pdu's own API, so the automated breaking-change label may not + apply here even though this is a real one. + + ## Validation + + cargo xtask check fmt/lints/tests/typos/locks all pass. Extended + ironrdp-activex's existing inline COM boundary test to cover value 8 + round tripping and a negative value still being rejected. + +- Retain Progressive difference tiles ([#1698](https://github.com/Devolutions/IronRDP/issues/1698)) ([69e323ae47](https://github.com/Devolutions/IronRDP/commit/69e323ae473264e13b2bb3f70a356cd967debea8)) + + Retain quantized DWT coefficients per Progressive tile so difference + updates compose with their matching surface reference while progressive + codec-context state remains isolated. + + Reject difference tiles that lack a retained reference instead of + decoding them against zeros. + + Keep retained surface references across codec grid replacement and + ResetGraphics; only deleting the surface releases them. + +- Accept a 6-byte Share Control Header ([#1719](https://github.com/Devolutions/IronRDP/issues/1719)) ([d4b728a1d4](https://github.com/Devolutions/IronRDP/commit/d4b728a1d40b917dd94da6d2f8f12a32ce0965ec)) + + MS-RDPBCGR 2.2.8.1.1.1.1 defines the Share Control Header as 6 bytes + (totalLength, pduType, pduSource); shareId belongs to the PDU bodies. + decode gates on FIXED_PART_SIZE (10), so a legal header-only PDU is + rejected before any field is read: + `ShareControlHeader::decode: not enough bytes provided to decode: + received 6 bytes, expected 10` + + xrdp sends exactly such a Deactivate All PDU, which drops the session. + FreeRDP 3.30.0 accepts it against the same server (xrdp 0.10.1-4.1, + Ubuntu 26.04); ironrdp-pdu 0.9.0 does not. + + [#1130](https://github.com/Devolutions/IronRDP/pull/1130) relaxed + ServerDeactivateAll::decode, but that runs after this gate — a 6-byte + buffer never reaches it. This is the outer half suggested in + [#314](https://github.com/Devolutions/IronRDP/issues/314) and never + submitted. + +- Clamp wheel rotation units to the wire's 9-bit range ([#1818](https://github.com/Devolutions/IronRDP/issues/1818)) ([2685d85cc4](https://github.com/Devolutions/IronRDP/commit/2685d85cc4f95e2ee3294e4edda4d0ff41f9ff80)) + +- Retain unknown protocol bits in the GCC negotiation echo fields ([#1846](https://github.com/Devolutions/IronRDP/issues/1846)) ([9af99317c2](https://github.com/Devolutions/IronRDP/commit/9af99317c217f4fcc38fa381d28e30d9ecfc5be5)) + + The two GCC core-data fields that echo the X.224 negotiation values are + decoded strictly: serverSelectedProtocol in the client core data and + clientRequestedProtocols in the server core data both reject the whole + settings exchange when the value carries a bit outside the five defined + PROTOCOL_* flags. The negotiation layer itself already decodes this + exact type with from_bits_retain in both the RDP Negotiation Request and + Response, so a value the library accepts leniently at X.224 becomes + connection-fatal when echoed back one phase later. + + The strictness also works against the field's purpose. [MS-RDPBCGR] + 3.3.5.3.3's mandated handling of serverSelectedProtocol is a value + comparison: if the field does not contain the same value the server + transmitted in the RDP Negotiation Response, the server SHOULD drop the + connection. That comparison needs the value to survive decode; rejecting + unknown bits at parse time replaces the spec's check with a decode + error. The client-side echo is symmetric: the client knows what it + requested and compares, rather than validating the bit set. + + The exposure is demonstrated by history: the PROTOCOL_* list has grown + three times (RDSTLS, HYBRID_EX, RDSAAD), and each addition would have + made older strict decoders refuse newer peers whose negotiation had + already succeeded. + + Both sites switch to from_bits_retain, the crate-wide policy since + #1144; re-encoding a decoded block reproduces the wire value. + + This continues the interop line of #1489, #1458, #1837, #1843, #1844 and + #1845. + +- Retain unknown fast-path input eventFlags bits instead of dropping the session ([#1847](https://github.com/Devolutions/IronRDP/issues/1847)) ([0c3fc7027c](https://github.com/Devolutions/IronRDP/commit/0c3fc7027cd02a49f892379ced124304b84ee2c9)) + + The fast-path input decoder rejects the whole event stream when a + keyboard or synchronize event carries an eventFlags bit outside the + defined set, and a decode error there is session-fatal mid-use: input + events arrive continuously, so a single unknown flag bit from a client + drops a live session with work in progress. + + The strictness is also internally inconsistent: the slow-path decoders + for the very same events already tolerate unknown bits. Slow-path + keyboard flags (scan_code.rs), synchronize toggle flags (sync.rs) and + unicode keyboard flags (unicode.rs) all decode with from_bits_retain, so + the identical keystroke is tolerated on one path and connection-fatal on + the other. + + The spec's only mandated strictness in this area is about event types, + not flags: [MS-RDPBCGR] 3.3.5.8.2 says the server SHOULD drop the + connection when an event structure does not match one of the known + types. That check is the existing eventCode rejection, which stays + exactly as it is. The eventFlags tables (2.2.8.1.2.2.1 and + 2.2.8.1.2.2.5) carry no receiver-validation language. + + All three flag sites (scancode, sync, unicode) switch to + from_bits_retain, the crate-wide policy since #1144. The retained bits + fit the 5-bit header field, so re-encoding reproduces the wire value, + and the server-side consumers read these flags with contains(), so + retained bits flow through harmlessly. + + This continues the interop line of #1489, #1458, #1837, #1843, #1844, + #1845 and #1846, and it is the last flag-site conversion of that series. + +- Don't reject a Share Data PDU whose totalLength is under-declared ([#1541](https://github.com/Devolutions/IronRDP/issues/1541)) ([9b04c90ca2](https://github.com/Devolutions/IronRDP/commit/9b04c90ca204ac8dc861cc743881df3f6b6eb501)) + + ## The problem + + VirtualBox's built-in RDP server (VRDP) cannot complete a connection + with IronRDP. It fails on the last message of the handshake: + + ``` + ironrdp_connector::connection_finalization: Server Control (Granted Control) + ironrdp_blocking::connector: Wait for PDU connector.state="ConnectionFinalization" hint=X224Hint + RDP protocol decode error: Error { + context: ">::decode", + kind: NotEnoughBytes { received: 18, expected: 26 }, + } + ``` + + Nothing is missing. Those two numbers come from the `total_length` + cross-check at the end of `ShareControlHeader::decode`, so `received` is + the length the *server declared* and `expected` is the size the PDU + *actually decoded to* — neither is a byte count off the wire: + + ```rust + if total_length < header_length { + return Err(not_enough_bytes_err!(total_length, header_length)); + } + ``` + + For the Server Font Map PDU those sizes are 18 and 26: + `SHARE_CONTROL_HEADER_SIZE` (10) + `ShareDataHeader::FIXED_PART_SIZE` + (8) + `FontPdu` (8). VRDP declares `totalLength` as the two headers and + never counts its own 8-byte body. All 26 bytes arrive; only the number + is wrong. + + The inner PDU has already decoded successfully out of those bytes by the + time this check runs, which is what makes the rejection avoidable — the + check is a consistency assertion, not a bounds check. + + ## The change + + Only a server declaring **more** than was decoded leaves anything to do, + and that case already has a handler — the trailing-padding path added + for Windows. So the check collapses to: + + ```rust + if total_length > header_length { + let padding = total_length - header_length; + ensure_size!(in: src, size: padding); + read_padding!(src, padding); + } + ``` + + The `is_empty_output_pdu` special case goes with it: a `total_length` of + 0 now falls through as the no-op it already was. + + ## Genuine truncation is unaffected + + This is the part worth checking rather than taking on trust. A short + buffer fails *earlier* — inside the inner PDU's own `Decode` impl, via + `ensure_fixed_part_size!` — and never reaches this check. The existing + test proves it and still passes: + + ```rust + fn from_header_only_buffer_rejects_rdp_pdu_client_font_list() { + assert!(decode::(&CLIENT_FONT_LIST_BUFFER[..18]).is_err()); + } + ``` + + ## Tests + + Added one that fails without this change with the reported error + verbatim: + + ```rust + fn from_buffer_with_under_declared_total_length_parses_rdp_pdu_server_font_map() { + let mut buf = SERVER_FONT_MAP_BUFFER; + buf[0] = 18; + assert_eq!(SERVER_FONT_MAP.clone(), decode(buf.as_ref()).unwrap()); + } + ``` + + It sits next to + `from_header_only_buffer_defaults_rdp_pdu_server_font_map`, which + already tolerates an 18-byte *buffer* — this covers the neighbouring + case of a 26-byte buffer with an 18-byte declaration. + + `cargo test -p ironrdp-testsuite-core`: 1049 passed, 0 failed. + + ## Where this was found + + Downstream in [Haven](https://github.com/GlassHaven/Haven), from a user + who could not connect to VirtualBox at all + ([GlassHaven/Haven#422](https://github.com/GlassHaven/Haven/issues/422)). + The same fix also resolves an earlier report on that issue where VRDP + under-declares a Pointer update mid-session as 24 bytes on an 8565-byte + frame — same defect, different PDU, and one we had been working around + client-side because it happened after connect. This one happens during + finalization, where there is no session yet to work around it in. + + I have not captured the VRDP bytes myself, so which PDU the server sends + at that point rests on sequence position — it follows Granted Control, + where the client waits for Font Map — rather than on a capture. The size + arithmetic matches Font Map exactly, but a Control PDU is also 26 bytes, + so I would not claim more than that. The fix does not depend on which it + is. + + --------- + +- Add missing error info codes ([#1966](https://github.com/Devolutions/IronRDP/issues/1966)) ([d2bb737664](https://github.com/Devolutions/IronRDP/commit/d2bb7376642c8b03178226e1cc0f87698d511cfb)) + +- Keep undefined channel option bits instead of refusing ([#1911](https://github.com/Devolutions/IronRDP/issues/1911)) ([e56bb42cca](https://github.com/Devolutions/IronRDP/commit/e56bb42cca9b5a5d5afdef353e502ab078ef24b7)) + +### Build + +- Bump the crypto group across 1 directory with 3 updates ([#1449](https://github.com/Devolutions/IronRDP/issues/1449)) ([e1725e8c8a](https://github.com/Devolutions/IronRDP/commit/e1725e8c8a581b83835647b6ee563a5b3f6c7a1b)) + + + ## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-pdu-v0.8.0...ironrdp-pdu-v0.9.0)] - 2026-07-10 ### Security diff --git a/crates/ironrdp-pdu/Cargo.toml b/crates/ironrdp-pdu/Cargo.toml index 4e07b14543..b792178088 100644 --- a/crates/ironrdp-pdu/Cargo.toml +++ b/crates/ironrdp-pdu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-pdu" -version = "0.9.0" +version = "0.10.0" readme = "README.md" description = "RDP PDU encoding and decoding" edition.workspace = true @@ -26,7 +26,7 @@ arbitrary = ["alloc", "dep:arbitrary", "bitflags/arbitrary"] [dependencies] bitflags = "2.11" -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["std"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["std"] } # public ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public arbitrary = { version = "1", features = ["derive"], optional = true } tap = "1" diff --git a/crates/ironrdp-rail/CHANGELOG.md b/crates/ironrdp-rail/CHANGELOG.md new file mode 100644 index 0000000000..e7979ec9de --- /dev/null +++ b/crates/ironrdp-rail/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-rail-v0.1.0)] - 2026-09-30 + +### Features + +- Add RemoteApp protocol primitives ([#1636](https://github.com/Devolutions/IronRDP/issues/1636)) ([0161906731](https://github.com/Devolutions/IronRDP/commit/0161906731757356953cdb389a2cd6a42863deb2)) + + Add portable RAIL wire types and a typed Remote Programs capability set. + + Validate the RAIL crate's bare `no_std` and allocation-backed + configurations in the workspace feature matrix. + + Keep connection setup and windowing behavior outside this protocol + layer. + + diff --git a/crates/ironrdp-rail/Cargo.toml b/crates/ironrdp-rail/Cargo.toml index b526186fb3..3afe4757ad 100644 --- a/crates/ironrdp-rail/Cargo.toml +++ b/crates/ironrdp-rail/Cargo.toml @@ -18,8 +18,8 @@ std = ["alloc", "ironrdp-core/std", "dep:ironrdp-svc"] alloc = ["dep:ironrdp-core", "ironrdp-core/alloc"] [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", default-features = false, optional = true } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8", optional = true } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", default-features = false, optional = true } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9", optional = true } # public [lints] workspace = true diff --git a/crates/ironrdp-rdcleanpath/CHANGELOG.md b/crates/ironrdp-rdcleanpath/CHANGELOG.md index 57da670c5a..17bf427f67 100644 --- a/crates/ironrdp-rdcleanpath/CHANGELOG.md +++ b/crates/ironrdp-rdcleanpath/CHANGELOG.md @@ -6,6 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.2.2...ironrdp-rdcleanpath-v0.2.3)] - 2026-09-30 + +### Features + +- Support Hyper-V connection ordering ([#1505](https://github.com/Devolutions/IronRDP/issues/1505)) ([5c1816244e](https://github.com/Devolutions/IronRDP/commit/5c1816244e83187a04249e9d9c240d096cb78f55)) + + Hyper-V over RDCleanPath needs PCB → TLS on the proxy, then CredSSP → + X.224 on the client. Ordinary RDCleanPath stays X.224-first. + + Still VERSION_1 with the same DER fields. An explicit VMConnect request + carries a Unicode PCB payload in `preconnection_blob` with no X.224; the + proxy encodes the binary PCB. Generic PCB requests keep their existing + X.224-first behavior. + + Gateway reference implementation: + [Devolutions/devolutions-gateway#1372](https://github.com/Devolutions/devolutions-gateway/pull/1372) + + Checked locally: Rust builds, formatting, Svelte typecheck, and .NET + build. Real nested Hyper-V E2E through Gateway: Native rendered 18 + frames, Avalonia connected and rendered its first frame, and Web + rendered a non-empty 1280×720 canvas. + + --------- + +### Build + +- Bump the crypto group across 1 directory with 3 updates ([#1449](https://github.com/Devolutions/IronRDP/issues/1449)) ([e1725e8c8a](https://github.com/Devolutions/IronRDP/commit/e1725e8c8a581b83835647b6ee563a5b3f6c7a1b)) + + + ## [[0.2.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.2.1...ironrdp-rdcleanpath-v0.2.2)] - 2026-06-05 ## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdcleanpath-v0.2.0...ironrdp-rdcleanpath-v0.2.1)] - 2025-10-02 diff --git a/crates/ironrdp-rdcleanpath/Cargo.toml b/crates/ironrdp-rdcleanpath/Cargo.toml index dada9e3bec..b942a39b70 100644 --- a/crates/ironrdp-rdcleanpath/Cargo.toml +++ b/crates/ironrdp-rdcleanpath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-rdcleanpath" -version = "0.2.2" +version = "0.2.3" readme = "README.md" description = "RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway" edition.workspace = true diff --git a/crates/ironrdp-rdpdr-native/CHANGELOG.md b/crates/ironrdp-rdpdr-native/CHANGELOG.md index bf89103bd6..3861d38784 100644 --- a/crates/ironrdp-rdpdr-native/CHANGELOG.md +++ b/crates/ironrdp-rdpdr-native/CHANGELOG.md @@ -6,6 +6,105 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.7.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-native-v0.7.0...ironrdp-rdpdr-native-v0.7.1)] - 2026-09-30 + +### Security + +- Add advanced Windows filesystem semantics ([#1590](https://github.com/Devolutions/IronRDP/issues/1590)) ([d1c63ecd7b](https://github.com/Devolutions/IronRDP/commit/d1c63ecd7bd13c9ae3d88f3ae84176f214f41f61)) + + Extend the Windows-native RDPDR backend with confined directory, + notification, lock, security, stream, control, and volume support. + Decode only the portable IRPs and capability needed to dispatch these + native operations. + +### Features + +- Add Windows filesystem backend ([#1587](https://github.com/Devolutions/IronRDP/issues/1587)) ([7dce8a306f](https://github.com/Devolutions/IronRDP/commit/7dce8a306f43462677879905642967066c42337f)) + + Add a handle-relative Windows RDPDR backend for one selected volume. + It confines protocol paths below an opened root and supports bounded + file I/O + and basic metadata. + + Unsupported advanced filesystem operations return STATUS_NOT_SUPPORTED; + later + stack layers will add advanced Windows semantics and host integration. + +- Wire RDPDR backends into client connections ([#1600](https://github.com/Devolutions/IronRDP/issues/1600)) ([1fbc9bab0b](https://github.com/Devolutions/IronRDP/commit/1fbc9bab0bc26d8fe0789d5215005d7ea22e2a54)) + + Build a fresh RDPDR backend product for every connection attempt. + + Attach RDPDR only when its product has filesystem devices, advertise + RDPSND for Windows interoperability, and deliver deferred responses. + +- Add static drive redirection ([#1616](https://github.com/Devolutions/IronRDP/issues/1616)) ([a724f783d1](https://github.com/Devolutions/IronRDP/commit/a724f783d1638b4e215507849c1cf148887f30d5)) + + Expose Windows logical volumes through the ActiveX drive collection and + configure a static RDPDR backend from the selected pre-connect snapshot. + + DisableRdpdr remains a hard override. + +- Plumb smartcard device into RDPDR backends ([#1656](https://github.com/Devolutions/IronRDP/issues/1656)) ([66831bbbba](https://github.com/Devolutions/IronRDP/commit/66831bbbbabe3bf36bedff769c3e62819f60d46b)) + + Return immediate SvcMessage completions from handle_scard_call so + backends can finish MS-RDPESC IRPs without blocking the channel path. + + Wire WindowsRdpdrBackendFactory::with_smartcard and a minimal + ScardSession stub that answers decoded calls with + SCARD_E_UNSUPPORTED_FEATURE. Allow smartcard-only RDPDR products (no + drives). Full WinSCard work and product CLI/ActiveX enablement remain + follow-ups. + + Depends on #1654. + +- Windows WinSCard core smartcard backend ([#1661](https://github.com/Devolutions/IronRDP/issues/1661)) ([1e68bf1a1f](https://github.com/Devolutions/IronRDP/commit/1e68bf1a1f74631c0a1e3dcd16ffb927cae2080c)) + + Replace the PR2 Windows smartcard stub with a trimmed WinSCard core so + logon-critical MS-RDPESC IOCTLs complete against the host resource + manager. + +- Extend Windows WinSCard IOCTL coverage ([#1668](https://github.com/Devolutions/IronRDP/issues/1668)) ([992f7d8f5e](https://github.com/Devolutions/IronRDP/commit/992f7d8f5ec46ed1faea7ab57d876811df9f8c42)) + + Implement remaining MS-RDPESC WinSCard paths on the PR3 core backend: + LocateCards/ByATR, Control, Get/SetAttrib, GetTransmitCount, + Read/WriteCache, GetReaderIcon, GetDeviceTypeId, and ContextAndString* + reader-group admin. Keep A→W execution, buffer probes, and deferred + workers. Product wiring stays out of scope. + + Size gate (counted .rs only): +467/-12 = 479 lines, 2 files (size/L). + PR5 still owns daemon/agent/viewer/ActiveX product wiring. + +- Enable Windows smartcard in product surfaces ([#1672](https://github.com/Devolutions/IronRDP/issues/1672)) ([1561a4c327](https://github.com/Devolutions/IronRDP/commit/1561a4c327512a1a3a6c618d4b06dc388fab132f)) + + Wire daemon, agent, viewer, and ActiveX to the WinSCard RDPDR backend so + products can announce a smartcard device only with a matching factory. + + Expose `WindowsRdpdrBackendFactory::with_smartcard`, daemon/agent + `--smartcard` and `ironrdp_smartcard` connect overrides, viewer + `--smartcard`, and ActiveX `RedirectSmartCards`. Smartcard-only RDPDR is + valid on Windows; non-Windows paths refuse or hard-disable enablement. + +- Redirect dynamic drives ([#1780](https://github.com/Devolutions/IronRDP/issues/1780)) ([5e05637f06](https://github.com/Devolutions/IronRDP/commit/5e05637f0659d460d6e02b5567bb728f1d04585a)) + + Keep drive capability active for hotplug-only sessions and route ActiveX + drive rescans and selection changes through the live RDPDR channel. + Preserve stable drive IDs, defer removals until server acknowledgement, + and keep generic devices, printers, and ports explicitly unsupported. + + Smartcard redirection remains independent. + +- Redirect default Windows printer ([#1876](https://github.com/Devolutions/IronRDP/issues/1876)) ([eaadd650c9](https://github.com/Devolutions/IronRDP/commit/eaadd650c9e2be98b57274b72095bf41da6ac3dd)) + + Expose the preconnect RedirectPrinters setting and carry printer + metadata through the RDPDR client factory. + + Discover the current Windows default queue, negotiate its driver + metadata, and stream bounded RAW jobs on a module-pinned worker. Ports, + generic PnP/USB devices, multiple printers, and Easy Print/XPS remain + unsupported. + + + ## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-native-v0.6.0...ironrdp-rdpdr-native-v0.7.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-rdpdr-native/Cargo.toml b/crates/ironrdp-rdpdr-native/Cargo.toml index 79c7fb3be6..9a400a6480 100644 --- a/crates/ironrdp-rdpdr-native/Cargo.toml +++ b/crates/ironrdp-rdpdr-native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-rdpdr-native" -version = "0.7.0" +version = "0.7.1" readme = "README.md" description = "Native RDPDR static channel backend implementations for IronRDP" edition.workspace = true @@ -16,17 +16,17 @@ categories.workspace = true doctest = false [target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public -ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.7" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public +ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.8" } # public nix = { version = "0.31", features = ["fs", "dir"] } tracing = { version = "0.1", features = ["log"] } [target.'cfg(windows)'.dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.7" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.8" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public tracing = { version = "0.1", features = ["log"] } windows = { version = "0.62", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_SystemServices", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Graphics_Printing", "Win32_Security", "Win32_Security_Authorization", "Win32_Security_Credentials", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Threading"] } diff --git a/crates/ironrdp-rdpdr/CHANGELOG.md b/crates/ironrdp-rdpdr/CHANGELOG.md index ec466f8009..99e5517490 100644 --- a/crates/ironrdp-rdpdr/CHANGELOG.md +++ b/crates/ironrdp-rdpdr/CHANGELOG.md @@ -6,6 +6,360 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.7.0...ironrdp-rdpdr-v0.8.0)] - 2026-09-30 + +### Security + +- Add advanced Windows filesystem semantics ([#1590](https://github.com/Devolutions/IronRDP/issues/1590)) ([d1c63ecd7b](https://github.com/Devolutions/IronRDP/commit/d1c63ecd7bd13c9ae3d88f3ae84176f214f41f61)) + + Extend the Windows-native RDPDR backend with confined directory, + notification, lock, security, stream, control, and volume support. + Decode only the portable IRPs and capability needed to dispatch these + native operations. + +### Features + +- Add filesystem PDU foundation ([#1566](https://github.com/Devolutions/IronRDP/issues/1566)) ([161409e18d](https://github.com/Devolutions/IronRDP/commit/161409e18dd185de9bb30730303e56f5e8d28941)) + + ## Summary + - Add portable MS-RDPEFS/MS-FSCC filesystem request and completion + codecs with malformed-input validation and wire tests. + - Keep RDPDR runtime dispatch, Windows-native backend implementation, + and client/session integration out of this foundation. + + ## Follow-up + Later stacked PRs provide the backend implementation and runtime + integration. + + --------- + +- Add filesystem backend dispatch ([#1578](https://github.com/Devolutions/IronRDP/issues/1578)) ([8f5f3e2515](https://github.com/Devolutions/IronRDP/commit/8f5f3e25154a5edd324a8e89e30ef509a682dbaa)) + + Route confirmed filesystem requests through portable backend contracts + and make lifecycle, completion, and announcement state explicit. + Validate filesystem close padding, release dynamically activated drives + after rejected announcements, and prevent raw device removal from + bypassing backend cleanup. The noop backend now rejects unsupported + filesystem I/O. + + Later stacked PRs supply the concrete Windows backend and host + integration. + +- Add Windows filesystem backend ([#1587](https://github.com/Devolutions/IronRDP/issues/1587)) ([7dce8a306f](https://github.com/Devolutions/IronRDP/commit/7dce8a306f43462677879905642967066c42337f)) + + Add a handle-relative Windows RDPDR backend for one selected volume. + It confines protocol paths below an opened root and supports bounded + file I/O + and basic metadata. + + Unsupported advanced filesystem operations return STATUS_NOT_SUPPORTED; + later + stack layers will add advanced Windows semantics and host integration. + +- Wire RDPDR backends into client connections ([#1600](https://github.com/Devolutions/IronRDP/issues/1600)) ([1fbc9bab0b](https://github.com/Devolutions/IronRDP/commit/1fbc9bab0bc26d8fe0789d5215005d7ea22e2a54)) + + Build a fresh RDPDR backend product for every connection attempt. + + Attach RDPDR only when its product has filesystem devices, advertise + RDPSND for Windows interoperability, and deliver deferred responses. + +- Complete MS-RDPESC PDUs and native handles ([#1654](https://github.com/Devolutions/IronRDP/issues/1654)) ([9f3ab27f88](https://github.com/Devolutions/IronRDP/commit/9f3ab27f887a2ee291afaffc97ace455b02e1ac7)) + + Fill out the remaining MS-RDPESC call/return PDUs and encode + SCARDCONTEXT/SCARDHANDLE as variable-length native values so x86 + (4-byte) and x64 (8-byte) WinSCard handles round-trip on the wire. Keep + this change PDU-only; the WinSCard backend lands in stacked follow-ups. + +- Plumb smartcard device into RDPDR backends ([#1656](https://github.com/Devolutions/IronRDP/issues/1656)) ([66831bbbba](https://github.com/Devolutions/IronRDP/commit/66831bbbbabe3bf36bedff769c3e62819f60d46b)) + + Return immediate SvcMessage completions from handle_scard_call so + backends can finish MS-RDPESC IRPs without blocking the channel path. + + Wire WindowsRdpdrBackendFactory::with_smartcard and a minimal + ScardSession stub that answers decoded calls with + SCARD_E_UNSUPPORTED_FEATURE. Allow smartcard-only RDPDR products (no + drives). Full WinSCard work and product CLI/ActiveX enablement remain + follow-ups. + + Depends on #1654. + +- Windows WinSCard core smartcard backend ([#1661](https://github.com/Devolutions/IronRDP/issues/1661)) ([1e68bf1a1f](https://github.com/Devolutions/IronRDP/commit/1e68bf1a1f74631c0a1e3dcd16ffb927cae2080c)) + + Replace the PR2 Windows smartcard stub with a trimmed WinSCard core so + logon-critical MS-RDPESC IOCTLs complete against the host resource + manager. + +- Extend Windows WinSCard IOCTL coverage ([#1668](https://github.com/Devolutions/IronRDP/issues/1668)) ([992f7d8f5e](https://github.com/Devolutions/IronRDP/commit/992f7d8f5ec46ed1faea7ab57d876811df9f8c42)) + + Implement remaining MS-RDPESC WinSCard paths on the PR3 core backend: + LocateCards/ByATR, Control, Get/SetAttrib, GetTransmitCount, + Read/WriteCache, GetReaderIcon, GetDeviceTypeId, and ContextAndString* + reader-group admin. Keep A→W execution, buffer probes, and deferred + workers. Product wiring stays out of scope. + + Size gate (counted .rs only): +467/-12 = 479 lines, 2 files (size/L). + PR5 still owns daemon/agent/viewer/ActiveX product wiring. + +- Make the PDU layer bidirectional for a future server ([#1779](https://github.com/Devolutions/IronRDP/issues/1779)) ([a993f91408](https://github.com/Devolutions/IronRDP/commit/a993f914080bcca69341031e1ac54bc5c4c60213)) + + ## Summary + + - ironrdp-rdpdr only decodes what a client receives from a server (6 + PacketIds) and only encodes what a client sends to a server. A server + needs the opposite. This makes the PDU layer bidirectional: decode() + added to every response type the client currently only encodes, and + encode() added to every request type the client currently only decodes. + - CoreCapability::decode already branched on packet_id for both + directions and just needed the client-capability arm wired into + RdpdrPdu::decode_body, alongside three new decodes (ClientNameRequest, + ClientDeviceListAnnounce, ClientDeviceListRemove). + - CoreDeviceIoCompletion is deliberately not wired into the automatic + RdpdrPdu::decode dispatch. Fourteen different response variants share + that one PacketId (see the header() method's match), and nothing on the + wire disambiguates which one a given completion answers. That requires + knowing the MajorFunction (and, for DirectoryControl, MinorFunction) of + the original request, which only a future IRP-tracking layer will have. + Added RdpdrPdu::decode_io_completion as an explicit entry point for that + layer to call once it exists, taking the major/minor function and, for + the three completions whose body layout depends on it, the + FileInformationClass the original request asked for. + - DeviceControlResponse's output_buffer is a Box on + the encode side, since it carries IOCTL-specific NDR content only the + issuer of the original request can interpret. That can't be decoded back + into a concrete type. Decode reads it as opaque bytes through a private + RawOutputBuffer implementing the same trait, so the decoded value has + the same shape without pretending to know a structure it can't actually + know. + - Filled two response-decode gaps that were there before this PR touched + the file, found while checking the new decode paths against MS-RDPEFS + directly: FileStandardInformation and FileAttributeTagInformation are + two of the exact three classes 2.2.3.3.8 allows for a Query Information + response (the third, Basic, already round-tripped) and had encode with + no decode. FileDirectoryInformation, FileFullDirectoryInformation, + FileBothDirectoryInformation, and FileNamesInformation are the complete + set 2.2.3.3.10 allows for a Query Directory response and were entirely + unwired in FileInformationClass::decode, so directory listing had no + working decode path at all. + - VersionAndIdPdu::decode always tagged PacketId::CoreClientidConfirm as + ServerClientIdConfirm. Per MS-RDPEFS 2.2.1.1, that PacketId is shared + between Server Client ID Confirm (2.2.2.6) and Client Announce Reply + (2.2.2.3): correct for a client, which never decodes its own reply, but + wrong for a server decoding the client's reply. Added + decode_client_announce_reply as an explicit entry point for that case, + mirroring decode_io_completion's precedent for the same class of + PacketId ambiguity, and left decode() itself untouched. + - DeviceControlRequest had decode and decode_with_input_buffer but no + encode, the one server-sent I/O request missing it. Adding encode + required tightening IoCtlCode to also require Into, which surfaced + that ScardIoCtlCode (the trait's only other implementor) was missing + that conversion too. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Round-trip + tests added for every new decode()/encode() pair in + ironrdp-testsuite-core/tests/rdpdr/mod.rs, including one per Query + Information and Query Directory class, plus the two additions above. + + ## Notes + + PDU-codec layer only. The RdpdrServer state machine, IRP tracking, and + ironrdp-server integration (a factory trait plus attach_channels wiring, + the same shape as the recently landed RdpeiServerFactory) are a + follow-up PR once this lands, so the server-relevant surface can be + reviewed in a size a reviewer can actually hold in their head at once. + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +- Redirect dynamic drives ([#1780](https://github.com/Devolutions/IronRDP/issues/1780)) ([5e05637f06](https://github.com/Devolutions/IronRDP/commit/5e05637f0659d460d6e02b5567bb728f1d04585a)) + + Keep drive capability active for hotplug-only sessions and route ActiveX + drive rescans and selection changes through the live RDPDR channel. + Preserve stable drive IDs, defer removals until server acknowledgement, + and keep generic devices, printers, and ports explicitly unsupported. + + Smartcard redirection remains independent. + +- Add RdpdrServer orchestration on top of the bidirectional PDU layer ([#1783](https://github.com/Devolutions/IronRDP/issues/1783)) ([fb5f662af9](https://github.com/Devolutions/IronRDP/commit/fb5f662af9d8f3ee39429b9da00f84c5cad8582a)) + + ## Depends on #1779 + + This branch is stacked on `feat/rdpdr-server-pdu-decode` ([#1779](https://github.com/Devolutions/IronRDP/issues/1779)) and + only makes sense once that lands: `RdpdrServer` is built entirely on the + decode/encode surface #1779 adds. #1779's own Notes section already + flags this as the intended follow-up. + + ## Summary + + - `RdpdrServer` is a state machine (`Initializing`, `AwaitingAnnounce`, + `CapabilityExchange`, `Active`) driven by `SvcProcessor::process`, + following the handshake sequence MS-RDPEFS 3.1.5/3.2.5 describe. + `CapabilityExchange` and `Active` both accept `CoreDevicelistAnnounce` + since real clients don't always send it strictly during capability + exchange. A client re-sending its Announce Reply while already `Active` + re-initializes (issue #195's duplicate-init case) rather than erroring: + devices and in-flight IRPs clear, the backend sees a fresh + `on_device_announce` batch. + - Outstanding drive I/O is tracked by `CompletionId` in an `IrpTracker`, + mirroring `ironrdp-rdpeusb`'s `RequestIdAllocator`/`pending_io` + precedent. Each `PendingIrp` variant carries exactly the context + `RdpdrPdu::decode_io_completion` needs (`MajorFunction`, + `MinorFunction`, and the file/filesystem information class where the + response layout depends on it). An unrecognized `CompletionId` is logged + and ignored, not treated as an error. + - `RdpdrServerBackend` has one completion callback per `PendingIrp` + variant (fourteen, matching `MajorFunction` exactly except the + deliberately unsupported `SetVolumeInformation`) plus + `on_device_announce`, `on_device_remove`, `on_client_name`. + `NoopRdpdrServerBackend` accepts every device and no-ops the rest. + - Fourteen `drive_*` methods build and send each request type. Thirteen + share a `DriveRequestBody` trait and a `send_device_io_request` helper; + `device_control` needs its own wrapper since `DeviceControlRequest`'s + encode doesn't include the input buffer that follows it on the wire, + matching its decode-side counterpart. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Full + handshake, per-drive-method completion round trips (all fourteen), + device accept/reject, duplicate-init, and orphaned-completion-id tests + added in `ironrdp-testsuite-core/tests/rdpdr/mod.rs`. + + ## Notes + + `ironrdp-server` integration (a factory trait plus `attach_channels` + wiring, matching `RdpeiServerFactory`'s recently landed shape) is a + further follow-up once this lands. + +- Wire RdpdrServer into ironrdp-server ([#1784](https://github.com/Devolutions/IronRDP/issues/1784)) ([695af5a6e8](https://github.com/Devolutions/IronRDP/commit/695af5a6e814badb3daa60184a8d2048d0e66716)) + + ## Depends on #1783 + + Stacked on `feat/rdpdr-server-core` ([#1783](https://github.com/Devolutions/IronRDP/issues/1783)), which itself depends on + #1779. Neither PDU codec bidirectionality nor RdpdrServer's + orchestration is reachable from ironrdp-server without this. + + ## Summary + + - Wires RdpdrServer into ironrdp-server, mirroring SoundServerFactory + and RdpsndServer exactly. RDPDR is a static virtual channel with the + same build-a-backend-then-attach shape as audio, not a dynamic channel + like EGFX and not a combined backend-plus-factory like clipboard. + - RdpdrServerFactory extends the ServerEventSender supertrait rather + than inlining a set_sender method, matching the live convention + SoundServerFactory and CliprdrServerFactory already use. build_backend + is named to match SoundServerFactory::build_backend. + - Server-initiated drive I/O needs the same async relay every other + channel uses. Added RdpdrServerMessage, one variant per RdpdrServer + drive_* method, and a ServerEvent::Rdpdr(RdpdrServerMessage) arm in + dispatch_server_events that looks up the live RdpdrServer instance, + calls the matching drive_* method, and writes the encoded result, the + same shape as the existing Rdpsnd arm. + - Adding a fourteen-variant enum to ServerEvent pushed + AutoReconnectCookieHandle::set's Result past clippy's result_large_err + threshold, a method this change otherwise has nothing to do with. + Suppressed locally with an explained reason rather than boxing the Rdpdr + payload, which would have changed ServerEvent's public shape for a size + concern from one unrelated method. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Added an + exhaustive-match test over every RdpdrServerMessage variant in + ironrdp-testsuite-core/tests/server/rdpdr.rs (no wildcard arm), so a + future variant added to one side of the dispatch without the other fails + to compile rather than silently falling through. + + ## Notes + + This closes out the RDPDR server-side contribution's three-part sequence + (PDU codec, RdpdrServer orchestration, ironrdp-server wiring). + +- Redirect default Windows printer ([#1876](https://github.com/Devolutions/IronRDP/issues/1876)) ([eaadd650c9](https://github.com/Devolutions/IronRDP/commit/eaadd650c9e2be98b57274b72095bf41da6ac3dd)) + + Expose the preconnect RedirectPrinters setting and carry printer + metadata through the RDPDR client factory. + + Discover the current Windows default queue, negotiate its driver + metadata, and stream bounded RAW jobs on a module-pinned worker. Ports, + generic PnP/USB devices, multiple printers, and Easy Print/XPS remain + unsupported. + +### Bug Fixes + +- Consume query information body ([#1810](https://github.com/Devolutions/IronRDP/issues/1810)) ([476e8a174d](https://github.com/Devolutions/IronRDP/commit/476e8a174d6a61b7dbadbf4fe0e3e2290fbfc491)) + + DR_DRIVE_QUERY_INFORMATION_REQ previously decoded only the + FsInformationClass field and ignored Length, Padding, and QueryBuffer, + leaving those bytes unconsumed on the wire cursor. Servers that send a + non-empty QueryBuffer (per MS-RDPEFS 2.2.3.3.8) desynchronized + subsequent PDU parsing. + + Decode and encode the full fixed part (FsInformationClass, Length, + 24-byte Padding) and advance the cursor past the Length-bounded + QueryBuffer on decode. Encode continues to emit an empty QueryBuffer + since this representation only retains the information class. + + + ## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpdr-v0.6.0...ironrdp-rdpdr-v0.7.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-rdpdr/Cargo.toml b/crates/ironrdp-rdpdr/Cargo.toml index db5b239e3a..ab832cfd91 100644 --- a/crates/ironrdp-rdpdr/Cargo.toml +++ b/crates/ironrdp-rdpdr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-rdpdr" -version = "0.7.0" +version = "0.8.0" readme = "README.md" description = "RDPDR channel implementation." edition.workspace = true @@ -16,10 +16,10 @@ categories.workspace = true doctest = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public tracing = { version = "0.1", features = ["log"] } bitflags = "2.11" getrandom = { version = "0.3", features = ["std"] } diff --git a/crates/ironrdp-rdpeai/Cargo.toml b/crates/ironrdp-rdpeai/Cargo.toml index 7a5b03bc78..6b02b6a241 100644 --- a/crates/ironrdp-rdpeai/Cargo.toml +++ b/crates/ironrdp-rdpeai/Cargo.toml @@ -19,11 +19,11 @@ doctest = false test = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["alloc"] } # public -ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.9" } # public — shared AUDIO_FORMAT -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["alloc"] } # public +ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.10" } # public — shared AUDIO_FORMAT +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-rdpecam/Cargo.toml b/crates/ironrdp-rdpecam/Cargo.toml index c44e108ae9..922e47db57 100644 --- a/crates/ironrdp-rdpecam/Cargo.toml +++ b/crates/ironrdp-rdpecam/Cargo.toml @@ -18,10 +18,10 @@ doctest = false test = false [dependencies] -ironrdp-core = { version = "0.2.1", path = "../ironrdp-core", features = ["alloc"] } # public -ironrdp-dvc = { version = "0.8.0", path = "../ironrdp-dvc" } # public -ironrdp-pdu = { version = "0.9.0", path = "../ironrdp-pdu", features = ["alloc"] } # public -ironrdp-svc = { version = "0.8.0", path = "../ironrdp-svc" } # public +ironrdp-core = { version = "0.3.0", path = "../ironrdp-core", features = ["alloc"] } # public +ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public +ironrdp-pdu = { version = "0.10.0", path = "../ironrdp-pdu", features = ["alloc"] } # public +ironrdp-svc = { version = "0.9.0", path = "../ironrdp-svc" } # public tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-rdpei/Cargo.toml b/crates/ironrdp-rdpei/Cargo.toml index ced700a76c..c9148b8707 100644 --- a/crates/ironrdp-rdpei/Cargo.toml +++ b/crates/ironrdp-rdpei/Cargo.toml @@ -17,10 +17,10 @@ doctest = false [dependencies] bitflags = "2.11" -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-rdpel/CHANGELOG.md b/crates/ironrdp-rdpel/CHANGELOG.md new file mode 100644 index 0000000000..76fd3dea35 --- /dev/null +++ b/crates/ironrdp-rdpel/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.0.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-rdpel-v0.0.0)] - 2026-09-30 + +### Features + +- Add location redirection ([#1778](https://github.com/Devolutions/IronRDP/issues/1778)) ([1cee7a8613](https://github.com/Devolutions/IronRDP/commit/1cee7a86135a0556c01965d0406233bd7df367a9)) + + Implement MS-RDPEL v1 codecs and the location DVC state machine, then + route the ActiveX methods through the bounded client input queue. + + Preserve mstsc-compatible validation and altitude caching while + surfacing inactive sessions, channel readiness, queue pressure, and + encoding failures. Coordinates are caller-supplied only and are never + logged or persisted. + + diff --git a/crates/ironrdp-rdpel/Cargo.toml b/crates/ironrdp-rdpel/Cargo.toml index 59067241d6..9909e411f3 100644 --- a/crates/ironrdp-rdpel/Cargo.toml +++ b/crates/ironrdp-rdpel/Cargo.toml @@ -17,10 +17,10 @@ doctest = false test = false [dependencies] -ironrdp-core = { version = "0.2.1", path = "../ironrdp-core", features = ["alloc"] } # public -ironrdp-dvc = { version = "0.8.0", path = "../ironrdp-dvc" } # public -ironrdp-pdu = { version = "0.9.0", path = "../ironrdp-pdu", features = ["alloc"] } # public -ironrdp-svc = { version = "0.8.0", path = "../ironrdp-svc" } # public +ironrdp-core = { version = "0.3.0", path = "../ironrdp-core", features = ["alloc"] } # public +ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public +ironrdp-pdu = { version = "0.10.0", path = "../ironrdp-pdu", features = ["alloc"] } # public +ironrdp-svc = { version = "0.9.0", path = "../ironrdp-svc" } # public tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-rdpemt/CHANGELOG.md b/crates/ironrdp-rdpemt/CHANGELOG.md new file mode 100644 index 0000000000..0f72a91cdf --- /dev/null +++ b/crates/ironrdp-rdpemt/CHANGELOG.md @@ -0,0 +1,87 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-rdpemt-v0.1.0)] - 2026-09-30 + +### Security + +- Add the RDPEMT multitransport tunnel crate ([#1626](https://github.com/Devolutions/IronRDP/issues/1626)) ([f3b770e9c1](https://github.com/Devolutions/IronRDP/commit/f3b770e9c1582e5c87d6b258319ccaf343bcb339)) + + # feat(rdpemt): add the RDPEMT multitransport tunnel crate + + First of six. Adds `ironrdp-rdpemt`, the tunnel described by [MS-RDPEMT] + that + carries dynamic virtual channel traffic over a sideband connection. + + This is the smallest and most self-contained piece of the RDP-UDP work + @mamoreau-devolutions asked for on #140, and it stands alone: nothing + here + depends on the UDP transport, and the tunnel is driven by decoded PDUs + rather + than by a socket. + + ## What is here + + **PDUs.** The tunnel create request and response, the data PDU with its + subheader chain, and the Initiate Multitransport request and response + from + [MS-RDPBCGR] 2.2.15 that begin the exchange. + + **Tunnel state machine.** Both sides of the handshake: a client that + sends the + create request and waits for a response, a server that checks the + security + cookie it receives against the one it issued, and the established state + where + both sides simply carry data. Sans-I/O, so it takes PDUs in and produces + events + out; `no_std` with an `alloc` dependency. + + ## Notes for review + +### Features + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + +- Log the multitransport tunnel ([#2024](https://github.com/Devolutions/IronRDP/issues/2024)) ([21e33b242e](https://github.com/Devolutions/IronRDP/commit/21e33b242ec8d184d73569bf7c068dc0ead16cd8)) + + - `ironrdp-rdpemt` had no log statements. It now depends on `tracing` + (the same line `ironrdp-dvc` uses) and the tunnel state machine logs its + actions: create request and response queued, received and validated + (debug), tunnel data sent and received (trace), and at warn a create + request rejected (with whether the request id and the cookie matched, + never the cookie itself), a rejected create response, a PDU that fails + to decode and a PDU unexpected in the current state. + - The PDU encoders and decoders stay log-free, like `ironrdp-pdu`. The + error values returned are unchanged. + + diff --git a/crates/ironrdp-rdpemt/Cargo.toml b/crates/ironrdp-rdpemt/Cargo.toml index ce0fe666c3..ec1e8d0eb2 100644 --- a/crates/ironrdp-rdpemt/Cargo.toml +++ b/crates/ironrdp-rdpemt/Cargo.toml @@ -29,7 +29,7 @@ arbitrary = ["dep:arbitrary", "std"] [dependencies] arbitrary = { version = "1", features = ["derive"], optional = true } -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public ironrdp-error = { path = "../ironrdp-error", version = "0.2", features = ["alloc"] } # public subtle = { version = "2", default-features = false } tracing = { version = "0.1", features = ["log"] } diff --git a/crates/ironrdp-rdpeudp-tokio/CHANGELOG.md b/crates/ironrdp-rdpeudp-tokio/CHANGELOG.md new file mode 100644 index 0000000000..4184cb19ad --- /dev/null +++ b/crates/ironrdp-rdpeudp-tokio/CHANGELOG.md @@ -0,0 +1,346 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-rdpeudp-tokio-v0.1.0)] - 2026-09-30 + +### Security + +- Add the async driver for the RDP-UDP transport ([#1687](https://github.com/Devolutions/IronRDP/issues/1687)) ([9aa8a4b063](https://github.com/Devolutions/IronRDP/commit/9aa8a4b063d71a1b98f2306de045c32c08bc70f9)) + + Fifth of six. **Stacked on the connection state machine PR**, and needs + the + RDPEMT PR too. + + Wires the sans-I/O connection and the RDPEMT tunnel to a real socket. A + single + task owns the `UdpSocket` and the `RdpeudpConnection` and runs a select + loop over + three sources: datagrams arriving, plaintext the TLS layer wants to + send, and + timer expiry. It is the only place in the stack that reads a clock. + + `RdpeudpStream` gives tokio-rustls something to wrap, bridging the + driver to TLS + through a pair of buffers so the TLS layer never learns it is running + over UDP. + `connect_udp` and `accept_udp` run the whole sequence: RDP-UDP + handshake, TLS, + RDPEMT tunnel, then a data pump. `UdpTransport` implements `FramedRead` + and + `FramedWrite`, so the session layer reaches it through the same traits + as the TCP + path. + + ## Three things this layer has to get right + + **A spawned task that is dropped keeps running.** All three handles + (driver, + read pump, write pump) live in a guard that aborts on drop and can be + taken + back when the task is meant to be awaited. That matters because the TLS + upgrade and the tunnel handshake both sit between spawning the driver + and + returning the transport, and both propagate with `?`. Making the guard + structural means each early return cleans up without every path having + to + remember. `shutdown()` follows the same three-task shape in reverse: the + read + pump gets EOF once the shared I/O bridge is marked closed, the write + pump + exits on its own once the send channel is dropped, and the driver is + aborted + last since it may still be sitting in its select loop. + + **The read buffer is bounded.** Ceasing to read from the socket is the + backpressure the protocol expects rather than a drop policy: unread + datagrams + stay in the socket buffer, acknowledgements stop, and the receive window + closes + on the sender, which is what [MS-RDPEUDP2] 3.1.1.2.2 has it for. The + half that is + easy to leave out is resuming, so `poll_read` wakes the driver once it + has taken + bytes out. + + **An empty tunnel PDU is not end of stream.** `Framed` reads a + zero-length result + as EOF, and [MS-RDPEMT] 2.2.2.3 puts no minimum on `HigherLayerData`. + [MS-RDPBCGR] 1.3.9 sends the four Continuous Auto-Detection messages + "encapsulated in the RDP_TUNNEL_SUBHEADER structure ... over the + sideband + channels that are in active use", and those carriers have nothing behind + the + subheaders. + + ## On the cookie hash + + The SHA-256 the version 3 SYN carries is derived here from the tunnel + config, + which already holds the security cookie. That keeps `ironrdp-rdpeudp` + free of a + cryptographic dependency and means a caller cannot supply a hash over a + different + cookie than the tunnel will present. + + ## Test plan + + `cargo xtask check fmt/lints/tests/typos/locks` plus `cargo xtask wasm + check` + + Unit tests for the stream adapter, the framing adapter, task lifetime, + backpressure, and clean-vs-truncated tunnel PDU framing, plus nine + full-stack + loopback tests in `ironrdp-testsuite-extra` that run a client and server + through handshake, TLS, tunnel and bidirectional data over localhost, + including IPv4 and IPv6, an oversized-payload rejection, a + caller-supplied + certificate verifier, and a failed-reconnect state check. + + One caveat on those loopback tests, since it caught me out: localhost + carries an + oversized datagram perfectly well, so they cannot detect an MTU + violation. The + datagram sizes are asserted in the connection tests instead. + +- Wire UDP multitransport into ironrdp-server ([#1954](https://github.com/Devolutions/IronRDP/issues/1954)) ([73dfa30e38](https://github.com/Devolutions/IronRDP/commit/73dfa30e3831e51cf8aa58f93e27b1b00102a7ec)) + + - Add `RdpServerBuilder::with_udp_transport(udp_bind_addr)`: opt-in, + `None` by default, no behavior change unless called. + - When set (and the security mode is `Tls` or `Hybrid`, matching the + reference client's Enhanced-Security-only gate), the acceptor offers UDP + multitransport, and `accept_finalize` uses + `accept_finalize_with_multitransport` with a callback that binds a fresh + UDP socket per connection, reuses the connection's own TLS certificate + (`TlsAcceptor::config()`) for the sideband transport, and calls + `accept_udp()`. + - Once established, the transport is used to migrate EGFX graphics + traffic off TCP: `request_reliable_udp` is called opportunistically the + first time EGFX has data to send (its dynamic channel id is only known + once the client opens it). From the request on, every server message on + that channel goes over the tunnel, starting with the batch that + triggered it: the request carries SOFT_SYNC_TCP_FLUSHED and the server + MUST keep using the named tunnel immediately after sending it + (MS-RDPEDYC 2.2.5.1, 3.3.5.3.1). DRDYNVC replies for a tunneled channel + go over the tunnel too, whichever path produced them. A new + `client_loop` select arm feeds incoming tunnel payloads into + `DrdynvcServer::process_tunnel()`; payloads that arrive before the + client's Soft-Sync Response are held and processed once it does + (3.3.5.3.2), rather than dropped. + - The UDP accept runs as an ordinary `tokio::spawn` task, so enabling + UDP adds no runtime requirement for the caller. + - A failure before EGFX has moved (bind, handshake, TLS, or the tunnel + closing) leaves the session on TCP, matching the reference client's + posture. Once EGFX is on the tunnel, the tunnel closing ends the + connection: Soft-Sync cannot move a channel back to TCP (MS-RDPEDYC + 2.2.5.1), and the tunnel lasts as long as the connection (MS-RDPEMT + 1.3.3). + - A client that answers the Initiate Multitransport Request with E_ABORT + (MS-RDPBCGR 2.2.15.2) has given up on the sideband transport, so the + pending UDP accept is stopped as soon as that response arrives, whether + during finalization or later on the message channel, instead of holding + its socket until the 15 s accept timeout. Windows clients send it about + 2.7 s after connecting. + - When the UDP bind address has an unspecified IP, each connection's + socket binds to the local address that client reached over TCP instead. + A socket bound to the unspecified address replies from whichever address + the routing table picks, and on a host with several IPv6 addresses that + is not always the one the client sent to: mstsc dropped the replies and + gave up with E_ABORT. `run()` records the address itself; embedders + driving `run_connection_with` pass it with the new + `RdpServer::set_connection_local_addr`. + - A successful Initiate Multitransport Response that arrives after + finalization now enables EGFX migration for the rest of the session, + provided Soft-Sync was negotiated. mstsc finishes its UDP bootstrap + after the TCP finalization (0.87 s later in my test), so migration was + previously decided before its response existed and the session never + left TCP even with the sideband transport up. + - The EGFX channel is found whichever way it was registered: an embedder + that takes a frame handle from its `GfxServerFactory` registers it as + `GfxDvcBridge`, which the migration lookup did not recognise, so it + never sent the Soft-Sync Request. The Soft-Sync Request, the client's + response (with the tunnels and channels it accepted) and the switch of + EGFX onto UDP are now logged at debug level. + +### Features + +- MS-RDPEUDP version 1/2 reliable data transfer ([#1919](https://github.com/Devolutions/IronRDP/issues/1919)) ([8fd2b2ed3e](https://github.com/Devolutions/IronRDP/commit/8fd2b2ed3ec19a2560badfc0f36b3f32015aa066)) + + Every Windows host tested on a LAN answers the RDPUDP SYN with uUdpVer + 0x0002: they speak MS-RDPEUDP (version 1/2), not MS-RDPEUDP2 (version + 3). A client that only implements the version 3 framing therefore always + times out on the UDP handshake and falls back to TCP, so the + multitransport path never carries data against Windows. + + This adds the version 1/2 reliable data path to the sans-I/O connection + state machine, negotiated from the SYN+ACK and reusing the existing + send/receive windows, RTT estimator, loss detection and timers: + + - pdu: RDPUDP_SOURCE_PAYLOAD_HEADER (2.2.2.4) and the data payload on a + version 1/2 Source Packet. DATA datagrams decode; FEC (lossy) datagrams + are rejected. + - connection: NegotiatedParams records the wire format. Version 1/2 uses + a single Source sequence space from ISN+1; snCoded numbers each + transmission, snSourceAck drives an ACK vector walked down from the + highest received sequence (3.1.1.4), ACK-of-ACKs resets the receiver's + vector base (2.2.2.6), a Congestion Notify is answered with CWR at most + once per RTT (3.1.1.8), and ACKDELAYED marks timer-driven ACKs + (3.1.6.3). Timers take the version's floors. + - The 6-bit ACK-vector run length is encoded as count minus one on the + wire, which MS-RDPEUDP 2.2.2.7.1 leaves ambiguous but Windows requires: + element 0x00 acknowledges one Source Packet and 0x02 acknowledges three. + Reading it literally leaves the first Source Packet unacknowledged + forever, collapsing the congestion window. + - seq: 32-bit wrap-aware sequence reconstruction and narrowing. + - ConnectionConfig::offer_version lets a client offer version 2 or 1 + outright (no cookieHash), which is what the measured hosts answer + immediately. + - RdpeudpConnection::v1_stats exposes retransmit, ACK and datagram + counters for observability. + + Verified against the spec's 4.2.1 example capture and new state-machine + tests; the existing tests are unchanged. + + **Note on MS-RDPEUDP 4.2.3.** The spec's worked example annotates ACK + vector element 0x04 as "4 datagrams received," a literal reading of the + six-bit run length. Captures against Windows Server show the field is + the run length minus one: after one Source Packet the server answers + `snSourceAck = 1` with element 0x00, after four it answers `snSourceAck + = 4` with 0x03. Read literally, the first is an empty run and the second + never acknowledges seq 1, which stalls the sender. This implementation + follows the wire, not the example. Evidence: + https://gist.github.com/AKolenda/51931b094dd454ef0eb1430503629c8a + +- Log the UDP transport driver ([#2023](https://github.com/Devolutions/IronRDP/issues/2023)) ([dcf2c35e5f](https://github.com/Devolutions/IronRDP/commit/dcf2c35e5f4ba9f23d6154c4756dd2803bdfdbfb)) + + - `ironrdp-rdpeudp-tokio` logged only a few handshake milestones. It now + logs the driver and transport actions: socket bind and connect, the + RDP-UDP, TLS and RDPEMT stages on both sides with their failures and + timeouts, driver start and stop and why, datagrams and tunnel data sent + and received, send-buffer and read-buffer backpressure, and shutdown. + - Levels follow STYLE.md: info when a connection is established (connect + or accept side) and when it closes, debug for one-off events and + failures, trace for everything per datagram. The existing "dropping an + unusable datagram" line keeps its debug level with a capitalized + message. + - Existing lines now use `peer` for the remote address (previously + `server` and `client`). No payloads, cookies or key material are logged. + +### Bug Fixes + +- Detect driver exit during the async handshake wait ([#1704](https://github.com/Devolutions/IronRDP/issues/1704)) ([f24685cde6](https://github.com/Devolutions/IronRDP/commit/f24685cde66bd290a3f7d35f506b9d8840452b92)) + + ## Summary + - connect_udp and accept_udp_inner both waited on connected_notify + without racing it against the driver task's own completion + - a driver that exits early with a real error (socket failure, fatal + protocol error) went unnoticed until the configured handshake + timeout elapsed, or until the caller's outer accept_timeout + canceled the whole accept sequence for accept_udp_inner, which had + no timeout of its own on this wait at all + - either way the caller got a generic HandshakeTimeout instead of the + driver's actual error + - both call sites now race connected_notify against the driver's + JoinHandle directly, via a shared driver_exit_during_handshake + helper that turns a join result into the right UdpTransportError + - added AbortOnDrop::handle_mut to borrow the wrapped JoinHandle for + a select! branch without taking ownership of it + + ## Validation + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Four new + tests cover the shared helper's three outcomes (real driver error, + clean exit treated as failure, panic) and the select! race itself + (a driver that dies immediately is detected well inside a 5-second + bound instead of falling through to a 60-second stand-in timeout). + + ## Notes + Surfaced by Copilot's second review pass on #1687, posted 11 minutes + before that PR merged; not addressed there. + +- [**breaking**] Thread server_cert_verifier through MultitransportBootstrap::connect ([#1706](https://github.com/Devolutions/IronRDP/issues/1706)) ([0047a28740](https://github.com/Devolutions/IronRDP/commit/0047a287407bbf06e3c1f8331a299294d4eddf4c)) + + ## Summary + - connect_udp already accepted a server_cert_verifier to opt into real + TLS certificate validation, added in #1687's own first review round + - MultitransportBootstrap builds its UdpTransportConfig internally + and never set that field, so a caller going through the high-level + bootstrap API had no way to reach the verification connect_udp + itself already supported + - connect() now takes server_cert_verifier as a fourth parameter and + forwards it the same way connection_config already is + - breaking change to this crate's public API (adds a required + parameter); the crate has no external consumers yet + + ## Validation + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Added a + full-stack test proving the verifier is actually consulted through + the bootstrap path, reusing the AlwaysRejectVerifier double from the + sibling connect_udp test; mutation-verified by temporarily reverting + the fix and confirming the new test fails. + + ## Notes + Surfaced by Copilot's second review pass on #1687, posted 11 minutes + before that PR merged; not addressed there. Third of three follow-up + PRs (see #1704 and #1705 for the other two). + +- [**breaking**] Treat a full send buffer as backpressure, not a fatal error ([#1705](https://github.com/Devolutions/IronRDP/issues/1705)) ([c0267494ab](https://github.com/Devolutions/IronRDP/commit/c0267494aba535088d74f3bfed6929a176f2f01c)) + + ## Summary + - RdpeudpConnection::send's SendBufferFull is documented as transient, + but the async driver treated it as fatal, tearing down an otherwise + healthy connection under sustained write load + - send() took the payload by value with no way to hand it back on + error, so the rejected bytes were also gone by the time the error + propagated + - send()'s signature now returns the rejected data alongside the + error on every rejection, via a new SendError type; breaking change + to this crate's public API, but it has no external consumers yet + - the driver holds a backpressured write in a new pending_write field + instead of dropping it, and retries once poll_transmit has moved + entries out of the send buffer (an incoming ACK or a retransmit + both qualify) + - the write-data branch of the driver's select loop stops pulling new + data out of the shared write buffer while a write is already + pending, so a retry never gets interleaved out of order + + ## Validation + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Four new + driver-level tests build a real, in-memory-established connection and + mutation-verify the fix: filling the send buffer to its bound no + longer errors, the rejected payload comes back intact, a retry + delivers once drain_transmits frees room, and a retry into an + already-closed connection does not turn a clean shutdown into an + error. + + ## Notes + Surfaced by Copilot's second review pass on #1687, posted 11 minutes + before that PR merged; not addressed there. Second of three follow-up + PRs (see #1704 for the first). + +- Harden TLS sideband setup ([#1811](https://github.com/Devolutions/IronRDP/issues/1811)) ([7292970955](https://github.com/Devolutions/IronRDP/commit/729297095564a9640183556078cf8726dd3afeed)) + + Extract the reusable rustls verifier and client-config builder so the + RDPEUDP2 reliable-UDP TLS sideband shares the primary transport's + certificate-validation policy and callback semantics without selecting a + TLS stream backend. + + Build callback-free client configurations on the blocking pool, and run + callback-based handshakes on a dedicated blocking thread, so synchronous + platform trust-store access and certificate decisions cannot starve a + current-thread RDPEUDP driver. Bound the TLS and RDPEMT establishment + phases with dedicated timeout errors. A TLS timeout cancels the + connection attempt but cannot cancel a synchronous certificate callback + that is already running. + + Close `SharedIo` and wake parked readers when a driver is dropped before + releasing its stream, preventing aborted connection attempts from + stranding detached TLS work. + + Pass `UdpTlsConfig` directly through `MultitransportBootstrap::connect`, + and document that `S_OK` is sent only after Soft-Sync negotiation. + + diff --git a/crates/ironrdp-rdpeudp-tokio/Cargo.toml b/crates/ironrdp-rdpeudp-tokio/Cargo.toml index 9eedff4f5a..462b10e0df 100644 --- a/crates/ironrdp-rdpeudp-tokio/Cargo.toml +++ b/crates/ironrdp-rdpeudp-tokio/Cargo.toml @@ -23,13 +23,13 @@ rustls-aws-lc-rs = ["tokio-rustls/aws_lc_rs"] rustls-ring = ["tokio-rustls/ring"] [dependencies] -ironrdp-async = { path = "../ironrdp-async", version = "0.10" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["std"] } # public +ironrdp-async = { path = "../ironrdp-async", version = "0.11" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["std"] } # public ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["std"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["std"] } # public ironrdp-rdpemt = { path = "../ironrdp-rdpemt", version = "0.1", features = ["std"] } # public ironrdp-rdpeudp = { path = "../ironrdp-rdpeudp", version = "0.1", features = ["std"] } # public -ironrdp-tls = { path = "../ironrdp-tls", version = "0.2.2", default-features = false, features = ["rustls-verifier"] } # public +ironrdp-tls = { path = "../ironrdp-tls", version = "0.2.3", default-features = false, features = ["rustls-verifier"] } # public bytes = "1" tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] } # public sha2 = "0.10" diff --git a/crates/ironrdp-rdpeudp/CHANGELOG.md b/crates/ironrdp-rdpeudp/CHANGELOG.md new file mode 100644 index 0000000000..af2ec06204 --- /dev/null +++ b/crates/ironrdp-rdpeudp/CHANGELOG.md @@ -0,0 +1,472 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-rdpeudp-v0.1.0)] - 2026-09-30 + +### Security + +- [**breaking**] Add the connection state machine ([#1681](https://github.com/Devolutions/IronRDP/issues/1681)) ([1723385068](https://github.com/Devolutions/IronRDP/commit/1723385068ee3a4be635c199c752eb2bbd183f1e)) + + Fourth of six. Filed against master directly; #1626, #1627 and #1679 + have + all merged. + + Adds the sans-I/O `RdpeudpConnection` and the machinery it drives: send + and + receive windows, loss detection, NewReno congestion control, an RFC 6298 + RTT + estimator, the timer table, the reliability controller that matches + retransmissions to the packets they replace, and sequence-number + reconstruction + from 16-bit wire values. + + This is the largest PR in the stack. I looked at splitting the state + machine + from the reliability primitives, but `connection.rs` drives all of them + and the + tests span both, so the seam would have been artificial. + + ## Sans-I/O + + No clock, no socket. Time arrives as a `MonotonicInstant` argument and + outgoing + packets leave as `Transmit` values. That keeps it testable without a + network, + and avoids `std::time::Instant`, whose `now` panics on + `wasm32-unknown-unknown`. + + **`MonotonicInstant` is defined here, and #1530 adds one to + `ironrdp-connector` + for the same reason.** Two copies of one abstraction is a wart. + `ironrdp-core` + looks like the right home to me, and I raised it on #140; happy to move + it + wherever you prefer, in this PR or a follow-up. + + ## Behaviour that is required rather than chosen + + **Version 3 in the SYN.** That is the version [MS-RDPEUDP] 1.3.2.2 and + the + 2.2.2.9 table tie to the MS-RDPEUDP2 data transfer. Version 2 selects + the + MS-RDPEUDP one, which this crate does not implement. Version 3 requires + the + SHA-256 of the `securityCookie` in the client's SYN (2.2.2.9), so + `ConnectionConfig` carries it, `connect` refuses without it, and the + server + performs the check 3.1.5.1.1 asks for. + + **The handshake retransmits.** 1.3.1 delivers the SYN, SYN+ACK and ACK + by + persistent retransmits whatever mode the transport runs in; 3.1.5.4.1 + gives up + after between three and five unanswered tries. A repeated datagram from + the peer + draws a repeat of ours rather than an error, including a SYN+ACK + arriving after + we have moved on to v2, which is how a client learns its final ACK was + lost. + + **The delayed-ACK timer tracks the RTT instead of a fixed duration.** + MS-RDPEUDP2 3.1.5.2 gives half the round trip time as the receiver's + default. + The handshake round trip seeds the existing RFC 6298 estimator (skipped + when + the handshake datagram was retransmitted, per Karn's algorithm), and the + computed timeout is clamped to the 50-200ms band [MS-RDPEUDP] 3.1.6.3 + gives a + version-2 connection, since MS-RDPEUDP2 itself states no floor or cap of + its + own. + + **`UdpVersion` widens from a closed enum to a newtype carrying the raw + wire + value.** This is a breaking change to the public API #1627 shipped. + MS-RDPEUDP + 1.7 and 3.1.5.1.3 require a responder to negotiate down to a version it + supports when the peer advertises one it does not recognize; + hard-failing + decode on an unrecognized value made that MUST clause unsatisfiable. + Every + call site already used the named constants, so this is the only + consequential + part of the change. + + **`error.rs` and its `Cargo.toml`/README wiring are back.** #1627 + correctly + dropped them at its own narrower, PDU-only scope; this PR's state + machine is + what actually needs them. + + **The receive window advances on both events in 3.1.1.2.2**, not only on + AckOfAcks. The first one is what fires on a connection that is losing + nothing; + without it the window fills one window in and stops accepting. + + **AckOfAcks carries our own lowest unacknowledged sequence number** + (2.2.1.2.4, + 3.1.5.3). It is the only thing that can move a receiver past a packet + the sender + gave up on, since the retransmission carries a fresh `DataSeqNum` and + the + original is never filled. + + **Writes are split to the MTU.** MS-RDPEUDP2 does not segment: + 3.1.1.2.4.2 + forwards each packet's payload straight up and nothing marks a first or + last + fragment. `ChannelSeqNum` looks like it would serve but 3.1.5.5 gives it + a + different job, matching a retransmission to the packet it replaces. So + anything + longer than one packet is split before it becomes packets. + + **Dummy packets** are accounted for by the transport and their contents + dropped, + per 3.1.1.1.5. + + ## Test plan + + `cargo xtask check fmt/lints/tests/typos/locks` + + 179 rdpeudp tests in `ironrdp-testsuite-core` with this PR applied, plus + 179 + inline unit tests across the crate's components. The connection tests + cover a + clean transfer longer than the window, a loss in the middle of one, + handshake + retransmission to the give-up limit, the version and cookie negotiation + paths, + decoding an unrecognized `uUdpVer`, the ack-delay timeout's RTT-tracking + and + clamping behaviour, and the review round's findings: an out-of-range ACK + no + longer discards outstanding data, the ACK vector respects its 127-entry + wire + limit, `log_window_size` is validated, the ACK builders encode real + timestamps and gaps, the handshake's Karn's-algorithm check is ordered + correctly, the retransmit timer restarts on real progress, `accept` + completes + the negotiate-down behaviour, and `send` enforces a buffer bound. + +### Features + +- Add the RDP-UDP crate and the v1 handshake PDUs ([#1627](https://github.com/Devolutions/IronRDP/issues/1627)) ([0f252c2715](https://github.com/Devolutions/IronRDP/commit/0f252c2715490841793924f989b92e32fe54bfa0)) + + # feat(rdpeudp): add the RDP-UDP crate and the v1 handshake PDUs + + Second of six, and the first of the RDP-UDP transport + @mamoreau-devolutions + asked for on #140. Independent of the RDPEMT PR; the two can land in + either + order. + + Adds `ironrdp-rdpeudp` with the structures [MS-RDPEUDP] section 2.2 + defines for + the three-way handshake: the FEC header and flags, the SYN and extended + SYN + payloads that negotiate initial sequence numbers, MTU and protocol + version, the + ACK vector with its run-length encoding, the AckOfAcks header, the + correlation + ID payload, and the composite datagram that assembles them in the order + 2.2.2 + requires. + + ## The convention that matters when reading this + + Everything here is **big-endian**, and the diagrams number bits **most + significant first**. Section 2.2: "all of the messages written to the + network or + read from the network MUST be in network byte order." + + MS-RDPEUDP2, which the data transfer switches to, is little-endian and + numbers + diagram bits least significant first. The two documents are opposite on + both + counts. That is why the two wire formats are split across two PRs rather + than + reviewed together. + + ## Two readings I would particularly like checked + + **The ACK flag does not always announce an ACK vector.** 2.2.2.1 defines + it that + way, but 3.1.5.1.3 builds the SYN+ACK as a plain SYN with the flag set + and + `snSourceAck` filled in, and the capture in 4.1.2 confirms it: `uFlags` + is + `0x0005` and the SYNDATA payload follows the 8-byte header directly, + with no + vector between them. So on a SYN the flag says only that `snSourceAck` + is + meaningful. `encode` refuses a SYN carrying a vector rather than writing + bytes a + peer would read as the start of SYNDATA. + + **The ACK vector's element layout and padding** were settled against the + ACK + packet capture in 4.2.3 (`00 01 04 00`): a big-endian `uAckVectorSize`, + the + state in the top two bits of each element, and padding out to a DWORD + boundary. + + Both captures are decoded byte by byte in `pdu_v1_datagram.rs` rather + than only + round tripped against our own encoder. A mistake made symmetrically in + encode + and decode is invisible to a round trip, so the captures are the only + thing that + can catch it. + + ## What is not here + + No connection state machine, no v2 data transfer format. Those are the + next two + PRs in the stack. + + ## Test plan + + `cargo xtask check fmt/lints/tests/typos/locks` + + 43 tests in `ironrdp-testsuite-core`, including the section 4.1.1, 4.1.2 + and + 4.2.3 captures decoded whole. + +- Add the RDP-UDP2 data transfer PDUs and packet framing ([#1679](https://github.com/Devolutions/IronRDP/issues/1679)) ([0eeed99a9a](https://github.com/Devolutions/IronRDP/commit/0eeed99a9ac186fd324360c1f39a541d5f8eea21)) + + # feat(rdpeudp): add the RDP-UDP2 data transfer PDUs and packet framing + + Third of six. Builds on the RDPEMT tunnel crate ([#1626](https://github.com/Devolutions/IronRDP/issues/1626)) and the v1 + handshake + PDUs ([#1627](https://github.com/Devolutions/IronRDP/issues/1627)), both merged, adding files to the crate the second one + creates. + + Adds the structures [MS-RDPEUDP2] section 2.2.1 defines for data + transfer, which + is where a connection goes once the handshake settles on protocol + version 3: the + packet header and flags, the ACK and acknowledgment vector payloads, the + OverheadSize, DelayAckInfo and AckOfAcks control payloads, DataHeader + and + DataBody, the composite packet, and the PacketPrefixByte framing from + 2.2.1.3. + + ## Why this is a separate PR from the v1 PDUs + + The two documents take **opposite conventions**. MS-RDPEUDP is + big-endian and + numbers diagram bits most significant first; MS-RDPEUDP2 is + little-endian and + numbers them least significant first. Reading a v2 diagram with v1 + habits + produces a plausible and wrong layout for several fields, so I would + rather each + be reviewed against one document at a time. + + Four fields where that difference bites, all settled against worked + examples + rather than against the diagrams: + + **The prefix byte.** `Packet_Type_Index` occupies bits 1 to 4 and + `Short_Packet_Length` bits 5 to 7. The worked example in 3.1.1.1.5.1 + gives + `PacketPrefixByte = 0x10` for a 10-byte packet, which is + `Packet_Type_Index = 8` + (dummy) only under least-significant-first numbering. Read the other way + round + the same byte says something else entirely. + + **The ACK payload nibbles.** 2.2.1.2.1 puts `numDelayedAcks` at bits + 48-51 and + `delayAckTimeScale` at 52-55, so the count is the low nibble. + + **The acknowledgment vector field order.** 2.2.1.2.6 places `TimeStamp` + (bits + 24-47) before `SendAckTimeGapInMs` (48-55). + + **DelayAckInfo.** `MaxDelayedAcks` is a whole byte, not a nibble. + + ## On the flag set + + The header carries exactly the six flags 2.2.1.1 lists, every one of + them + announcing a payload. There is deliberately no `CN`, `CWR` or `DUMMY` + here: the + first two belong to MS-RDPEUDP's own `RDPUDP_FEC_HEADER`, and a dummy + packet is + marked by `Packet_Type_Index` 8 in the prefix byte, one layer down. With + no + standalone flags the field is derived entirely from which payloads are + present, + so a caller cannot set a bit that describes nothing. + + ## What is not here + + No connection state machine. That is the next PR. + + ## Test plan + + `cargo xtask check fmt/lints/tests/typos/locks` + + 122 rdpeudp tests in `ironrdp-testsuite-core` with this PR applied, plus + 44 + inline `#[cfg(test)]` tests in the crate itself covering packet-prefix + framing + and header/flags internals, including the 3.1.1.1.5.1 worked example. + +- Add fuzz targets for the RDP-UDP transport and RDPEMT tunnel ([#1707](https://github.com/Devolutions/IronRDP/issues/1707)) ([2191cf47a1](https://github.com/Devolutions/IronRDP/commit/2191cf47a1027e59527ab23bf8d7d63d610cd6c6)) + +- [**breaking**] Pass frame arrival time into Sequence::step ([#1530](https://github.com/Devolutions/IronRDP/issues/1530)) ([6a499faece](https://github.com/Devolutions/IronRDP/commit/6a499faece8911e50a715a3fb08d4fd8e7d7dc87)) + + ## Summary + + - Connect-time bandwidth measurement needs to know when bytes arrived, + and nothing in the sans-I/O layer could tell it. #1465, now merged, + answers the server's Bandwidth Measure Stop with a nominal interval for + exactly that reason: the connector has no way to observe the real one. + - Introduce `MonotonicInstant`, a millisecond counter with an arbitrary + epoch, and make `Option` a required parameter of + `Sequence::step`. The I/O drivers already know when a read completed, so + `Framed` records the arrival time of each read and hands it to the state + machine. A driver with no clock passes `None`. + - With arrival times available, measure for real: a Bandwidth Measure + Start opens a window, Payload messages accumulate their byte counts, and + Stop reports the elapsed time between its own arrival and the Start's. + + #1465 has merged, so this applies directly to master and carries no + merge-order dependency. That PR was the FreeRDP unblock on its own; this + is the design change behind it, split out at @CBenoit's suggestion in + review. + + ## Why the clock lives in the driver + + Two reasons, both of which rule out having the sequence read a clock + itself. + +- MS-RDPEUDP version 1/2 reliable data transfer ([#1919](https://github.com/Devolutions/IronRDP/issues/1919)) ([8fd2b2ed3e](https://github.com/Devolutions/IronRDP/commit/8fd2b2ed3ec19a2560badfc0f36b3f32015aa066)) + + Every Windows host tested on a LAN answers the RDPUDP SYN with uUdpVer + 0x0002: they speak MS-RDPEUDP (version 1/2), not MS-RDPEUDP2 (version + 3). A client that only implements the version 3 framing therefore always + times out on the UDP handshake and falls back to TCP, so the + multitransport path never carries data against Windows. + + This adds the version 1/2 reliable data path to the sans-I/O connection + state machine, negotiated from the SYN+ACK and reusing the existing + send/receive windows, RTT estimator, loss detection and timers: + + - pdu: RDPUDP_SOURCE_PAYLOAD_HEADER (2.2.2.4) and the data payload on a + version 1/2 Source Packet. DATA datagrams decode; FEC (lossy) datagrams + are rejected. + - connection: NegotiatedParams records the wire format. Version 1/2 uses + a single Source sequence space from ISN+1; snCoded numbers each + transmission, snSourceAck drives an ACK vector walked down from the + highest received sequence (3.1.1.4), ACK-of-ACKs resets the receiver's + vector base (2.2.2.6), a Congestion Notify is answered with CWR at most + once per RTT (3.1.1.8), and ACKDELAYED marks timer-driven ACKs + (3.1.6.3). Timers take the version's floors. + - The 6-bit ACK-vector run length is encoded as count minus one on the + wire, which MS-RDPEUDP 2.2.2.7.1 leaves ambiguous but Windows requires: + element 0x00 acknowledges one Source Packet and 0x02 acknowledges three. + Reading it literally leaves the first Source Packet unacknowledged + forever, collapsing the congestion window. + - seq: 32-bit wrap-aware sequence reconstruction and narrowing. + - ConnectionConfig::offer_version lets a client offer version 2 or 1 + outright (no cookieHash), which is what the measured hosts answer + immediately. + - RdpeudpConnection::v1_stats exposes retransmit, ACK and datagram + counters for observability. + + Verified against the spec's 4.2.1 example capture and new state-machine + tests; the existing tests are unchanged. + + **Note on MS-RDPEUDP 4.2.3.** The spec's worked example annotates ACK + vector element 0x04 as "4 datagrams received," a literal reading of the + six-bit run length. Captures against Windows Server show the field is + the run length minus one: after one Source Packet the server answers + `snSourceAck = 1` with element 0x00, after four it answers `snSourceAck + = 4` with 0x03. Read literally, the first is an empty run and the second + never acknowledges seq 1, which stalls the sender. This implementation + follows the wire, not the example. Evidence: + https://gist.github.com/AKolenda/51931b094dd454ef0eb1430503629c8a + +### Bug Fixes + +- [**breaking**] Treat a full send buffer as backpressure, not a fatal error ([#1705](https://github.com/Devolutions/IronRDP/issues/1705)) ([c0267494ab](https://github.com/Devolutions/IronRDP/commit/c0267494aba535088d74f3bfed6929a176f2f01c)) + + ## Summary + - RdpeudpConnection::send's SendBufferFull is documented as transient, + but the async driver treated it as fatal, tearing down an otherwise + healthy connection under sustained write load + - send() took the payload by value with no way to hand it back on + error, so the rejected bytes were also gone by the time the error + propagated + - send()'s signature now returns the rejected data alongside the + error on every rejection, via a new SendError type; breaking change + to this crate's public API, but it has no external consumers yet + - the driver holds a backpressured write in a new pending_write field + instead of dropping it, and retries once poll_transmit has moved + entries out of the send buffer (an incoming ACK or a retransmit + both qualify) + - the write-data branch of the driver's select loop stops pulling new + data out of the shared write buffer while a write is already + pending, so a retry never gets interleaved out of order + + ## Validation + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Four new + driver-level tests build a real, in-memory-established connection and + mutation-verify the fix: filling the send buffer to its bound no + longer errors, the rejected payload comes back intact, a retry + delivers once drain_transmits frees room, and a retry into an + already-closed connection does not turn a clean shutdown into an + error. + + ## Notes + Surfaced by Copilot's second review pass on #1687, posted 11 minutes + before that PR merged; not addressed there. Second of three follow-up + PRs (see #1704 for the first). + +- Accept version 1/2 SYN offers on the server side too ([#1965](https://github.com/Devolutions/IronRDP/issues/1965)) ([46be4a2569](https://github.com/Devolutions/IronRDP/commit/46be4a25690359656ca602e59d2e3987d4b2ee6d)) + + ## Summary + + accept() rejected any client offering a protocol version below 3 + outright, with a comment noting this crate only implemented the + MS-RDPEUDP2 (version 3) data transfer. That gap closed for the + client-connects-out role (ConnectionConfig::offer_version, connect(), + handle_syn_ack()'s negotiation), but accept(), the server-accept role, + was never updated to match. Every real-world Windows client I have + observed offers version 1 or 2 in its SYN, so a server built on this + crate could never actually negotiate the sideband UDP transport with + such a client, only ever fall back to the main transport. + + ## Changes + + - accept() now mirrors handle_syn_ack()'s already-proven version + selection: settle on the client's offered version when it is 1 or 2, + otherwise settle on our own highest (3), per MS-RDPEUDP 1.7's + negotiate-down MUST clause. WireFormat is selected via + UdpVersion::uses_v2_wire_format(), the same helper the client side + already uses. + - enqueue_syn_ack() now echoes the negotiated version instead of + unconditionally claiming 3. + - Fixes an adjacent gap in the same code: 3.1.5.1.1 says an invalid + cookieHash on a version 3 SYN MUST drop the connection to version 2 + rather than refuse it, which the crate could not do until now either. + - Rewrote the two existing tests that encoded the old + refuse-on-low-version behavior. Added coverage for settling on version + 1, rejecting a genuinely unrecognized low version, and a full handshake + plus bidirectional data exchange with the server on the accept side + settling on version 2. + + ## Test plan + + cargo xtask check fmt/lints/tests/typos/locks all pass. + + diff --git a/crates/ironrdp-rdpeudp/Cargo.toml b/crates/ironrdp-rdpeudp/Cargo.toml index 133d5360ca..d8b5393934 100644 --- a/crates/ironrdp-rdpeudp/Cargo.toml +++ b/crates/ironrdp-rdpeudp/Cargo.toml @@ -29,7 +29,7 @@ arbitrary = ["dep:arbitrary", "std", "bitflags/arbitrary"] [dependencies] arbitrary = { version = "1", features = ["derive"], optional = true } -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public ironrdp-error = { path = "../ironrdp-error", version = "0.2", features = ["alloc"] } # public bitflags = "2.11" # public diff --git a/crates/ironrdp-rdpeusb/Cargo.toml b/crates/ironrdp-rdpeusb/Cargo.toml index d3e58f93cf..27412e8662 100644 --- a/crates/ironrdp-rdpeusb/Cargo.toml +++ b/crates/ironrdp-rdpeusb/Cargo.toml @@ -21,11 +21,11 @@ default = [] std = [] [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["alloc"] } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["alloc"] } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public ironrdp-usb = { path = "../ironrdp-usb", version = "0.1" } # public -ironrdp-str = { path = "../ironrdp-str", version = "0.1" } +ironrdp-str = { path = "../ironrdp-str", version = "0.2" } [lints] workspace = true diff --git a/crates/ironrdp-rdpewa-native/CHANGELOG.md b/crates/ironrdp-rdpewa-native/CHANGELOG.md index c37782ed5c..5126a021b8 100644 --- a/crates/ironrdp-rdpewa-native/CHANGELOG.md +++ b/crates/ironrdp-rdpewa-native/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file. ## Unreleased +## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-rdpewa-native-v0.1.0)] - 2026-09-30 + +### Features + +- Add native MS-RDPEWA WebAuthn redirection ([#1644](https://github.com/Devolutions/IronRDP/issues/1644)) ([66da78bc4e](https://github.com/Devolutions/IronRDP/commit/66da78bc4e6b37a7780dbf9f333234be63d96afb)) + + Implement the RDPEWA dynamic channel with a Windows WebAuthn backend and + wire RedirectWebAuthn for ActiveX, the optional client feature, and the + viewer CLI. + + Prefer System32\webauthn.dll via the DVC COM plugin for MSTSC parity. + The pure-Rust backend forwards ceremonies through a webauthn.dll IWTS + oneshot so hash-only hosts that omit clientDataJSON still work; public + WebAuthN* remains a fallback when JSON is present. Recreate + WebAuthN_Channel opens through shared COM/listener factories because + Windows opens and closes the channel around each RPC. + + Side effects: + - New crates ironrdp-rdpewa and ironrdp-rdpewa-native + - Config key redirectwebauthn; ActiveX ExtendedSettings property + - ironrdp-daemon webauthn feature for ironrdp-agent + - IRONRDP_WEBAUTHN_FORCE_NATIVE debug switch + - Viewer --webauthn/--no-webauthn flags; .rdp redirectwebauthn default + - ActiveX docs note no AdvancedSettings slot and no IPersist persistence + + + ### Added - Initial Windows WebAuthn backend for MS-RDPEWA ([MS-RDPEWA]). diff --git a/crates/ironrdp-rdpewa/CHANGELOG.md b/crates/ironrdp-rdpewa/CHANGELOG.md index 54e8475401..2ad51d751c 100644 --- a/crates/ironrdp-rdpewa/CHANGELOG.md +++ b/crates/ironrdp-rdpewa/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-rdpewa-v0.1.0)] - 2026-09-30 + +### Features + +- Add native MS-RDPEWA WebAuthn redirection ([#1644](https://github.com/Devolutions/IronRDP/issues/1644)) ([66da78bc4e](https://github.com/Devolutions/IronRDP/commit/66da78bc4e6b37a7780dbf9f333234be63d96afb)) + + Implement the RDPEWA dynamic channel with a Windows WebAuthn backend and + wire RedirectWebAuthn for ActiveX, the optional client feature, and the + viewer CLI. + + Prefer System32\webauthn.dll via the DVC COM plugin for MSTSC parity. + The pure-Rust backend forwards ceremonies through a webauthn.dll IWTS + oneshot so hash-only hosts that omit clientDataJSON still work; public + WebAuthN* remains a fallback when JSON is present. Recreate + WebAuthN_Channel opens through shared COM/listener factories because + Windows opens and closes the channel around each RPC. + + Side effects: + - New crates ironrdp-rdpewa and ironrdp-rdpewa-native + - Config key redirectwebauthn; ActiveX ExtendedSettings property + - ironrdp-daemon webauthn feature for ironrdp-agent + - IRONRDP_WEBAUTHN_FORCE_NATIVE debug switch + - Viewer --webauthn/--no-webauthn flags; .rdp redirectwebauthn default + - ActiveX docs note no AdvancedSettings slot and no IPersist persistence + + + ### Added - Initial MS-RDPEWA protocol crate with CBOR PDU codec, client processor, and server skeleton. diff --git a/crates/ironrdp-rdpewa/Cargo.toml b/crates/ironrdp-rdpewa/Cargo.toml index 183fd46a9a..237f7e7d8c 100644 --- a/crates/ironrdp-rdpewa/Cargo.toml +++ b/crates/ironrdp-rdpewa/Cargo.toml @@ -16,10 +16,10 @@ categories.workspace = true doctest = false [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-rdpsnd-native/CHANGELOG.md b/crates/ironrdp-rdpsnd-native/CHANGELOG.md index 88bab83e23..d8ec014724 100644 --- a/crates/ironrdp-rdpsnd-native/CHANGELOG.md +++ b/crates/ironrdp-rdpsnd-native/CHANGELOG.md @@ -6,6 +6,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.7.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.7.0...ironrdp-rdpsnd-native-v0.7.1)] - 2026-09-30 + +### Features + +- Harden Windows client playback path ([#1648](https://github.com/Devolutions/IronRDP/issues/1648)) ([2d9a9bf114](https://github.com/Devolutions/IronRDP/commit/2d9a9bf114dcf41a1ddc7343f564bc2e8d1d06db)) + + Keep client format order for wFormatNo, play pre-v8 Wave PDUs, and apply + volume on a broader CPAL PCM offer so ActiveX mode 0 can redirect remote + audio reliably. + + Also fix clippy noise in the RDPSND client suite and keep interleaved + volume L/R phase stable across wave blocks. Volume scaling is a simple + amplitude map, not a logarithmic MS-RDPEA model. + +- Wire MS-RDPEAI capture into Windows client and ActiveX ([#1642](https://github.com/Devolutions/IronRDP/issues/1642)) ([205fe038cc](https://github.com/Devolutions/IronRDP/commit/205fe038cc693598adf803fe181526b789b2ec3d)) + + Add the client MS-RDPEAI capture path on top of hardened RDPSND + playback: connector CFG + static channel wiring, CPAL PCM capture + backend, ironrdp-client --audio-capture, and ActiveX + AudioCaptureRedirectionMode. + + PCM capture only accepts encode formats that match the Open capture + stream, rejects non-16-bit capture (Data PDU size contract), and gates + the capture backend behind ironrdp-rdpsnd-native/capture. + + Depends on #1648 (playback). + +### Bug Fixes + +- Make source locations opt-in ([#1480](https://github.com/Devolutions/IronRDP/issues/1480)) ([f84cd01450](https://github.com/Devolutions/IronRDP/commit/f84cd01450e18d12838b225859878b311802b805)) + + Default error display omits locations; alternate formatting and reports + with explicit location opt-in preserve diagnostic context. + +- Stop blocking the real-time playback callback ([#1803](https://github.com/Devolutions/IronRDP/issues/1803)) ([850f654673](https://github.com/Devolutions/IronRDP/commit/850f6546730cff1a6b858864f6fbf9cb7d6264fd)) + + + ## [[0.7.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-native-v0.6.0...ironrdp-rdpsnd-native-v0.7.0)] - 2026-07-10 ### Bug Fixes diff --git a/crates/ironrdp-rdpsnd-native/Cargo.toml b/crates/ironrdp-rdpsnd-native/Cargo.toml index 77a7b9696f..619b3368d5 100644 --- a/crates/ironrdp-rdpsnd-native/Cargo.toml +++ b/crates/ironrdp-rdpsnd-native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-rdpsnd-native" -version = "0.7.0" +version = "0.7.1" description = "Native RDPSND playback and optional MS-RDPEAI capture backends for IronRDP" edition.workspace = true rust-version = "1.94" @@ -26,7 +26,7 @@ bytemuck = { version = "1.24", optional = true } cpal = "0.17" ironrdp-error = { path = "../ironrdp-error", version = "0.2", features = ["std"] } # public ironrdp-rdpeai = { path = "../ironrdp-rdpeai", version = "0.1", optional = true } # public -ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.9" } # public +ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.10" } # public opus2 = { version = "0.4", optional = true, features = ["bundled"] } ringbuf = "0.5" tracing = { version = "0.1", features = ["log"] } diff --git a/crates/ironrdp-rdpsnd/CHANGELOG.md b/crates/ironrdp-rdpsnd/CHANGELOG.md index 0c3cb06b0d..4d7973760a 100644 --- a/crates/ironrdp-rdpsnd/CHANGELOG.md +++ b/crates/ironrdp-rdpsnd/CHANGELOG.md @@ -6,6 +6,218 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-v0.9.0...ironrdp-rdpsnd-v0.10.0)] - 2026-09-30 + +### Security + +- Support advanced session settings ([#1781](https://github.com/Devolutions/IronRDP/issues/1781)) ([14ef4fd49f](https://github.com/Devolutions/IronRDP/commit/14ef4fd49fcf950169806866ee67db0c49662cfc)) + + Wire administrative-session GCC data, opaque load-balance routing + tokens, and RDPSND quality selection through the ActiveX settings + objects and client configuration. + + Keep unrelated transport, cache, video, device, and security policy + slots as explicit E_NOTIMPL failures. + + --------- + +### Features + +- Add AUDIO_INPUT protocol crate ([#1645](https://github.com/Devolutions/IronRDP/issues/1645)) ([50fa88b29e](https://github.com/Devolutions/IronRDP/commit/50fa88b29e5d57c5c6353229bda8aa786a9906fd)) + + Introduce `ironrdp-rdpeai` for MS-RDPEAI (AUDIO_INPUT) PDUs and client + handler, plus the shared RDPSND format matching helper used during + negotiation. + + The crate is workspace-internal (`publish = false`) with unit coverage + in `ironrdp-testsuite-core`. Capture backends and ActiveX wiring land in + a follow-up stacked PR. + +- Harden Windows client playback path ([#1648](https://github.com/Devolutions/IronRDP/issues/1648)) ([2d9a9bf114](https://github.com/Devolutions/IronRDP/commit/2d9a9bf114dcf41a1ddc7343f564bc2e8d1d06db)) + + Keep client format order for wFormatNo, play pre-v8 Wave PDUs, and apply + volume on a broader CPAL PCM offer so ActiveX mode 0 can redirect remote + audio reliably. + + Also fix clippy noise in the RDPSND client suite and keep interleaved + volume L/R phase stable across wave blocks. Volume scaling is a simple + amplitude map, not a logarithmic MS-RDPEA model. + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + +- Carry the capture timestamp on waves and surface wave confirms ([#1720](https://github.com/Devolutions/IronRDP/issues/1720)) ([160752fcf3](https://github.com/Devolutions/IronRDP/commit/160752fcf3293889f1ce1bdd3d2e7afc790a9cd2)) + + MS-RDPEA 2.2.3.8 has the client answer a wave's wTimeStamp in the Wave + Confirm + PDU, set to "the same field of the originating WaveInfo PDU [...] plus + the + time, in milliseconds, between receiving the complete wave PDU from the + network + and sending this PDU". The server hardcoded that field to zero for both + Wave + and Wave2, so the client's answer was relative to nothing and the + confirm was + only logged, never delivered - an embedder chasing an audio/video offset + had no + way to tell "the delay is on my side of the socket" from "the client is + buffering". + + Waves now carry the low bits of the same capture time the 32-bit + dwAudioTimeStamp gets, and RdpsndServerHandler gains a defaulted + wave_confirm + method that receives the block number and that timestamp. Existing + handlers are + unaffected. + + The value is not an echo, and one block can be confirmed more than once. + FreeRDP's client sends two confirms per wave: the first on receipt with + the + timestamp unchanged, "to determine the network latency", and a second + after + playback with the elapsed time added, "to determine the actual render + latency" + (channels/rdpsnd/client/rdpsnd_main.c, both comments citing 2.2.3.8). A + handler + therefore sees the two measurements as two calls with the same block_no, + which + the rustdoc now says. + + The trait method has no consumer inside this repository. Its consumer is + hypr-rdp, which times its own PipeWire captures and needs the confirm to + separate capture-to-wire delay from client-side buffering; without a + hook there + is no way to reach the PDU at all, since the server discards it after a + debug + log. Setting the field and surfacing the confirm are separable, but only + in the + sense that either alone leaves the measurement impossible: a timestamp + nobody + receives, or a confirm relative to zero. + +- Log the server side of the channel ([#1991](https://github.com/Devolutions/IronRDP/issues/1991)) ([966a842f7e](https://github.com/Devolutions/IronRDP/commit/966a842f7e4ca6ff045a860a9fec68eb9206615b)) + + The RDPSND server logged each incoming PDU as a bare Debug dump and + nothing it sent. When a session ended up with no audio, the log did not + show which formats each side offered, which one was chosen, or whether + the client had set TSSNDCAPS_ALIVE. + + Negotiation is now logged at debug: the server's offered formats and the + client's reply (each entry with its position, which for the client list + is the wFormatNo the waves go out under), the client version, flags and + volume, the quality mode, training, the chosen format, and which case + applied when none is chosen. A client that omits TSSNDCAPS_ALIVE gets a + warning, since MS-RDPEA 2.2.2.2 requires it for any audio to be + transferred. + + Per-wave traffic is logged at trace. MS-RDPEA 2.2.3.8 describes one Wave + Confirm per wave, but Windows clients and xfreerdp3 both send two: One + on receipt that echoes the wave's wTimeStamp, and a later one for the + same block with a later timestamp. The server keeps the wTimeStamp of + each block it sent and tracks both confirms, so the second is logged as + such with how long the client held the data (about 1.7 s once playback + settles, with a Windows client), and only a confirm beyond those two, or + one for a block the server never sent, counts as unmatched. A confirm + whose timestamp comes before the send of the wave now under its block + number belongs to an earlier wave whose number has been reused, and is + counted as stale instead. With an eight-bit block number this is exact + while fewer than 256 waves are sent within the client's confirm delay. A + block number reused after a single confirm is not flagged, since a + client may confirm once as the specification describes. Volume and close + are logged at debug, and a summary of waves, bytes and confirms is + logged when the stream ends. The unexpected-PDU errors now name the PDU + and the state instead of "Invalid PDU". + + Levels follow the STYLE.md guidance: nothing new at info, one-off + negotiation events at debug, anything per wave at trace. The format + lists are rendered through Display wrappers, so they cost nothing unless + the level is enabled. + + Tests check that first, second, extra and unmatched confirms all still + reach the handler after the bookkeeping, that confirms are classified + correctly when a block number is reused and across the u16 timestamp + wrap, and that the stream counters come out exact, including + never-confirmed waves and stale confirms after the block number wraps. + cargo xtask check fmt/lints/tests/typos/locks all pass. + +### Bug Fixes + +- Isolate malformed encrypted waves ([#1514](https://github.com/Devolutions/IronRDP/issues/1514)) ([c87ab68e9c](https://github.com/Devolutions/IronRDP/commit/c87ab68e9c6adbf524cb0b2783ff4bd61178fb9b)) + + ## Summary + + - Treat malformed RDPSND server-audio PDUs as recoverable channel input + and ignore them without failing the desktop session. + - Preserve the RDPSND state after a decode failure so valid subsequent + audio continues normally. + - Add a regression test for an encrypted wave missing its required v5 + signature. + + ## Testing + + - `cargo test -p ironrdp-testsuite-core --test integration_tests_core -- + rdpsnd::client` + - `cargo fmt --all -- --check` + + --------- + +- Drop RDPSND waves that arrive before the channel negotiates ([#1990](https://github.com/Devolutions/IronRDP/issues/1990)) ([2e872b085b](https://github.com/Devolutions/IronRDP/commit/2e872b085b504cf3b9ce9994237270f71a35d13a)) + + The server event channel outlives a single connection. When a client + with audio disconnects, waves its sound handler already queued stay in + the channel and become the first events the next connection dispatches, + before that connection's RdpsndServer has received the client's formats. + wave() then fails in version() with "client format not yet received", + and the map_err_kind at the dispatch site ends the new client loop, so a + disconnect of an audio-enabled client can turn into a failed connect for + whoever comes next. The per-batch WAVE_KEEP trim only removes waves + beyond the newest four, so it does not cover this. + + This adds RdpsndServer::is_ready() and drops waves and volume changes + with a debug log while the channel is not ready. A volume change fails + the same way before negotiation, since set_volume() needs the client's + format flags. A wave has nowhere to go before negotiation, and after a + failed negotiation (no common format, or the handler's start failing) it + never will. + + A test drives the handshake and checks is_ready() before negotiation, + after a successful one, with no common format, and when start fails. It + also checks that wave() and set_volume() fail before negotiation, and + that wave() goes through once is_ready() holds. + + + ## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpsnd-v0.8.1...ironrdp-rdpsnd-v0.9.0)] - 2026-07-10 ### Features diff --git a/crates/ironrdp-rdpsnd/Cargo.toml b/crates/ironrdp-rdpsnd/Cargo.toml index 11e2e29445..72e0de7614 100644 --- a/crates/ironrdp-rdpsnd/Cargo.toml +++ b/crates/ironrdp-rdpsnd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-rdpsnd" -version = "0.9.0" +version = "0.10.0" readme = "README.md" description = "RDPSND static channel for audio output implemented as described in MS-RDPEA" edition.workspace = true @@ -28,9 +28,9 @@ __test = ["dep:visibility"] [dependencies] bitflags = "2.11" tracing = { version = "0.1", features = ["log"] } -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["alloc"] } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["alloc"] } # public visibility = { version = "0.1", optional = true } [lints] diff --git a/crates/ironrdp-rpc/Cargo.toml b/crates/ironrdp-rpc/Cargo.toml index 58851ec2db..a825fc27ab 100644 --- a/crates/ironrdp-rpc/Cargo.toml +++ b/crates/ironrdp-rpc/Cargo.toml @@ -18,9 +18,9 @@ categories.workspace = true __test = [] [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public ironrdp-input = { path = "../ironrdp-input", version = "0.7" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public tokio = { version = "1", features = ["net", "io-util"] } anyhow = "1" diff --git a/crates/ironrdp-server/CHANGELOG.md b/crates/ironrdp-server/CHANGELOG.md index 6bc9421f45..61e54cb322 100644 --- a/crates/ironrdp-server/CHANGELOG.md +++ b/crates/ironrdp-server/CHANGELOG.md @@ -6,6 +6,1940 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.14.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.13.0...ironrdp-server-v0.14.0)] - 2026-09-30 + +### Security + +- Send the Server Auto-Reconnect Cookie during logon ([#1405](https://github.com/Devolutions/IronRDP/issues/1405)) ([7d35d65248](https://github.com/Devolutions/IronRDP/commit/7d35d6524886ab4e9f610b4b70566eaa21ea8177)) + + ## What + + Adds an optional **Server Auto-Reconnect Cookie** (MS-RDPBCGR 2.2.4.3 + `ARC_SC_PRIVATE_PACKET`) to `ironrdp-server`. When set, `RdpServer` + sends a Save Session Info PDU (`LogonExtended` + + `AUTO_RECONNECT_COOKIE`) carrying it once per connection, right after + activation (Confirm Active processed, encoder built), on the IO channel. + + ## Why + + A client only enters its **automatic reconnection sequence** (MS-RDPBCGR + 1.3.1.5) on an *ungraceful* disconnect if it was handed this cookie + during logon. Without it, a dropped connection just reports as + disconnected — **mstsc in particular won't auto-reconnect at all**. + Today `ironrdp-server` never sends the cookie, so there's no way for a + server to opt into that behavior. + + The concrete use case: a server that *intentionally* drops a connection + and expects the client to come straight back — e.g. a recovery path that + cycles the session — currently forces the user to reconnect by hand. + With the cookie provisioned, the client re-establishes on its own + (re-authenticating via NLA/CredSSP from cached credentials). It's also + just standard behavior a real RDP server provides. + + ## API + + Mirrors the existing `credential_validator` pattern exactly — a builder + method plus a runtime setter: + + ```rust + // build time + RdpServer::builder() + .with_auto_reconnect_cookie(Some(ServerAutoReconnect { logon_id, random_bits })) + // ... + + // or dynamically + server.set_auto_reconnect_cookie(Some(cookie)); + ``` + + `ServerAutoReconnect` (already public in + `ironrdp-pdu::rdp::session_info`) carries a `logon_id` and a 16-byte + `random_bits` (generate from a CSPRNG). A per-connection guard + (`auto_reconnect_sent`, reset in `run_connection_with`) sends it exactly + once — not again on a Deactivation-Reactivation. + + ## Scope / additive + + - **Additive, non-breaking.** Default is `None` (send nothing); existing + servers are byte-for-byte unaffected. + - All PDU types already exist in `ironrdp-pdu` + (`rdp::session_info::{SaveSessionInfoPdu, LogonInfoExtended, + LogonExFlags, ServerAutoReconnect, InfoType, InfoData}`) — this is + purely wiring the server-side send. + - Reuses the existing `encode_share_data_pdu` helper. + + ## Design point for review — the returning cookie + + This PR implements the **send** side only: it *enables* the client's + automatic reconnection. It does **not** validate the + `ARC_CS_PRIVATE_PACKET` the client sends back on reconnect (MS-RDPBCGR + 2.2.4.4). For a server that re-authenticates every connection by other + means (NLA/CredSSP) that's sufficient and safe, and it's documented as + such on the setter. If you'd prefer the crate to also offer *validation* + of the returning cookie (so it can be an authentication factor — the + server would store issued `(logon_id, random_bits)` and verify the + client's `SecurityData`/`ARC_CS` on the next connect), I'm happy to do + that as a follow-up, or fold it in here — it's a larger, stateful + feature so I kept this PR to the send path. Let me know which you'd + prefer. + + Built + `clippy --features egfx -D warnings` clean; tests compile. + + --------- + +- Validate auto-reconnect cookies ([#1509](https://github.com/Devolutions/IronRDP/issues/1509)) ([44f675e244](https://github.com/Devolutions/IronRDP/commit/44f675e244ee76b5311756668ffbbe28e98c7175)) + + ## Summary + - parse and carry `ARC_CS_PRIVATE_PACKET` data through the acceptor + - validate returning Enhanced RDP Security cookies with HMAC-MD5 before + reconnecting + - rotate reconnect randoms per connection and hourly, with runtime + cookie updates + - restrict cookie authentication to TLS/Hybrid and document the behavior + + ## Testing + - `cargo test -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server` + - `cargo clippy -p ironrdp-pdu -p ironrdp-acceptor -p ironrdp-server + --all-targets -- -D warnings` + +- [**breaking**] Support session resume via the auto-reconnect cookie ([#1501](https://github.com/Devolutions/IronRDP/issues/1501)) ([74b3365c1f](https://github.com/Devolutions/IronRDP/commit/74b3365c1f98c0da6feed7507779c67e1b8e6d08)) + + > **Rebased onto post-#1522 master.** #1509 landed the server half of + #1508 while this was open, including the `ClientAutoReconnect` + structure. This PR no longer declares it; it extends it, and picks up + the parts #1509 did not build. + + ## What + + The client half of automatic reconnection. The session layer surfaces + the Server Auto-Reconnect Cookie, `ironrdp-pdu` derives and verifies the + client's response to it, and the connector sends that response when + resuming a session. + + ## Why + + A client whose connection drops ungracefully can reattach to its session + instead of making the user log on again, provided it returns the cookie + the server issued during logon ([MS-RDPBCGR] 1.3.1.5). + + #1509 built the server side of that: it validates a returning + `ARC_CS_PRIVATE_PACKET` and rotates the random. Nothing answers it. + `ironrdp-session` decodes the cookie and drops it, `ironrdp-connector` + has no way to send one back, and `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` still sits in + `ironrdp-client`. So `ironrdp-client` cannot resume a session against + `ironrdp-server`, and the validation #1509 added has no in-tree + counterpart to exercise it. + + The wire encoding was already there. `ExtendedClientOptionalInfo` + carries, encodes and decodes a 28-byte `autoReconnectCookie` and its + builder already had a `reconnect_cookie` step; `ServerAutoReconnect` + already decoded; #1509 added `ClientAutoReconnect` and its decode. + Nothing connected them. + + ## The three parts + + **Receive.** `SaveSessionInfo` now also surfaces the cookie, as + `ProcessorOutput::AutoReconnectCookie` and + `ActiveStageOutput::AutoReconnectCookie`. #1522 added a `SaveSessionInfo + { logon_complete }` output on that same handler; the two coexist rather + than compete, since both are read off one PDU and neither supersedes the + other. The handler emits the logon notification unconditionally and + appends the cookie when one is present, and a test pins that surfacing + the cookie does not suppress the notification. #1509's server replaces + the cookie whenever a client connects and again hourly ([MS-RDPBCGR] + 3.3.6.2), so this can arrive more than once in a session and the + consumer keeps the most recent. + + **Derive.** `ClientAutoReconnect::from_server_cookie` implements + [MS-RDPBCGR] 5.5: + + > The auto-reconnect random is used to key the HMAC function + ([RFC2104]), which uses MD5 as the iterative hash function. The security + verifier is derived by applying the HMAC to the client random received + in Step 3. + > + > `SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom)` + > + > When Enhanced RDP Security is in effect the client random value is not + generated (section 5.3.2). In this case, for the purpose of generating + the security verifier, the client random is assumed to be an array of 32 + zero bytes. + + IronRDP implements no Standard RDP Security path (there is no Security + Exchange PDU), so the zero-client-random case is the only one that + arises. As 5.5 notes, that makes the verifier constant for a given + cookie, so it proves possession of the cookie and nothing more; session + security comes from the outer TLS/CredSSP handshake. + + @clintcan independently confirmed this construction against real + **mstsc** while validating #1509 + ([comment](https://github.com/Devolutions/IronRDP/pull/1509#issuecomment-5151200681)): + a Windows client's `ARC_CS_PRIVATE_PACKET` verifies against + `HMAC-MD5(random_bits, [0u8; 32])`. That is the same derivation + implemented here, so the two halves interoperate with Microsoft's client + and not only with each other. + + **Send.** `ClientConnector::with_auto_reconnect_cookie` takes the cookie + last received and makes the connector put the derived Client + Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. + Absent, that PDU is byte-for-byte what it was. + + Unlike the server packet, this structure has no enclosing logon-info + field header, so it encodes to exactly the 28 bytes the cookie field + expects. `to_bytes` writes that layout directly rather than going + through `Encode`, so filling a fixed-size field has no error path a + caller must handle; a test pins the two to agree. + + ## One derivation, not two + + Putting `from_server_cookie` in `ironrdp-pdu` would leave the workspace + with two implementations of 5.5, since #1509 added a private HMAC to + `ironrdp-server`. So `ClientAutoReconnect` also gains `verify`, and the + server routes through it. + + `verify` keeps the constant-time comparison the server had. The verifier + is the whole credential, so a comparison returning early on the first + differing byte would let a peer recover it a byte at a time from the + timing; the session identifier is not secret and is compared normally. + `ironrdp-server` keeps the policy around the check, which cookies are + live and whether the security protocol permits auto-reconnect, and drops + its `hmac` and `md-5` dependencies. `hmac` moves to `ironrdp-pdu` as + `default-features = false`; the crate's full feature powerset still + checks clean, including `--no-default-features`. + + I would rather not have reached into `ironrdp-server` in a + `pdu,session,connector` change, but the alternative was shipping the + duplicate and filing a follow-up to remove it, which is a worse trade + for reviewer time. + + ## Tests that were not running + + That move also rehomes the known-answer tests @clintcan contributed on + #1509. They went in as an inline `#[cfg(test)]` module in + `crates/ironrdp-server/src/server.rs`, and that crate sets `[lib] test = + false`, so they have never executed in CI. They now live in + `ironrdp-testsuite-core` against the public API, where CI runs them: his + HMAC-MD5 reference vector is kept as a second vector alongside a + differently-keyed one, plus the cases for a tampered verifier and a + mismatched logon ID. + + Worth flagging separately: `ironrdp-server` is not alone. + `ironrdp-agent`, `ironrdp-session` and `ironrdp-web` also set `[lib] + test = false` and between them carry 16 files of inline `#[cfg(test)]` + modules that CI never runs. That is out of scope here, but I am happy to + open an issue if it would be useful. + + ## Breaking changes + + `ActiveStageOutput` and `x224::ProcessorOutput` gain a variant, and + `ClientConnector` gains a public field, so exhaustive matches and struct + literals need updating. + + Confirmed with `cargo-semver-checks` against the merge-base: those three + are the only findings this branch introduces. The others it reports on + `master` today (`ShareDataPdu::Compressed` and the `ShareDataCtx` fields + from #1518, `ProcessorBuilder.bulk_decompressor` from #1518, + `ServerEvent::SetAutoReconnectCookie` from #1509) are present on + `master` unchanged. The `ironrdp-pdu` additions are additive. + + ## Scope + + This is the library half. `ironrdp-client`, `ironrdp-web` and the FFI + bindings gain an arm for the new output but none of them reconnect + automatically yet; that is the remaining part of #271, and the existing + `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` in `ironrdp-client` marks where it goes. + + I kept receive, derive and send together deliberately. Split up, none of + them is usable on its own: without the receive half there is no way to + obtain a cookie, and without the send half there is nothing to do with + one. + + ## Tests + + Thirteen, all in `ironrdp-testsuite-core`. + + On the packet and the derivation: the `SecurityVerifier` matches two + independently computed HMAC-MD5 vectors of 32 zero bytes under different + keys, so the tests pin the derivation rather than restating the code; + the logon ID carries over from the server cookie; the encoding matches + the 2.2.4.3 field layout byte for byte with `cbLen` fixed at `0x1C`; + `to_bytes` agrees with `Encode`; it round-trips; and it rejects both a + wrong packet length and an unknown version. + + On verification: a derived answer is accepted, a single flipped byte in + the verifier is rejected, a correct verifier under a different logon ID + is rejected, and an answer derived from a different random is rejected. + + On the surfacing path: a Save Session Info PDU framed the way a server + sends it, through the real x224 processor, yields an + `AutoReconnectCookie` carrying the right logon ID and random bits, + alongside #1522's logon notification rather than in place of it; and one + without a cookie surfaces no cookie. + + ## Verification + + `cargo xtask check fmt/lints/tests/typos/locks` all pass on 1.94.1, + including a `fuzz/` build before the lock check. + + ## Note + + #1496 also touches the `ClientAutoReconnect` declaration. Whichever of + the two lands second needs a one-line rebase on the derive attribute; + happy to take that in either order. + +- Send periodic Heartbeat PDUs on the message channel ([#1842](https://github.com/Devolutions/IronRDP/issues/1842)) ([8f57691a9a](https://github.com/Devolutions/IronRDP/commit/8f57691a9a6e497388dc2824bffe93eeed7bb698)) + + The server never sends Server Heartbeat PDUs (MS-RDPBCGR 2.2.16.1), even + though the client half of the feature is in place: HeartbeatPdu + encode/decode landed with #1814 and clients decode and ignore them. This + adds the send half, opt-in. + +- Recognize the Initiate Multitransport Response on the message channel ([#1964](https://github.com/Devolutions/IronRDP/issues/1964)) ([6aebe6d0b1](https://github.com/Devolutions/IronRDP/commit/6aebe6d0b11c4aa3c99252bd986e138e43756a6d)) + + ## Summary + + handle_message_channel_data unconditionally decoded every PDU received + on the MCS message channel as an AutoDetectRspPdu, per a comment + claiming the channel "currently carries only the auto-detect response." + That is no longer accurate: once a server sends an Initiate + Multitransport Request, the client answers on this same channel with an + Initiate Multitransport Response (MS-RDPBCGR 2.2.15.2) to report whether + it could establish the sideband transport. + + MultitransportResponsePdu already has full Encode/Decode support in + ironrdp-pdu (with success()/abort() constructors), but has zero + consumers anywhere in ironrdp-server. In practice every such response + from a real client failed to decode as AutoDetectRspPdu (its + securityHeader carries SEC_TRANSPORT_RSP, not SEC_AUTODETECT_RSP) and + was dropped with an "Unhandled MCS message channel PDU" warning: + legitimate protocol traffic logged as an error. + + Reproduced against a real Windows client (mstsc) that offered UDP + multitransport but could not establish it: the raw bytes it sent decode + exactly as MultitransportResponsePdu { security_header: TRANSPORT_RSP, + request_id, hr_response: E_ABORT }. + + ## Changes + + - Added `ironrdp_pdu::rdp::message_channel::ClientMessageChannelPdu`, an + enum with Encode/Decode covering the two PDUs a client sends on this + channel. Decode peeks at the Basic Security Header flags and dispatches: + SEC_TRANSPORT_RSP to MultitransportResponsePdu, anything else to + AutoDetectRspPdu, so a malformed PDU is reported against the type its + header names. + - handle_message_channel_data decodes that type and has a Multitransport + arm that logs the response at debug level (ordinary protocol traffic, + not an error), with the same wording the acceptor uses for this PDU, + instead of falling through to the warn!. + - Six tests in ironrdp-testsuite-core (tests/pdu/message_channel.rs): + recognition of both PDUs, a round trip, a truncated transport response + reported as one, a payload without SEC_TRANSPORT_RSP handed to the + auto-detect decoder, and too-short input. + + ## Test plan + + cargo xtask check fmt/lints/tests/typos/locks all pass. + +- Wire UDP multitransport into ironrdp-server ([#1954](https://github.com/Devolutions/IronRDP/issues/1954)) ([73dfa30e38](https://github.com/Devolutions/IronRDP/commit/73dfa30e3831e51cf8aa58f93e27b1b00102a7ec)) + + - Add `RdpServerBuilder::with_udp_transport(udp_bind_addr)`: opt-in, + `None` by default, no behavior change unless called. + - When set (and the security mode is `Tls` or `Hybrid`, matching the + reference client's Enhanced-Security-only gate), the acceptor offers UDP + multitransport, and `accept_finalize` uses + `accept_finalize_with_multitransport` with a callback that binds a fresh + UDP socket per connection, reuses the connection's own TLS certificate + (`TlsAcceptor::config()`) for the sideband transport, and calls + `accept_udp()`. + - Once established, the transport is used to migrate EGFX graphics + traffic off TCP: `request_reliable_udp` is called opportunistically the + first time EGFX has data to send (its dynamic channel id is only known + once the client opens it). From the request on, every server message on + that channel goes over the tunnel, starting with the batch that + triggered it: the request carries SOFT_SYNC_TCP_FLUSHED and the server + MUST keep using the named tunnel immediately after sending it + (MS-RDPEDYC 2.2.5.1, 3.3.5.3.1). DRDYNVC replies for a tunneled channel + go over the tunnel too, whichever path produced them. A new + `client_loop` select arm feeds incoming tunnel payloads into + `DrdynvcServer::process_tunnel()`; payloads that arrive before the + client's Soft-Sync Response are held and processed once it does + (3.3.5.3.2), rather than dropped. + - The UDP accept runs as an ordinary `tokio::spawn` task, so enabling + UDP adds no runtime requirement for the caller. + - A failure before EGFX has moved (bind, handshake, TLS, or the tunnel + closing) leaves the session on TCP, matching the reference client's + posture. Once EGFX is on the tunnel, the tunnel closing ends the + connection: Soft-Sync cannot move a channel back to TCP (MS-RDPEDYC + 2.2.5.1), and the tunnel lasts as long as the connection (MS-RDPEMT + 1.3.3). + - A client that answers the Initiate Multitransport Request with E_ABORT + (MS-RDPBCGR 2.2.15.2) has given up on the sideband transport, so the + pending UDP accept is stopped as soon as that response arrives, whether + during finalization or later on the message channel, instead of holding + its socket until the 15 s accept timeout. Windows clients send it about + 2.7 s after connecting. + - When the UDP bind address has an unspecified IP, each connection's + socket binds to the local address that client reached over TCP instead. + A socket bound to the unspecified address replies from whichever address + the routing table picks, and on a host with several IPv6 addresses that + is not always the one the client sent to: mstsc dropped the replies and + gave up with E_ABORT. `run()` records the address itself; embedders + driving `run_connection_with` pass it with the new + `RdpServer::set_connection_local_addr`. + - A successful Initiate Multitransport Response that arrives after + finalization now enables EGFX migration for the rest of the session, + provided Soft-Sync was negotiated. mstsc finishes its UDP bootstrap + after the TCP finalization (0.87 s later in my test), so migration was + previously decided before its response existed and the session never + left TCP even with the sideband transport up. + - The EGFX channel is found whichever way it was registered: an embedder + that takes a frame handle from its `GfxServerFactory` registers it as + `GfxDvcBridge`, which the migration lookup did not recognise, so it + never sent the Soft-Sync Request. The Soft-Sync Request, the client's + response (with the tunnels and channels it accepted) and the switch of + EGFX onto UDP are now logged at debug level. + +### Features + +- [**breaking**] Clamp honored client desktop size to an operator maximum ([#1404](https://github.com/Devolutions/IronRDP/issues/1404)) ([d3747a05b2](https://github.com/Devolutions/IronRDP/commit/d3747a05b202ba2d87ac19698354ae7e487850a2)) + + Follow-up to #1373 (the resource-hardening angle you flagged in review — + thanks for the go-ahead 🙂). + + ## Problem + + `#1373` gated honor-client-desktop-size behind a bare `bool`. With it + on, the acceptor adopts the client-requested desktop size bounded only + by the protocol range `[200, 8192]`. But the desktop size is a + client-controlled `u16`, and the server still builds its + framebuffer/encoder from the negotiated size — so a client could request + e.g. `8192x8192` and drive the server's allocation off an untrusted + number (~256 MiB per frame buffer). Mild, and only on an opt-in + default-off path, but it's a resource-exhaustion vector driven purely by + a number the client picks. + + Your review comment: *"[200, 8192] is a protocol ceiling, not a resource + guard … tracked the 'clamp/range policy rather than a bare bool' idea as + a future follow-up (an operator-set max size)."* This is that PR. + + ## Change + + Replace the `bool` with `Option` carrying an **operator-set + maximum**: + + - `None` (default) — disabled; always enforce the server-provided size + (unchanged behavior). + - `Some(max)` — honor the client's request, **clamped per dimension to + `max`**. The client can ask for a smaller desktop, never a larger one. + + The acceptor clamps the requested `width`/`height` to `max` *before* the + existing `validate_desktop_size` protocol-range check, so the negotiated + size can never exceed what the operator is willing to render — set `max` + to the host display's native resolution (or whatever ceiling the server + can afford). + +- Support runtime-defined static virtual channels ([#1517](https://github.com/Devolutions/IronRDP/issues/1517)) ([8b4c483ba0](https://github.com/Devolutions/IronRDP/commit/8b4c483ba0c900a8de0b2718347754f56dd363ba)) + + ## Summary + - add keyed runtime-defined static-channel registration, lookup, and + negotiated ID attachment + - enforce the static-channel limit and reject malformed SVC fragment + sequences + - wire generic connector, acceptor, and session name-based dispatch + support + + ## Testing + - `cargo test -p ironrdp-testsuite-core --test integration_tests_core + svc::` + - `cargo clippy -p ironrdp-testsuite-core --test integration_tests_core + -- -D warnings` + + --------- + +- Add static-channel factories ([#1633](https://github.com/Devolutions/IronRDP/issues/1633)) ([e48b29c017](https://github.com/Devolutions/IronRDP/commit/e48b29c0173096c1718c9f657bed3a59926aa97c)) + + Create fresh static-channel processors before GCC negotiation, expose + the acceptor type needed to configure them, and exercise RDPDR + initialization and drive I/O end to end. + + --------- + +- Measure and report network characteristics ([#1470](https://github.com/Devolutions/IronRDP/issues/1470)) ([224e8db7ce](https://github.com/Devolutions/IronRDP/commit/224e8db7cec2dad39032aac097f550a6600e742c)) + + ## Summary + + - The server measures round-trip time and bandwidth from the continuous + auto-detect exchange and reports both to the client in a Network + Characteristics Result on the MCS message channel ([MS-RDPBCGR] + 2.2.14.1.5). + - Nothing is sent until both are known. A result carrying RTT alone + reports part + of the picture as though it were the whole, which is what krdp and grd + avoid by + returning early when they have no bandwidth figure. + - The result is paced at one per second on its own clock rather than + once per + probe, and is withheld unless a client response has arrived since the + last one. + A client that stops answering stops producing results instead of leaving + the + last window values advertised indefinitely. + - `baseRTT` is the lowest RTT seen over the session, per 2.2.14.1.5's + "lowest + detected round-trip time". `averageRTT` is the window average, so the + difference between them is queueing delay. + + ## Validation + + - `cargo xtask check fmt/lints/tests/typos/locks` and `cargo xtask wasm + check` + all pass. + - `cargo semver-checks -p ironrdp-server --baseline-rev master`: no + update + required. + - Tests live in `ironrdp-testsuite-core`. `ironrdp-server` sets + `[lib] test = false`, so an inline module would compile and never run. + Each new + test was checked by planting the corresponding regression and confirming + it + fails. + + ## Notes + + - Supersedes #1471, which added bandwidth as a follow-up. With the gate + above, + this PR alone could only ever emit the form it now declines to send, so + the two + are one change. + - #1487 has merged, so the earlier dependency note no longer applies. + +- Make the RemoteFX quantization table configurable ([#1685](https://github.com/Devolutions/IronRDP/issues/1685)) ([925e7c0f7c](https://github.com/Devolutions/IronRDP/commit/925e7c0f7cb7ae937a92ec93d4cb758289594cc0)) + + Add Quant::try_new(), a validating constructor that rejects any subband + value outside the 6..=15 range, and + RdpServerBuilder::with_remotefx_quant(quant: Quant), wiring it through + RdpServerOptions to the same capability-negotiation call site #1684 + touched. + + Per [MS-RDPRFX] 2.2.2.1.5, each of the 10 TS_RFX_CODEC_QUANT values is a + 4-bit field, and the legal range is 6 to 15. #1557 reports the quant + table is hardcoded to Quant::default(); this gives callers a validated + way to set it instead. + + Builds on #1684, which added the storage this PR wires up. Default + behavior is unchanged: with_remotefx_quant is opt-in, and a server that + doesn't call it still gets Quant::default(), the same values Windows RDP + servers send. + +- [**breaking**] Expose per-connection keyboard metadata via ConnectionHandler ([#1691](https://github.com/Devolutions/IronRDP/issues/1691)) ([393869b30b](https://github.com/Devolutions/IronRDP/commit/393869b30b1078da7204c6bf20e8a5472e419070)) + + ## Summary + + AcceptorResult already carried keyboard_layout (the client's GCC Client + Core Data keyboardLayout, MS-RDPBCGR 2.2.1.3.2), but ironrdp-server's + client_accepted never read it, and the only extension point that could + plausibly expose it, ConnectionHandler::on_accept/on_disconnected, only + fires from RdpServer::run's own accept loop. An embedder with its own + accept loop calling run_connection or run_connection_with directly never + sees these hooks at all. + + Added keyboard_type and ime_file_name to Acceptor and AcceptorResult, + captured from the same Client Core Data alongside keyboard_layout. + + Added a new ConnectionInfo struct and a default-no-op + ConnectionHandler::on_connection_info(&ConnectionInfo) method, fired + from client_accepted itself, right after credential and auto-reconnect + validation succeed. This is reachable from every code path that + completes connection setup, not only run's accept loop, so it is usable + by embedders that never call run. + + Kept the hook synchronous. It only hands the embedder a small Clone-able + struct; an embedder that needs to do blocking work in response can spawn + its own task, the same way the existing on_accept/on_disconnected hooks + already work. + + Open question: AcceptorResult is a public struct without non_exhaustive, + so the two new fields are a real breaking change for any consumer + destructuring it exhaustively, same class as the keyboardType change in + #1689. AcceptorResult's attributes are unchanged here since marking it + non_exhaustive is a broader decision than this PR's two fields. + + ## Validation + + cargo xtask check fmt/lints/tests/typos/locks all pass. + + ## Review round and rebase, 2026-08-19 + + #1689 (KeyboardType) merged. This branch was still carrying a stale + pre-merge copy of that commit, so the diff was cumulative against + master. Rebased onto current master, which dropped the redundant + duplicate commit (its content was already upstream) and left this PR's + own single commit. + + Four review findings from the bot review, all addressed: + + - `on_connection_info` fired on every Deactivation-Reactivation resize, + not just the initial connection, since `accept_finalize` loops back into + `client_accepted` with `result.reactivation` set. Gated the call on + `!result.reactivation`, matching the existing gate on the static-channel + start block just below it. + - `get_result()` took `ime_file_name` via `mem::take`, emptying it out + of the acceptor before `new_deactivation_reactivation` copied the same + acceptor's field into the next result, so every reactivation after the + first reported an empty IME name. Changed to a clone, matching how the + Copy-type sibling fields on the same lines already survive. + - No regression test covered the permissive zero/unrecognized-value + keyboardType decode in the Input capability set. Added + `keyboard_type_zero_decodes_to_none` and + `keyboard_type_unrecognized_value_round_trips` (0x51). + - A fourth finding asked for the same coverage on Client Core Data's own + keyboardType field; that test already exists on master, added to #1689 + in response to its own review. The rebase above inherits it directly, so + no new code was needed there. + +- Expose session-lifetime baseline RTT to the embedder ([#1737](https://github.com/Devolutions/IronRDP/issues/1737)) ([ac7800c989](https://github.com/Devolutions/IronRDP/commit/ac7800c989dd5926eef2a1adf983458ad0aec9ee)) + + ## Summary + + - RttSnapshot.min_ms is a sliding-window low that can rise as low + samples + age out of the window, and is explicitly documented as not baseRTT per + MS-RDPBCGR 2.2.14.1.5, which defines baseRTT as the session-lifetime + lowest. AutoDetectManager already tracks that true low internally as + min_rtt_ms for the wire NetworkCharacteristicsResult, but nothing + exposed it as its own value: an embedder reading snapshot() or + autodetect_rtt_handle() alone cannot derive averageRTT - baseRTT as a + queueing-delay signal, since that relationship only holds when the + floor cannot rise. + - Adds AutoDetectManager::baseline_rtt_ms() as the public getter for the + existing field, and mirrors the autodetect_rtt_handle plumbing on + RdpServer: a new autodetect_baseline_rtt: Arc field, + autodetect_baseline_rtt_handle() accessor, and + with_autodetect_baseline_rtt_handle() builder method. + - A matched RTT sample always updates min_rtt_ms in the same + handle_response call that returns it, so the store site reads the new + getter unconditionally right after storing the RTT sample, with no + before/after comparison needed (unlike the bandwidth case in #1734, + where the underlying value can be cleared to None on an unusable + measurement). + - Adds a manager-level test pinning the session-low-not-window-low + property on the new getter directly, plus the usual pair of + handle-plumbing tests (sentinel default, injected-handle round trip). + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass, including the + 3 new tests. + + ## Notes + + No public API break: `RdpServer::new` is crate-private, and the public + surface (`RdpServerBuilder`) only gains an additive optional field and a + new method. + +- [**breaking**] Give mouse button events a position, fix X1/X2 and middle-button/hwheel handling ([#1769](https://github.com/Devolutions/IronRDP/issues/1769)) ([e9c5b1c54a](https://github.com/Devolutions/IronRDP/commit/e9c5b1c54a1153e5d7116d7fadf636bd551020ea)) + + ## Summary + + - Fixes #1466: `MouseEvent`'s button variants were position-less, so + `From` and `From` silently discarded the wire PDU's + x/y whenever a button flag was set (position only survived for a pure + Move). A client that sends a tap as a single button PDU with no + preceding Move (confirmed: Windows App on iOS/iPadOS, touch mode) clicks + at the stale cursor position rather than where it tapped. + - While fixing that, found two more bugs in the same conversion layer, + both confirmed against MS-RDPBCGR: `From` never checked + `MIDDLE_BUTTON_OR_WHEEL` or `HORIZONTAL_WHEEL`, so middle-click and + horizontal wheel silently fell through to the Move fallback. + `From` mapped `PointerXFlags::BUTTON1`/`BUTTON2` to + Left/Right, but per 2.2.8.1.2.2.4 these are "Extended mouse button 1 + (also referred to as button 4)" and "...2 (...button 5)", the X1/X2 side + buttons. `MouseRelPdu`'s own sibling `From` impl already distinguishes + these correctly via `XBUTTON1`/`XBUTTON2`, so this was an inconsistency, + not a deliberate choice. + - Checked FreeRDP, xrdp, KDE krdp, and GNOME Remote Desktop before + settling on a shape. All four check explicit MOVE flags rather than + falling through on else, all four handle middle button and both wheel + flags, none conflate X1/X2 with primary buttons. xrdp's own source has a + maintainer comment confirming real clients exercise the HWHEEL gap: "As + mstsc does MOUSE not MOUSEX for horizontal scrolling, PTRFLAGS_HWHEEL + must be handled here." + - `MouseEvent::Button { x, y, button, pressed }` replaces the ten + position-less `LeftPressed`/`LeftReleased`/.../`Button5Released` + variants, scoped to the PDU types that actually carry absolute position + (`MousePdu`, `MouseXPdu`, `ainput::MousePdu`, which already carried x/y + on every event but never applied it to buttons either). `MouseRelPdu` + gets `MouseEvent::ButtonRel { button, pressed }` instead, since it only + has relative deltas. Both share a new `MouseButton` enum + (`Left`/`Right`/`Middle`/`X1`/`X2`). Added + `MouseEvent::HorizontalScroll` to pair with the existing + `VerticalScroll`. Breaking anyway, so `MouseEvent` and `MouseButton` are + both `#[non_exhaustive]`. + - There's independent, unmerged prior art for the middle-button half of + this on branch `probakowski/xrdp` (commit `e924272`, ad hoc xrdp-interop + debugging, never opened as a PR) that arrived at the same + `MIDDLE_BUTTON_OR_WHEEL` fix. Crediting it here since I found it while + researching this. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + Single file, `crates/ironrdp-server/src/handler.rs`. Grepped the whole + workspace for `MouseEvent::` usage; this file is the only one that + constructs or matches it, no other call sites need updating for this PR. + Downstream, I'm updating my own `lamco-rdp-input`/`lamco-rdp-server` + consumers to the new shape as a follow-up. + +- Wire MS-RDPEI server-side into ironrdp-server ([#1773](https://github.com/Devolutions/IronRDP/issues/1773)) ([aa260fef9e](https://github.com/Devolutions/IronRDP/commit/aa260fef9ebbc0a420b17e796616d710fb41e702)) + + ## Summary + + - MS-RDPEI (multitouch and pen) got server-side plumbing in + `ironrdp-rdpei` (the recent Input DVC PR), but nothing in + `ironrdp-server` ever registers the `RdpeiServer`/`RdpeiHandler` it + added. Grepped the whole crate for `rdpei` before starting: zero + references. This wires it in. + - Follows the exact shape of the existing + `SoundServerFactory`/`with_sound_factory` (the simplest of the three + existing opt-in factories, and RDPEI's needs are equally simple): a new + `RdpeiServerFactory` trait, a `with_rdpei_factory` builder method, and + registration in `attach_channels` alongside the other optional DVC + factories. The factory builds and returns the whole configured + `RdpeiServer`, not just a handler, so it can reach + `with_protocol_version`/`with_supported_features` to advertise + non-default capabilities. + - Re-exports everything a downstream `RdpeiHandler` implementation needs + from the crate root: `RdpeiServerFactory`, `RdpeiHandler`, + `RdpeiServer`, and the touch/pen/ready PDU types plus the nested payload + types their callbacks embed (`CsReadyFlags`, `PenContact` and its + fields/flags, `PenFrame`, `TouchContactFields`/`TouchContactDataFlags`). + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Adds a + regression test in `ironrdp-testsuite-core` (`ironrdp-server` has `[lib] + test = false`, so an inline test would never run in CI) covering that a + registered `RdpeiServerFactory` is invoked during per-connection channel + setup. + + ## Notes + + Two crates: `ironrdp-server` (`Cargo.toml`, `Cargo.lock`, `builder.rs`, + `lib.rs`, `server.rs`, the new `rdpei.rs`) and `ironrdp-testsuite-core` + (`Cargo.toml`, `tests/server/mod.rs`, the new `tests/server/rdpei.rs`). + `RdpServer::new` is `pub(crate)`, only ever called from the builder, so + adding the new parameter there has no external API impact beyond the new + builder method itself. + +- Add rdpeusb server integration ([#1417](https://github.com/Devolutions/IronRDP/issues/1417)) ([4755faec5e](https://github.com/Devolutions/IronRDP/commit/4755faec5efb1649e2cb839f553c8aaae7534aad)) + + Related discussion: #1516. + + *Currently blocked by:* + - #1682 — `ironrdp-usb`, the protocol-independent USB layer + - #1683 — `ironrdp-rdpeusb::usb`, the RDPEUSB adapter, which is also + blocked by #1682 + + > **Reviewing this PR:** because #1682 and #1683 have not merged yet, + the *Files changed* tab shows the cumulative diff. The incremental diff + for this PR alone is here: + > + https://github.com/uchouT/IronRDP/compare/ironrdp-rdpeusb-usb...ironrdp-server + + This PR carries the design rationale for all three, since it is the + first place where the whole stack is visible at once. + + [qemu-rdp](https://gitlab.com/uchouT/qemu-display/-/tree/feat/usbredir) + now redirects USB mass storage over RDP end to end, verified against a + FreeRDP client built from source. These three PRs split the reusable + part out into IronRDP, so that any RDP server can offer USB redirection + instead of reimplementing the channel. + + ## The layering + + #1516 proposed splitting this into three layers. The implementation + followed that split; this section records what each layer ended up + owning. + + ```text + ironrdp-usb USB semantics: descriptors, control requests, transfers. + ↑ Knows nothing about any transport. + ironrdp-rdpeusb RDPEUSB adapter: TS_URB forms, USBD conventions, + ↑ capability negotiation, channel state. + ironrdp-server Server facade: request lifetime, cancellation, routing. + ↑ Speaks ironrdp-usb types outward. + application A usbredir bridge, like qemu-rdp, macrdp, or any other consumer. + ``` + + The rule used to place a concept is: **it may live in a layer only if + its contract can be explained using that layer's vocabulary alone** — + USB, or MS-RDPEUSB, or a generic server facade. A concept that can only + be justified by what one downstream consumer happens to need stays in + that consumer. + + That rule is not free, so two worked examples: + + - `CompletionData` ([#1683](https://github.com/Devolutions/IronRDP/issues/1683)) and `InterfaceAlloc` (this PR) were both + added because the server layer needed them, and both stayed in + `ironrdp-rdpeusb` because both are explainable purely in RDPEUSB terms: + one is the union of the four completion result types the channel + defines, the other allocates the interface IDs RDPEUSB requires per + redirected device. Being needed downstream was neither sufficient on its + own nor disqualifying. + - Going the other way: usbredir packet types and IDs, its + synchronous/asynchronous ordering rules, its inflight scheduling, and + its stream policy all stayed out of these crates, because none of them + can be stated without naming usbredir. + + ## What the facade exposes + + Behind an optional `usb` feature: + + - **`DeviceFactory`** creates a per-device backend when the client opens + a redirection channel; **`UsbRedirDevice`** receives the device + announcement, device text, and channel close. + - **`UsbDeviceHandle`** drives the device in `ironrdp-usb` terms: + `get_descriptor`, `get_configuration`, `select_configuration`, + `get_interface`, `select_interface`, `control_transfer`, + `bulk_transfer`, `interrupt_transfer`, `isochronous_transfer`, + `clear_halt`, `reset_device`, and `current_frame_number`, plus + `query_device_text_request` and `retract_request`. + - **`PendingRequest`**, **`PendingHandle`**, **`CompletionFut`**, and + **`RawPending`** own the request lifetime: a caller can await a + completion, hold a cancellation handle, or drop the request. Dropping a + submitted request emits `CANCEL_REQUEST`, so an abandoned transfer does + not stay in flight on the client. + - **`RdpServerBuilder::with_usb_factory`** opts a server in. The channel + is created only when the client negotiates the capability. + + No `TS_URB`, RDPEUSB processor, or `ServerEvent` wiring appears in any + of those signatures. + + ## What stays in this layer, and why + + Request identifiers, the pending-request map, cancellation, device + closure, and completion routing all live here rather than in + `ironrdp-rdpeusb`. Keeping them out of the protocol crate is what lets + #1683 stay sans-I/O and independently testable, and it means a second + consumer of the facade gets the lifetime handling for free without + inheriting RDPEUSB details. + + ## Also in this PR + + `refactor(rdpeusb): expose InterfaceAlloc at the crate root` — + `InterfaceAlloc` was a private helper of `UrbdrcListener`, so a server + driving the channel had no way to allocate the interface IDs it must + assign to redirected devices. It is now public at the crate root, with + the initial value expressed through `Default`. This is a pure + relocation: the allocation range and the exhaustion behaviour are + unchanged. + + ## TODOs + + Stated up front rather than left to be discovered: + + 1. **The `usb` feature is not linted in CI.**. Locally, `cargo clippy -p + ironrdp-server --features usb --all-targets -- -D warnings` is clean. + Whether to add the feature to the CI matrix is a maintainer call, so I + have not touched CI configuration here. + 2. **RDPEUSB types still reaches the consumer.** + `RdpUsbDeviceAnnounceInfo` carries the raw `DeviceAnnounce`, so a + consumer that wants the device speed reads `DeviceSpeed`, an RDPEUSB PDU + type. The goal is for a downstream bridge to need no `ironrdp-rdpeusb` + dependency at all. I would rather agree on the facade shape than guess + at it, so it is left as a follow-up. + + --------- + +- [**breaking**] Introduce typed ServerError on the public API boundary ([#1242](https://github.com/Devolutions/IronRDP/issues/1242)) ([b5b9558eae](https://github.com/Devolutions/IronRDP/commit/b5b9558eae164a3500ace069b4db12be25eba7bd)) + + ## Summary + + First in a staged migration toward a typed public error story for + `ironrdp-server`, addressing #1209. Introduces: + + ```rust + pub type ServerError = ironrdp_error::Error; + pub type ServerResult = Result; + ``` + + `ServerErrorKind` is a `#[non_exhaustive]` enum with concretely typed + variants (`Encode`, `Io`, `Channel`, `Unsupported`, `Reason`, `Custom`). + Sources are attached through `ironrdp_error::Error::with_source` rather + than embedded as `Box` in variant data. This mirrors the + shape of `ConnectorErrorKind` in `ironrdp-connector`, so the + connection-management layer stays internally consistent. + + ## Why this shape + + The audit before drafting found that `ironrdp-connector`, + `ironrdp-session`, `ironrdp-pdu`, `ironrdp-mstsgu`, and `ironrdp-core` + (`EncodeError`/`DecodeError`) all use the same + `ironrdp_error::Error` pattern. The `thiserror`-based bare enums + elsewhere in the workspace (`GccError`, `BulkError`, etc.) are + leaf-layer errors at the PDU/codec level; the connection-management + layer sister crate to `ironrdp-server` is the right template. + + ## Scope of this PR + + Public function signatures only: + + - `RdpServer::run` → `ServerResult<()>` + - `RdpServer::run_connection` → `ServerResult<()>` + - `RdpServer::run_connection_with` → `ServerResult<()>` + - `TlsIdentityCtx::init_from_paths` → `ServerResult` + - `TlsIdentityCtx::make_acceptor` → `ServerResult` + - `EchoServerHandle::send_request` → `ServerResult<()>` + + Internal call sites continue to use `anyhow::Result`. A private + `from_anyhow_with_context` helper bridges at the public boundary, + tagging each call site with the operation that failed. The + `ConnectionHandler::on_disconnected(error: Option<&anyhow::Error>)` + parameter is unchanged in this PR. + + `run_connection_with` keeps its anyhow body in a private + `run_connection_with_inner`, which the accept loop calls directly so + `on_disconnected` still receives an `anyhow::Error`. That helper is + transitional and its removal is inside this stack rather than deferred + indefinitely: #1244 converts `on_disconnected` to `ServerError` and + deletes it, with the accept loop calling `run_connection` from there. + `TlsIdentityCtx::init_from_paths` and `make_acceptor` use the same + wrapper-plus-private-body shape, matching the `run` / `run_inner` pair + this PR already introduces. + + ## Companion ext traits + + ```rust + pub trait ServerErrorExt { + fn encode(error: EncodeError) -> Self; + fn io(context: &'static str, error: io::Error) -> Self; + fn channel(context: &'static str) -> Self; + fn unsupported(context: &'static str) -> Self; + fn reason(context: &'static str, reason: impl Into) -> Self; + fn custom(context: &'static str, error: E) -> Self + where E: core::error::Error + Sync + Send + 'static; + } + pub trait ServerResultExt { + fn with_context(self, context: &'static str) -> Self; + } + ``` + + Mirrors `ConnectorErrorExt` / `ConnectorResultExt`, trimmed to the + constructors and the one result-side helper that have call sites in this + stack (`decode` and `with_source` did not, so neither shipped). + + ## Stack ordering + + This is step 1 of 4. The full chain (must merge in PR-number order): + + | Step | PR | Scope | + |------|-----|-------| + | **1** | **this PR** | Add `ServerError` / `ServerErrorKind` / ext + traits, convert 5 public functions, internal anyhow stays via private + bridge | + | 2 | #1243 | Convert encoder/helper/echo internals (anyhow → typed) | + | 3 | #1244 | Convert server.rs internals + `on_disconnected` parameter + (breaking) | + | 4 | #1245 | Convert display traits, drop anyhow dep, finish (breaking) + | + + All four close #1209 together. Each step is independently reviewable but + is rebased onto its predecessor as a Stacked branch; please merge in the + order above so the rebases land cleanly. + + ## Breaking change + + Marked with `!` in the conventional commit. Consumers of the five listed + public functions need to update their `Result` types. Pre-1.0, and per + @elmarco's note on #1209: "I am ok with breaking API at this point :)". + + ## Test plan + + - `cargo xtask check fmt -v` clean + - `cargo xtask check lints -v` clean (workspace, all-targets, with + helper + __bench features) + - `cargo xtask check tests -v` passes + - `cargo build --workspace --all-targets` clean (one example consumer in + `crates/ironrdp/examples/server.rs` updated to convert at its own + boundary) + + Closes part of #1209. + +- Convert encoder/helper/echo internals from anyhow to ServerError ([#1243](https://github.com/Devolutions/IronRDP/issues/1243)) ([f25d7083e6](https://github.com/Devolutions/IronRDP/commit/f25d7083e662368c4667d5f27c3a489fb5866a5a)) + + ## Summary + + Second step of the staged migration toward a typed public error story + for `ironrdp-server` ([#1209](https://github.com/Devolutions/IronRDP/issues/1209)). Stacks on #1242. + + Replaces `anyhow` construction sites in modules whose internal flow does + **not** pass through the `ConnectionHandler::on_disconnected` callback + (which still takes `&anyhow::Error` and is the subject of #1244): + + - **`encoder/mod.rs`**: ~15 `anyhow!` / `.context()` / `bail!` sites + converted to typed `ServerError` variants (`Encode`, `Reason`, + `Custom`). `EncodeError` sources go through `ServerError::encode` + (matching `ConnectorErrorExt::encode`). `spawn_blocking` `JoinError`, + qoi codec errors, and zstd error codes go through `ServerError::custom` + or `ServerError::reason` as appropriate. + - **`helper.rs`**: TLS cert/key loading paths construct + `ServerError::io` for `std::io::Error` sources, `ServerError::reason` + for missing-key cases, `ServerError::custom` for `x509-cert` and PEM + parsing errors. Removes the `from_anyhow` bridge and the inner-fn split + introduced in #1242. + - **`echo.rs`**: `build_echo_request` returns `ServerResult`, builds + errors via `ServerError::custom` directly. `send_request` keeps its + already-typed `Channel` and `Reason` variants. + + ## What this PR does NOT touch + + - `server.rs` internals (the heavy `.context()` chain in `run_inner` and + `run_connection_inner`) stay on `anyhow` because they propagate into + `ConnectionHandler::on_disconnected(error: Option<&anyhow::Error>)`. + #1244 will (a) change that parameter to `Option<&ServerError>`, and (b) + convert the server.rs internal sites in the same change. + - `RdpServerDisplay` / `RdpServerDisplayUpdates` traits (which return + `anyhow::Result`) stay unchanged. #1245 will convert those, drop the + `anyhow` dependency entirely, and complete the migration. + + ## Why split this way + + `server.rs` and the display traits are both `anyhow`-flavored at the + boundary; converting them in a single PR with `on_disconnected` keeps + the type story coherent. Splitting them now would either require a + temporary `anyhow ↔ ServerError` two-way bridge (ugly) or leave the + public trait inconsistent with the rest of the crate (worse). Better to + ship this batch (encoder + helper + echo all internal-only) and then + take server.rs + on_disconnected as one coherent step. + + ## Stack ordering + + This is step 2 of 4. The full chain (must merge in PR-number order): + + | Step | PR | Scope | + |------|-----|-------| + | 1 | #1242 | Add `ServerError` / `ServerErrorKind` / ext traits, + convert 5 public functions, internal anyhow stays via private bridge | + | **2** | **this PR** | Convert encoder/helper/echo internals (anyhow → + typed) | + | 3 | #1244 | Convert server.rs internals + `on_disconnected` parameter + (breaking) | + | 4 | #1245 | Convert display traits, drop anyhow dep, finish (breaking) + | + + Branch is stacked on `feat/server-typed-error` ([#1242](https://github.com/Devolutions/IronRDP/issues/1242)). Rebase onto + master after #1242 merges. + + ## Test plan + + - `cargo xtask check fmt -v` clean + - `cargo xtask check lints -v` clean (workspace, all-targets, with + helper + __bench features) + - `cargo xtask check tests -v` passes + - `cargo build --workspace --all-targets` clean + +- [**breaking**] Convert server.rs internals + on_disconnected to ServerError ([#1244](https://github.com/Devolutions/IronRDP/issues/1244)) ([b919a41932](https://github.com/Devolutions/IronRDP/commit/b919a419320dfabe4b932c0d3d623386883b072f)) + + ## Summary + + Third step of the staged migration started in #1242 and continued in + #1243. Combines the `on_disconnected` signature change with the + `server.rs` internal site conversion since both touch anyhow-flowing + code; doing them together avoids a temporary `anyhow ↔ ServerError` + two-way bridge during the intermediate state. + + ## Public API change + + ```diff + fn on_disconnected( + &mut self, + peer: SocketAddr, + duration: Duration, + - error: Option<&anyhow::Error>, + + error: Option<&ServerError>, + ) -> PostConnectionAction { ... } + ``` + + This is a **breaking change** for handler implementations of + `ConnectionHandler`. Pre-1.0, and per @elmarco's note on #1209: "I am ok + with breaking API at this point :)". + + ## Internal changes + + - The two `run_inner` / `run_connection_inner` wrapper functions + introduced in #1242 are folded back into `run` / `run_connection`. The + accept loop calls the public method directly; `result.as_ref().err()` + now feeds the new `ServerError`-typed parameter naturally. + - ~25 `.context()` / `bail!` sites in `run`, `run_connection`, + `accept_finalize`, `handle_io_channel_data`, `handle_x224`, + `handle_input_backlog`, and the `encode_share_data_pdu` / + `deactivate_all` helpers replaced with typed `ServerError` variants. + Pattern alignment with `ConnectorErrorExt`: + - `EncodeError` sources → `ServerError::encode` + - `DecodeError` sources → `ServerError::decode` + - `std::io::Error` sources → `ServerError::io` + - `Option` with `.ok_or_else` → `ServerError::channel` + - `bail!("Fastpath output not supported!")` → `ServerError::unsupported` + - everything else → `ServerError::custom` with a static context. + + ## What this PR does NOT touch + + - The `RdpServerDisplay::updates()` and + `RdpServerDisplayUpdates::next_update()` trait methods still return + `anyhow::Result`. Their conversion is #1245, which also drops the + `anyhow` dependency entirely. + - The `from_anyhow` private helper introduced in #1242 is retained only + at one boundary: the `display.updates()` call site in `run_connection` + wraps its anyhow result via `from_anyhow` until #1245 lands. + + ## Stack ordering + + This is step 3 of 4. The full chain (must merge in PR-number order): + + | Step | PR | Scope | + |------|-----|-------| + | 1 | #1242 | Add `ServerError` / `ServerErrorKind` / ext traits, + convert 5 public functions, internal anyhow stays via private bridge | + | 2 | #1243 | Convert encoder/helper/echo internals (anyhow → typed) | + | **3** | **this PR** | Convert server.rs internals + `on_disconnected` + parameter (breaking) | + | 4 | #1245 | Convert display traits, drop anyhow dep, finish (breaking) + | + + Branch is stacked on `feat/server-typed-error-internal` ([#1243](https://github.com/Devolutions/IronRDP/issues/1243)), which + is stacked on `feat/server-typed-error` ([#1242](https://github.com/Devolutions/IronRDP/issues/1242)). + + ## Coordinate with #1239 + + The `ConnectionHandler` trait was extended in #1239 (SuppressOutput / + RefreshRectangle / FrameAcknowledge handlers). If this PR lands first, + #1239 needs a trivial rebase to the new trait shape (the new methods + stay; only `on_disconnected`'s signature changes around them). If #1239 + lands first, this PR rebases onto its trait shape. Either order works. + + ## Test plan + + - `cargo xtask check fmt -v` clean + - `cargo xtask check lints -v` clean (workspace, all-targets, with + helper + __bench features) + - `cargo xtask check tests -v` passes + - `cargo build --workspace --all-targets` clean + +- [**breaking**] Convert display traits to ServerResult, drop anyhow dep ([#1245](https://github.com/Devolutions/IronRDP/issues/1245)) ([9abd6eb482](https://github.com/Devolutions/IronRDP/commit/9abd6eb482d8e7bcc64d578fc0a97bb12b6c82d9)) + + ## Summary + + Final step of the staged migration started in #1242 and continued in + #1243 / #1244. Completes the typed error story for `ironrdp-server` by + converting the last consumer-facing trait surface and dropping the + `anyhow` dependency entirely. Closes #1209. + + ## Public API changes + + ```diff + pub trait RdpServerDisplayUpdates { + - async fn next_update(&mut self) -> anyhow::Result>; + + async fn next_update(&mut self) -> ServerResult>; + } + + pub trait RdpServerDisplay: Send { + - async fn updates(&mut self) -> anyhow::Result>; + + async fn updates(&mut self) -> ServerResult>; + } + ``` + + These are **breaking changes** for handler implementations of the two + display traits. + + ## Internal changes + + - `from_anyhow` private bridge and `AnyhowError` wrapper struct removed + from `error.rs`. + - `anyhow` dependency removed from `ironrdp-server/Cargo.toml`. + - `builder.rs` `NoopDisplayUpdates` / `NoopDisplay` impls and the + docstring examples in `display.rs` and `README.md` updated to match the + new trait shapes. + - `crates/ironrdp/examples/server.rs` and + `crates/ironrdp-testsuite-extra/tests/main.rs` updated to return + `ServerResult` from their `RdpServerDisplay/Updates` impls. + - `benches/src/perfenc.rs` updated to construct `ServerError` variants + instead of `anyhow::Error` and converts at its own `anyhow::Result` main + boundary via `.map_err(|e| anyhow::anyhow!(e))`. + - `urbdrc.rs`, the `usb`-gated USB device facade, converted from + `anyhow` to `ServerResult`/`ServerError` (69 sites). This file predates + the typed error migration and was never touched by it, so removing the + `anyhow` dependency broke it under the `usb` feature. Typed external + errors from `ironrdp-usb`/`ironrdp-rdpeusb` are wrapped with + `ServerError::custom`; hand-rolled invariant checks (former + `bail!`/`ensure!`) use `ServerError::reason`. + + ## After this PR + + `ironrdp-server` has **no anyhow dependency** and the public surface is + fully typed against `ServerError`, including the `usb`-gated facade. The + full chain: + + | Step | PR | Scope | + |------|-----|-------| + | 1 | #1242 | Add `ServerError` / `ServerErrorKind` / ext traits, + convert 5 public functions, internal anyhow stays via private bridge | + | 2 | #1243 | Convert encoder/helper/echo internals | + | 3 | #1244 | Convert server.rs internals + `on_disconnected` parameter + | + | **4** | **this PR** | Convert display traits and the usb facade, drop + anyhow dep, finish | + + ## Stacking note + + Stacked on `feat/server-typed-error-server` ([#1244](https://github.com/Devolutions/IronRDP/issues/1244)). Rebase on landing + in PR-number order. + + ## Test plan + + - `cargo xtask check fmt -v` clean + - `cargo xtask check lints -v` clean (workspace, all-targets, with + helper + __bench features) + - `cargo xtask check tests -v` passes (including doctests) + - `cargo xtask check features --case workspace/powerset-runtime` clean + (44/44, covers `ironrdp-server` across the full feature powerset + including `usb`) + - `cargo build --workspace --all-targets` clean + +- Expose measured bandwidth to the embedder ([#1734](https://github.com/Devolutions/IronRDP/issues/1734)) ([e01839bfd8](https://github.com/Devolutions/IronRDP/commit/e01839bfd84ff5a392ca81ae80c0350bf457f7b1)) + + ## Summary + + - After #1470/#1471, the server can tell the client its measured + bandwidth over the wire (Network Characteristics Result), but nothing + exposes that figure outside the crate: `snapshot()` returns + `RttSnapshot` (min/max/avg/sample_count only) and + `autodetect_rtt_handle()` carries RTT alone. An embedder's own + health or flow-control layer that wants the same bandwidth figure + the client already received has no way to read it. + - Mirrors the existing `autodetect_rtt_handle` shape exactly: a new + `autodetect_bandwidth` `Arc` field (`u32::MAX` sentinel + until the first measurement, matching `autodetect_rtt`), an + `autodetect_bandwidth_handle()` accessor, and a + `with_autodetect_bandwidth_handle()` builder method for injecting a + shared instance. Adds `AutoDetectManager::bandwidth_kbps()` as the + underlying getter; the field already existed internally with no + public accessor. + - The store site needed a before/after comparison rather than a plain + `else` branch on `handle_response`'s result: that function reports a + matched RTT sample through its return value but only updates + bandwidth internally, so a naive `else` would also fire, and + mislabel the store as a fresh bandwidth measurement, on any + unmatched or stray RTT response once a bandwidth figure had been + recorded at least once. Comparing `bandwidth_kbps()` before and + after the call only stores and logs when a measurement genuinely + completed. + - Adds three tests to the existing autodetect test file, mirroring the + RTT-handle tests already there: `bandwidth_kbps()` reflecting a + completed measurement, the handle's sentinel default, and the + injected-handle round trip. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass, including the + 3 new tests (22/22 passing in `server::autodetect`). + + ## Notes + + No public API break: `RdpServer::new` is crate-private, and the public + surface (`RdpServerBuilder`) only gains an additive optional field and a + new method. + +- Wire RdpdrServer into ironrdp-server ([#1784](https://github.com/Devolutions/IronRDP/issues/1784)) ([695af5a6e8](https://github.com/Devolutions/IronRDP/commit/695af5a6e814badb3daa60184a8d2048d0e66716)) + + ## Depends on #1783 + + Stacked on `feat/rdpdr-server-core` ([#1783](https://github.com/Devolutions/IronRDP/issues/1783)), which itself depends on + #1779. Neither PDU codec bidirectionality nor RdpdrServer's + orchestration is reachable from ironrdp-server without this. + + ## Summary + + - Wires RdpdrServer into ironrdp-server, mirroring SoundServerFactory + and RdpsndServer exactly. RDPDR is a static virtual channel with the + same build-a-backend-then-attach shape as audio, not a dynamic channel + like EGFX and not a combined backend-plus-factory like clipboard. + - RdpdrServerFactory extends the ServerEventSender supertrait rather + than inlining a set_sender method, matching the live convention + SoundServerFactory and CliprdrServerFactory already use. build_backend + is named to match SoundServerFactory::build_backend. + - Server-initiated drive I/O needs the same async relay every other + channel uses. Added RdpdrServerMessage, one variant per RdpdrServer + drive_* method, and a ServerEvent::Rdpdr(RdpdrServerMessage) arm in + dispatch_server_events that looks up the live RdpdrServer instance, + calls the matching drive_* method, and writes the encoded result, the + same shape as the existing Rdpsnd arm. + - Adding a fourteen-variant enum to ServerEvent pushed + AutoReconnectCookieHandle::set's Result past clippy's result_large_err + threshold, a method this change otherwise has nothing to do with. + Suppressed locally with an explained reason rather than boxing the Rdpdr + payload, which would have changed ServerEvent's public shape for a size + concern from one unrelated method. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Added an + exhaustive-match test over every RdpdrServerMessage variant in + ironrdp-testsuite-core/tests/server/rdpdr.rs (no wildcard arm), so a + future variant added to one side of the dispatch without the other fails + to compile rather than silently falling through. + + ## Notes + + This closes out the RDPDR server-side contribution's three-part sequence + (PDU codec, RdpdrServer orchestration, ironrdp-server wiring). + +- Add graceful client disconnect with ServerSetErrorInfo ([#1798](https://github.com/Devolutions/IronRDP/issues/1798)) ([f7253861fa](https://github.com/Devolutions/IronRDP/commit/f7253861faa430d28c9dbce29d3e9e930c54748f)) + + ## Summary + + `RdpServer` had no framework-level way to disconnect a client + mid-session with a wire-visible reason. A handler wanting to reject a + client had to build a `ServerSetErrorInfoPdu` by hand and find a way to + reach the active connection's writer, or fall back to + `ServerEvent::Quit`, which tears the connection down silently with no + signal to the client at all. + + ## Public API changes + + Adds `ServerEvent::Disconnect(ErrorInfo)` and + `ErrorInfoDisconnectHandle`, mirroring the existing + `AutoReconnectCookieHandle` shape: + + ```rust + pub fn error_info_disconnect_handle(&self) -> ErrorInfoDisconnectHandle; + + impl ErrorInfoDisconnectHandle { + pub fn disconnect(&self, error: ErrorInfo) -> Result<(), mpsc::error::SendError>; + } + ``` + + `ErrorInfo` is re-exported at the crate root, matching how + `ServerAutoReconnect` is already re-exported for the auto-reconnect + cookie API. Purely additive, no existing signature changes. + + ## Internal changes + + `Disconnect` is handled in `dispatch_server_events`, where the writer is + already in scope: encodes `ServerSetErrorInfoPdu(error)` (MS-RDPBCGR + 2.2.5.1) via the existing `encode_share_data_pdu` helper (the same + mechanism the auto-reconnect cookie's Save Session Info PDU already + uses), writes it, then returns `RunState::Disconnect`. + + ## Notes + + No test added. The closest existing precedent for this class of feature, + `AutoReconnectCookieHandle`, has no test coverage anywhere in the + workspace either, so this is consistent with the established bar rather + than a gap specific to this PR. + + ## Test plan + + `cargo xtask check fmt/lints/tests/typos/locks` clean. `cargo xtask + check features --case workspace/powerset-runtime` clean (44/44, covers + `ironrdp-server`). + +- [**breaking**] Support the Large Pointer Capability Set ([#1787](https://github.com/Devolutions/IronRDP/issues/1787)) ([bde5824213](https://github.com/Devolutions/IronRDP/commit/bde5824213bfbc59d83c8fb3d2cfbc69ad9dab77)) + + ## Depends on #1786 + + Stacked on top of #1786's branch: Both touch `client_accepted()`'s + capabilities loop + and `UpdateEncoder::new()`'s constructor signature. #1786 must land + first. + + ## Summary + + - `ironrdp-server` never advertises or reads the Large Pointer + Capability Set + (MS-RDPBCGR 2.2.7.2.7), even though `ironrdp-pdu` already defines + everything needed + (`LargePointer`/`LargePointerSupportFlags`, `LargePointerAttribute`) and + the client + side already decodes and renders `UpdateCode::LargePointer`. The server + side was the + only gap: A pointer shape above 32x32 (or 96x96 with just the base + Pointer + Capability Set) could never be sent. + - Advertises the capability derived from `ironrdp-server`'s own + configured + `max_request_size`, not asserted independently: MS-RDPBCGR 2.2.7.2.7 + requires the + Multifragment Update Capability Set's `MaxRequestSize` to be at least + 38,055 bytes + for `LARGE_POINTER_FLAG_96x96` and at least 608,299 bytes for + `LARGE_POINTER_FLAG_384x384`. The default `max_request_size` (8 MiB) + clears both, so + this is enabled by default unless a server has lowered it. + - Reads the client's negotiated flags back and enforces a distinction + the spec draws + explicitly: `LARGE_POINTER_FLAG_96x96` only raises the existing + Color/New Pointer + Update's size ceiling from 32x32 to 96x96 (New Pointer Update wraps the + same + `ColorPointerAttribute` as Color Pointer Update, so MS-RDPBCGR + 2.2.9.1.1.4.4's + width/height field docs apply to both); it does not enable the separate + Large + Pointer Update PDU. Only `LARGE_POINTER_FLAG_384x384` does that. + `UpdateEncoder` now enforces both halves: `RGBAPointer` is dropped + (debug log) if it + exceeds the negotiated 32x32/96x96 ceiling, and the new + `DisplayUpdate::LargePointer` is dropped if the client never advertised + `LARGE_POINTER_FLAG_384x384`, or if the shape exceeds the protocol's + absolute + 384x384 maximum regardless of what was advertised. + - `DisplayUpdate::LargePointer` mirrors `RGBAPointer`'s shape exactly + (32bpp XOR mask + only, no AND mask) since that's the only color depth this crate's + pointer path + produces. `ColorPointer`'s own ceiling (same `LargePointerSupportFlags`) + is left + unenforced for the same reason the prior PR left its separate + availability gate + (`colorPointerCacheSize`) ungated: Nothing in this crate constructs a + `ColorPointer` + update. + + ## Breaking change + + `DisplayUpdate` gains a new variant (`LargePointer`), and + `UpdateEncoder`'s (crate + internal, `#[cfg(feature = "__bench")]`-exposed for benchmarks) + constructor gains a + 6th parameter. No exhaustive match over `DisplayUpdate` exists elsewhere + in the + workspace today, so nothing else needed updating. + + ## Validation + + `cargo xtask check fmt/typos/locks/lints/tests` all pass. + +- Add diagnostic logging for EGFX flow control and dispatch timing ([#1834](https://github.com/Devolutions/IronRDP/issues/1834)) ([8d1e91eb32](https://github.com/Devolutions/IronRDP/commit/8d1e91eb3256e0ba007e909512bbaf84cb76d67b)) + + I ran into a case where I needed to debug slow frame acknowledgement and + dispatch stalls in ironrdp-server and ironrdp-egfx, and found the + relevant signals were either missing or buried at TRACE level where + nobody enables them in normal operation. This PR adds targeted + diagnostic logging without changing any behavior. + + - FrameTracker now edge-triggers a debug log when backpressure or + ack_suspended change state, instead of logging every call (which would + flood at 30+/sec) or nothing at all. + - FrameAcknowledge is now logged at DEBUG with latency, queue_depth, and + in-flight count. An ack for an unknown frame_id (protocol violation or + stale ack per MS-RDPEGFX 2.2.4.3) is now a warning instead of silent. + - drain_output (ZGFX compression) now tracks per-batch compress time and + ratio, logging at INFO when a batch exceeds a 10ms budget and DEBUG + otherwise, since it runs under both the state lock and the writer lock + and can block inbound PDU processing. + - Incoming EGFX DVC PDUs are logged at DEBUG with their kind, so the + client-to-server side of the channel is visible without enabling TRACE. + - dispatch_pdu and dispatch_events now separately time lock-acquisition + wait and handler dispatch, warning when either exceeds 50ms so the two + causes (lock contention vs. handler/runtime stall) can be told apart + instead of both surfacing as one generic slow-dispatch symptom. + + No public API changes, no behavioral changes, logging only. + + Note on CI: the workspace-wide test compile is currently broken on + master independent of this PR, ironrdp-daemon's consume_output() call + site passes a raw Receiver where OutputEventReceiver is expected. I + confirmed this reproduces on a clean, unmodified checkout of master. + This PR only touches ironrdp-server and ironrdp-egfx. + +- Let an authenticated connection preempt an existing session ([#1476](https://github.com/Devolutions/IronRDP/issues/1476)) ([5198cde0f7](https://github.com/Devolutions/IronRDP/commit/5198cde0f73485f3fbd999ea7c49293c86b80387)) + + Adds opt-in session preemption so a new RDP client can replace an active connection. + +- Add ConnectionPolicy to reject a second connection ([#1913](https://github.com/Devolutions/IronRDP/issues/1913)) ([f7732a1639](https://github.com/Devolutions/IronRDP/commit/f7732a16392b5857a698182ffd37221da1010a2a)) + +- Add ironrdp-server integration for AUDIO_INPUT ([#1946](https://github.com/Devolutions/IronRDP/issues/1946)) ([5b139ba3df](https://github.com/Devolutions/IronRDP/commit/5b139ba3df92b05154bcedc5bc13b2c0b61866d8)) + + ## Summary + + RdpeaiServerFactory (ironrdp-server/src/rdpeai.rs) is a thin factory + trait mirroring RdpdrServerFactory and SoundServerFactory: + build_backend() returns the RdpeaiServerBackend the embedding + application supplies. attach_channels registers RdpeaiServer on the DVC + stack alongside AInput, DisplayControl, Echo, and RdpeiServer when a + factory is configured. RdpServerBuilder::with_rdpeai_factory() follows + with_rdpdr_factory's shape. + +- Add bandwidth-measure generation counter ([#1963](https://github.com/Devolutions/IronRDP/issues/1963)) ([ded2caa118](https://github.com/Devolutions/IronRDP/commit/ded2caa1184fd3b2671b9725ff26052383a0be16)) + + ## Summary + + - `autodetect_bandwidth_handle()` ([#1734](https://github.com/Devolutions/IronRDP/issues/1734)) exposes the latest Bandwidth + Measure figure, but a consumer doing its own smoothing/filtering on + top of it (e.g. rejecting noisy low samples unless real demand + existed) needs to know when a *new* measurement window has closed, + not just what the figure currently reads. The figure itself is a bad + freshness signal: a quiet link reads the same low value for several + consecutive windows in a row, so diffing it cannot tell "fresh + window, same result" apart from "stale, no new window yet". + - Adds `autodetect_bandwidth_generation`, mirroring + `autodetect_bandwidth`'s exact shape: an `Arc` field, + `autodetect_bandwidth_generation_handle()` accessor, and + `with_autodetect_bandwidth_generation_handle()` builder method. It + increments on every completed Bandwidth Measure transaction, + successful or not: a completed-but-unusable window still needs to + advance the generation so a consumer's own stale-demand bookkeeping + gets cleared instead of carrying over into the next window. The + server increments it with `Release` after storing the figure, so a + consumer that loads it with `Acquire` reads a bandwidth value at + least as new as the generation it saw. + - Along the way, upgrades the existing bandwidth-measured debug log + from just the computed `bandwidth_kbps` to the whole Bandwidth + Measure Results response, which carries the raw + `byte_count`/`time_delta_ms` inputs. Useful on its + own for anyone diagnosing why the figure looks noisy against a + bursty traffic source (as this crate's own damage-driven server + usage does): the raw inputs make it immediately visible whether a + low reading came from a genuinely idle window or a short one that + happened to close early. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. testsuite-core + covers the generation handle's default and injection; the increment + itself + runs in the private connection loop, which no harness reaches. + + ## Notes + + No public API break: both changes are purely additive to + `RdpServerBuilder` and `RdpServer`. + +- Let RdpServerDisplay report its monitor count ([#1918](https://github.com/Devolutions/IronRDP/issues/1918)) ([25331598f3](https://github.com/Devolutions/IronRDP/commit/25331598f37f49573205c54ee45c878d291317f2)) + + ## Summary + + - DisplayControlBackend (ironrdp-server's internal DisplayControlHandler + implementor) never overrode capabilities(), so it fell through to the + single-monitor default regardless of how many monitors the consumer's + RdpServerDisplay actually serves. + - Added a default RdpServerDisplay::monitor_count() method (returns 1, + matching today's behavior), fetched once alongside size() before the + dynamic channels are attached. + - Threaded the count into DisplayControlBackend so its capabilities() + now reports the real value instead of the hardcoded literal. + - A consumer serving more than one monitor can override monitor_count() + to report the real total. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + - Builds on #1917 (DisplayControlHandler::capabilities()), now merged; + rebased onto master. + - Additive only: one new trait method with a default body, one new + field, one call-site change. No existing implementor is affected unless + it opts in. + +### Bug Fixes + +- Weigh RemoteFX ICAP entropy coders instead of taking the last ([#1686](https://github.com/Devolutions/IronRDP/issues/1686)) ([185f312019](https://github.com/Devolutions/IronRDP/commit/185f3120199f0373a11c7e21cecfac77b7be56af)) + + The client's advertised TS_RFX_ICAP array was folded with a plain + assignment in the capability exchange loop, so whichever entry happened + to be parsed last silently decided the entropy coder. Add + pick_remotefx_entropy_coder and + RdpServerBuilder::with_remotefx_entropy_coder so the caller can state a + preferred coder, with a documented fallback instead of an accidental + one. + + Per MS-RDPRFX 2.2.1.1.1.1, the TS_RFX_ICAP array is the set of codecs + the client supports, not a ranked preference, so the server is entitled + to pick any entry. With no preference configured, the new fallback is + whichever coder the client offers first, which is deterministic and + matches the array's natural order, instead of whichever happened to be + parsed last. mstsc sends RLGR1 then RLGR3, so today's last-wins behavior + always lands on RLGR3, and RLGR1 never runs against a real client. + + Builds on #1684 and #1685, which give the quant table ([#1557](https://github.com/Devolutions/IronRDP/issues/1557)) the same + server-side shape. Default behavior changes for clients that offer both + coders: the server now uses the first-offered coder instead of the + last-offered one, unless with_remotefx_entropy_coder states otherwise. + +- Release the static channels when a connection ends ([#1721](https://github.com/Devolutions/IronRDP/issues/1721)) ([8b1f0dab08](https://github.com/Devolutions/IronRDP/commit/8b1f0dab088c2205ec1d9e8cb1b49bf902971b23)) + + `run_connection` leaves the connection's static channels attached to the + server. `run` clears them right after each session, so servers driven by + it + are unaffected — but the channel set is per-connection state, and + `run_connection` is the public entry point for embedders that run their + own + accept loop. There is no public API to clear it from outside. + + Channel backends own real resources, and several are released through + `Drop`: + `RdpsndServer::drop` stops its handler, which is how an audio backend + learns + to stop capturing. Held past the connection, they keep running with no + client + to serve, and their events accumulate in the server event queue until + the + next client attaches new channels. + + Measured on hypr-rdp, a Wayland RDP server that accepts through + `run_connection` so it can answer concurrent connections instead of + leaving + them in the backlog. After the client was killed: + + | | audio capture | virtual sink | RSS over 50 s | + |---|---|---|---| + | `run()` | stopped | removed | flat | + | `run_connection()` | still running | still the default output | +176 + KB/s | + + 176 KB/s is exactly 44100 Hz x 2ch x 16-bit — the capture thread still + recording into the queue. + + The reset moves into `run_connection_with` so it covers every exit path, + including the early returns, and the now-redundant one in `run` is + dropped. + The doc comment above the same function already explains why + per-connection + state has to be handled here rather than by the caller. + +- [**breaking**] Drop the tokio and tokio_rustls re-exports ([#1796](https://github.com/Devolutions/IronRDP/issues/1796)) ([cac7bd6f37](https://github.com/Devolutions/IronRDP/commit/cac7bd6f37120711570b0156dd87c8ce6145fad6)) + + ## Summary + + `ironrdp-server` re-exported both `tokio` and `tokio_rustls` wholesale + so consumers did not need their own `tokio` dependency. This pins every + consumer's `tokio` version to whatever `ironrdp-server` picks, with no + way to upgrade independently. Drops the re-export. + + ```diff + -pub use {tokio, tokio_rustls}; + ``` + + ## Public API changes + + - `ironrdp_server::tokio` and `ironrdp_server::tokio_rustls` no longer + exist. Consumers using either path need their own `tokio` and + `tokio-rustls` dependencies. + - `tokio-rustls` stays marked `# public` in `Cargo.toml`: `TlsAcceptor` + genuinely appears in public signatures (`with_tls`, `with_hybrid`, + `TransportTls::Tls`). `tokio`'s `# public` marker is dropped since no + public signature returns or takes a bare `tokio` type; it was only + public via the re-export. + + ## Internal changes + + - The one workspace consumer, `crates/ironrdp/examples/server.rs`, now + imports `tokio` directly instead of through the facade. + - `tokio` added as a direct dev-dependency of the `ironrdp` facade + crate, and to its existing `#[cfg(test)] use { ... as _ }` + unused-dependency silencer, the same pattern already used there for + every other example-only dependency. + + ## Test plan + + `cargo xtask check fmt/lints/tests/typos/locks` clean. `cargo xtask + check features --case workspace/powerset-runtime` clean (44/44, covers + `ironrdp-server`). + +- Honor the client's negotiated pointer cache size ([#1786](https://github.com/Devolutions/IronRDP/issues/1786)) ([e01cc76116](https://github.com/Devolutions/IronRDP/commit/e01cc76116fc0b238bca25acccce03eadb158550)) + + ## Summary + + - MS-RDPBCGR 2.2.7.1.5 (Pointer Capability Set): pointerCacheSize is the + client's advertised cache size for the New Pointer Update specifically + (colorPointerCacheSize is the separate, always-supported Color Pointer + Update cache). A zero or absent pointerCacheSize means the client did + not advertise New Pointer Update support at all, and the server must not + use it. + - client_accepted() already decodes the client's Pointer capability set + correctly, but the value was discarded: CapabilitySet::Pointer(_) fell + into a wildcard arm and never reached anything downstream. RGBAPointer + and CachedPointer (the New Pointer Update and its cache-reference + companion) were emitted unconditionally regardless of what the client + actually negotiated. + - Threads the negotiated pointerCacheSize into UpdateEncoder, alongside + the other capability-derived construction parameters (surface_flags, + codecs) it already receives from the same capabilities loop. RGBAPointer + and CachedPointer are now dropped with a debug log rather than encoded + when pointerCacheSize is zero, the same drop-and-log shape the encoder + already uses for a Bitmap update that exceeds the desktop size. + - ColorPointer is left alone: nothing in this crate currently constructs + a ColorPointer update, so gating it on the separate + colorPointerCacheSize field would be validation for a path that can't + happen yet. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + ## Notes + + Found while scoping server-driven cursor shape support for a downstream + embedder: RGBAPointer emission needs this gate to be spec-compliant + before it can be used at all. + +- Suppress result_large_err on ErrorInfoDisconnectHandle::disconnect ([#1812](https://github.com/Devolutions/IronRDP/issues/1812)) ([b828a6cf3d](https://github.com/Devolutions/IronRDP/commit/b828a6cf3d91688a14b1ccd84d48ca2e2cc84325)) + + ## Summary + + `cargo xtask check lints` is currently red on master. + `ErrorInfoDisconnectHandle::disconnect` (added in #1798) returns + `Result<(), mpsc::error::SendError>`, and `ServerEvent` + grew past clippy's `result_large_err` threshold when #1784 added + `RdpdrServerMessage`. `AutoReconnectCookieHandle::set` already carries + the same error type and got a scoped `#[expect(clippy::result_large_err, + ...)]` for exactly this reason; `disconnect` didn't get the matching + suppression when it landed. + + This adds the identical `#[expect]`, same attribute, same reason text, + to `disconnect`. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. Confirmed the + failure reproduces on bare master before this change and clears after. + + ## Notes + + No behavior change. `ServerEvent`'s size is driven by its largest + per-channel payload regardless of this method; boxing the payload to + shrink the type is a separate, larger change out of scope here. + +- Disable Nagle on accepted connections ([#1799](https://github.com/Devolutions/IronRDP/issues/1799)) ([915e681845](https://github.com/Devolutions/IronRDP/commit/915e681845d9c27198776d735952c272e19566b9)) + + RDP output is a stream of small writes the peer is waiting on: a frame, + a pointer update, a channel PDU. Nagle holds the trailing partial + segment of each until the previous one is acknowledged, so against a + peer using delayed acknowledgements every such write waits on a timer + that has nothing to do with the encoder or the link. `run` sets + `SO_REUSEADDR` on the listening socket and nothing on the accepted ones. + + A failure is logged rather than propagated: the session works either + way, only less promptly, and refusing a connection over a socket option + would cost more than the latency it avoids. + + `run_connection` and `run_connection_with` take a stream from the caller + and cannot set this themselves, so their documentation now says whose + job it is. For reference, `ironrdp-client` sets it on its RDCleanPath + transport and not on a direct connection. + +- [**breaking**] Stop deriving pipe MaximumPacketSize from device speed ([#1871](https://github.com/Devolutions/IronRDP/issues/1871)) ([6a5d7c23fe](https://github.com/Devolutions/IronRDP/commit/6a5d7c23fe312b6c7b01f25c6a8e4c436309df00)) + + `interface_information` derived + `TS_USBD_PIPE_INFORMATION.MaximumPacketSize` from + the announced device speed. The client opens the pipe and reports the + size it + actually established in `TS_USBD_PIPE_INFORMATION_RESULT`, so the + request field + only states a preference, and a Windows server sends zero. + `select_configuration` + and `select_interface` now send zero and no longer take a speed. + + `USB_DEVICE_CAPABILITIES` ([MS-RDPEUSB] 2.2.11) expresses only full and + high speed, + so the derived value was also unreachable for anything faster. + + `ironrdp-server` tracked the announced speed only to feed those two + calls, so + `UsbCapabilities` and its accessor go with them; nothing in its public + API changes. + + Observed against a Windows server redirecting a USB 3.1 flash drive from + FreeRDP: + it sends `MaximumPacketSize` 0 for both bulk pipes and receives 1024 + back for each. + +- Bound accept_finalize so a wedged client cannot hold the server ([#1890](https://github.com/Devolutions/IronRDP/issues/1890)) ([a8b4b2a7ed](https://github.com/Devolutions/IronRDP/commit/a8b4b2a7ed6f86a16a31e304538823520db0d830)) + +- A clipboard failure must not disconnect the session ([#1980](https://github.com/Devolutions/IronRDP/issues/1980)) ([124406d881](https://github.com/Devolutions/IronRDP/commit/124406d881af377cb339e8c131831f69e3cf7e45)) + + Two halves of the same problem: a rejected clipboard operation is both + invisible to the backend waiting on it and fatal to the session. + + ## `ironrdp-server`: a clipboard error ends `client_loop` + + `dispatch_server_events` propagates every `CliprdrServer` error with + `?`: + + ```rust + ClipboardMessage::SendFileContentsRequest(request) => cliprdr.request_file_contents(request), + ... + } + .map_err_kind("failed to send clipboard event", ServerErrorKind::Pdu)?; + ``` + + That error unwinds through `dispatch_events` → `client_loop` → + `client_accepted` → `accept_finalize`, and the accept loop logs + `"Connection error"`, resets the static channels and calls + `on_disconnected`. **The session is torn down.** + + The things that can trigger it are all ordinary: a file contents request + that became stale because the remote clipboard changed under it, a + capability that was never negotiated, `MAX_PENDING_FILE_REQUESTS` + backpressure, a malformed flag combination. None is a reason to + disconnect a peer whose display, audio and input are fine. + + Three lines above, `ClipboardMessage::Error` already takes the opposite + view: + + ```rust + ClipboardMessage::Error(error) => { + error!(?error, "Handling clipboard event"); + continue; + } + ``` + + This makes the two paths agree. + + ## `ironrdp-cliprdr`: a rejection strands its caller + + `request_file_contents` has ten rejection paths and all of them return a + bare `PduError`. The error carries no stream id, so an embedder that + surfaces it has no way to map it back to the transfer that failed — + whatever is awaiting that stream just hangs until an unrelated timeout + fires. + + The crate already solves this elsewhere. `FormatListResponse::Fail` + does: + + ```rust + // Notify backend for each pending request so it can clean up + // (e.g. reject pending download promises in WASM). + for stream_id in stream_ids { + self.backend.on_file_contents_response(FileContentsResponse::new_error(stream_id)); + } + ``` + + Every rejection in `request_file_contents` now does the same before + returning. **The `Err` is unchanged** — this is strictly additive for + existing callers. + + ## Tests + + - + `crates/ironrdp-testsuite-core/tests/clipboard/file_contents_state_machine.rs` + — `a_rejected_request_fails_its_stream_on_the_backend` (out-of-bounds + index) and + `a_request_rejected_before_ready_fails_its_stream_on_the_backend` + (`require_ready`, the path most likely to strand a caller). Both assert + the error is still returned *and* that the backend saw an error response + for the right stream id. + - `crates/ironrdp-server/src/server.rs` — + `a_refused_clipboard_message_does_not_end_the_client_loop` drives + `dispatch_server_events` with a request the channel refuses and asserts + `RunState::Continue`. + + All 211 existing clipboard tests and the 15 `ironrdp-server` lib tests + pass unchanged. + + 🤖 Generated with [Claude Code](https://claude.com/claude-code) + + --------- + +- Drop RDPSND waves that arrive before the channel negotiates ([#1990](https://github.com/Devolutions/IronRDP/issues/1990)) ([2e872b085b](https://github.com/Devolutions/IronRDP/commit/2e872b085b504cf3b9ce9994237270f71a35d13a)) + + The server event channel outlives a single connection. When a client + with audio disconnects, waves its sound handler already queued stay in + the channel and become the first events the next connection dispatches, + before that connection's RdpsndServer has received the client's formats. + wave() then fails in version() with "client format not yet received", + and the map_err_kind at the dispatch site ends the new client loop, so a + disconnect of an audio-enabled client can turn into a failed connect for + whoever comes next. The per-batch WAVE_KEEP trim only removes waves + beyond the newest four, so it does not cover this. + + This adds RdpsndServer::is_ready() and drops waves and volume changes + with a debug log while the channel is not ready. A volume change fails + the same way before negotiation, since set_volume() needs the client's + format flags. A wave has nowhere to go before negotiation, and after a + failed negotiation (no common format, or the handler's start failing) it + never will. + + A test drives the handshake and checks is_ready() before negotiation, + after a successful one, with no common format, and when start fails. It + also checks that wave() and set_volume() fail before negotiation, and + that wave() goes through once is_ready() holds. + +### Build + +- Bump the crypto group across 1 directory with 3 updates ([#1449](https://github.com/Devolutions/IronRDP/issues/1449)) ([e1725e8c8a](https://github.com/Devolutions/IronRDP/commit/e1725e8c8a581b83835647b6ee563a5b3f6c7a1b)) + +### Refactor + +- [**breaking**] Take auto-detect timestamps from the caller ([#1487](https://github.com/Devolutions/IronRDP/issues/1487)) ([f614e4acfc](https://github.com/Devolutions/IronRDP/commit/f614e4acfcb8abf7113e0f5c653112af94ed80af)) + + ## Summary + + - `AutoDetectManager` read the clock itself: `std::time::Instant` in + `pending_probes`, `Instant::now()` in `send_rtt_request`, and + `Instant::elapsed()` in `handle_response` and `expire_stale_probes`. + - It now takes `now_ms`, a caller-supplied monotonic millisecond counter + whose epoch is arbitrary as long as it's consistent across calls. + `ironrdp-server` supplies it from a process-wide monotonic origin in + `server.rs`, so the clock lives in the I/O driver rather than in the + state machine. + + ## Why + + - **Testability.** The RTT assertions were wall-clock dependent. + `snapshot_reflects_measurements` could only check that the average came + out under an arbitrary 100 ms bound, which almost any bug would satisfy. + It now supplies both timestamps and asserts exact values: samples of 10, + 20 and 30 ms giving min 10, max 30, average 20. + - **Portability.** `std::time::Instant::now` panics on + `wasm32-unknown-unknown`, so a type that reads it internally can't be + reused from a WASM build. + - **Layering.** A state machine that reads ambient time can't satisfy + the no-I/O rule the Core Tier crates follow, which forecloses moving + this code in that direction later. + + Also covers the edges of the arithmetic the injected clock exposes. + There are two `saturating_sub` sites and they saturate in opposite + directions: + + - `handle_response`: a clock that ran backwards between request and + response yields a zero sample rather than a wrapped value near + `u32::MAX`, and the zero reaches the sample window, not just the return + value. + - `expire_stale_probes`: the same backwards clock makes the age zero, + which is below any maximum, so the probe stays pending. Wrapping would + make it look older than any limit and drop a probe whose response is + still in flight. + + A third test covers the `u32::try_from(..).unwrap_or(u32::MAX)` on the + first of those lines, where a gap wider than about 49.7 days clamps + rather than truncating to the low 32 bits. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + Each of the three new tests was checked against a mutation of the code + it guards rather than only for passing: the two backwards-clock tests + fail if either `saturating_sub` becomes `wrapping_sub`, and the clamp + test fails if the `try_from` becomes a truncating `as u32`, which + reports `Some(0)` for a 49.7 day gap. + + ## Notes + + - Came out of the discussion on #1465, where the same question arises on + the connector side. `ironrdp-connector` reads no clock at all today, and + answering a connect-time Bandwidth Measure properly needs one; taking + the timestamp from the caller is the shape that works on every target. + - `AutoDetectManager` arrived in #1177 with the internal clock, so this + corrects code I wrote rather than anyone else's. + + + ## [[0.13.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-server-v0.12.0...ironrdp-server-v0.13.0)] - 2026-07-10 ### Security diff --git a/crates/ironrdp-server/Cargo.toml b/crates/ironrdp-server/Cargo.toml index ae73ff2379..2a4c81750f 100644 --- a/crates/ironrdp-server/Cargo.toml +++ b/crates/ironrdp-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-server" -version = "0.13.0" +version = "0.14.0" readme = "README.md" description = "Extendable skeleton for implementing custom RDP servers" edition.workspace = true @@ -38,24 +38,24 @@ rand = "0.9" tokio = { version = "1", features = ["net", "macros", "sync", "rt", "time"] } tokio-rustls = "0.26" # public async-trait = "0.1" -ironrdp-async = { path = "../ironrdp-async", version = "0.10" } -ironrdp-ainput = { path = "../ironrdp-ainput", version = "0.8" } -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } -ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.3", optional = true } +ironrdp-async = { path = "../ironrdp-async", version = "0.11" } +ironrdp-ainput = { path = "../ironrdp-ainput", version = "0.9" } +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } +ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.4", optional = true } ironrdp-error = { path = "../ironrdp-error", version = "0.2", features = ["std"] } # public ironrdp-nscodec = { path = "../ironrdp-nscodec", version = "0.2", optional = true, features = ["encoder"] } -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public -ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.7" } # public -ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.8" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8" } # public +ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.9" } # public ironrdp-echo = { path = "../ironrdp-echo", version = "0.4" } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.10", features = ["reqwest"] } -ironrdp-acceptor = { path = "../ironrdp-acceptor", version = "0.10" } # public -ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public -ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.7" } # public -ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.9" } # public +ironrdp-acceptor = { path = "../ironrdp-acceptor", version = "0.11" } # public +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10" } # public +ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.8" } # public +ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.10" } # public ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1" } # public ironrdp-rdpeai = { path = "../ironrdp-rdpeai", version = "0.1" } # public ironrdp-rdpeusb = { path = "../ironrdp-rdpeusb", version = "0.1", optional = true } diff --git a/crates/ironrdp-session/CHANGELOG.md b/crates/ironrdp-session/CHANGELOG.md index 240f41962d..2c5e35e67c 100644 --- a/crates/ironrdp-session/CHANGELOG.md +++ b/crates/ironrdp-session/CHANGELOG.md @@ -6,6 +6,754 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.12.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.11.0...ironrdp-session-v0.12.0)] - 2026-09-30 + +### Security + +- [**breaking**] Support session resume via the auto-reconnect cookie ([#1501](https://github.com/Devolutions/IronRDP/issues/1501)) ([74b3365c1f](https://github.com/Devolutions/IronRDP/commit/74b3365c1f98c0da6feed7507779c67e1b8e6d08)) + + > **Rebased onto post-#1522 master.** #1509 landed the server half of + #1508 while this was open, including the `ClientAutoReconnect` + structure. This PR no longer declares it; it extends it, and picks up + the parts #1509 did not build. + + ## What + + The client half of automatic reconnection. The session layer surfaces + the Server Auto-Reconnect Cookie, `ironrdp-pdu` derives and verifies the + client's response to it, and the connector sends that response when + resuming a session. + + ## Why + + A client whose connection drops ungracefully can reattach to its session + instead of making the user log on again, provided it returns the cookie + the server issued during logon ([MS-RDPBCGR] 1.3.1.5). + + #1509 built the server side of that: it validates a returning + `ARC_CS_PRIVATE_PACKET` and rotates the random. Nothing answers it. + `ironrdp-session` decodes the cookie and drops it, `ironrdp-connector` + has no way to send one back, and `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` still sits in + `ironrdp-client`. So `ironrdp-client` cannot resume a session against + `ironrdp-server`, and the validation #1509 added has no in-tree + counterpart to exercise it. + + The wire encoding was already there. `ExtendedClientOptionalInfo` + carries, encodes and decodes a 28-byte `autoReconnectCookie` and its + builder already had a `reconnect_cookie` step; `ServerAutoReconnect` + already decoded; #1509 added `ClientAutoReconnect` and its decode. + Nothing connected them. + + ## The three parts + + **Receive.** `SaveSessionInfo` now also surfaces the cookie, as + `ProcessorOutput::AutoReconnectCookie` and + `ActiveStageOutput::AutoReconnectCookie`. #1522 added a `SaveSessionInfo + { logon_complete }` output on that same handler; the two coexist rather + than compete, since both are read off one PDU and neither supersedes the + other. The handler emits the logon notification unconditionally and + appends the cookie when one is present, and a test pins that surfacing + the cookie does not suppress the notification. #1509's server replaces + the cookie whenever a client connects and again hourly ([MS-RDPBCGR] + 3.3.6.2), so this can arrive more than once in a session and the + consumer keeps the most recent. + + **Derive.** `ClientAutoReconnect::from_server_cookie` implements + [MS-RDPBCGR] 5.5: + + > The auto-reconnect random is used to key the HMAC function + ([RFC2104]), which uses MD5 as the iterative hash function. The security + verifier is derived by applying the HMAC to the client random received + in Step 3. + > + > `SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom)` + > + > When Enhanced RDP Security is in effect the client random value is not + generated (section 5.3.2). In this case, for the purpose of generating + the security verifier, the client random is assumed to be an array of 32 + zero bytes. + + IronRDP implements no Standard RDP Security path (there is no Security + Exchange PDU), so the zero-client-random case is the only one that + arises. As 5.5 notes, that makes the verifier constant for a given + cookie, so it proves possession of the cookie and nothing more; session + security comes from the outer TLS/CredSSP handshake. + + @clintcan independently confirmed this construction against real + **mstsc** while validating #1509 + ([comment](https://github.com/Devolutions/IronRDP/pull/1509#issuecomment-5151200681)): + a Windows client's `ARC_CS_PRIVATE_PACKET` verifies against + `HMAC-MD5(random_bits, [0u8; 32])`. That is the same derivation + implemented here, so the two halves interoperate with Microsoft's client + and not only with each other. + + **Send.** `ClientConnector::with_auto_reconnect_cookie` takes the cookie + last received and makes the connector put the derived Client + Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. + Absent, that PDU is byte-for-byte what it was. + + Unlike the server packet, this structure has no enclosing logon-info + field header, so it encodes to exactly the 28 bytes the cookie field + expects. `to_bytes` writes that layout directly rather than going + through `Encode`, so filling a fixed-size field has no error path a + caller must handle; a test pins the two to agree. + + ## One derivation, not two + + Putting `from_server_cookie` in `ironrdp-pdu` would leave the workspace + with two implementations of 5.5, since #1509 added a private HMAC to + `ironrdp-server`. So `ClientAutoReconnect` also gains `verify`, and the + server routes through it. + + `verify` keeps the constant-time comparison the server had. The verifier + is the whole credential, so a comparison returning early on the first + differing byte would let a peer recover it a byte at a time from the + timing; the session identifier is not secret and is compared normally. + `ironrdp-server` keeps the policy around the check, which cookies are + live and whether the security protocol permits auto-reconnect, and drops + its `hmac` and `md-5` dependencies. `hmac` moves to `ironrdp-pdu` as + `default-features = false`; the crate's full feature powerset still + checks clean, including `--no-default-features`. + + I would rather not have reached into `ironrdp-server` in a + `pdu,session,connector` change, but the alternative was shipping the + duplicate and filing a follow-up to remove it, which is a worse trade + for reviewer time. + + ## Tests that were not running + + That move also rehomes the known-answer tests @clintcan contributed on + #1509. They went in as an inline `#[cfg(test)]` module in + `crates/ironrdp-server/src/server.rs`, and that crate sets `[lib] test = + false`, so they have never executed in CI. They now live in + `ironrdp-testsuite-core` against the public API, where CI runs them: his + HMAC-MD5 reference vector is kept as a second vector alongside a + differently-keyed one, plus the cases for a tampered verifier and a + mismatched logon ID. + + Worth flagging separately: `ironrdp-server` is not alone. + `ironrdp-agent`, `ironrdp-session` and `ironrdp-web` also set `[lib] + test = false` and between them carry 16 files of inline `#[cfg(test)]` + modules that CI never runs. That is out of scope here, but I am happy to + open an issue if it would be useful. + + ## Breaking changes + + `ActiveStageOutput` and `x224::ProcessorOutput` gain a variant, and + `ClientConnector` gains a public field, so exhaustive matches and struct + literals need updating. + + Confirmed with `cargo-semver-checks` against the merge-base: those three + are the only findings this branch introduces. The others it reports on + `master` today (`ShareDataPdu::Compressed` and the `ShareDataCtx` fields + from #1518, `ProcessorBuilder.bulk_decompressor` from #1518, + `ServerEvent::SetAutoReconnectCookie` from #1509) are present on + `master` unchanged. The `ironrdp-pdu` additions are additive. + + ## Scope + + This is the library half. `ironrdp-client`, `ironrdp-web` and the FFI + bindings gain an arm for the new output but none of them reconnect + automatically yet; that is the remaining part of #271, and the existing + `TODO([#271](https://github.com/Devolutions/IronRDP/issues/271))` in `ironrdp-client` marks where it goes. + + I kept receive, derive and send together deliberately. Split up, none of + them is usable on its own: without the receive half there is no way to + obtain a cookie, and without the send half there is nothing to do with + one. + + ## Tests + + Thirteen, all in `ironrdp-testsuite-core`. + + On the packet and the derivation: the `SecurityVerifier` matches two + independently computed HMAC-MD5 vectors of 32 zero bytes under different + keys, so the tests pin the derivation rather than restating the code; + the logon ID carries over from the server cookie; the encoding matches + the 2.2.4.3 field layout byte for byte with `cbLen` fixed at `0x1C`; + `to_bytes` agrees with `Encode`; it round-trips; and it rejects both a + wrong packet length and an unknown version. + + On verification: a derived answer is accepted, a single flipped byte in + the verifier is rejected, a correct verifier under a different logon ID + is rejected, and an answer derived from a different random is rejected. + + On the surfacing path: a Save Session Info PDU framed the way a server + sends it, through the real x224 processor, yields an + `AutoReconnectCookie` carrying the right logon ID and random bits, + alongside #1522's logon notification rather than in place of it; and one + without a cookie surfaces no cookie. + + ## Verification + + `cargo xtask check fmt/lints/tests/typos/locks` all pass on 1.94.1, + including a `fuzz/` build before the lock check. + + ## Note + + #1496 also touches the `ClientAutoReconnect` declaration. Whichever of + the two lands second needs a one-line rebase on the derive attribute; + happy to take that in either order. + +- Decode the Server Heartbeat PDU on the message channel ([#1814](https://github.com/Devolutions/IronRDP/issues/1814)) ([4275b3d7fd](https://github.com/Devolutions/IronRDP/commit/4275b3d7fdf33c0aedeeb6bc9f58bfec83d0893d)) + + ## Summary + + Adds `HeartbeatPdu` (MS-RDPBCGR 2.2.16.1), decoded on the MCS message + channel that #1347/#1348 wired up for Auto-Detect. The Heartbeat PDU is + server-to-client only and defines no client response; the client is free + to ignore `count1`/`count2`. + + Fixes a real gap this exposed: the message-channel demux in + `ironrdp-session` unconditionally decoded incoming traffic as an + Auto-Detect Request PDU. A Heartbeat PDU arriving on the same channel + would fail that decode and error the session. The demux now peeks the + security-header flags first (masking + `SEC_RESET_SEQNO`/`SEC_IGNORE_SEQNO`, which MS-RDPBCGR 2.2.8.1.1.2.1 + says MUST be ignored, the same way + `MultitransportRequestPdu`/`MultitransportResponsePdu` already do) and + dispatches by the masked value. Anything it doesn't recognize is logged + and ignored rather than treated as session-fatal, matching the + forward-safe posture the connect-time demux in `ironrdp-connector` + already has. + + Multitransport Bootstrapping, the other optional feature the message + channel carries, is already fully implemented ([#1098](https://github.com/Devolutions/IronRDP/issues/1098)); this PR is the + Heartbeat half only. + + ## Validation + + `cargo xtask check fmt/typos/tests/locks` and `wasm check` all pass. + `lints` has one pre-existing failure on master unrelated to this diff + (`clippy::result_large_err` on `ErrorInfoDisconnectHandle::disconnect`, + fix filed as #1812); this PR's own diff is clean under the same lints + run. + + New tests: `HeartbeatPdu` encode/decode round-trip plus the same + seqno-flag and cross-PDU-discriminator coverage + `MultitransportRequestPdu` has, in `ironrdp-pdu`; a `Processor::process` + integration test in `ironrdp-testsuite-core` covering the full + message-channel path (Heartbeat produces no output, Auto-Detect still + works alongside it, and an unrecognized PDU is ignored rather than + erroring). + +- Honor input timing settings ([#1872](https://github.com/Devolutions/IronRDP/issues/1872)) ([ffb921c211](https://github.com/Devolutions/IronRDP/commit/ffb921c211477c6b77cccaf98eb500d627513374)) + + Wire MinInputSendInterval and KeepAliveInterval through the client + configuration and active session loop. + + Delay mouse-only batches without delaying forced input, keep the + software-rendered cursor aligned with the final batched position, and + synthesize current-position keepalives using native defaults and value + handling. + + Keep idle timeout, cache, video, device, and security policy settings + explicitly unsupported until their owning backends exist. + +### Features + +- Support runtime-defined static virtual channels ([#1517](https://github.com/Devolutions/IronRDP/issues/1517)) ([8b4c483ba0](https://github.com/Devolutions/IronRDP/commit/8b4c483ba0c900a8de0b2718347754f56dd363ba)) + + ## Summary + - add keyed runtime-defined static-channel registration, lookup, and + negotiated ID attachment + - enforce the static-channel limit and reject malformed SVC fragment + sequences + - wire generic connector, acceptor, and session name-based dispatch + support + + ## Testing + - `cargo test -p ironrdp-testsuite-core --test integration_tests_core + svc::` + - `cargo clippy -p ironrdp-testsuite-core --test integration_tests_core + -- -D warnings` + + --------- + +- Expose generic session configuration and lifecycle APIs ([#1522](https://github.com/Devolutions/IronRDP/issues/1522)) ([57b1366650](https://github.com/Devolutions/IronRDP/commit/57b13666506dc40c15b4c4702d35150beee99133)) + + ## Summary + - expose generic client configuration for connection metadata, + compression, shell/work directory, audio, and runtime static-channel + factories + - add bounded input delivery with independent close cancellation, host + clipboard plumbing, lifecycle events, and Display Control resize + readiness/fallback handling + - update agent, viewer, web, FFI, examples, and tests for the generic + APIs + + ## Stack dependencies + This PR is stacked on `copilot/tls-validation-policy` (`b2bbcece`), + which already includes the merged runtime static-channel support from + `master`. It intentionally contains no TLS implementation/policy, + ActiveX/COM, SVC implementation, decompression, or bitmap-recovery + changes. + + ## Validation + - `cargo fmt --check --all` + - `cargo xtask check tests --no-run -v` + - `cargo xtask check lints -v` + - `cargo test -p ironrdp-client --lib --features rustls` + - `cargo check -p ironrdp-agent -p ironrdp-viewer -p ironrdp-web -p ffi` + + --------- + +- Negotiate static channel chunk sizing ([#1622](https://github.com/Devolutions/IronRDP/issues/1622)) ([4e3903fbbe](https://github.com/Devolutions/IronRDP/commit/4e3903fbbef2904505f35e3437a7106807ac5987)) + + Use the validated server VCChunkSize for outgoing static virtual channel + data and retain 1600-byte chunks when it is absent or invalid. + + Apply refreshed values after reactivation across native, web, and FFI + active stages while preserving channel flags. + +- Forward negotiated windowing orders ([#1631](https://github.com/Devolutions/IronRDP/issues/1631)) ([0c3fbe78b4](https://github.com/Devolutions/IronRDP/commit/0c3fbe78b4366533b9fcea046b2b53654e003a72)) + + Preserve Window List support during activation. + Forward validated orders through ActiveStage and the raw FFI output. + Desktop and web consumers retain their existing behavior. + +- Add Input DVC and ActiveX touch ([#1647](https://github.com/Devolutions/IronRDP/issues/1647)) ([a912e19bd2](https://github.com/Devolutions/IronRDP/commit/a912e19bd2bb31f403fd7c35c8efd729a5ab5f6f)) + + Implement MS-RDPEI for multi-touch over the dynamic virtual channel + Microsoft::Windows::RDS::Input, and wire Windows pointer messages in + ActiveX through session encode helpers. + + Introduce ironrdp-rdpei PDUs and processors, register the channel from + the client, encode touch frames from ActiveX WM_POINTER*, and cover the + protocol with unit and integration tests. + +- Expose MS-RDPEI pen and multi-touch ([#1652](https://github.com/Devolutions/IronRDP/issues/1652)) ([51f8738ea1](https://github.com/Devolutions/IronRDP/commit/51f8738ea156daeea292c63bba09d9137791af57)) + + Extend the agent RPC path beyond single-contact touch so multi-contact + frames, pen frames, and dismiss-hovering can be driven end-to-end + against a real host. + + Wire Pen/Dismiss through rpc, daemon, CLI, ActiveX control RPC, client + input loop, and session encode helpers. Add pen contact flag legality + checks and round-trip coverage. + +- Support automatic reconnect ([#1662](https://github.com/Devolutions/IronRDP/issues/1662)) ([34d7b31dc5](https://github.com/Devolutions/IronRDP/commit/34d7b31dc5039273dc5502d0ca3301f256eb6154)) + + Support automatic reconnect through the ActiveX control with bounded + host-approved retries after transport loss. + + Reuse ARC cookies only for resumed sessions, reject server ARC status + failures, and report success after active-session traffic. + + --------- + +- Negotiate monitor topology ([#1675](https://github.com/Devolutions/IronRDP/issues/1675)) ([063efcdc30](https://github.com/Devolutions/IronRDP/commit/063efcdc3088d8f44e423cc322077d40bf9aadf2)) + + Negotiate the client monitor layout from UseMultimon and expose the + confirmed remote topology through the ActiveX compatibility interface. + + Advertise Monitor Layout PDU support whenever Extended Client Data is + negotiated, and forward layouts from activation, active sessions, and + reactivation so advertised support does not terminate sessions. + + Keep fallback reporting truthful when servers do not honor the request, + while preserving single-monitor resize behavior and blocking + multi-monitor resizing. + + Do not send Client Monitor Extended Data; per-monitor DPI and + orientation remain unavailable. + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + +- Add location redirection ([#1778](https://github.com/Devolutions/IronRDP/issues/1778)) ([1cee7a8613](https://github.com/Devolutions/IronRDP/commit/1cee7a86135a0556c01965d0406233bd7df367a9)) + + Implement MS-RDPEL v1 codecs and the location DVC state machine, then + route the ActiveX methods through the bounded client input queue. + + Preserve mstsc-compatible validation and altitude caching while + surfacing inactive sessions, channel readiness, queue pressure, and + encoding failures. Coordinates are caller-supplied only and are never + logged or persisted. + +- Render the EGFX graphics pipeline output ([#1461](https://github.com/Devolutions/IronRDP/issues/1461)) ([d414622231](https://github.com/Devolutions/IronRDP/commit/d414622231ed3a944d8896118f409edead1d3df3)) + + ## Summary + + - The `ironrdp-egfx` compositor exposes changed output regions via + `drain_output()`, but nothing consumed them, so an EGFX session decoded + frames and dropped them. + - Drains the compositor from `ActiveStage::process`: composites each + completed-frame `OutputUpdate` into the `DecodedImage` and emits + `ActiveStageOutput::GraphicsUpdate`, so every session consumer renders + EGFX with no new code. No-op when the graphics DVC is not registered. + - Adds a by-type mutable DVC accessor `DrdynvcClient::get_dvc_mut`, + mirrored on the session (the x224 processor and `ActiveStage`), matching + the existing `get_dvc`. + - All regions drained in one pass are composited first by + `composite_graphics_updates`, then surfaced as a single `GraphicsUpdate` + covering their union. A consumer may redraw whatever region an update + names, and `ironrdp-client` rebuilds the whole framebuffer for each one, + so emitting per region would copy the desktop once per rectangle. A + single `RDPGFX_SOLIDFILL_PDU` or `RDPGFX_CACHE_TO_SURFACE_PDU` can name + up to `u16::MAX` of them. + + ## Validation + + - `cargo xtask check fmt/lints/tests/typos/locks` all pass, including + the dependency guard. + - Four tests cover the coalescing: two disjoint deltas collapsing to the + rectangle spanning both, 64 deltas yielding exactly one region, a single + delta passing through unwidened, and an empty drain surfacing nothing. + Checked against a reverted coalescing, where the first two fail. + - The loop lives in `composite_graphics_updates` rather than inline in + `ActiveStage::process` so it can be tested without standing up an x224 + processor. It takes `(ExclusiveRectangle, Vec)` rather than + `OutputUpdate`, since that type is `#[non_exhaustive]` and cannot be + constructed from `ironrdp-session`; this keeps the change inside the + crate instead of widening an already-merged API for testability. + + ## Notes + + - `ironrdp-session` already depends on `ironrdp-displaycontrol` and + `ironrdp-graphics`; `ironrdp-egfx` is consumed the same way. The guard + keeping `ironrdp-connector` and `sspi` out of `ironrdp-session` still + passes. + - Reuses `apply_rgba32` (previously gated behind the `qoi` feature); + converts the compositor's exclusive-rectangle regions to the session's + inclusive-rectangle convention. + - #1377 and #1460 have both merged, so this now sits directly on master + and the diff is its own change alone: 6 files, +160/-6. + - #1462 is stacked on this one, so the merge order is this then #1462. + - Part of #1464. Motivated by #1446. + +- Handle multitransport on message channel ([#1836](https://github.com/Devolutions/IronRDP/issues/1836)) ([561aebd0d8](https://github.com/Devolutions/IronRDP/commit/561aebd0d85e1a01a5c7ae63e5befd71d55887e3)) + + Route SEC_TRANSPORT_REQ through the negotiated MCS message channel and + surface the decoded request to active-session consumers. Encode the + client response on the same channel, expose that encoder through + ActiveStage, and fail explicitly when the message channel was not + negotiated. + + Ignore a request received on the I/O channel because the specification + does not require terminating the main session for this optional + transport anomaly. + +- Expose routable DVC batches ([#1857](https://github.com/Devolutions/IronRDP/issues/1857)) ([8369ea6989](https://github.com/Devolutions/IronRDP/commit/8369ea69897e6d6892d2c7d8f93f35b943467e9f)) + + Expose Soft-Sync tunnel state and typed processing through ActiveStage. + + Return validated DVC batches for resize and RDPEI requests. + Keep existing TCP encoding methods as route-safe compatibility wrappers. + +- Present EGFX dirty regions ([#1874](https://github.com/Devolutions/IronRDP/issues/1874)) ([2b44c62bdb](https://github.com/Devolutions/IronRDP/commit/2b44c62bdbd997f4aa33bdbaf4749ac4ecc85140)) + + Propagate validated ResetGraphics extents into the session framebuffer + and deliver exact packed desktop damage to ActiveX without rebuilding + full image snapshots. + + Coalesce only fully covered regions, preserve sparse updates with a + 64-event and 256 MiB pixel-data budget, and backpressure without + evicting accepted frame or static-channel payloads. Update retained GDI + and RPC surfaces in place. Reuse framebuffer storage with fallible + growth, and retain software cursor shape, position, visibility, and + clipping across graphics resets. Keep V8 non-AVC negotiation and + full-frame output fallback unchanged. Rejected oversized resets still + destroy prior surfaces and are reported as unsupported without + allocating a session framebuffer. + +### Bug Fixes + +- Preserve bulk compression across reactivation ([#1474](https://github.com/Devolutions/IronRDP/issues/1474)) ([8fcffb9e8f](https://github.com/Devolutions/IronRDP/commit/8fcffb9e8f1a2c468321c05a56ec96144316c90a)) + + Any session that reactivates (Deactivate All → re-activate) loses bulk + decompression and dies right after. Windows consoles reactivate right + after logon, and compression is on by default, so this hits pretty + easily. + + The reactivation path rebuilt the FastPath processor with + [`bulk_decompressor: + None`](https://github.com/Devolutions/IronRDP/blob/079b4842/crates/ironrdp-client/src/rdp.rs#L988). + After that every compressed update got parsed as a raw bitmap: + + ``` + Received compressed FastPath data but no decompressor is configured + BitmapData decode NotEnoughBytes: received 1662, expected 17134 + ``` + +- Preserve bitmap source stride ([#1486](https://github.com/Devolutions/IronRDP/issues/1486)) ([80bb81b344](https://github.com/Devolutions/IronRDP/commit/80bb81b344dba0197aa7b870c685f398dc4bcaee)) + + ## Summary + + - Preserve `TS_BITMAP_DATA` source stride independently of destination + bounds for raw, Interleaved RLE, and RDP6 bitmap updates. + - Remove raw 4-byte scanline padding without collapsing padded source + columns into following rows. + - Crop only the source extent beyond the destination and reject empty + dimensions explicitly; do not add framebuffer bounds suppression. + - Render decoded RDP6 RGB data top-down and retain bottom-up rendering + for raw and RLE data. + + ## Why this supersedes the overlapping proposals + +- [**breaking**] Always own a bulk decompressor for FastPath updates ([#1255](https://github.com/Devolutions/IronRDP/issues/1255)) ([0dc0194418](https://github.com/Devolutions/IronRDP/commit/0dc0194418375d504a8041b75ba250dc8eeb21ad)) + + ## Summary + + - A compressed FastPath update is dropped whenever the client did not + negotiate compression, because the decompressor is only built when a + compression type was negotiated. Servers send compressed updates + regardless, for example on a full-frame redraw after a resize, and the + session then fails. Closes #1193. + - The negotiated type is the wrong thing to condition on. It describes + what the client would send, and nothing in `ironrdp-session`, + `ironrdp-client`, `ironrdp-web` or the FFI ever compresses outbound. On + the receive path `BulkCompressor` holds a context per algorithm and + `decompress` selects one per update from the packet's own type bits, so + a decompressor built with any type decodes all of them. + - The `Processor` now owns the decompressor and builds it on the first + update that needs one. `ProcessorBuilder` has no corresponding field, so + there is no `None` a consumer can pass and no path that drops a + compressed update. + - On demand rather than at construction because `ironrdp-web` hardcodes + `compression_type: None` in `build_config` and so never negotiates + compression. Constructing eagerly would charge every web session for a + full set of algorithm contexts, and the two XCRUSH history buffers alone + are 2 MB each, for a decompressor most of those sessions never use. That + consumer is also the one most exposed to this bug, for the same reason. + - `BulkCompressor::new` is now infallible. Its only failure path was a + self-check over NCRUSH's static Huffman tables, a compile-time + invariant, now a `debug_assert`. + + ## Relationship to #1474 + + #1474 is kept, not reverted. `ActiveStage::reactivate` is adopted as the + reactivation entry point at all four call sites it introduced: native + client, web, FFI and the e2e test. + + What this PR removes is the `compression_type` retained on `ActiveStage` + and the `make_bulk_decompressor` helper, because an on-demand + decompressor makes both unnecessary. `reactivate` keeps its behaviour + and loses only the compression plumbing. + + #1474 closed the reactivation instance of #1193, where a rebuild passed + `None` and silently disabled decompression for the rest of the session. + The general case is still open on master: when compression was never + negotiated the retained type is `None`, `make_bulk_decompressor` returns + `None`, and every compressed update takes the drop path in + `fast_path.rs` for the lifetime of the session. Conditioning on the + negotiated type gates the ability to receive on what was negotiated to + send, and nothing sends. + + The evidence that removing the field is safe is #1474's own test. + `test_reactivation_processes_compressed_fastpath_updates` passes + unchanged with `compression_type` gone from the builder: the rebuilt + processor decompresses because every processor can, not because a type + was carried across the rebuild. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + The gated regression test is + `testsuite-core/tests/session/fast_path.rs`, which renders the same + bitmap update plain and bulk-compressed through fresh processors and + asserts identical framebuffers. #1474's + `test_reactivation_processes_compressed_fastpath_updates` in + `testsuite-extra` passes unchanged. + + There is also an inline test in `fast_path.rs` pinning the allocation + invariant, that no contexts are built until an update needs them. Note + that `ironrdp-session` sets `[lib] test = false`, so inline tests in + this crate are not run by `cargo test --workspace`; it runs under `cargo + test -p ironrdp-session --lib`. + + ## Notes + + - This addresses the four points from the 2026-06-24 review. Point 4, + that the `Option` is misleading, is the shape of this change: it is gone + from the public API, and the private one that remains carries no + implication that a consumer could choose not to decompress. Point 1, + whether a cold `Rdp61` context decodes `RDP40` and `RDP50` updates + correctly, is a non-issue: `decompress` selects the algorithm per update + through `CompressionType::from_flags` against per-algorithm receive + contexts, so the construction-time type never constrains the receive + path. Point 3, silent degradation if the constructor fails, is removed + by making `new` infallible. Point 2 is the tests above. + - Breaking across two crates, hence the `fix(bulk,session)!` scope: + `ProcessorBuilder` loses `bulk_decompressor`, `ActiveStageBuilder` loses + `compression_type`, and `ironrdp_bulk::BulkCompressor::new` returns + `Self`. + - Incidental: `ironrdp-session` no longer exposes any `ironrdp_bulk` + type in its public API, so that dependency's lack of a `# public` marker + in `Cargo.toml` is now correct. + +- Decode indexed pointers and foreground RLE runs ([#1519](https://github.com/Devolutions/IronRDP/issues/1519)) ([ad19280762](https://github.com/Devolutions/IronRDP/commit/ad192807620bcc3a0467eaeb07173a79cb1da257)) + + ## Summary + + 4bpp and 8bpp New/Large pointer shapes previously could not use the + active session palette, and malformed or unsupported pointer data could + terminate the session. This decodes indexed XOR masks with the current + palette and falls back to the default cursor while evicting stale cached + data when decoding fails. + + It also corrects RLE foreground runs so only set-foreground variants + consume a foreground pixel. + + Palette updates now follow the RDP wire format (type, padding, 256 + packed RGB triplets) and are applied from both fast- and slow-path + updates, ensuring indexed pointer decoding uses the negotiated palette. + + ## Tests + + - `cargo test -p ironrdp-graphics -p ironrdp-session` + - `cargo test -p ironrdp-session palette` + - `cargo clippy -p ironrdp-graphics -p ironrdp-session --all-targets -- + -D warnings` + + --------- + +- Share bulk decompression across output paths ([#1518](https://github.com/Devolutions/IronRDP/issues/1518)) ([6151e21bf5](https://github.com/Devolutions/IronRDP/commit/6151e21bf58b7297e9b4abc2167aa36fc2ba77e4)) + + Bulk compression state is stream-wide, but Fast-Path and slow-path + outputs previously used separate or missing decompression paths. This + could corrupt history-dependent server updates or leave negotiated + slow-path compression undecodable. + + This change owns the negotiated bulk decompressor in `ActiveStage` and + passes it to both X.224 and Fast-Path processing. It retains Share Data + compression metadata through the PDU context, resets decompression + history on reactivation, and initializes consumers from the connection's + negotiated compression type. + + Fast-Path now decompresses each fragment before reassembly so + compression flags apply at packet boundaries. Failures expose bounded + protocol metadata without retaining remote payloads or decoder details. + + Tests cover Share Data metadata propagation, slow-path decompression + behavior, fragmented Fast-Path reassembly and bounded errors, and + compressed Fast-Path updates after reactivation. + + --------- + +- Recover from malformed bitmap updates ([#1521](https://github.com/Devolutions/IronRDP/issues/1521)) ([20e2d414e5](https://github.com/Devolutions/IronRDP/commit/20e2d414e5ac060db25a100ed219f417e19f79b2)) + + ## Summary + - safely discard malformed bitmap and pointer updates without + terminating the session + - request at most one capability-gated full redraw per activation + - propagate Refresh Rect and Suppress Output support through the + connector and generic client + - retain FFI compatibility and focused malformed-update regression + coverage + + ## Stack + Depends on `copilot/fix-session-share-bulk-decompression` (`47270c2a`). + + ## Validation + - `cargo fmt --all -- --check` + - `cargo test -p ironrdp-session --lib` + - `cargo test -p ironrdp-error --lib` + - `cargo check -p ironrdp-connector` + - `cargo check -p ironrdp-client --features native-tls` + - `cargo check -p ffi --features ironrdp/native-tls` + + --------- + +- Preserve RDP6 bitmap orientation ([#1524](https://github.com/Devolutions/IronRDP/issues/1524)) ([8f1737a287](https://github.com/Devolutions/IronRDP/commit/8f1737a28765c0e29bfab1584cd376805b7e174e)) + + ## Summary + - restore bottom-up scanline composition for RDP6 bitmap updates + - add asymmetric raw/RLE bitmap-update regression coverage + - synchronize retained ActiveX DIB updates with GDI and surface copy + failures + + ## Validation + - `cargo test -p ironrdp-session` + - `cargo test -p ironrdp-activex` + - `cargo build -p ironrdp-activex --release` + - verified the real `mstscex.exe -> mstsc.exe -> MsRdpEx.dll -> + ironrdpax.dll` path renders the authorized test desktop upright without + bands + + --------- + +- [**breaking**] Replace DVC wrappers with typed accessors ([#1377](https://github.com/Devolutions/IronRDP/issues/1377)) ([d43ecf9a54](https://github.com/Devolutions/IronRDP/commit/d43ecf9a54363d37e0c485a1e9e73da0d47ae540)) + + Follow-up to #1368. This is not urgent; review whenever the DVC API + direction is worth revisiting. + + Rework DVC channel access APIs so callers can recover a typed processor + together with its dynamic channel id, without exposing internal channel + wrapper types. + + - Add typed borrowed DVC accessors carrying both channel id and + processor borrow for `DrdynvcClient`. + - Keep dynamic channel wrapper types private. + - Align client listener/registration APIs on `DvcClientProcessor`. + +- Batch Fast-Path input events ([#1630](https://github.com/Devolutions/IronRDP/issues/1630)) ([3818b48037](https://github.com/Devolutions/IronRDP/commit/3818b480375ec9411d5319d8e7af161f1d662cbf)) + + Keep outgoing Fast-Path input frames within the 255-event protocol limit + and preserve their order across FFI. + +- Handle MCS Disconnect Provider Ultimatum ([#1692](https://github.com/Devolutions/IronRDP/issues/1692)) ([75ea3b96f0](https://github.com/Devolutions/IronRDP/commit/75ea3b96f08a59e3b70fb75fedc4eb358aa8682c)) + + Some servers (xrdp) end a session by sending a Disconnect Provider + Ultimatum on its own, not wrapped in a Send Data Indication. We only + decoded Send Data Indications, so a clean session end came out as a + protocol error. + + Check for it when the Send Data Indication decode fails and return a + Disconnect instead. + +- Decode concatenated Share Control PDUs on I/O channel ([#1861](https://github.com/Devolutions/IronRDP/issues/1861)) ([2971839d59](https://github.com/Devolutions/IronRDP/commit/2971839d59e5269b511deba4e17868bc4710db84)) + + Win7 and other legacy servers may concatenate multiple Share Control + PDUs in one MCS Send Data Indication. Walk each PDU using the Share + Control Header totalLength field, and fall back to whole-buffer decode + when the first length is unusable. + + This is one of three independent follow-ups from cxxzhang's work in + #1408. + +- Rfx decoder incorrectly computes update region for multi-monitor sessions ([#1982](https://github.com/Devolutions/IronRDP/issues/1982)) ([ea5f247174](https://github.com/Devolutions/IronRDP/commit/ea5f247174eb4af18c3f9150c4263c7fbc785759)) + + I came across this issue while prototyping multi-monitor support in the + Teleport client.`clipping_rectangles` incorrectly clamps the incoming + frame's right/bottom bounds to the channel width/height. This may work + for single monitor sessions, but fails in the multi-monitor use case + because frame updates where the framebuffer's dimensions exceed a given + frame's dimensions. The result is that only the primary display renders + correctly correctly for sessions with multiple monitors. + +- [**breaking**] Serve channels and graphics that Windows moves onto a tunnel ([#2008](https://github.com/Devolutions/IronRDP/issues/2008)) ([bfd16a2e55](https://github.com/Devolutions/IronRDP/commit/bfd16a2e5573d4cd6617dd71b2802b4e7b944a59)) + + Once Soft-Sync has moved dynamic channels onto the reliable UDP tunnel, + Windows keeps using the tunnel for more than channel data. Two gaps kept + the graphics pipeline from working there. + + + ## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.10.0...ironrdp-session-v0.11.0)] - 2026-07-10 ### Security diff --git a/crates/ironrdp-session/Cargo.toml b/crates/ironrdp-session/Cargo.toml index 3657cf87a8..dfcf2c3870 100644 --- a/crates/ironrdp-session/Cargo.toml +++ b/crates/ironrdp-session/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-session" -version = "0.11.0" +version = "0.12.0" readme = "README.md" description = "State machines to drive an RDP session" edition.workspace = true @@ -23,15 +23,15 @@ qoiz = ["dep:zstd-safe", "qoi"] __test = ["dep:visibility"] [dependencies] -ironrdp-bulk = { path = "../ironrdp-bulk", version = "0.1" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public +ironrdp-bulk = { path = "../ironrdp-bulk", version = "0.2" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public -ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["std"] } # public -ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.8" } -ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.3" } +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["std"] } # public +ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.9" } +ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.4" } ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1" } tracing = { version = "0.1", features = ["log"] } qoicoubeh = { version = "0.5", optional = true } diff --git a/crates/ironrdp-str/CHANGELOG.md b/crates/ironrdp-str/CHANGELOG.md index 1bae5ef861..a3d93c7743 100644 --- a/crates/ironrdp-str/CHANGELOG.md +++ b/crates/ironrdp-str/CHANGELOG.md @@ -6,6 +6,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-str-v0.1.1...ironrdp-str-v0.2.0)] - 2026-09-30 + +### Features + +- [**breaking**] Populate decode/encode error offsets from cursor positions ([#1275](https://github.com/Devolutions/IronRDP/issues/1275)) ([8607ac5d1c](https://github.com/Devolutions/IronRDP/commit/8607ac5d1c2ea14efcac02921e54d951ab1045ec)) + + ## Summary + + The workspace sweep that follows #1266. Decode and encode error + construction sites now pass the cursor, so the reported position is the + byte the decoder or encoder actually stopped at. + + Stacked on #1266 and merges after it. + + ## What "no position" means here + + #1266 makes `offset` an `Option` where `None` means the error has + no position in the input stream at all, rather than a position that + happened to be unavailable. This PR is the other half of that: it walks + the workspace and gives a real position to every site that has one, so + the sites left reporting `None` are the ones that genuinely never had + one. + + Those are constructors validating their arguments, integer conversions, + cache lookups that missed, accessors on already-decoded structures, and + the declared-size checks described below. They report nothing rather + than byte zero, and that is now their permanent answer rather than a gap + awaiting another sweep. + + There are no `at: 0` sites left anywhere in the workspace. + + ## The rule + + The position is attached where the cursor identifies the bytes being + complained about. It is omitted where the complaint is about a size the + peer declared, computed from data already consumed, because there the + cursor points at a byte that is not the problem. + + + ## [[0.1.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-str-v0.1.0...ironrdp-str-v0.1.1)] - 2026-05-27 ### Build diff --git a/crates/ironrdp-str/Cargo.toml b/crates/ironrdp-str/Cargo.toml index 33128a55c4..2a0a90ed41 100644 --- a/crates/ironrdp-str/Cargo.toml +++ b/crates/ironrdp-str/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-str" -version = "0.1.1" +version = "0.2.0" description = "Typed wire-aware string primitives for RDP protocol fields" edition.workspace = true rust-version = "1.94" @@ -18,7 +18,7 @@ alloc = ["ironrdp-core/alloc", "bytemuck/extern_crate_alloc"] [dependencies] bytemuck = { version = "1", default-features = false } -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } [lints] workspace = true diff --git a/crates/ironrdp-svc/CHANGELOG.md b/crates/ironrdp-svc/CHANGELOG.md index 4aa33691e9..5fc9f6cef7 100644 --- a/crates/ironrdp-svc/CHANGELOG.md +++ b/crates/ironrdp-svc/CHANGELOG.md @@ -6,6 +6,64 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-svc-v0.8.0...ironrdp-svc-v0.9.0)] - 2026-09-30 + +### Features + +- Support runtime-defined static virtual channels ([#1517](https://github.com/Devolutions/IronRDP/issues/1517)) ([8b4c483ba0](https://github.com/Devolutions/IronRDP/commit/8b4c483ba0c900a8de0b2718347754f56dd363ba)) + + ## Summary + - add keyed runtime-defined static-channel registration, lookup, and + negotiated ID attachment + - enforce the static-channel limit and reject malformed SVC fragment + sequences + - wire generic connector, acceptor, and session name-based dispatch + support + + ## Testing + - `cargo test -p ironrdp-testsuite-core --test integration_tests_core + svc::` + - `cargo clippy -p ironrdp-testsuite-core --test integration_tests_core + -- -D warnings` + + --------- + +- Negotiate static channel chunk sizing ([#1622](https://github.com/Devolutions/IronRDP/issues/1622)) ([4e3903fbbe](https://github.com/Devolutions/IronRDP/commit/4e3903fbbef2904505f35e3437a7106807ac5987)) + + Use the validated server VCChunkSize for outgoing static virtual channel + data and retain 1600-byte chunks when it is absent or invalid. + + Apply refreshed values after reactivation across native, web, and FFI + active stages while preserving channel flags. + +- [**breaking**] Record byte offset on decode and encode error variants ([#1266](https://github.com/Devolutions/IronRDP/issues/1266)) ([a1f9189c30](https://github.com/Devolutions/IronRDP/commit/a1f9189c307516361a8faff6ecb7c1690b267998)) + + ## Summary + + Records a byte offset on every `DecodeErrorKind` and `EncodeErrorKind` + variant that can know one, so decode and encode errors surface the + position in the input stream where the failure was detected. Reshaped + twice after review; see "Review history" below if you reviewed an + earlier shape. + + Contributes to the structured-fuzzing roadmap in #1120 by giving + crash-replay analysis and Wireshark-style malformed-PDU reporting the + byte-offset dimension that source `Location` ([#1262](https://github.com/Devolutions/IronRDP/issues/1262)) alone does not + provide. + + ## API + + Variants that gain `offset: Option`: + + - `DecodeErrorKind::NotEnoughBytes { received, expected, offset }` + - `DecodeErrorKind::InvalidField { field, reason, offset }` + - `DecodeErrorKind::UnexpectedMessageType { got, offset }` + - `DecodeErrorKind::UnsupportedVersion { got, offset }` + - `DecodeErrorKind::UnsupportedValue { name, value, offset }` + - `EncodeErrorKind` mirrors the same shape for the encode side + + + ## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-svc-v0.7.0...ironrdp-svc-v0.8.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-svc/Cargo.toml b/crates/ironrdp-svc/Cargo.toml index 3dfd276013..275a65d894 100644 --- a/crates/ironrdp-svc/Cargo.toml +++ b/crates/ironrdp-svc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-svc" -version = "0.8.0" +version = "0.9.0" readme = "README.md" description = "IronRDP traits to implement RDP static virtual channels" edition.workspace = true @@ -21,8 +21,8 @@ default = [] std = [] [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["alloc", "std"] } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["alloc", "std"] } # public bitflags = "2.11" [lints] diff --git a/crates/ironrdp-tls/CHANGELOG.md b/crates/ironrdp-tls/CHANGELOG.md index 7d50b6b2a8..ac84a44fc6 100644 --- a/crates/ironrdp-tls/CHANGELOG.md +++ b/crates/ironrdp-tls/CHANGELOG.md @@ -6,6 +6,80 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-tls-v0.2.2...ironrdp-tls-v0.2.3)] - 2026-09-30 + +### Features + +- Apply certificate validation to gateways ([#1775](https://github.com/Devolutions/IronRDP/issues/1775)) ([312d4466e7](https://github.com/Devolutions/IronRDP/commit/312d4466e7cae15d45c73a4ffcb4ae730d5e6a30)) + + Apply the RDP client's certificate-validation policy and callback to + every + gateway HTTPS connection. + + Existing gateway callers retain their compatibility default. + +### Bug Fixes + +- Make certificate validation explicit ([#1520](https://github.com/Devolutions/IronRDP/issues/1520)) ([f1d53c78d3](https://github.com/Devolutions/IronRDP/commit/f1d53c78d390de1c6778773cdc859d59901466f0)) + + IronRDP deployments commonly use self-signed or private-CA certificates. + This keeps the historical permissive behavior for unmodified callers + while making platform-root and server-name validation an explicit + opt-in. + + ## Approach + + - Keep `upgrade` and `ConfigBuilder` defaults compatible with existing + self-signed endpoints, including the prior native-TLS SNI behavior. + - Expose `CertificateValidation::Strict` for callers that require normal + certificate-chain and hostname validation. + - Retain the Rustls callback path for certificate pinning or other + explicit exception decisions; configuring a callback selects strict + validation before invoking it. + - Preserve CredSSP's existing public-key binding and disabled TLS + resumption behavior. + + MS-CSSP section 3.1.5 does not require a common trusted CA root and + permits servers to use self-signed certificates, so strict verification + cannot be introduced as a transparent default. + + ## Validation + + - `cargo xtask check fmt -v` + - `cargo xtask check lints -v` + - Focused Rustls default/strict/callback runtime test + - Focused native-TLS default/strict runtime test + + --------- + +- Harden TLS sideband setup ([#1811](https://github.com/Devolutions/IronRDP/issues/1811)) ([7292970955](https://github.com/Devolutions/IronRDP/commit/729297095564a9640183556078cf8726dd3afeed)) + + Extract the reusable rustls verifier and client-config builder so the + RDPEUDP2 reliable-UDP TLS sideband shares the primary transport's + certificate-validation policy and callback semantics without selecting a + TLS stream backend. + + Build callback-free client configurations on the blocking pool, and run + callback-based handshakes on a dedicated blocking thread, so synchronous + platform trust-store access and certificate decisions cannot starve a + current-thread RDPEUDP driver. Bound the TLS and RDPEMT establishment + phases with dedicated timeout errors. A TLS timeout cancels the + connection attempt but cannot cancel a synchronous certificate callback + that is already running. + + Close `SharedIo` and wake parked readers when a driver is dropped before + releasing its stream, preventing aborted connection attempts from + stranding detached TLS work. + + Pass `UdpTlsConfig` directly through `MultitransportBootstrap::connect`, + and document that `S_OK` is sent only after Soft-Sync negotiation. + +### Build + +- Bump the crypto group across 1 directory with 3 updates ([#1449](https://github.com/Devolutions/IronRDP/issues/1449)) ([e1725e8c8a](https://github.com/Devolutions/IronRDP/commit/e1725e8c8a581b83835647b6ee563a5b3f6c7a1b)) + + + ## [[0.2.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-tls-v0.2.1...ironrdp-tls-v0.2.2)] - 2026-07-10 ### Features diff --git a/crates/ironrdp-tls/Cargo.toml b/crates/ironrdp-tls/Cargo.toml index 0592b510af..a515a5ca16 100644 --- a/crates/ironrdp-tls/Cargo.toml +++ b/crates/ironrdp-tls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-tls" -version = "0.2.2" +version = "0.2.3" readme = "README.md" description = "TLS boilerplate common with most IronRDP clients" edition.workspace = true diff --git a/crates/ironrdp-tokio/CHANGELOG.md b/crates/ironrdp-tokio/CHANGELOG.md index 893bfda462..dc09943063 100644 --- a/crates/ironrdp-tokio/CHANGELOG.md +++ b/crates/ironrdp-tokio/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.10.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.10.0...ironrdp-tokio-v0.10.1)] - 2026-09-30 + + + ## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-tokio-v0.9.0...ironrdp-tokio-v0.10.0)] - 2026-07-10 ### Build diff --git a/crates/ironrdp-tokio/Cargo.toml b/crates/ironrdp-tokio/Cargo.toml index eefdb458f5..de2b18f4f4 100644 --- a/crates/ironrdp-tokio/Cargo.toml +++ b/crates/ironrdp-tokio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-tokio" -version = "0.10.0" +version = "0.10.1" readme = "README.md" description = "`Framed*` traits implementation above Tokio’s traits" edition.workspace = true @@ -23,8 +23,8 @@ reqwest-rustls-ring = ["reqwest", "reqwest?/rustls-tls-webpki-roots"] reqwest-native-tls = ["reqwest", "reqwest?/native-tls"] [dependencies] -ironrdp-async = { path = "../ironrdp-async", version = "0.10" } # public -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10", optional = true } +ironrdp-async = { path = "../ironrdp-async", version = "0.11" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11", optional = true } tokio = { version = "1", features = ["io-util"] } reqwest = { version = "0.12", default-features = false, features = ["http2", "system-proxy"], optional = true } url = { version = "2.5", optional = true } diff --git a/crates/ironrdp-usb/CHANGELOG.md b/crates/ironrdp-usb/CHANGELOG.md new file mode 100644 index 0000000000..c7f8b416db --- /dev/null +++ b/crates/ironrdp-usb/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-usb-v0.1.0)] - 2026-09-30 + +### Features + +- Add the ironrdp-usb crate ([#1682](https://github.com/Devolutions/IronRDP/issues/1682)) ([cec0c1487f](https://github.com/Devolutions/IronRDP/commit/cec0c1487fad817435a9a70de387d2c57eb9caad)) + + Introduce a protocol-independent, `no_std`, sans-I/O crate holding + USB-standard data structures and semantics that are shared by any USB + redirection transport adapter: + + - typed USB values and identifiers; + - descriptor storage, parsing, validation, and traversal for device, + configuration, interface, and endpoint descriptors (`descriptor`); + - standard control requests and setup packets (`control`); + - endpoint identity, addressing, and companion metadata (`endpoint`); + - transfer direction, type, status, and request/result data + (`transfer`). + + The crate is dependency-free, borrows its parsing input, and is generic + over caller-owned buffer storage, so it never allocates. It describes + USB operations and data but does not execute them: device I/O, async + runtime integration, request routing, and pending-request management all + stay in the consuming protocol layers. + + **Parsing is restricted to byte layouts defined by USB itself**. RDPEUSB + PDUs, usbredir packets, and other transport framing belong to their + respective crates. + + Descriptor parsing is a hostile-input surface, so the tests in + `ironrdp-testsuite-core` cover the framing, length, and topology + boundaries rather than the internals: `bLength` framing, minimum and + trailing-byte rules, `wTotalLength` completeness, the configuration + topology rules enforced by `validate`, setup packet size and field + partitioning, and the endpoint address and packet-size encodings. + +- [**breaking**] Support SuperSpeed packet sizes ([#1883](https://github.com/Devolutions/IronRDP/issues/1883)) ([8fd0b174a9](https://github.com/Devolutions/IronRDP/commit/8fd0b174a969d8166053d5cdea59b923e4dc4cd5)) + + `ironrdp-usb` could not describe a SuperSpeed device: + `is_valid_for_usb2` rejected every SuperSpeed endpoint, and + `endpoint_zero_max_packet_size` required a bus speed to read + `bMaxPacketSize0`. + + That speed parameter added nothing. USB 3.2 Section 9.6.1 admits only 9 + there, as a base-2 exponent, while USB 2.0 admits only 8, 16, 32 and 64, + stated literally, so the two encodings are disjoint and the value + identifies itself. Requiring a speed only let a caller contradict the + descriptor, which is exactly what a transport that cannot express + SuperSpeed ends up doing. + + Endpoint zero now decodes from the value alone, and the endpoint + predicate carries the USB 3.2 Section 9.6.6 limits, so + `is_valid_for_usb2` and `usb2_max_payload` lose their USB 2.0 names. + + diff --git a/crates/ironrdp-viewer/CHANGELOG.md b/crates/ironrdp-viewer/CHANGELOG.md index d146d9388d..6dc97e5a0e 100644 --- a/crates/ironrdp-viewer/CHANGELOG.md +++ b/crates/ironrdp-viewer/CHANGELOG.md @@ -6,6 +6,293 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-viewer-v0.1.0...ironrdp-viewer-v0.2.0)] - 2026-09-30 + +### Features + +- Expose generic session configuration and lifecycle APIs ([#1522](https://github.com/Devolutions/IronRDP/issues/1522)) ([57b1366650](https://github.com/Devolutions/IronRDP/commit/57b13666506dc40c15b4c4702d35150beee99133)) + + ## Summary + - expose generic client configuration for connection metadata, + compression, shell/work directory, audio, and runtime static-channel + factories + - add bounded input delivery with independent close cancellation, host + clipboard plumbing, lifecycle events, and Display Control resize + readiness/fallback handling + - update agent, viewer, web, FFI, examples, and tests for the generic + APIs + + ## Stack dependencies + This PR is stacked on `copilot/tls-validation-policy` (`b2bbcece`), + which already includes the merged runtime static-channel support from + `master`. It intentionally contains no TLS implementation/policy, + ActiveX/COM, SVC implementation, decompression, or bitmap-recovery + changes. + + ## Validation + - `cargo fmt --check --all` + - `cargo xtask check tests --no-run -v` + - `cargo xtask check lints -v` + - `cargo test -p ironrdp-client --lib --features rustls` + - `cargo check -p ironrdp-agent -p ironrdp-viewer -p ironrdp-web -p ffi` + + --------- + +- Add IronRDP ActiveX COM server ([#1523](https://github.com/Devolutions/IronRDP/issues/1523)) ([ee58b7c5f2](https://github.com/Devolutions/IronRDP/commit/ee58b7c5f283ef64be93a8483242f49070c6cdc9)) + + ## Summary + - Add the IronRDP ActiveX COM server and native MSTSC host integration. + - Provide bounded native-host diagnostics, credential-bridge support, + and an AxHost test harness. + - Preserve the minimal client, connector, and error integrations needed + by the control. + + ## Validation + - cargo check -p ironrdp-activex + - Focused ironrdp-error and ironrdp-connector tests + - cargo test -p ironrdp-activex --lib --no-run + - cargo fmt --all -- --check + - cargo xtask check locks -v + + --------- + +- Host agent RPC endpoint ([#1545](https://github.com/Devolutions/IronRDP/issues/1545)) ([358ca8d4f3](https://github.com/Devolutions/IronRDP/commit/358ca8d4f382e49251f91243b6d9e5488c2dface)) + + ## Summary + - host the existing `ironrdp-agent` RPC contract from `ironrdp-viewer + --rpc` + - retain one shared daemon state for GUI and RPC input, frames, session + lifecycle, screenshots, logs, and NOW operations + - use the agent's default endpoint so `ironrdp-agent` itself remains + unchanged + + ## Usage + Start `ironrdp-viewer --rpc` before invoking `ironrdp-agent`; the viewer + owns the usual agent endpoint until its window closes. Use matching + explicit `--rpc-endpoint` and agent `--endpoint` values for a custom + endpoint. + + ## Validation + - `cargo fmt --check` + - `cargo test -p ironrdp-viewer` + - `cargo test -p ironrdp-daemon -p ironrdp-agent --lib` + - `cargo check -p ironrdp-viewer -p ironrdp-daemon -p ironrdp-agent` + +- Hyper-V vmconnect support ([#1503](https://github.com/Devolutions/IronRDP/issues/1503)) ([a7cc067d50](https://github.com/Devolutions/IronRDP/commit/a7cc067d5069cbbcb13bae3e0561c0611da3bcf6)) + + Adds Hyper-V VMConnect's direct ordering: PCB → TLS → CredSSP → X.224. + + Enhanced Session is the default (`GUID;EnhancedMode=1`), with + `--vmconnect-basic` for the synthetic console. Kept this separate in + `ironrdp-vmconnect`; no SPN changes. + + Tested against the nested Hyper-V lab: + - Enhanced: `HYBRID_EX`, rendered 1280×720 + - Basic: `HYBRID`, rendered 1280×720 + - `cargo xtask check fmt/lints/tests -v` + + --------- + +- Add RemoteApp channel support ([#1637](https://github.com/Devolutions/IronRDP/issues/1637)) ([ab48c6cb8c](https://github.com/Devolutions/IronRDP/commit/ab48c6cb8c017504f8a92799aeb91b821c50a13a)) + + Configure and negotiate RAIL connections, then route its static channel + through the portable client with bounded request queues and server + control events. + +- Project RemoteApp windows ([#1641](https://github.com/Devolutions/IronRDP/issues/1641)) ([f5554f40dc](https://github.com/Devolutions/IronRDP/commit/f5554f40dc280d93506ea8352e3992e641d58e96)) + + Project server-authoritative RAIL windows for an enabled ActiveX + RemoteApp session and launch the configured program after RAIL becomes + available. + + Forward validated opaque windowing orders to the worker, maintain their + basic HWND lifecycle, and retain windows until the server removes them. + Leave desktop behavior and unsupported shell features unchanged. + +- Add RAIL audit commands ([#1646](https://github.com/Devolutions/IronRDP/issues/1646)) ([77759dca03](https://github.com/Devolutions/IronRDP/commit/77759dca032eb829b5be54a3c44d9be92252cf41)) + + Expose bounded client-validated RAIL events and RemoteApp launch + requests through the daemon IPC so headless agents can verify sessions. + + Preserve cursors across resize reconnects, wake waiting readers for + locally queued launches, and redact launch data in logs. + + Report terminal local Execute failures without disrupting an otherwise + valid RDP session. + +- Add native MS-RDPEWA WebAuthn redirection ([#1644](https://github.com/Devolutions/IronRDP/issues/1644)) ([66da78bc4e](https://github.com/Devolutions/IronRDP/commit/66da78bc4e6b37a7780dbf9f333234be63d96afb)) + + Implement the RDPEWA dynamic channel with a Windows WebAuthn backend and + wire RedirectWebAuthn for ActiveX, the optional client feature, and the + viewer CLI. + + Prefer System32\webauthn.dll via the DVC COM plugin for MSTSC parity. + The pure-Rust backend forwards ceremonies through a webauthn.dll IWTS + oneshot so hash-only hosts that omit clientDataJSON still work; public + WebAuthN* remains a fallback when JSON is present. Recreate + WebAuthN_Channel opens through shared COM/listener factories because + Windows opens and closes the channel around each RPC. + + Side effects: + - New crates ironrdp-rdpewa and ironrdp-rdpewa-native + - Config key redirectwebauthn; ActiveX ExtendedSettings property + - ironrdp-daemon webauthn feature for ironrdp-agent + - IRONRDP_WEBAUTHN_FORCE_NATIVE debug switch + - Viewer --webauthn/--no-webauthn flags; .rdp redirectwebauthn default + - ActiveX docs note no AdvancedSettings slot and no IPersist persistence + +- Support automatic reconnect ([#1662](https://github.com/Devolutions/IronRDP/issues/1662)) ([34d7b31dc5](https://github.com/Devolutions/IronRDP/commit/34d7b31dc5039273dc5502d0ca3301f256eb6154)) + + Support automatic reconnect through the ActiveX control with bounded + host-approved retries after transport loss. + + Reuse ARC cookies only for resumed sessions, reject server ARC status + failures, and report success after active-session traffic. + + --------- + +- Enable Windows smartcard in product surfaces ([#1672](https://github.com/Devolutions/IronRDP/issues/1672)) ([1561a4c327](https://github.com/Devolutions/IronRDP/commit/1561a4c327512a1a3a6c618d4b06dc388fab132f)) + + Wire daemon, agent, viewer, and ActiveX to the WinSCard RDPDR backend so + products can announce a smartcard device only with a matching factory. + + Expose `WindowsRdpdrBackendFactory::with_smartcard`, daemon/agent + `--smartcard` and `ironrdp_smartcard` connect overrides, viewer + `--smartcard`, and ActiveX `RedirectSmartCards`. Smartcard-only RDPDR is + valid on Windows; non-Windows paths refuse or hard-disable enablement. + +- Negotiate monitor topology ([#1675](https://github.com/Devolutions/IronRDP/issues/1675)) ([063efcdc30](https://github.com/Devolutions/IronRDP/commit/063efcdc3088d8f44e423cc322077d40bf9aadf2)) + + Negotiate the client monitor layout from UseMultimon and expose the + confirmed remote topology through the ActiveX compatibility interface. + + Advertise Monitor Layout PDU support whenever Extended Client Data is + negotiated, and forward layouts from activation, active sessions, and + reactivation so advertised support does not terminate sessions. + + Keep fallback reporting truthful when servers do not honor the request, + while preserving single-monitor resize behavior and blocking + multi-monitor resizing. + + Do not send Client Monitor Extended Data; per-monitor DPI and + orientation remain unavailable. + +- Try a direct connection before the RD Gateway ([#1713](https://github.com/Devolutions/IronRDP/issues/1713)) ([a67dbf47e4](https://github.com/Devolutions/IronRDP/commit/a67dbf47e429a73b29e2b36966b6dc5b870625cd)) + +- Add /microphone command-line switch ([#1790](https://github.com/Devolutions/IronRDP/issues/1790)) ([a06478227e](https://github.com/Devolutions/IronRDP/commit/a06478227e027d60db21a77c32f0ab4be348bbdb)) + + Expose a `--microphone` CLI flag in ironrdp-viewer to enable the + AUDIO_INPUT (MS-RDPEAI) dynamic virtual channel, redirecting the local + microphone into the RDP session. It wires into the existing + `ConfigBuilder::with_audio_capture(true)` and is off by default, + matching `ChannelConfig::default().audio_capture == false`. + + The switch name is in pair with FreeRDP's own `/microphone` option, for + consistency. + +- Add current-user VMConnect ([#1771](https://github.com/Devolutions/IronRDP/issues/1771)) ([9a87c26585](https://github.com/Devolutions/IronRDP/commit/9a87c2658547123ef37f6809e60bd1f7b37d9f7a)) + + Expose current-user Hyper-V VMConnect authentication through the viewer + CLI. Document the local Windows invocation while preserving existing + credential requirements for ordinary RDP sessions. + +- [**breaking**] Add drop-policy classification for RdpOutputEvent ([#1815](https://github.com/Devolutions/IronRDP/issues/1815)) ([b0d5ca88a8](https://github.com/Devolutions/IronRDP/commit/b0d5ca88a8c77dc8b49dbde9392d36a989a5a990)) + + ## Summary + +- Present EGFX dirty regions ([#1874](https://github.com/Devolutions/IronRDP/issues/1874)) ([2b44c62bdb](https://github.com/Devolutions/IronRDP/commit/2b44c62bdbd997f4aa33bdbaf4749ac4ecc85140)) + + Propagate validated ResetGraphics extents into the session framebuffer + and deliver exact packed desktop damage to ActiveX without rebuilding + full image snapshots. + + Coalesce only fully covered regions, preserve sparse updates with a + 64-event and 256 MiB pixel-data budget, and backpressure without + evicting accepted frame or static-channel payloads. Update retained GDI + and RPC surfaces in place. Reuse framebuffer storage with fallible + growth, and retain software cursor shape, position, visibility, and + clipping across graphics resets. Keep V8 non-AVC negotiation and + full-frame output fallback unchanged. Rejected oversized resets still + destroy prior surfaces and are reported as unsupported without + allocating a session framebuffer. + +### Bug Fixes + +- Make source locations opt-in ([#1480](https://github.com/Devolutions/IronRDP/issues/1480)) ([f84cd01450](https://github.com/Devolutions/IronRDP/commit/f84cd01450e18d12838b225859878b311802b805)) + + Default error display omits locations; alternate formatting and reports + with explicit location opt-in preserve diagnostic context. + +- [**breaking**] Accept the full documented range of Client Core Data keyboardType values ([#1689](https://github.com/Devolutions/IronRDP/issues/1689)) ([c74e7c5c94](https://github.com/Devolutions/IronRDP/commit/c74e7c5c9431c87e57a1550b037867d235c1d362)) + + ## Summary + + KeyboardType (TS_UD_CS_CORE's keyboardType field, MS-RDPBCGR 2.2.1.3.2) + was a closed enum covering discriminants 1 through 7, transcribed from + the field's other documentation in 2.2.7.1.6 (TS_INPUT_CAPABILITYSET), + which omits value 8 (Korean keyboard). The table this type actually + decodes against, 2.2.1.3.2, documents 1 through 8. Any client outside + the narrower range, including every genuine Korean keyboard, had its + whole Client Core Data parse hard rejected before the connection + started. + + Converted KeyboardType to a repr(transparent) struct KeyboardType(pub + u32) with named constants for the eight documented values, mirroring + RdpVersion one field above it in the same struct, and made decode + infallible at all three sites that previously disagreed with each other: + gcc::core_data::client hard rejected, rdp::capability_sets::input + silently dropped to None, and ironrdp-activex's COM setter returned + E_INVALIDARG. + + FreeRDP hit the identical gap in its own documentation-only enum until a + two-line fix (FreeRDP#11035). Neither FreeRDP nor xrdp gate connection + admission on this field at all; both decode it as a raw integer. + Windows' own GetKeyboardType additionally documents 0x51 for generic HID + keyboards, which a narrower fix adding only Korean would not survive. + + Marked as breaking since KeyboardType's public shape changes for any + downstream consumer of ironrdp-pdu outside this workspace. Note that + cargo-semver-checks in this repo's PR automation only runs against the + facade ironrdp crate, which will not see a break confined to + ironrdp-pdu's own API, so the automated breaking-change label may not + apply here even though this is a real one. + + ## Validation + + cargo xtask check fmt/lints/tests/typos/locks all pass. Extended + ironrdp-activex's existing inline COM boundary test to cover value 8 + round tripping and a negative value still being rejected. + +- Map navigation scan codes ([#1807](https://github.com/Devolutions/IronRDP/issues/1807)) ([f427d8994e](https://github.com/Devolutions/IronRDP/commit/f427d8994e4f68145d6280a913f161abe1d664eb)) + + Translate the affected portable winit key codes into the PC/AT set-1 + scan codes and extended flags required by RDP. Keep platform-native + conversion for unaffected keys. + +- Map native physical keycodes ([#1929](https://github.com/Devolutions/IronRDP/issues/1929)) ([a59b609e16](https://github.com/Devolutions/IronRDP/commit/a59b609e16dd20d0a8c45126d844ba17ad535996)) + + Translate winit physical keys to PC/AT set-1 scancodes before sending + RDP so macOS and Linux native codes are not interpreted as RDP codes. + Retain Winit's layout-specific Korean `Lang1` and `Lang2` mappings on + Windows, including their E0 scancodes. + + Map extended media, browser, launch, volume, power, and editing keys + explicitly. Unsupported or unidentified keys are ignored rather than + forwarded. Pause remains unsupported because it requires a multi-event + `EXTENDED1` sequence that the ordinary key path cannot represent. + +- Exit reliably after RDP disconnects ([#1989](https://github.com/Devolutions/IronRDP/issues/1989)) ([33ee8123ec](https://github.com/Devolutions/IronRDP/commit/33ee8123eceeb94157603cbe5bd7d8919cb32b03)) + + The RDP client can finish before the task forwarding its final failure + or termination event runs. Dropping the Tokio runtime then cancels that + task, leaving the viewer open with no active session. + + Wait for the output task to finish before dropping the runtime so the + GUI receives the event and closes. Log an unexpected forwarding-task + failure. + + + ## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-viewer-v0.1.0)] - 2026-07-10 Initial release. diff --git a/crates/ironrdp-viewer/Cargo.toml b/crates/ironrdp-viewer/Cargo.toml index 31d9462f26..b92633430a 100644 --- a/crates/ironrdp-viewer/Cargo.toml +++ b/crates/ironrdp-viewer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-viewer" -version = "0.1.0" +version = "0.2.0" readme = "README.md" description = "Portable RDP viewer (GUI binary) without GPU acceleration" edition.workspace = true @@ -30,8 +30,8 @@ qoiz = ["ironrdp/qoiz"] [dependencies] ironrdp-daemon = { path = "../ironrdp-daemon", version = "0.1" } # public ironrdp-rpc = { path = "../ironrdp-rpc", version = "0.1" } -ironrdp = { path = "../ironrdp", version = "0.17", features = ["connector", "cliprdr", "input", "pdu", "client", "client-all", "client-vmconnect"] } -ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.1" } +ironrdp = { path = "../ironrdp", version = "0.18", features = ["connector", "cliprdr", "input", "pdu", "client", "client-all", "client-vmconnect"] } +ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" } ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } ironrdp-rdpfile = { path = "../ironrdp-rdpfile", version = "0.1" } diff --git a/crates/ironrdp-vmconnect/CHANGELOG.md b/crates/ironrdp-vmconnect/CHANGELOG.md new file mode 100644 index 0000000000..b3697e8f9c --- /dev/null +++ b/crates/ironrdp-vmconnect/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-vmconnect-v0.1.0)] - 2026-09-30 + +### Features + +- Hyper-V vmconnect support ([#1503](https://github.com/Devolutions/IronRDP/issues/1503)) ([a7cc067d50](https://github.com/Devolutions/IronRDP/commit/a7cc067d5069cbbcb13bae3e0561c0611da3bcf6)) + + Adds Hyper-V VMConnect's direct ordering: PCB → TLS → CredSSP → X.224. + + Enhanced Session is the default (`GUID;EnhancedMode=1`), with + `--vmconnect-basic` for the synthetic console. Kept this separate in + `ironrdp-vmconnect`; no SPN changes. + + Tested against the nested Hyper-V lab: + - Enhanced: `HYBRID_EX`, rendered 1280×720 + - Basic: `HYBRID`, rendered 1280×720 + - `cargo xtask check fmt/lints/tests -v` + + --------- + +- Support Hyper-V connection ordering ([#1505](https://github.com/Devolutions/IronRDP/issues/1505)) ([5c1816244e](https://github.com/Devolutions/IronRDP/commit/5c1816244e83187a04249e9d9c240d096cb78f55)) + + Hyper-V over RDCleanPath needs PCB → TLS on the proxy, then CredSSP → + X.224 on the client. Ordinary RDCleanPath stays X.224-first. + + Still VERSION_1 with the same DER fields. An explicit VMConnect request + carries a Unicode PCB payload in `preconnection_blob` with no X.224; the + proxy encodes the binary PCB. Generic PCB requests keep their existing + X.224-first behavior. + + Gateway reference implementation: + [Devolutions/devolutions-gateway#1372](https://github.com/Devolutions/devolutions-gateway/pull/1372) + + Checked locally: Rust builds, formatting, Svelte typecheck, and .NET + build. Real nested Hyper-V E2E through Gateway: Native rendered 18 + frames, Avalonia connected and rendered its first frame, and Web + rendered a non-empty 1280×720 canvas. + + --------- + +- Add current-user CredSSP ([#1766](https://github.com/Devolutions/IronRDP/issues/1766)) ([70b9a37aab](https://github.com/Devolutions/IronRDP/commit/70b9a37aab79a2858eb8580797ca72577db3390b)) + + Authenticate Hyper-V hosts with the caller's Windows logon token through + native SSPI. Keep reusable host passwords out of process configuration + and require nonce-bound CredSSP public-key verification. + +- Implement framebuffer channel ([#1767](https://github.com/Devolutions/IronRDP/issues/1767)) ([307bcbd98a](https://github.com/Devolutions/IronRDP/commit/307bcbd98a8f4e324f820672d80c252c2c7b109c)) + + Implement Hyper-V's private framebuffer-redirection DVC for same-host + VMConnect sessions. Validate the channel handshake, open the + host-created synchronization and mapping objects, and expose the shared + 32-bpp DIB as client frames. + + diff --git a/crates/ironrdp-vmconnect/Cargo.toml b/crates/ironrdp-vmconnect/Cargo.toml index b7fbd6c7c4..c20f3ebfcb 100644 --- a/crates/ironrdp-vmconnect/Cargo.toml +++ b/crates/ironrdp-vmconnect/Cargo.toml @@ -22,14 +22,14 @@ test = false __test = [] [dependencies] -ironrdp-async = { path = "../ironrdp-async", version = "0.10" } # public -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public -ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public +ironrdp-async = { path = "../ironrdp-async", version = "0.11" } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public tracing = { version = "0.1", features = ["log"] } [target.'cfg(windows)'.dependencies] -ironrdp-dvc = { version = "0.8.0", path = "../ironrdp-dvc" } # public +ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public sha2 = "0.10" windows = { version = "0.62", features = [ "Win32_Foundation", diff --git a/crates/ironrdp/CHANGELOG.md b/crates/ironrdp/CHANGELOG.md index 04b8244a06..d9c6c76aea 100644 --- a/crates/ironrdp/CHANGELOG.md +++ b/crates/ironrdp/CHANGELOG.md @@ -6,6 +6,611 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[0.18.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.17.0...ironrdp-v0.18.0)] - 2026-09-30 + +### Security + +- Connect to Windows Sandbox named pipes ([#1580](https://github.com/Devolutions/IronRDP/issues/1580)) ([39b020343d](https://github.com/Devolutions/IronRDP/commit/39b020343d962962bfbefc89939be64d5c716196)) + + Windows Sandbox's default attach path is a local named pipe carrying + plain TPKT/X.224 with PROTOCOL_RDP and ENCRYPTION_LEVEL_NONE, not + TCP:3389 or VMConnect. Allow the connector and client to complete that + sequence only via an explicit opt-in (`enable_standard_rdp_security`; + NamedPipe enables it), and teach ironrdp-agent to resolve pipe path and + guest credentials from WindowsSandboxServer after `wsb start`. + + Adds Transport::NamedPipe, ironrdp_named_pipe/ironrdp_sandbox_id + properties, sandbox list/config/stop CLI helpers via an in-process + h2/gRPC client on the per-user `\\.\pipe\wsandbox\{guid}` pipe (no .NET + helper), and connect --sandbox-id / --sandbox-pipe. Sandbox-derived + properties are the merge base; explicit .rdp/--prop/flags override them + while NamedPipe TLS/CredSSP stay forced off. Local :2179+PCB remains + unsupported. + +- Wire UDP multitransport into ironrdp-server ([#1954](https://github.com/Devolutions/IronRDP/issues/1954)) ([73dfa30e38](https://github.com/Devolutions/IronRDP/commit/73dfa30e3831e51cf8aa58f93e27b1b00102a7ec)) + + - Add `RdpServerBuilder::with_udp_transport(udp_bind_addr)`: opt-in, + `None` by default, no behavior change unless called. + - When set (and the security mode is `Tls` or `Hybrid`, matching the + reference client's Enhanced-Security-only gate), the acceptor offers UDP + multitransport, and `accept_finalize` uses + `accept_finalize_with_multitransport` with a callback that binds a fresh + UDP socket per connection, reuses the connection's own TLS certificate + (`TlsAcceptor::config()`) for the sideband transport, and calls + `accept_udp()`. + - Once established, the transport is used to migrate EGFX graphics + traffic off TCP: `request_reliable_udp` is called opportunistically the + first time EGFX has data to send (its dynamic channel id is only known + once the client opens it). From the request on, every server message on + that channel goes over the tunnel, starting with the batch that + triggered it: the request carries SOFT_SYNC_TCP_FLUSHED and the server + MUST keep using the named tunnel immediately after sending it + (MS-RDPEDYC 2.2.5.1, 3.3.5.3.1). DRDYNVC replies for a tunneled channel + go over the tunnel too, whichever path produced them. A new + `client_loop` select arm feeds incoming tunnel payloads into + `DrdynvcServer::process_tunnel()`; payloads that arrive before the + client's Soft-Sync Response are held and processed once it does + (3.3.5.3.2), rather than dropped. + - The UDP accept runs as an ordinary `tokio::spawn` task, so enabling + UDP adds no runtime requirement for the caller. + - A failure before EGFX has moved (bind, handshake, TLS, or the tunnel + closing) leaves the session on TCP, matching the reference client's + posture. Once EGFX is on the tunnel, the tunnel closing ends the + connection: Soft-Sync cannot move a channel back to TCP (MS-RDPEDYC + 2.2.5.1), and the tunnel lasts as long as the connection (MS-RDPEMT + 1.3.3). + - A client that answers the Initiate Multitransport Request with E_ABORT + (MS-RDPBCGR 2.2.15.2) has given up on the sideband transport, so the + pending UDP accept is stopped as soon as that response arrives, whether + during finalization or later on the message channel, instead of holding + its socket until the 15 s accept timeout. Windows clients send it about + 2.7 s after connecting. + - When the UDP bind address has an unspecified IP, each connection's + socket binds to the local address that client reached over TCP instead. + A socket bound to the unspecified address replies from whichever address + the routing table picks, and on a host with several IPv6 addresses that + is not always the one the client sent to: mstsc dropped the replies and + gave up with E_ABORT. `run()` records the address itself; embedders + driving `run_connection_with` pass it with the new + `RdpServer::set_connection_local_addr`. + - A successful Initiate Multitransport Response that arrives after + finalization now enables EGFX migration for the rest of the session, + provided Soft-Sync was negotiated. mstsc finishes its UDP bootstrap + after the TCP finalization (0.87 s later in my test), so migration was + previously decided before its response existed and the session never + left TCP even with the sideband transport up. + - The EGFX channel is found whichever way it was registered: an embedder + that takes a frame handle from its `GfxServerFactory` registers it as + `GfxDvcBridge`, which the migration lookup did not recognise, so it + never sent the Soft-Sync Request. The Soft-Sync Request, the client's + response (with the tunnels and channels it accepted) and the switch of + EGFX onto UDP are now logged at debug level. + +### Features + +- Expose generic session configuration and lifecycle APIs ([#1522](https://github.com/Devolutions/IronRDP/issues/1522)) ([57b1366650](https://github.com/Devolutions/IronRDP/commit/57b13666506dc40c15b4c4702d35150beee99133)) + + ## Summary + - expose generic client configuration for connection metadata, + compression, shell/work directory, audio, and runtime static-channel + factories + - add bounded input delivery with independent close cancellation, host + clipboard plumbing, lifecycle events, and Display Control resize + readiness/fallback handling + - update agent, viewer, web, FFI, examples, and tests for the generic + APIs + + ## Stack dependencies + This PR is stacked on `copilot/tls-validation-policy` (`b2bbcece`), + which already includes the merged runtime static-channel support from + `master`. It intentionally contains no TLS implementation/policy, + ActiveX/COM, SVC implementation, decompression, or bitmap-recovery + changes. + + ## Validation + - `cargo fmt --check --all` + - `cargo xtask check tests --no-run -v` + - `cargo xtask check lints -v` + - `cargo test -p ironrdp-client --lib --features rustls` + - `cargo check -p ironrdp-agent -p ironrdp-viewer -p ironrdp-web -p ffi` + + --------- + +- Hyper-V vmconnect support ([#1503](https://github.com/Devolutions/IronRDP/issues/1503)) ([a7cc067d50](https://github.com/Devolutions/IronRDP/commit/a7cc067d5069cbbcb13bae3e0561c0611da3bcf6)) + + Adds Hyper-V VMConnect's direct ordering: PCB → TLS → CredSSP → X.224. + + Enhanced Session is the default (`GUID;EnhancedMode=1`), with + `--vmconnect-basic` for the synthetic console. Kept this separate in + `ironrdp-vmconnect`; no SPN changes. + + Tested against the nested Hyper-V lab: + - Enhanced: `HYBRID_EX`, rendered 1280×720 + - Basic: `HYBRID`, rendered 1280×720 + - `cargo xtask check fmt/lints/tests -v` + + --------- + +- Add RemoteApp channel support ([#1637](https://github.com/Devolutions/IronRDP/issues/1637)) ([ab48c6cb8c](https://github.com/Devolutions/IronRDP/commit/ab48c6cb8c017504f8a92799aeb91b821c50a13a)) + + Configure and negotiate RAIL connections, then route its static channel + through the portable client with bounded request queues and server + control events. + +- Add Input DVC and ActiveX touch ([#1647](https://github.com/Devolutions/IronRDP/issues/1647)) ([a912e19bd2](https://github.com/Devolutions/IronRDP/commit/a912e19bd2bb31f403fd7c35c8efd729a5ab5f6f)) + + Implement MS-RDPEI for multi-touch over the dynamic virtual channel + Microsoft::Windows::RDS::Input, and wire Windows pointer messages in + ActiveX through session encode helpers. + + Introduce ironrdp-rdpei PDUs and processors, register the channel from + the client, encode touch frames from ActiveX WM_POINTER*, and cover the + protocol with unit and integration tests. + +- Wire MS-RDPEAI capture into Windows client and ActiveX ([#1642](https://github.com/Devolutions/IronRDP/issues/1642)) ([205fe038cc](https://github.com/Devolutions/IronRDP/commit/205fe038cc693598adf803fe181526b789b2ec3d)) + + Add the client MS-RDPEAI capture path on top of hardened RDPSND + playback: connector CFG + static channel wiring, CPAL PCM capture + backend, ironrdp-client --audio-capture, and ActiveX + AudioCaptureRedirectionMode. + + PCM capture only accepts encode formats that match the Open capture + stream, rejects non-16-bit capture (Data PDU size contract), and gates + the capture backend behind ironrdp-rdpsnd-native/capture. + + Depends on #1648 (playback). + +- Negotiate monitor topology ([#1675](https://github.com/Devolutions/IronRDP/issues/1675)) ([063efcdc30](https://github.com/Devolutions/IronRDP/commit/063efcdc3088d8f44e423cc322077d40bf9aadf2)) + + Negotiate the client monitor layout from UseMultimon and expose the + confirmed remote topology through the ActiveX compatibility interface. + + Advertise Monitor Layout PDU support whenever Extended Client Data is + negotiated, and forward layouts from activation, active sessions, and + reactivation so advertised support does not terminate sessions. + + Keep fallback reporting truthful when servers do not honor the request, + while preserving single-monitor resize behavior and blocking + multi-monitor resizing. + + Do not send Client Monitor Extended Data; per-monitor DPI and + orientation remain unavailable. + +- [**breaking**] Introduce typed ServerError on the public API boundary ([#1242](https://github.com/Devolutions/IronRDP/issues/1242)) ([b5b9558eae](https://github.com/Devolutions/IronRDP/commit/b5b9558eae164a3500ace069b4db12be25eba7bd)) + + ## Summary + + First in a staged migration toward a typed public error story for + `ironrdp-server`, addressing #1209. Introduces: + + ```rust + pub type ServerError = ironrdp_error::Error; + pub type ServerResult = Result; + ``` + + `ServerErrorKind` is a `#[non_exhaustive]` enum with concretely typed + variants (`Encode`, `Io`, `Channel`, `Unsupported`, `Reason`, `Custom`). + Sources are attached through `ironrdp_error::Error::with_source` rather + than embedded as `Box` in variant data. This mirrors the + shape of `ConnectorErrorKind` in `ironrdp-connector`, so the + connection-management layer stays internally consistent. + + ## Why this shape + + The audit before drafting found that `ironrdp-connector`, + `ironrdp-session`, `ironrdp-pdu`, `ironrdp-mstsgu`, and `ironrdp-core` + (`EncodeError`/`DecodeError`) all use the same + `ironrdp_error::Error` pattern. The `thiserror`-based bare enums + elsewhere in the workspace (`GccError`, `BulkError`, etc.) are + leaf-layer errors at the PDU/codec level; the connection-management + layer sister crate to `ironrdp-server` is the right template. + + ## Scope of this PR + + Public function signatures only: + + - `RdpServer::run` → `ServerResult<()>` + - `RdpServer::run_connection` → `ServerResult<()>` + - `RdpServer::run_connection_with` → `ServerResult<()>` + - `TlsIdentityCtx::init_from_paths` → `ServerResult` + - `TlsIdentityCtx::make_acceptor` → `ServerResult` + - `EchoServerHandle::send_request` → `ServerResult<()>` + + Internal call sites continue to use `anyhow::Result`. A private + `from_anyhow_with_context` helper bridges at the public boundary, + tagging each call site with the operation that failed. The + `ConnectionHandler::on_disconnected(error: Option<&anyhow::Error>)` + parameter is unchanged in this PR. + + `run_connection_with` keeps its anyhow body in a private + `run_connection_with_inner`, which the accept loop calls directly so + `on_disconnected` still receives an `anyhow::Error`. That helper is + transitional and its removal is inside this stack rather than deferred + indefinitely: #1244 converts `on_disconnected` to `ServerError` and + deletes it, with the accept loop calling `run_connection` from there. + `TlsIdentityCtx::init_from_paths` and `make_acceptor` use the same + wrapper-plus-private-body shape, matching the `run` / `run_inner` pair + this PR already introduces. + + ## Companion ext traits + + ```rust + pub trait ServerErrorExt { + fn encode(error: EncodeError) -> Self; + fn io(context: &'static str, error: io::Error) -> Self; + fn channel(context: &'static str) -> Self; + fn unsupported(context: &'static str) -> Self; + fn reason(context: &'static str, reason: impl Into) -> Self; + fn custom(context: &'static str, error: E) -> Self + where E: core::error::Error + Sync + Send + 'static; + } + pub trait ServerResultExt { + fn with_context(self, context: &'static str) -> Self; + } + ``` + + Mirrors `ConnectorErrorExt` / `ConnectorResultExt`, trimmed to the + constructors and the one result-side helper that have call sites in this + stack (`decode` and `with_source` did not, so neither shipped). + + ## Stack ordering + + This is step 1 of 4. The full chain (must merge in PR-number order): + + | Step | PR | Scope | + |------|-----|-------| + | **1** | **this PR** | Add `ServerError` / `ServerErrorKind` / ext + traits, convert 5 public functions, internal anyhow stays via private + bridge | + | 2 | #1243 | Convert encoder/helper/echo internals (anyhow → typed) | + | 3 | #1244 | Convert server.rs internals + `on_disconnected` parameter + (breaking) | + | 4 | #1245 | Convert display traits, drop anyhow dep, finish (breaking) + | + + All four close #1209 together. Each step is independently reviewable but + is rebased onto its predecessor as a Stacked branch; please merge in the + order above so the rebases land cleanly. + + ## Breaking change + + Marked with `!` in the conventional commit. Consumers of the five listed + public functions need to update their `Result` types. Pre-1.0, and per + @elmarco's note on #1209: "I am ok with breaking API at this point :)". + + ## Test plan + + - `cargo xtask check fmt -v` clean + - `cargo xtask check lints -v` clean (workspace, all-targets, with + helper + __bench features) + - `cargo xtask check tests -v` passes + - `cargo build --workspace --all-targets` clean (one example consumer in + `crates/ironrdp/examples/server.rs` updated to convert at its own + boundary) + + Closes part of #1209. + +- [**breaking**] Convert display traits to ServerResult, drop anyhow dep ([#1245](https://github.com/Devolutions/IronRDP/issues/1245)) ([9abd6eb482](https://github.com/Devolutions/IronRDP/commit/9abd6eb482d8e7bcc64d578fc0a97bb12b6c82d9)) + + ## Summary + + Final step of the staged migration started in #1242 and continued in + #1243 / #1244. Completes the typed error story for `ironrdp-server` by + converting the last consumer-facing trait surface and dropping the + `anyhow` dependency entirely. Closes #1209. + + ## Public API changes + + ```diff + pub trait RdpServerDisplayUpdates { + - async fn next_update(&mut self) -> anyhow::Result>; + + async fn next_update(&mut self) -> ServerResult>; + } + + pub trait RdpServerDisplay: Send { + - async fn updates(&mut self) -> anyhow::Result>; + + async fn updates(&mut self) -> ServerResult>; + } + ``` + + These are **breaking changes** for handler implementations of the two + display traits. + + ## Internal changes + + - `from_anyhow` private bridge and `AnyhowError` wrapper struct removed + from `error.rs`. + - `anyhow` dependency removed from `ironrdp-server/Cargo.toml`. + - `builder.rs` `NoopDisplayUpdates` / `NoopDisplay` impls and the + docstring examples in `display.rs` and `README.md` updated to match the + new trait shapes. + - `crates/ironrdp/examples/server.rs` and + `crates/ironrdp-testsuite-extra/tests/main.rs` updated to return + `ServerResult` from their `RdpServerDisplay/Updates` impls. + - `benches/src/perfenc.rs` updated to construct `ServerError` variants + instead of `anyhow::Error` and converts at its own `anyhow::Result` main + boundary via `.map_err(|e| anyhow::anyhow!(e))`. + - `urbdrc.rs`, the `usb`-gated USB device facade, converted from + `anyhow` to `ServerResult`/`ServerError` (69 sites). This file predates + the typed error migration and was never touched by it, so removing the + `anyhow` dependency broke it under the `usb` feature. Typed external + errors from `ironrdp-usb`/`ironrdp-rdpeusb` are wrapped with + `ServerError::custom`; hand-rolled invariant checks (former + `bail!`/`ensure!`) use `ServerError::reason`. + + ## After this PR + + `ironrdp-server` has **no anyhow dependency** and the public surface is + fully typed against `ServerError`, including the `usb`-gated facade. The + full chain: + + | Step | PR | Scope | + |------|-----|-------| + | 1 | #1242 | Add `ServerError` / `ServerErrorKind` / ext traits, + convert 5 public functions, internal anyhow stays via private bridge | + | 2 | #1243 | Convert encoder/helper/echo internals | + | 3 | #1244 | Convert server.rs internals + `on_disconnected` parameter + | + | **4** | **this PR** | Convert display traits and the usb facade, drop + anyhow dep, finish | + + ## Stacking note + + Stacked on `feat/server-typed-error-server` ([#1244](https://github.com/Devolutions/IronRDP/issues/1244)). Rebase on landing + in PR-number order. + + ## Test plan + + - `cargo xtask check fmt -v` clean + - `cargo xtask check lints -v` clean (workspace, all-targets, with + helper + __bench features) + - `cargo xtask check tests -v` passes (including doctests) + - `cargo xtask check features --case workspace/powerset-runtime` clean + (44/44, covers `ironrdp-server` across the full feature powerset + including `usb`) + - `cargo build --workspace --all-targets` clean + +- Add location redirection ([#1778](https://github.com/Devolutions/IronRDP/issues/1778)) ([1cee7a8613](https://github.com/Devolutions/IronRDP/commit/1cee7a86135a0556c01965d0406233bd7df367a9)) + + Implement MS-RDPEL v1 codecs and the location DVC state machine, then + route the ActiveX methods through the bounded client input queue. + + Preserve mstsc-compatible validation and altitude caching while + surfacing inactive sessions, channel readiness, queue pressure, and + encoding failures. Coordinates are caller-supplied only and are never + logged or persisted. + +- [**breaking**] Expose SUPPORT_DYN_VC_GFX_PROTOCOL early-cap flag for EGFX clients ([#1237](https://github.com/Devolutions/IronRDP/issues/1237)) ([5bdb67980c](https://github.com/Devolutions/IronRDP/commit/5bdb67980cb98b070076acbff802254316e786de)) + + Currently, `early_capability_flags` in the GCC core data is built from a + fixed set in `connection.rs`. Clients that want to use the Graphics + Pipeline Extension (MS-RDPEGFX) — by attaching a `DvcClientProcessor` + for `Microsoft::Windows::RDS::Graphics` — have no way to set + `SUPPORT_DYN_VC_GFX_PROTOCOL` without forking the connector, and modern + Windows servers won't open the EGFX channel unless the client advertises + support. + + This PR adds an opt-in `Config.support_dyn_vc_gfx_protocol: bool` + (default `false`). When set, the flag is OR'd into + `early_capability_flags` alongside the existing `WANT_32_BPP_SESSION` + conditional. Existing consumers are unaffected; the doc comment includes + a safety note that setting this without an EGFX implementation will + cause Windows to stop sending legacy bitmap updates, leaving the desktop + blank. + + Used downstream by [Haven](https://github.com/GlassHaven/Haven) (an + Android RDP/VNC client) which implements EGFX with ClearCodec + + RemoteFxProgressive decoders; this lets us drop a vendored fork of + `ironrdp-connector` we currently carry just for this one flag. + + Default `false` to preserve current behaviour. `cargo check --workspace` + clean — six other in-tree `Config { … }` builders updated with the + default-false field. + + --------- + +### Bug Fixes + +- [**breaking**] Always own a bulk decompressor for FastPath updates ([#1255](https://github.com/Devolutions/IronRDP/issues/1255)) ([0dc0194418](https://github.com/Devolutions/IronRDP/commit/0dc0194418375d504a8041b75ba250dc8eeb21ad)) + + ## Summary + + - A compressed FastPath update is dropped whenever the client did not + negotiate compression, because the decompressor is only built when a + compression type was negotiated. Servers send compressed updates + regardless, for example on a full-frame redraw after a resize, and the + session then fails. Closes #1193. + - The negotiated type is the wrong thing to condition on. It describes + what the client would send, and nothing in `ironrdp-session`, + `ironrdp-client`, `ironrdp-web` or the FFI ever compresses outbound. On + the receive path `BulkCompressor` holds a context per algorithm and + `decompress` selects one per update from the packet's own type bits, so + a decompressor built with any type decodes all of them. + - The `Processor` now owns the decompressor and builds it on the first + update that needs one. `ProcessorBuilder` has no corresponding field, so + there is no `None` a consumer can pass and no path that drops a + compressed update. + - On demand rather than at construction because `ironrdp-web` hardcodes + `compression_type: None` in `build_config` and so never negotiates + compression. Constructing eagerly would charge every web session for a + full set of algorithm contexts, and the two XCRUSH history buffers alone + are 2 MB each, for a decompressor most of those sessions never use. That + consumer is also the one most exposed to this bug, for the same reason. + - `BulkCompressor::new` is now infallible. Its only failure path was a + self-check over NCRUSH's static Huffman tables, a compile-time + invariant, now a `debug_assert`. + + ## Relationship to #1474 + + #1474 is kept, not reverted. `ActiveStage::reactivate` is adopted as the + reactivation entry point at all four call sites it introduced: native + client, web, FFI and the e2e test. + + What this PR removes is the `compression_type` retained on `ActiveStage` + and the `make_bulk_decompressor` helper, because an on-demand + decompressor makes both unnecessary. `reactivate` keeps its behaviour + and loses only the compression plumbing. + + #1474 closed the reactivation instance of #1193, where a rebuild passed + `None` and silently disabled decompression for the rest of the session. + The general case is still open on master: when compression was never + negotiated the retained type is `None`, `make_bulk_decompressor` returns + `None`, and every compressed update takes the drop path in + `fast_path.rs` for the lifetime of the session. Conditioning on the + negotiated type gates the ability to receive on what was negotiated to + send, and nothing sends. + + The evidence that removing the field is safe is #1474's own test. + `test_reactivation_processes_compressed_fastpath_updates` passes + unchanged with `compression_type` gone from the builder: the rebuilt + processor decompresses because every processor can, not because a type + was carried across the rebuild. + + ## Validation + + `cargo xtask check fmt/lints/tests/typos/locks` all pass. + + The gated regression test is + `testsuite-core/tests/session/fast_path.rs`, which renders the same + bitmap update plain and bulk-compressed through fresh processors and + asserts identical framebuffers. #1474's + `test_reactivation_processes_compressed_fastpath_updates` in + `testsuite-extra` passes unchanged. + + There is also an inline test in `fast_path.rs` pinning the allocation + invariant, that no contexts are built until an update needs them. Note + that `ironrdp-session` sets `[lib] test = false`, so inline tests in + this crate are not run by `cargo test --workspace`; it runs under `cargo + test -p ironrdp-session --lib`. + + ## Notes + + - This addresses the four points from the 2026-06-24 review. Point 4, + that the `Option` is misleading, is the shape of this change: it is gone + from the public API, and the private one that remains carries no + implication that a consumer could choose not to decompress. Point 1, + whether a cold `Rdp61` context decodes `RDP40` and `RDP50` updates + correctly, is a non-issue: `decompress` selects the algorithm per update + through `CompressionType::from_flags` against per-algorithm receive + contexts, so the construction-time type never constrains the receive + path. Point 3, silent degradation if the constructor fails, is removed + by making `new` infallible. Point 2 is the tests above. + - Breaking across two crates, hence the `fix(bulk,session)!` scope: + `ProcessorBuilder` loses `bulk_decompressor`, `ActiveStageBuilder` loses + `compression_type`, and `ironrdp_bulk::BulkCompressor::new` returns + `Self`. + - Incidental: `ironrdp-session` no longer exposes any `ironrdp_bulk` + type in its public API, so that dependency's lack of a `# public` marker + in `Cargo.toml` is now correct. + +- Share bulk decompression across output paths ([#1518](https://github.com/Devolutions/IronRDP/issues/1518)) ([6151e21bf5](https://github.com/Devolutions/IronRDP/commit/6151e21bf58b7297e9b4abc2167aa36fc2ba77e4)) + + Bulk compression state is stream-wide, but Fast-Path and slow-path + outputs previously used separate or missing decompression paths. This + could corrupt history-dependent server updates or leave negotiated + slow-path compression undecodable. + + This change owns the negotiated bulk decompressor in `ActiveStage` and + passes it to both X.224 and Fast-Path processing. It retains Share Data + compression metadata through the PDU context, resets decompression + history on reactivation, and initializes consumers from the connection's + negotiated compression type. + + Fast-Path now decompresses each fragment before reassembly so + compression flags apply at packet boundaries. Failures expose bounded + protocol metadata without retaining remote payloads or decoder details. + + Tests cover Share Data metadata propagation, slow-path decompression + behavior, fragmented Fast-Path reassembly and bounded errors, and + compressed Fast-Path updates after reactivation. + + --------- + +- [**breaking**] Accept the full documented range of Client Core Data keyboardType values ([#1689](https://github.com/Devolutions/IronRDP/issues/1689)) ([c74e7c5c94](https://github.com/Devolutions/IronRDP/commit/c74e7c5c9431c87e57a1550b037867d235c1d362)) + + ## Summary + + KeyboardType (TS_UD_CS_CORE's keyboardType field, MS-RDPBCGR 2.2.1.3.2) + was a closed enum covering discriminants 1 through 7, transcribed from + the field's other documentation in 2.2.7.1.6 (TS_INPUT_CAPABILITYSET), + which omits value 8 (Korean keyboard). The table this type actually + decodes against, 2.2.1.3.2, documents 1 through 8. Any client outside + the narrower range, including every genuine Korean keyboard, had its + whole Client Core Data parse hard rejected before the connection + started. + + Converted KeyboardType to a repr(transparent) struct KeyboardType(pub + u32) with named constants for the eight documented values, mirroring + RdpVersion one field above it in the same struct, and made decode + infallible at all three sites that previously disagreed with each other: + gcc::core_data::client hard rejected, rdp::capability_sets::input + silently dropped to None, and ironrdp-activex's COM setter returned + E_INVALIDARG. + + FreeRDP hit the identical gap in its own documentation-only enum until a + two-line fix (FreeRDP#11035). Neither FreeRDP nor xrdp gate connection + admission on this field at all; both decode it as a raw integer. + Windows' own GetKeyboardType additionally documents 0x51 for generic HID + keyboards, which a narrower fix adding only Korean would not survive. + + Marked as breaking since KeyboardType's public shape changes for any + downstream consumer of ironrdp-pdu outside this workspace. Note that + cargo-semver-checks in this repo's PR automation only runs against the + facade ironrdp crate, which will not see a break confined to + ironrdp-pdu's own API, so the automated breaking-change label may not + apply here even though this is a real one. + + ## Validation + + cargo xtask check fmt/lints/tests/typos/locks all pass. Extended + ironrdp-activex's existing inline COM boundary test to cover value 8 + round tripping and a negative value still being rejected. + +- [**breaking**] Drop the tokio and tokio_rustls re-exports ([#1796](https://github.com/Devolutions/IronRDP/issues/1796)) ([cac7bd6f37](https://github.com/Devolutions/IronRDP/commit/cac7bd6f37120711570b0156dd87c8ce6145fad6)) + + ## Summary + + `ironrdp-server` re-exported both `tokio` and `tokio_rustls` wholesale + so consumers did not need their own `tokio` dependency. This pins every + consumer's `tokio` version to whatever `ironrdp-server` picks, with no + way to upgrade independently. Drops the re-export. + + ```diff + -pub use {tokio, tokio_rustls}; + ``` + + ## Public API changes + + - `ironrdp_server::tokio` and `ironrdp_server::tokio_rustls` no longer + exist. Consumers using either path need their own `tokio` and + `tokio-rustls` dependencies. + - `tokio-rustls` stays marked `# public` in `Cargo.toml`: `TlsAcceptor` + genuinely appears in public signatures (`with_tls`, `with_hybrid`, + `TransportTls::Tls`). `tokio`'s `# public` marker is dropped since no + public signature returns or takes a bare `tokio` type; it was only + public via the re-export. + + ## Internal changes + + - The one workspace consumer, `crates/ironrdp/examples/server.rs`, now + imports `tokio` directly instead of through the facade. + - `tokio` added as a direct dev-dependency of the `ironrdp` facade + crate, and to its existing `#[cfg(test)] use { ... as _ }` + unused-dependency silencer, the same pattern already used there for + every other example-only dependency. + + ## Test plan + + `cargo xtask check fmt/lints/tests/typos/locks` clean. `cargo xtask + check features --case workspace/powerset-runtime` clean (44/44, covers + `ironrdp-server`). + +### Build + +- Bump the crypto group across 1 directory with 3 updates ([#1449](https://github.com/Devolutions/IronRDP/issues/1449)) ([e1725e8c8a](https://github.com/Devolutions/IronRDP/commit/e1725e8c8a581b83835647b6ee563a5b3f6c7a1b)) + + + ## [[0.17.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-v0.16.0...ironrdp-v0.17.0)] - 2026-07-10 ### Security diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index c4595af206..bb6cb04971 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp" -version = "0.17.0" +version = "0.18.0" readme = "README.md" description = "A meta crate re-exporting IronRDP crates for convenience" edition.workspace = true @@ -59,28 +59,28 @@ qoiz = ["ironrdp-server?/qoiz", "ironrdp-pdu?/qoiz", "ironrdp-connector?/qoiz", __bench = ["ironrdp-server/__bench"] [dependencies] -ironrdp-core = { path = "../ironrdp-core", version = "0.2", optional = true } # public -ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", optional = true } # public -ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.7", optional = true } # public -ironrdp-connector = { path = "../ironrdp-connector", version = "0.10", optional = true } # public -ironrdp-acceptor = { path = "../ironrdp-acceptor", version = "0.10", optional = true } # public -ironrdp-session = { path = "../ironrdp-session", version = "0.11", optional = true } # public -ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9", optional = true } # public +ironrdp-core = { path = "../ironrdp-core", version = "0.3", optional = true } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", optional = true } # public +ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8", optional = true } # public +ironrdp-connector = { path = "../ironrdp-connector", version = "0.11", optional = true } # public +ironrdp-acceptor = { path = "../ironrdp-acceptor", version = "0.11", optional = true } # public +ironrdp-session = { path = "../ironrdp-session", version = "0.12", optional = true } # public +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10", optional = true } # public ironrdp-input = { path = "../ironrdp-input", version = "0.7", optional = true } # public -ironrdp-server = { path = "../ironrdp-server", version = "0.13", optional = true, features = ["helper"] } # public -ironrdp-svc = { path = "../ironrdp-svc", version = "0.8", optional = true } # public -ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8", optional = true } # public -ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.7", optional = true } # public -ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.9", optional = true } # public -ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.8", optional = true } # public +ironrdp-server = { path = "../ironrdp-server", version = "0.14", optional = true, features = ["helper"] } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.9", optional = true } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9", optional = true } # public +ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.8", optional = true } # public +ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.10", optional = true } # public +ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.9", optional = true } # public ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1", optional = true } # public ironrdp-echo = { path = "../ironrdp-echo", version = "0.4", optional = true } # public -ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.1", optional = true } # public -ironrdp-client = { path = "../ironrdp-client", version = "0.1", optional = true } # public +ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.2", optional = true } # public +ironrdp-client = { path = "../ironrdp-client", version = "0.2", optional = true } # public ironrdp-vmconnect = { path = "../ironrdp-vmconnect", version = "0.1", optional = true } # public [dev-dependencies] -ironrdp-blocking = { path = "../ironrdp-blocking", version = "0.10" } +ironrdp-blocking = { path = "../ironrdp-blocking", version = "0.11" } ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.7" } anyhow = "1" async-trait = "0.1" diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index df75695a60..0f2a69d3c2 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -75,9 +75,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ "arbitrary", ] @@ -126,9 +126,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.67" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" dependencies = [ "find-msvc-tools", "jobserver", @@ -138,9 +138,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "const-oid" @@ -156,18 +156,18 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -203,9 +203,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" dependencies = [ "const-oid 0.10.2", "der_derive", @@ -272,13 +272,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -292,9 +292,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "flagset" @@ -304,12 +304,13 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.1", + "zlib-rs", ] [[package]] @@ -362,20 +363,20 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] [[package]] name = "ironrdp-bulk" -version = "0.1.1" +version = "0.2.0" [[package]] name = "ironrdp-cliprdr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bitflags", "ironrdp-core", @@ -386,7 +387,7 @@ dependencies = [ [[package]] name = "ironrdp-cliprdr-format" -version = "0.2.0" +version = "0.3.0" dependencies = [ "ironrdp-core", "png", @@ -394,14 +395,14 @@ dependencies = [ [[package]] name = "ironrdp-core" -version = "0.2.1" +version = "0.3.0" dependencies = [ "ironrdp-error", ] [[package]] name = "ironrdp-displaycontrol" -version = "0.8.0" +version = "0.9.0" dependencies = [ "ironrdp-core", "ironrdp-dvc", @@ -412,7 +413,7 @@ dependencies = [ [[package]] name = "ironrdp-dvc" -version = "0.8.0" +version = "0.9.0" dependencies = [ "ironrdp-core", "ironrdp-pdu", @@ -422,7 +423,7 @@ dependencies = [ [[package]] name = "ironrdp-egfx" -version = "0.3.0" +version = "0.4.0" dependencies = [ "arbitrary", "bit_field", @@ -436,7 +437,7 @@ dependencies = [ [[package]] name = "ironrdp-error" -version = "0.2.0" +version = "0.2.1" [[package]] name = "ironrdp-fuzz" @@ -471,7 +472,7 @@ dependencies = [ [[package]] name = "ironrdp-graphics" -version = "0.9.0" +version = "0.10.0" dependencies = [ "bit_field", "bitflags", @@ -487,7 +488,7 @@ dependencies = [ [[package]] name = "ironrdp-pdu" -version = "0.9.0" +version = "0.10.0" dependencies = [ "arbitrary", "bit_field", @@ -510,7 +511,7 @@ dependencies = [ [[package]] name = "ironrdp-rdpdr" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bitflags", "getrandom 0.3.4", @@ -566,7 +567,7 @@ dependencies = [ [[package]] name = "ironrdp-rdpsnd" -version = "0.9.0" +version = "0.10.0" dependencies = [ "bitflags", "ironrdp-core", @@ -577,7 +578,7 @@ dependencies = [ [[package]] name = "ironrdp-str" -version = "0.1.1" +version = "0.2.0" dependencies = [ "bytemuck", "ironrdp-core", @@ -585,7 +586,7 @@ dependencies = [ [[package]] name = "ironrdp-svc" -version = "0.8.0" +version = "0.9.0" dependencies = [ "bitflags", "ironrdp-core", @@ -624,9 +625,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "md-5" @@ -660,6 +661,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "nom" version = "7.1.3" @@ -688,14 +699,14 @@ checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -752,23 +763,23 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -849,7 +860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.1", + "der 0.8.2", ] [[package]] @@ -871,9 +882,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -899,22 +910,22 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] @@ -978,9 +989,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "version_check" @@ -1029,16 +1040,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b" dependencies = [ "const-oid 0.10.2", - "der 0.8.1", + "der 0.8.2", "spki 0.8.0", "tls_codec", ] [[package]] name = "yuv" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d85a782d94ee43f078bcfd6fa82d4e6a5b2d1cfbbad168e4df5a9f7b39ef48c" +checksum = "a295923535c00a4e72a50a0a1130097f93160b39892dd4d2d006b1e2b90d6bf9" dependencies = [ "num-traits", ] @@ -1062,3 +1073,9 @@ dependencies = [ "quote", "syn 2.0.119", ] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"