From 013cab5a5190a4c4dc861705eca35849238b44e5 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 22 Apr 2026 12:29:28 -0400 Subject: [PATCH 01/51] feat(pam): add rust rdp bridge with post-credssp passthrough Phase 1 of the native RDP client architecture. The bridge accepts an inbound RDP connection, opens an outbound connection to a Windows target, injects credentials via CredSSP/NLA on the target side, then byte-forwards raw TLS traffic in both directions. The acceptor and connector state machines are driven only up through CredSSP and then stopped. Client and target negotiate MCS, channels, capabilities, and share state directly through the passthrough pipe, which avoids the capability-flag drift and channel-ID drift that naive dual-handshake forwarding produces and that strict clients (Windows App, mstsc) reject. Validated end-to-end against a real Windows Server 2022 target with both sdl-freerdp (permissive) and Microsoft Windows App (strict). Phase 1 scope: standalone test binary only. No FFI, no event tap, no session recording. Crate produces a staticlib for Phase 2 CGo linking. --- packages/pam/handlers/rdp/native/.gitignore | 2 + packages/pam/handlers/rdp/native/Cargo.lock | 4016 +++++++++++++++++ packages/pam/handlers/rdp/native/Cargo.toml | 38 + packages/pam/handlers/rdp/native/README.md | 108 + .../pam/handlers/rdp/native/src/bridge.rs | 338 ++ .../pam/handlers/rdp/native/src/config.rs | 73 + packages/pam/handlers/rdp/native/src/lib.rs | 14 + packages/pam/handlers/rdp/native/src/main.rs | 87 + 8 files changed, 4676 insertions(+) create mode 100644 packages/pam/handlers/rdp/native/.gitignore create mode 100644 packages/pam/handlers/rdp/native/Cargo.lock create mode 100644 packages/pam/handlers/rdp/native/Cargo.toml create mode 100644 packages/pam/handlers/rdp/native/README.md create mode 100644 packages/pam/handlers/rdp/native/src/bridge.rs create mode 100644 packages/pam/handlers/rdp/native/src/config.rs create mode 100644 packages/pam/handlers/rdp/native/src/lib.rs create mode 100644 packages/pam/handlers/rdp/native/src/main.rs diff --git a/packages/pam/handlers/rdp/native/.gitignore b/packages/pam/handlers/rdp/native/.gitignore new file mode 100644 index 00000000..56071a5b --- /dev/null +++ b/packages/pam/handlers/rdp/native/.gitignore @@ -0,0 +1,2 @@ +/target +/target-docker diff --git a/packages/pam/handlers/rdp/native/Cargo.lock b/packages/pam/handlers/rdp/native/Cargo.lock new file mode 100644 index 00000000..912551bf --- /dev/null +++ b/packages/pam/handlers/rdp/native/Cargo.lock @@ -0,0 +1,4016 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8202ab55fcbf46ca829833f347a82a2a4ce0596f0304ac322c2d100030cd56" +dependencies = [ + "crypto-common 0.2.0-rc.4", + "inout", +] + +[[package]] +name = "aes" +version = "0.9.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e713c57c2a2b19159e7be83b9194600d7e8eb3b7c2cd67e671adf47ce189a05" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686ba04dc80c816104c96cd7782b748f6ad58c5dd4ee619ff3258cf68e83d54" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.3.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02eaa2d54d0fad0116e4b1efb65803ea0bf059ce970a67cd49718d87e807cb51" +dependencies = [ + "aes", + "const-oid 0.10.2", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-dnssd" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d49ffe175ab45bbfd74b548313d9d7cdfff27161a94b007b52eeeb5f9aaa15e" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-util", + "libc", + "log", + "pin-utils", + "pkg-config", + "tokio", + "winapi", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base16ct" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.11.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9ef36a6fcdb072aa548f3da057640ec10859eb4e91ddf526ee648d50c76a949" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e59c1aab3e6c5e56afe1b7e8650be9b5a791cb997bdea449194ae62e4bf8c73" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cbc" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dbf9e5b071e9de872e32b73f485e8f644ff47c7011d95476733e7482ee3e5c3" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.5.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e12a13eb01ded5d32ee9658d94f553a19e804204f2dc811df69ab4d9e0cb8c7" +dependencies = [ + "block-buffer 0.11.0-rc.5", + "crypto-common 0.2.0-rc.4", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.7.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4113edbc9f68c0a64d5b911f803eb245d04bb812680fd56776411f69c670f3e0" +dependencies = [ + "hybrid-array", + "num-traits", + "rand_core 0.9.5", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6" +dependencies = [ + "hybrid-array", + "rand_core 0.9.5", +] + +[[package]] +name = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-primes" +version = "0.7.0-pre.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f2523fbb68811c8710829417ad488086720a6349e337c38d12fa81e09e50bf" +dependencies = [ + "crypto-bigint", + "libm", + "rand_core 0.9.5", +] + +[[package]] +name = "cryptoki" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781357a7779a8e92ea985121bbf379a9adf0777f44ab6392efc6abd5aa9b67db" +dependencies = [ + "bitflags 1.3.2", + "cryptoki-sys", + "libloading", + "log", + "paste", + "secrecy", +] + +[[package]] +name = "cryptoki-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "753e27d860277930ae9f394c119c8c70303236aab0ffab1d51f3d207dbb2bc4b" +dependencies = [ + "libloading", +] + +[[package]] +name = "ctr" +version = "0.10.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e41d01c6f73b9330177f5cf782ae5b581b5f2c7840e298e0275ceee5001434" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.0-rc.3", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0-rc.3", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "des" +version = "0.9.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f51594a70805988feb1c85495ddec0c2052e4fbe59d9c0bb7f94bfc164f4f90" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac89f8a64533a9b0eaa73a68e424db0fb1fd6271c74cc0125336a05f090568d" +dependencies = [ + "block-buffer 0.11.0-rc.5", + "const-oid 0.10.2", + "crypto-common 0.2.0-rc.4", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.17.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ab355ec063f7a110eb627471058093aba00eb7f4e70afbd15e696b79d1077b" +dependencies = [ + "der 0.8.0-rc.9", + "digest 0.11.0-rc.3", + "elliptic-curve", + "rfc6979", + "signature", + "spki 0.8.0-rc.4", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef49c0b20c0ad088893ad2a790a29c06a012b3f05bcfc66661fd22a94b32129" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.9.5", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.0-rc.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e3be87c458d756141f3b6ee188828132743bf90c7d14843e2835d6443e5fb03" +dependencies = [ + "base16ct 0.3.0", + "crypto-bigint", + "digest 0.11.0-rc.3", + "ff", + "group", + "hkdf", + "hybrid-array", + "once_cell", + "pem-rfc7468 1.0.0-rc.3", + "pkcs8", + "rand_core 0.9.5", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ff" +version = "0.14.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d42dd26f5790eda47c1a2158ea4120e32c35ddc9a7743c98a292accc01b54ef3" +dependencies = [ + "rand_core 0.9.5", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f88107cb02ed63adcc4282942e60c4d09d80208d33b360ce7c729ce6dae1739" +dependencies = [ + "polyval", +] + +[[package]] +name = "group" +version = "0.14.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff6a0b2dd4b981b1ae9e3e6830ab146771f3660d31d57bafd9018805a91b0f1" +dependencies = [ + "ff", + "rand_core 0.9.5", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.4", + "ring", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.4", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.13.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ef30358b03ca095a5b910547f4f8d4b9f163e4057669c5233ef595b1ecf008" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3fd4dc94c318c1ede8a2a48341c250d6ddecd3ba793da2820301a9f92417ad9" +dependencies = [ + "digest 0.11.0-rc.3", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "infisical-rdp-bridge" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "clap", + "ironrdp-acceptor", + "ironrdp-connector", + "ironrdp-pdu", + "ironrdp-tls", + "ironrdp-tokio", + "rcgen", + "rustls", + "tokio", + "tokio-rustls", + "tracing", + "tracing-subscriber", + "x509-cert", +] + +[[package]] +name = "inout" +version = "0.2.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1603f76010ff924b616c8f44815a42eb10fb0b93d308b41deaa8da6d4251fd4b" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "ironrdp-acceptor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c18abf50681dda6ea22ac3a812a385ee915a4a69512c775c2358541e89fdd2" +dependencies = [ + "ironrdp-async", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-async" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62813c05253206699b2c8e44e268908dafd9668e07bb46ff262ee5b42d13e8cd" +dependencies = [ + "bytes", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-pdu", + "tracing", +] + +[[package]] +name = "ironrdp-connector" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a8d5c1b8f167bbd9c935b08a4d3b592fe0a163ded7e4cc8880d471f06b3e2fa" +dependencies = [ + "ironrdp-core", + "ironrdp-error", + "ironrdp-pdu", + "ironrdp-svc", + "picky", + "picky-asn1-der", + "picky-asn1-x509", + "rand 0.9.4", + "sspi", + "tracing", + "url", +] + +[[package]] +name = "ironrdp-core" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2db60a59716a84d09040d29c9e75e81545842510fccb0934c09b28e78b46680" +dependencies = [ + "ironrdp-error", +] + +[[package]] +name = "ironrdp-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9d7794e854eef2f13fdf79c8502bcc567a75a15fd0522885f37739386a4cef" + +[[package]] +name = "ironrdp-pdu" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409b96a94ca1fad1bfaa41789611bbb5efc112503b27b0513a1feb243e49eb60" +dependencies = [ + "bit_field", + "bitflags 2.11.1", + "byteorder", + "der-parser", + "ironrdp-core", + "ironrdp-error", + "md-5 0.10.6", + "num-bigint", + "num-derive", + "num-integer", + "num-traits", + "pkcs1 0.7.5", + "sha1 0.10.6", + "tap", + "thiserror", + "x509-cert", +] + +[[package]] +name = "ironrdp-svc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef43a3ea966070b0e12a3f49ffb863c80311bd15f26c2b3681622c85e70d729" +dependencies = [ + "bitflags 2.11.1", + "ironrdp-core", + "ironrdp-pdu", +] + +[[package]] +name = "ironrdp-tls" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a301516949e619a5bd9c4476dfeeccaf7709b9997ea5958d01c9432085dc64d8" +dependencies = [ + "tokio", + "tokio-rustls", + "x509-cert", +] + +[[package]] +name = "ironrdp-tokio" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af190b161daba5d88c614bbf5915fdb586e9a28cb4b938aaac7abf473a1109b" +dependencies = [ + "bytes", + "ironrdp-async", + "ironrdp-connector", + "reqwest", + "sspi", + "tokio", + "url", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "iso7816" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c7e91da489667bb054f9cd2f1c60cc2ac4478a899f403d11dbc62189215b0" +dependencies = [ + "heapless", +] + +[[package]] +name = "iso7816-tlv" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7660d28d24a831d690228a275d544654a30f3b167a8e491cf31af5fe5058b546" +dependencies = [ + "untrusted", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.2.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d546793a04a1d3049bd192856f804cfe96356e2cf36b54b4e575155babe9f41" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libz-sys" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9ec86664728010f574d67ef01aec964e6f1299241a3402857c1a8a390a62478" +dependencies = [ + "cfg-if", + "digest 0.11.0-rc.3", +] + +[[package]] +name = "md4" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da5ac363534dce5fabf69949225e174fbf111a498bf0ff794c8ea1fba9f3dda" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" +dependencies = [ + "serde", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "p256" +version = "0.14.0-pre.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b374901df34ee468167a58e2a49e468cb059868479cafebeb804f6b855423d" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.14.0-pre.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "701032b3730df6b882496d6cee8221de0ce4bc11ddc64e6d89784aa5b8a6de30" +dependencies = [ + "ecdsa", + "elliptic-curve", + "fiat-crypto", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.14.0-pre.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ba29c2906eb5c89a8c411c4f11243ee4e5517ee7d71d9a13fedc877a6057b1" +dependencies = [ + "base16ct 0.3.0", + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "rand_core 0.9.5", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.13.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3fc18bb4460ac250ba6b75dfa7cf9d0b2273e3e623f660bd6ce2c3e902342e" +dependencies = [ + "digest 0.11.0-rc.3", + "hmac", + "sha1 0.11.0-rc.2", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e58fab693c712c0d4e88f8eb3087b6521d060bcaf76aeb20cb192d809115ba" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "picky" +version = "7.0.0-rc.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cdc52be663aebd70d7006ae305c87eb32a2b836d6c2f26f7e384f845d80b621" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "aes-kw", + "base64", + "block-buffer 0.11.0-rc.5", + "block-padding", + "cbc", + "cipher", + "crypto-bigint", + "crypto-common 0.2.0-rc.4", + "crypto-primes", + "ctr", + "curve25519-dalek", + "der 0.8.0-rc.9", + "des", + "digest 0.11.0-rc.3", + "ecdsa", + "ed25519", + "ed25519-dalek", + "elliptic-curve", + "ff", + "ghash", + "group", + "hex", + "hkdf", + "hmac", + "http", + "inout", + "keccak", + "md-5 0.11.0-rc.2", + "p256", + "p384", + "p521", + "pbkdf2", + "pem-rfc7468 1.0.0-rc.3", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "pkcs1 0.8.0-rc.4", + "pkcs8", + "polyval", + "primefield", + "primeorder", + "rand 0.9.4", + "rand_core 0.9.5", + "rc2", + "rfc6979", + "rsa", + "sec1", + "serde", + "serde_json", + "sha1 0.11.0-rc.2", + "sha2", + "sha3", + "signature", + "spki 0.8.0-rc.4", + "thiserror", + "universal-hash", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "picky-asn1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff038f9360b934342fb3c0a1d6e82c438a2624b51c3c6e3e6d7cf252b6f3ee3" +dependencies = [ + "oid", + "serde", + "serde_bytes", + "time", + "zeroize", +] + +[[package]] +name = "picky-asn1-der" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d413165e4bf7f808b9a27cbaba657657a2921f0965db833f488c4d4be96dcd2e" +dependencies = [ + "picky-asn1", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c97cd14d567a17755910fa8718277baf39d08682a980b1b1a4b4da7d0bc61a04" +dependencies = [ + "base64", + "crypto-bigint", + "oid", + "picky-asn1", + "picky-asn1-der", + "serde", + "widestring", + "zeroize", +] + +[[package]] +name = "picky-krb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed61c8d7448649c031ecae02afb10c679524c7a9af5fb0fbee466b3cc0d6df1" +dependencies = [ + "aes", + "block-buffer 0.11.0-rc.5", + "block-padding", + "byteorder", + "cbc", + "cipher", + "crypto-bigint", + "crypto-common 0.2.0-rc.4", + "des", + "digest 0.11.0-rc.3", + "hmac", + "inout", + "oid", + "pbkdf2", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "rand 0.9.4", + "serde", + "sha1 0.11.0-rc.2", + "thiserror", + "uuid", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs1" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +dependencies = [ + "der 0.8.0-rc.9", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93eac55f10aceed84769df670ea4a32d2ffad7399400d41ee1c13b1cd8e1b478" +dependencies = [ + "der 0.8.0-rc.9", + "spki 0.8.0-rc.4", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "polyval" +version = "0.7.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffd40cc99d0fbb02b4b3771346b811df94194bc103983efa0203c8893755085" +dependencies = [ + "cfg-if", + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portpicker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" +dependencies = [ + "rand 0.8.6", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primefield" +version = "0.14.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcd4a163053332fd93f39b81c133e96a98567660981654579c90a99062fbf5" +dependencies = [ + "crypto-bigint", + "ff", + "rand_core 0.9.5", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c36e8766fcd270fa9c665b9dc364f570695f5a59240949441b077a397f15b74" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rc2" +version = "0.9.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03621ac292cc723def9e0fd0eb9573b1df8d6a9ee7ad637fe94dfc153705f3c" +dependencies = [ + "cipher", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.5.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369f9c4f79388704648e7bcb92749c0d6cf4397039293a9b747694fa4fb4bae" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.10.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8955ab399f6426998fde6b76ae27233cce950705e758a6c17afd2f6d0e5d52" +dependencies = [ + "const-oid 0.10.2", + "crypto-bigint", + "crypto-primes", + "digest 0.11.0-rc.3", + "pkcs1 0.8.0-rc.4", + "pkcs8", + "rand_core 0.9.5", + "sha1 0.11.0-rc.2", + "signature", + "spki 0.8.0-rc.4", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.8.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dff52f6118bc9f0ac974a54a639d499ac26a6cad7a6e39bc0990c19625e793b" +dependencies = [ + "base16ct 0.3.0", + "der 0.8.0-rc.9", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serdect" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" +dependencies = [ + "base16ct 1.0.0", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e046edf639aa2e7afb285589e5405de2ef7e61d4b0ac1e30256e3eab911af9" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.3", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.3", +] + +[[package]] +name = "sha3" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2103ca0e6f4e9505eae906de5e5883e06fc3b2232fb5d6914890c7bbcb62f478" +dependencies = [ + "digest 0.11.0-rc.3", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc280a6ff65c79fbd6622f64d7127f32b85563bca8c53cd2e9141d6744a9056d" +dependencies = [ + "digest 0.11.0-rc.3", + "rand_core 0.9.5", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der 0.8.0-rc.9", +] + +[[package]] +name = "sspi" +version = "0.18.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f4823ee743a4a0cc2153eb640e28ff95b55ca25c88085b559bae59fb6c317a" +dependencies = [ + "async-dnssd", + "async-recursion", + "bitflags 2.11.1", + "block-buffer 0.11.0-rc.5", + "byteorder", + "cfg-if", + "crypto-bigint", + "crypto-common 0.2.0-rc.4", + "crypto-mac", + "crypto-primes", + "cryptoki", + "curve25519-dalek", + "der 0.8.0-rc.9", + "digest 0.11.0-rc.3", + "ed25519-dalek", + "ff", + "futures", + "getrandom 0.3.4", + "group", + "hickory-proto", + "hickory-resolver", + "hmac", + "md-5 0.11.0-rc.2", + "md4", + "num-derive", + "num-traits", + "oid", + "p256", + "p384", + "p521", + "pem-rfc7468 1.0.0-rc.3", + "picky", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "picky-krb", + "pkcs1 0.8.0-rc.4", + "pkcs8", + "portpicker", + "primefield", + "primeorder", + "rand 0.9.4", + "reqwest", + "rsa", + "rustls", + "rustls-native-certs", + "serde", + "sha1 0.11.0-rc.2", + "sha2", + "signature", + "spki 0.8.0-rc.4", + "time", + "tokio", + "tracing", + "url", + "uuid", + "windows", + "windows-registry", + "winscard", + "zeroize", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.6.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55be643b40a21558f44806b53ee9319595bc7ca6896372e4e08e5d7d83c9cd6" +dependencies = [ + "crypto-common 0.2.0-rc.4", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winscard" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b6ec4e6176df62589d1ac9950f6295be87ca06ee61a7c9a579a2bcc80efe34" +dependencies = [ + "bitflags 2.11.1", + "crypto-bigint", + "flate2", + "iso7816", + "iso7816-tlv", + "num-derive", + "num-traits", + "picky", + "picky-asn1-x509", + "rand_core 0.9.5", + "rsa", + "sha1 0.11.0-rc.2", + "time", + "tracing", + "uuid", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a45998121837fd8c92655d2334aa8f3e5ef0645cdfda5b321b13760c548fd55" +dependencies = [ + "curve25519-dalek", + "rand_core 0.9.5", + "serde", + "zeroize", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "tls_codec", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/packages/pam/handlers/rdp/native/Cargo.toml b/packages/pam/handlers/rdp/native/Cargo.toml new file mode 100644 index 00000000..3ed19c85 --- /dev/null +++ b/packages/pam/handlers/rdp/native/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "infisical-rdp-bridge" +version = "0.1.0" +edition = "2021" +description = "IronRDP MITM bridge for Infisical PAM Windows/RDP support" +publish = false + +[lib] +name = "infisical_rdp_bridge" +crate-type = ["staticlib", "rlib"] +path = "src/lib.rs" + +[[bin]] +name = "rdp-bridge-test" +path = "src/main.rs" + +[dependencies] +ironrdp-acceptor = "0.8" +ironrdp-connector = "0.8" +ironrdp-tokio = { version = "0.8", features = ["reqwest"] } +ironrdp-pdu = "0.7" +ironrdp-tls = { version = "0.2", features = ["rustls"] } +x509-cert = { version = "0.2", features = ["std"] } + +tokio = { version = "1", features = ["full"] } +bytes = "1" +tokio-rustls = "0.26" +rustls = { version = "0.23", features = ["ring"] } +rcgen = "0.13" + +anyhow = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +clap = { version = "4", features = ["derive"] } + +[profile.release] +lto = true +codegen-units = 1 diff --git a/packages/pam/handlers/rdp/native/README.md b/packages/pam/handlers/rdp/native/README.md new file mode 100644 index 00000000..836d51ce --- /dev/null +++ b/packages/pam/handlers/rdp/native/README.md @@ -0,0 +1,108 @@ +# infisical-rdp-bridge + +Rust crate that implements the RDP MITM bridge for Infisical's PAM Windows +handler. The bridge terminates an inbound RDP connection from a native +client (Windows App, mstsc, xfreerdp, sdl-freerdp), opens an outbound +connection to a Windows target, injects credentials at CredSSP on the +target side, and then byte-forwards raw TLS traffic in both directions. + +## Architecture + +**Post-CredSSP passthrough.** The bridge drives each half of the +connection only far enough to complete credential injection: + +1. Inbound (acceptor): X.224 negotiation → TLS upgrade with a self-signed + cert → CredSSP/NLA with the fixed placeholder credential + `infisical`/`infisical`. +2. Outbound (connector): X.224 negotiation → TLS upgrade → CredSSP/NLA + with the real target credentials injected via sspi's NTLM. +3. Both halves run concurrently via `tokio::try_join!` so the client + doesn't sit waiting after its CredSSP completes. +4. After both CredSSP sequences finish, we drop the acceptor / connector + state machines and use `tokio::io::copy_bidirectional` on the raw TLS + streams. + +From this point, client and target negotiate MCS, channels, capabilities, +and share state **directly with each other through us**. We never +synthesize our own Connect Initial / Connect Response, so we avoid the +capability-drift and identifier-drift that naive acceptor+connector +handshake forwarding introduces. Strict clients (Windows App, mstsc) +that validate echoes like `ServerCoreData.clientRequestedProtocols` +accept the session because target's response reflects the values +**client** sent, not what our connector would have advertised. + +## Phase 1 scope + +Standalone test binary only. No FFI, no event tap, no session recording. +The crate compiles to both a `staticlib` (for later CGo linking) and an +`rlib` (for the in-tree test binary). + +## Build + +```sh +cargo build --release +``` + +## Manual validation + +Start the bridge pointing at a real Windows server: + +```sh +RUST_LOG=info cargo run --release -- \ + --listen 127.0.0.1:3390 \ + --target-host \ + --target-port 3389 \ + --username \ + --password +``` + +Then connect any native RDP client to `127.0.0.1:3390` with credentials +`infisical` / `infisical`. Examples: + +**Microsoft Windows App (macOS):** Add PC → `127.0.0.1:3390`, user +account `infisical` / `infisical`. Click through the self-signed cert +warning. This is the strict client and validates the full post-CredSSP +architecture end-to-end. + +**sdl-freerdp (Linux/macOS):** + +```sh +sdl-freerdp /v:127.0.0.1:3390 /u:infisical /p:infisical /cert:ignore +``` + +**mstsc (Windows):** Save a `.rdp` file with: + +``` +full address:s:127.0.0.1:3390 +username:s:infisical +``` + +and supply `infisical` as the password when prompted. + +### macOS dev note + +On macOS, sspi's Kerberos DNS fallback via Bonjour adds ~4s of DNS +timeouts during CredSSP. This does not affect production gateway +deployments (Linux, where `hickory-resolver` returns NXDOMAIN in +milliseconds). If validating locally on macOS with strict clients, +prefer running the bridge inside a Linux container: + +```sh +docker run --rm -v "$PWD":/work -v "$PWD/target-docker":/work/target \ + -w /work -p 127.0.0.1:3390:3390 rust:1-bookworm \ + bash -c "cargo build --release && \ + ./target/release/rdp-bridge-test \ + --listen 0.0.0.0:3390 \ + --target-host \ + --username --password ''" +``` + +The `target-docker` directory keeps the Linux build artifacts separate +from the host's macOS `target`. + +## Lints + +```sh +cargo fmt --check +cargo clippy --all-targets -- -D warnings +``` diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs new file mode 100644 index 00000000..6795384c --- /dev/null +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -0,0 +1,338 @@ +//! MITM bridge with post-CredSSP passthrough. +//! +//! We run the acceptor and connector only far enough to do credential +//! injection: accept client TLS + fixed-cred CredSSP on one side, connect +//! target TLS + real-cred CredSSP on the other. Once both CredSSP sequences +//! complete, we stop driving the IronRDP state machines and byte-forward +//! raw bytes between the two TLS streams. Client and target then negotiate +//! MCS, channels, capabilities, and share state directly with each other +//! through us, avoiding the feature-flag drift that breaks strict clients +//! (Windows App, mstsc) when acceptor and connector negotiate independently. + +use std::sync::Arc; + +use anyhow::{Context, Result}; +use ironrdp_acceptor::{Acceptor, BeginResult}; +use ironrdp_connector::credssp::{CredsspSequence, KerberosConfig}; +use ironrdp_connector::sspi::credssp::ClientState; +use ironrdp_connector::sspi::generator::GeneratorState; +use ironrdp_connector::{ClientConnector, ClientConnectorState}; +use ironrdp_pdu::ironrdp_core::WriteBuf; +use ironrdp_pdu::nego::SecurityProtocol; +use ironrdp_pdu::rdp::client_info::Credentials as AcceptorCredentials; +use ironrdp_tokio::reqwest::ReqwestNetworkClient; +use ironrdp_tokio::{FramedWrite, NetworkClient}; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use tokio::net::TcpStream; +use tracing::info; + +use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; + +/// Fixed credential presented by the native client through the acceptor. +/// The real access gate is upstream (Infisical auth + the gateway tunnel); +/// this value only needs to match what the CLI bakes into the `.rdp` file. +pub const ACCEPTOR_USERNAME: &str = "infisical"; +pub const ACCEPTOR_PASSWORD: &str = "infisical"; + +pub struct TargetEndpoint { + pub host: String, + pub port: u16, + pub username: String, + pub password: String, +} + +/// Run a single RDP MITM session. Injects credentials at CredSSP and then +/// passes everything else through between the two TLS streams. +pub async fn run_mitm(client_tcp: TcpStream, target: TargetEndpoint) -> Result<()> { + // rustls 0.23 requires an explicit crypto provider when more than one is + // compiled in. Our tree pulls both `ring` (direct) and `aws-lc-rs` + // (transitively from reqwest). Install ring as the default on first call; + // subsequent calls return Err("already installed") which we ignore. + let _ = rustls::crypto::ring::default_provider().install_default(); + + // Run the two halves concurrently so the client doesn't sit idle while + // the target side completes CredSSP. Functionally either order works; + // this is a latency optimization. + let (acceptor_output, connector_output) = + tokio::try_join!(run_acceptor_half(client_tcp), run_connector_half(target))?; + + let (mut client_stream, client_leftover) = acceptor_output; + let (mut target_stream, target_leftover) = connector_output; + + if !client_leftover.is_empty() { + target_stream + .write_all(&client_leftover) + .await + .context("flush client leftover to target")?; + } + if !target_leftover.is_empty() { + client_stream + .write_all(&target_leftover) + .await + .context("flush target leftover to client")?; + } + + // Flush anything the CredSSP phase left buffered before handing off to + // copy_bidirectional. Belt-and-suspenders: tokio-rustls normally + // flushes on write_all, but being explicit here avoids a subtle stall + // if the final EarlyUserAuthResult PDU is sitting in the write buffer. + client_stream + .flush() + .await + .context("flush client stream before passthrough")?; + target_stream + .flush() + .await + .context("flush target stream before passthrough")?; + + // Passthrough: client and target negotiate MCS, channels, capabilities + // and share state directly through us. Real RDP clients hard-close the + // TCP connection on session end (no TLS close_notify), so rustls + // returns an UnexpectedEof. We treat that specific error as a clean + // shutdown; any other IO error propagates. + match tokio::io::copy_bidirectional(&mut client_stream, &mut target_stream).await { + Ok(_) => info!("session ended cleanly"), + Err(e) if is_unexpected_eof(&e) => info!("session ended (peer hard-closed)"), + Err(e) => return Err(e).context("passthrough copy_bidirectional"), + } + Ok(()) +} + +/// rustls 0.23 raises `UnexpectedEof` when a peer closes the TCP connection +/// without sending `close_notify`. That's normal RDP client behavior and +/// should not surface as a session error. +fn is_unexpected_eof(err: &std::io::Error) -> bool { + err.kind() == std::io::ErrorKind::UnexpectedEof +} + +/// Accept the inbound connection, upgrade to TLS, and run CredSSP with the +/// fixed acceptor credential. Stops there: MCS and everything after is the +/// passthrough phase's job. Returns the underlying TLS stream and any bytes +/// the framed reader buffered beyond CredSSP. +async fn run_acceptor_half(client_tcp: TcpStream) -> Result<(ErasedStream, bytes::BytesMut)> { + let (server_tls, acceptor_public_key) = + build_acceptor_tls().context("build acceptor TLS config")?; + let server_tls = Arc::new(server_tls); + + let acceptor_framed = ironrdp_tokio::TokioFramed::new(client_tcp); + let expected_creds = AcceptorCredentials { + username: ACCEPTOR_USERNAME.to_owned(), + password: ACCEPTOR_PASSWORD.to_owned(), + domain: None, + }; + // Capabilities and desktop size passed here are unused because we never + // call `accept_finalize`. Acceptor::new requires them so we pass empty + // / sentinel values. + let mut acceptor = Acceptor::new( + SecurityProtocol::HYBRID_EX | SecurityProtocol::HYBRID | SecurityProtocol::SSL, + ironrdp_acceptor::DesktopSize { + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + }, + Vec::new(), + Some(expected_creds), + ); + + let begin_result = ironrdp_acceptor::accept_begin(acceptor_framed, &mut acceptor) + .await + .context("acceptor: accept_begin")?; + + let mut acceptor_framed: ironrdp_tokio::TokioFramed = match begin_result { + BeginResult::Continue(framed) => { + let (stream, leftover) = framed.into_inner(); + let erased: ErasedStream = Box::new(stream); + ironrdp_tokio::TokioFramed::new_with_leftover(erased, leftover) + } + BeginResult::ShouldUpgrade(tcp) => { + let tls_stream = tokio_rustls::TlsAcceptor::from(server_tls) + .accept(tcp) + .await + .context("acceptor: TLS accept")?; + acceptor.mark_security_upgrade_as_done(); + let erased: ErasedStream = Box::new(tls_stream); + ironrdp_tokio::TokioFramed::new(erased) + } + }; + + if acceptor.should_perform_credssp() { + ironrdp_acceptor::accept_credssp( + &mut acceptor_framed, + &mut acceptor, + &mut ReqwestNetworkClient::new(), + ironrdp_connector::ServerName::new("infisical-rdp-bridge"), + acceptor_public_key, + None, + ) + .await + .context("acceptor: CredSSP")?; + } + info!("acceptor: CredSSP complete"); + + Ok(acceptor_framed.into_inner()) +} + +/// Connect to the target, upgrade to TLS, and run CredSSP with the injected +/// credentials. Stops there. Returns the underlying TLS stream and any +/// bytes the framed reader buffered beyond CredSSP. +async fn run_connector_half(target: TargetEndpoint) -> Result<(ErasedStream, bytes::BytesMut)> { + let target_addr = format!("{}:{}", target.host, target.port); + let target_tcp = TcpStream::connect(&target_addr) + .await + .with_context(|| format!("connector: tcp connect to {target_addr}"))?; + let client_addr = target_tcp.local_addr().context("connector: local_addr")?; + + let mut target_framed = ironrdp_tokio::TokioFramed::new(target_tcp); + let config = connector_config(target.username.clone(), target.password.clone()); + let mut connector = ClientConnector::new(config, client_addr); + + let should_upgrade = ironrdp_tokio::connect_begin(&mut target_framed, &mut connector) + .await + .context("connector: connect_begin")?; + + let (initial_stream, leftover) = target_framed.into_inner(); + let (upgraded_stream, tls_cert) = ironrdp_tls::upgrade(initial_stream, &target.host) + .await + .context("connector: TLS upgrade")?; + + let _upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector); + let erased: ErasedStream = Box::new(upgraded_stream); + let mut target_framed = ironrdp_tokio::TokioFramed::new_with_leftover(erased, leftover); + + let server_public_key = ironrdp_tls::extract_tls_server_public_key(&tls_cert) + .ok_or_else(|| anyhow::anyhow!("connector: extract TLS server public key"))? + .to_vec(); + + if connector.should_perform_credssp() { + perform_connector_credssp( + &mut connector, + &mut target_framed, + &mut ReqwestNetworkClient::new(), + ironrdp_connector::ServerName::new(&target.host), + server_public_key, + None, + ) + .await + .context("connector: CredSSP")?; + } + info!("connector: CredSSP complete, credential injection succeeded"); + + Ok(target_framed.into_inner()) +} + +/// Drive the connector's CredSSP sequence to completion. Equivalent to +/// `perform_credssp_step` in `ironrdp-async`'s private module; replicated +/// here so we can stop before `connect_finalize` would start the MCS / +/// capability exchange (which is what we want client and target to do +/// directly via passthrough). +async fn perform_connector_credssp( + connector: &mut ClientConnector, + framed: &mut ironrdp_tokio::TokioFramed, + network_client: &mut ReqwestNetworkClient, + server_name: ironrdp_connector::ServerName, + server_public_key: Vec, + kerberos_config: Option, +) -> Result<()> +where + S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, +{ + let selected_protocol = match connector.state { + ClientConnectorState::Credssp { selected_protocol } => selected_protocol, + _ => anyhow::bail!("connector not in Credssp state"), + }; + + let (mut sequence, mut ts_request) = CredsspSequence::init( + connector.config.credentials.clone(), + connector.config.domain.as_deref(), + selected_protocol, + server_name, + server_public_key, + kerberos_config, + ) + .context("CredsspSequence::init")?; + + let mut buf = WriteBuf::new(); + + loop { + let client_state: ClientState = { + let mut generator = sequence.process_ts_request(ts_request); + let mut state = generator.start(); + loop { + match state { + GeneratorState::Suspended(request) => { + let response = network_client + .send(&request) + .await + .context("CredSSP network request")?; + state = generator.resume(Ok(response)); + } + GeneratorState::Completed(result) => { + break result.map_err(|e| anyhow::anyhow!("CredSSP process: {e:?}"))?; + } + } + } + }; + + buf.clear(); + let written = sequence + .handle_process_result(client_state, &mut buf) + .context("CredsspSequence::handle_process_result")?; + + if let Some(response_len) = written.size() { + framed + .write_all(&buf[..response_len]) + .await + .context("write CredSSP response")?; + } + + let Some(next_pdu_hint) = sequence.next_pdu_hint() else { + break; + }; + + let pdu = framed + .read_by_hint(next_pdu_hint) + .await + .context("read CredSSP PDU")?; + + if let Some(next_request) = sequence + .decode_server_message(&pdu) + .context("CredsspSequence::decode_server_message")? + { + ts_request = next_request; + } else { + break; + } + } + + connector.mark_credssp_as_done(); + Ok(()) +} + +/// Build the acceptor's TLS config and return the server's public key for +/// use as CredSSP TLS channel binding material. +fn build_acceptor_tls() -> Result<(tokio_rustls::rustls::ServerConfig, Vec)> { + use x509_cert::der::Decode; + + let subject_alt_names = vec!["localhost".to_string(), "infisical-rdp-bridge".to_string()]; + let cert = + rcgen::generate_simple_self_signed(subject_alt_names).context("rcgen self-signed cert")?; + + let cert_der = cert.cert.der().clone(); + let parsed = + x509_cert::Certificate::from_der(cert_der.as_ref()).context("parse self-signed cert")?; + let public_key = ironrdp_tls::extract_tls_server_public_key(&parsed) + .ok_or_else(|| anyhow::anyhow!("extract public key from self-signed cert"))? + .to_vec(); + + let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(cert.key_pair.serialize_der().into()); + let config = tokio_rustls::rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(vec![cert_der], key_der) + .context("rustls ServerConfig")?; + + Ok((config, public_key)) +} + +pub trait AsyncReadWrite: AsyncRead + AsyncWrite {} +impl AsyncReadWrite for T where T: AsyncRead + AsyncWrite {} + +pub type ErasedStream = Box; diff --git a/packages/pam/handlers/rdp/native/src/config.rs b/packages/pam/handlers/rdp/native/src/config.rs new file mode 100644 index 00000000..e752cebf --- /dev/null +++ b/packages/pam/handlers/rdp/native/src/config.rs @@ -0,0 +1,73 @@ +//! Connector config for the outbound half of the bridge. +//! +//! Post-CredSSP passthrough means we only need to drive the connector far +//! enough to complete CredSSP. After that, client and target negotiate +//! MCS / capabilities / share state directly through the byte-forwarding +//! pipe. Only CredSSP-relevant fields (credentials, security flags) are +//! load-bearing; other fields are required by `ironrdp_connector::Config` +//! but never hit the wire because we skip `connect_finalize`. + +use ironrdp_connector::{BitmapConfig, Config, Credentials, DesktopSize}; +use ironrdp_pdu::gcc::KeyboardType; +use ironrdp_pdu::rdp::capability_sets::{BitmapCodecs, MajorPlatformType}; +use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; + +pub const DEFAULT_WIDTH: u16 = 1920; +pub const DEFAULT_HEIGHT: u16 = 1080; + +pub fn connector_config(username: String, password: String) -> Config { + Config { + desktop_size: DesktopSize { + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + }, + desktop_scale_factor: 0, + + // Advertise the same security-protocol set that native clients + // typically send (HYBRID_EX | HYBRID | SSL). Target echoes this + // set back in its ServerCoreData.clientRequestedProtocols; strict + // clients (Windows App) validate that echo against what THEY sent + // via the acceptor side. If the sets diverge, Windows App closes + // the session immediately after Connect Response. + // + // Target still picks HYBRID_EX (highest priority) so credential + // injection via NLA is unaffected. The MITM-downgrade concern + // described in ironrdp-connector's Config docs is real for a + // direct client-to-target connection, but here the outbound + // connection is to a known Windows server over a trusted path + // (gateway -> target), not a user-facing leg. + enable_tls: true, + enable_credssp: true, + + credentials: Credentials::UsernamePassword { username, password }, + domain: None, + + // Unused after CredSSP because we switch to passthrough and target + // negotiates these values directly with the native client. Kept at + // sentinel values to satisfy the Config struct shape. + client_build: 0, + client_name: String::new(), + keyboard_type: KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_functional_keys_count: 12, + keyboard_layout: 0, + ime_file_name: String::new(), + bitmap: Some(BitmapConfig { + lossy_compression: false, + color_depth: 32, + codecs: BitmapCodecs(Vec::new()), + }), + dig_product_id: String::new(), + client_dir: String::new(), + platform: MajorPlatformType::UNSPECIFIED, + hardware_id: None, + request_data: None, + autologon: false, + enable_audio_playback: false, + performance_flags: PerformanceFlags::default(), + license_cache: None, + timezone_info: TimezoneInfo::default(), + enable_server_pointer: false, + pointer_software_rendering: false, + } +} diff --git a/packages/pam/handlers/rdp/native/src/lib.rs b/packages/pam/handlers/rdp/native/src/lib.rs new file mode 100644 index 00000000..e21b59e8 --- /dev/null +++ b/packages/pam/handlers/rdp/native/src/lib.rs @@ -0,0 +1,14 @@ +//! Infisical RDP MITM bridge. +//! +//! The bridge accepts an inbound RDP connection from a native client +//! (xfreerdp, mstsc) on one side and initiates an outbound RDP connection +//! to a Windows target on the other. The outbound handshake performs +//! CredSSP/NLA with credentials injected by the gateway, so the real +//! target credentials never reach the client. The inbound handshake +//! accepts a fixed placeholder credential (`infisical`/`infisical`) that +//! the CLI embeds in the generated .rdp file. +//! +//! Phase 1 scope: standalone test binary only, no FFI, no event tap. + +pub mod bridge; +pub mod config; diff --git a/packages/pam/handlers/rdp/native/src/main.rs b/packages/pam/handlers/rdp/native/src/main.rs new file mode 100644 index 00000000..41b19fc6 --- /dev/null +++ b/packages/pam/handlers/rdp/native/src/main.rs @@ -0,0 +1,87 @@ +//! Standalone test binary. Listens on a loopback port, accepts one +//! connection, runs the MITM bridge to a Windows target, and exits. +//! +//! Validate against a real Windows server with: +//! +//! ```sh +//! xfreerdp /v:127.0.0.1:3390 /u:infisical /p:infisical /sec:tls /cert:ignore +//! ``` +//! +//! (or `mstsc` with a `.rdp` file whose `authentication level:i:0` and +//! `enablecredsspsupport:i:0` are set). + +use std::net::SocketAddr; + +use anyhow::{Context, Result}; +use clap::Parser; +use tokio::net::TcpListener; +use tracing::{error, info}; +use tracing_subscriber::EnvFilter; + +use infisical_rdp_bridge::bridge::{run_mitm, TargetEndpoint}; + +#[derive(Parser, Debug)] +#[command(about = "Infisical RDP MITM bridge: manual validation harness")] +struct Args { + /// Loopback address to listen on for the native RDP client. + #[arg(long, default_value = "127.0.0.1:3390")] + listen: SocketAddr, + + /// Target Windows RDP server host. + #[arg(long)] + target_host: String, + + /// Target Windows RDP server port. + #[arg(long, default_value_t = 3389)] + target_port: u16, + + /// Username to inject on the outbound connection. + #[arg(long)] + username: String, + + /// Password to inject on the outbound connection. + #[arg(long)] + password: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + let args = Args::parse(); + + let listener = TcpListener::bind(args.listen) + .await + .with_context(|| format!("bind {}", args.listen))?; + info!( + listen = %args.listen, + target = %format!("{}:{}", args.target_host, args.target_port), + "bridge ready; waiting for one RDP client connection" + ); + + let (client_tcp, peer) = listener.accept().await.context("accept")?; + info!(%peer, "inbound connection; starting MITM"); + drop(listener); + + let endpoint = TargetEndpoint { + host: args.target_host, + port: args.target_port, + username: args.username, + password: args.password, + }; + + match run_mitm(client_tcp, endpoint).await { + Ok(()) => { + info!("session ended cleanly"); + Ok(()) + } + Err(e) => { + error!(error = ?e, "session failed"); + Err(e) + } + } +} From a91f967cb448a8e8f7c73f61f582c9960b28ca3f Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 22 Apr 2026 13:19:31 -0400 Subject: [PATCH 02/51] feat(pam): add c abi + cgo wrapper for rdp bridge Phase 2 of the native RDP client architecture. Exposes the Rust bridge as a handle-based C ABI and wraps it in an idiomatic Go package, making the bridge callable from gateway and CLI code. Rust side: - Handle-based FFI in src/ffi.rs: start/wait/cancel/free. Each session runs on a dedicated OS thread with its own current-thread tokio runtime, isolating failures between sessions. - Public C header at native/include/rdp_bridge.h. - run_mitm now accepts a CancellationToken so sessions can be aborted from any thread; tokio::select! in the bridge races the MITM future against token.cancelled() and drops the future on cancel, closing TCP streams automatically. Go side (packages/pam/handlers/rdp/): - bridge.go: always-compiled types and error sentinels. - bridge_cgo.go: real CGo wrapper, gated on `rdp && (linux || darwin)`. StartWithConn dups the caller's fd via syscall.Conn/Control so Go and Rust own independent references to the socket. - bridge_stub.go: ErrRdpUnavailable stubs for builds without -tags rdp or on platforms without the static lib yet (windows/freebsd/netbsd land in Phase 6). - cmd/bridge-test: Go harness that exercises the full Rust -> C ABI -> CGo -> Go path. Validated end-to-end against the existing Windows Server target with Microsoft Windows App. No product code calls this yet; Phase 3 wires the gateway handler. --- packages/pam/handlers/rdp/bridge.go | 27 ++ packages/pam/handlers/rdp/bridge_cgo.go | 129 +++++++++ packages/pam/handlers/rdp/bridge_stub.go | 22 ++ .../pam/handlers/rdp/cmd/bridge-test/main.go | 124 ++++++++ packages/pam/handlers/rdp/native/Cargo.lock | 1 + packages/pam/handlers/rdp/native/Cargo.toml | 1 + packages/pam/handlers/rdp/native/README.md | 40 +-- .../handlers/rdp/native/include/rdp_bridge.h | 108 +++++++ .../pam/handlers/rdp/native/src/bridge.rs | 20 +- packages/pam/handlers/rdp/native/src/ffi.rs | 274 ++++++++++++++++++ packages/pam/handlers/rdp/native/src/lib.rs | 1 + packages/pam/handlers/rdp/native/src/main.rs | 16 +- 12 files changed, 735 insertions(+), 28 deletions(-) create mode 100644 packages/pam/handlers/rdp/bridge.go create mode 100644 packages/pam/handlers/rdp/bridge_cgo.go create mode 100644 packages/pam/handlers/rdp/bridge_stub.go create mode 100644 packages/pam/handlers/rdp/cmd/bridge-test/main.go create mode 100644 packages/pam/handlers/rdp/native/include/rdp_bridge.h create mode 100644 packages/pam/handlers/rdp/native/src/ffi.rs diff --git a/packages/pam/handlers/rdp/bridge.go b/packages/pam/handlers/rdp/bridge.go new file mode 100644 index 00000000..e792b133 --- /dev/null +++ b/packages/pam/handlers/rdp/bridge.go @@ -0,0 +1,27 @@ +// Package rdp wraps the Rust MITM bridge for Infisical's PAM Windows +// handler. The real implementation is gated behind the `rdp` build tag +// and a supported platform; other builds receive stubs that return +// [ErrRdpUnavailable] from every constructor. +package rdp + +import "errors" + +// ErrRdpUnavailable is returned by constructors when the RDP bridge is +// not compiled in (built without `-tags rdp`, or on a platform that +// does not yet ship the Rust static library). +var ErrRdpUnavailable = errors.New("rdp bridge: not available in this build") + +// ErrInvalidHandle is returned when an operation references an unknown +// or already-freed bridge handle. +var ErrInvalidHandle = errors.New("rdp bridge: invalid handle") + +// ErrSessionFailed is returned from Wait when the session ended with a +// handshake or forwarding error (rather than a clean client disconnect). +var ErrSessionFailed = errors.New("rdp bridge: session ended with error") + +// Bridge owns the handle to a running RDP MITM session. Cancel may be +// called from any goroutine; Wait blocks until the session ends; Close +// releases the handle and must be called after Wait returns. +type Bridge struct { + handle uint64 +} diff --git a/packages/pam/handlers/rdp/bridge_cgo.go b/packages/pam/handlers/rdp/bridge_cgo.go new file mode 100644 index 00000000..ad716044 --- /dev/null +++ b/packages/pam/handlers/rdp/bridge_cgo.go @@ -0,0 +1,129 @@ +//go:build rdp && (linux || darwin) + +package rdp + +/* +#cgo CFLAGS: -I${SRCDIR}/native/include +#cgo linux LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -lm -ldl -lpthread -lz +#cgo darwin LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -lz -framework Security -framework CoreFoundation -framework SystemConfiguration + +#include "rdp_bridge.h" +#include +*/ +import "C" + +import ( + "fmt" + "net" + "syscall" + "unsafe" +) + +// StartWithConn starts a bridge session for the given TCP connection. +// Internally, an independent dup of the underlying file descriptor is +// handed to the bridge; the caller's conn stays fully usable and is not +// closed by this function. The bridge closes its dup when the session +// ends. +// +// `conn` must be a *net.TCPConn or any net.Conn that exposes a raw file +// descriptor via syscall.Conn. Passing a TLS-wrapped conn will fail; +// Phase 3 will introduce a loopback shim for that case. +func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { + dupFd, err := dupConnFD(conn) + if err != nil { + return nil, fmt.Errorf("rdp bridge: dup client fd: %w", err) + } + // If the start call fails below, we still own the dup; close it. + success := false + defer func() { + if !success { + _ = syscall.Close(dupFd) + } + }() + + cHost := C.CString(targetHost) + defer C.free(unsafe.Pointer(cHost)) + cUser := C.CString(username) + defer C.free(unsafe.Pointer(cUser)) + cPass := C.CString(password) + defer C.free(unsafe.Pointer(cPass)) + + var handle C.uint64_t + rc := C.rdp_bridge_start_unix_fd( + C.int(dupFd), + cHost, + C.uint16_t(targetPort), + cUser, + cPass, + &handle, + ) + if rc != C.RDP_BRIDGE_OK { + return nil, fmt.Errorf("rdp bridge: start failed (status %d)", int32(rc)) + } + success = true + return &Bridge{handle: uint64(handle)}, nil +} + +// dupConnFD returns a new file descriptor independent from `conn`'s +// internal one. The caller becomes responsible for closing the returned +// fd. Requires `conn` to implement syscall.Conn. +func dupConnFD(conn net.Conn) (int, error) { + sc, ok := conn.(syscall.Conn) + if !ok { + return -1, fmt.Errorf("conn %T does not expose syscall.Conn", conn) + } + raw, err := sc.SyscallConn() + if err != nil { + return -1, err + } + var dup int + var dupErr error + ctrlErr := raw.Control(func(fd uintptr) { + dup, dupErr = syscall.Dup(int(fd)) + }) + if ctrlErr != nil { + return -1, ctrlErr + } + if dupErr != nil { + return -1, dupErr + } + return dup, nil +} + +// Wait blocks until the session ends. Returns nil on a clean end +// (including the client hard-closing the TCP connection after a normal +// session), [ErrSessionFailed] on handshake or forwarding failure, or +// [ErrInvalidHandle] if the handle is unknown. Calling Wait a second +// time on the same handle returns nil (the session is already done). +func (b *Bridge) Wait() error { + rc := C.rdp_bridge_wait(C.uint64_t(b.handle)) + switch rc { + case C.RDP_BRIDGE_OK: + return nil + case C.RDP_BRIDGE_INVALID_HANDLE: + return ErrInvalidHandle + case C.RDP_BRIDGE_SESSION_ERROR, C.RDP_BRIDGE_THREAD_PANIC: + return ErrSessionFailed + default: + return fmt.Errorf("rdp bridge: wait returned unexpected status %d", int32(rc)) + } +} + +// Cancel signals the session to stop. Idempotent; safe from any +// goroutine even while another goroutine is inside Wait. +func (b *Bridge) Cancel() error { + rc := C.rdp_bridge_cancel(C.uint64_t(b.handle)) + if rc == C.RDP_BRIDGE_INVALID_HANDLE { + return ErrInvalidHandle + } + return nil +} + +// Close releases the bridge handle. Call after Wait has returned. +func (b *Bridge) Close() error { + rc := C.rdp_bridge_free(C.uint64_t(b.handle)) + if rc == C.RDP_BRIDGE_INVALID_HANDLE { + return ErrInvalidHandle + } + return nil +} diff --git a/packages/pam/handlers/rdp/bridge_stub.go b/packages/pam/handlers/rdp/bridge_stub.go new file mode 100644 index 00000000..4909c158 --- /dev/null +++ b/packages/pam/handlers/rdp/bridge_stub.go @@ -0,0 +1,22 @@ +//go:build !rdp || (!linux && !darwin) + +package rdp + +import "net" + +// StartWithConn is a stub that reports the RDP bridge is unavailable in +// this build. To enable the real implementation, build with `-tags rdp` +// on a supported platform (linux, darwin; windows and others land in +// later phases). +func StartWithConn(_ net.Conn, _ string, _ uint16, _, _ string) (*Bridge, error) { + return nil, ErrRdpUnavailable +} + +// Wait is a stub for builds without the RDP bridge. +func (b *Bridge) Wait() error { return ErrRdpUnavailable } + +// Cancel is a stub for builds without the RDP bridge. +func (b *Bridge) Cancel() error { return ErrRdpUnavailable } + +// Close is a stub for builds without the RDP bridge. +func (b *Bridge) Close() error { return ErrRdpUnavailable } diff --git a/packages/pam/handlers/rdp/cmd/bridge-test/main.go b/packages/pam/handlers/rdp/cmd/bridge-test/main.go new file mode 100644 index 00000000..758182cb --- /dev/null +++ b/packages/pam/handlers/rdp/cmd/bridge-test/main.go @@ -0,0 +1,124 @@ +// Standalone test harness for the Go bridge wrapper. +// +// Mirrors the Rust test binary's behavior but exercises the full +// Rust -> C ABI -> CGo -> Go path: +// +// 1. Bind a loopback TCP listener +// 2. Accept one RDP client connection +// 3. Hand it to rdp.StartWithConn +// 4. Block on bridge.Wait until the session ends +// 5. Exit +// +// Build with `-tags rdp` from the cli repo root: +// +// go run -tags rdp ./packages/pam/handlers/rdp/cmd/bridge-test -- \ +// -listen 127.0.0.1:3390 \ +// -target :3389 \ +// -user \ +// -pass +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "net" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + + "github.com/Infisical/infisical-merge/packages/pam/handlers/rdp" +) + +func main() { + listenAddr := flag.String("listen", "127.0.0.1:3390", "loopback address to accept the RDP client on") + target := flag.String("target", "", "target Windows server as host:port (port defaults to 3389)") + username := flag.String("user", "", "username to inject on the outbound connection") + password := flag.String("pass", "", "password to inject on the outbound connection") + flag.Parse() + + if *target == "" || *username == "" || *password == "" { + fmt.Fprintln(os.Stderr, "--target, --user, and --pass are required") + flag.Usage() + os.Exit(2) + } + + host, port, err := splitHostPort(*target) + if err != nil { + log.Fatalf("parse target: %v", err) + } + + listener, err := net.Listen("tcp", *listenAddr) + if err != nil { + log.Fatalf("bind %s: %v", *listenAddr, err) + } + log.Printf("bridge ready; listening on %s, target %s:%d", *listenAddr, host, port) + + // Accept one connection, then stop listening. + conn, err := listener.Accept() + if err != nil { + log.Fatalf("accept: %v", err) + } + _ = listener.Close() + log.Printf("inbound connection from %s; starting MITM", conn.RemoteAddr()) + + bridge, err := rdp.StartWithConn(conn, host, port, *username, *password) + if err != nil { + _ = conn.Close() + log.Fatalf("start bridge: %v", err) + } + // The bridge has its own dup of the fd; close the Go-side conn so we + // don't accidentally keep it alive. + _ = conn.Close() + + // If the user Ctrl-C's, cancel the session gracefully and let Wait + // return so Close can run. + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) + go func() { + sig := <-sigc + log.Printf("received %s; cancelling session", sig) + if err := bridge.Cancel(); err != nil { + log.Printf("cancel: %v", err) + } + }() + + waitErr := bridge.Wait() + switch { + case waitErr == nil: + log.Printf("session ended cleanly") + case errors.Is(waitErr, rdp.ErrSessionFailed): + log.Printf("session ended with error") + default: + log.Printf("wait: %v", waitErr) + } + + if err := bridge.Close(); err != nil { + log.Printf("close: %v", err) + } + + if waitErr != nil && !errors.Is(waitErr, rdp.ErrInvalidHandle) { + os.Exit(1) + } +} + +// splitHostPort accepts "host", "host:port", or "[ipv6]:port" and returns +// host + port, defaulting port to 3389 if omitted. +func splitHostPort(s string) (string, uint16, error) { + // If no colon, it's just a host. + if !strings.Contains(s, ":") { + return s, 3389, nil + } + host, portStr, err := net.SplitHostPort(s) + if err != nil { + return "", 0, err + } + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return "", 0, fmt.Errorf("parse port %q: %w", portStr, err) + } + return host, uint16(port), nil +} diff --git a/packages/pam/handlers/rdp/native/Cargo.lock b/packages/pam/handlers/rdp/native/Cargo.lock index 912551bf..0b7c85f9 100644 --- a/packages/pam/handlers/rdp/native/Cargo.lock +++ b/packages/pam/handlers/rdp/native/Cargo.lock @@ -1422,6 +1422,7 @@ dependencies = [ "rustls", "tokio", "tokio-rustls", + "tokio-util", "tracing", "tracing-subscriber", "x509-cert", diff --git a/packages/pam/handlers/rdp/native/Cargo.toml b/packages/pam/handlers/rdp/native/Cargo.toml index 3ed19c85..6e0144a3 100644 --- a/packages/pam/handlers/rdp/native/Cargo.toml +++ b/packages/pam/handlers/rdp/native/Cargo.toml @@ -23,6 +23,7 @@ ironrdp-tls = { version = "0.2", features = ["rustls"] } x509-cert = { version = "0.2", features = ["std"] } tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" bytes = "1" tokio-rustls = "0.26" rustls = { version = "0.23", features = ["ring"] } diff --git a/packages/pam/handlers/rdp/native/README.md b/packages/pam/handlers/rdp/native/README.md index 836d51ce..70538cd4 100644 --- a/packages/pam/handlers/rdp/native/README.md +++ b/packages/pam/handlers/rdp/native/README.md @@ -31,11 +31,12 @@ that validate echoes like `ServerCoreData.clientRequestedProtocols` accept the session because target's response reflects the values **client** sent, not what our connector would have advertised. -## Phase 1 scope +## Scope -Standalone test binary only. No FFI, no event tap, no session recording. -The crate compiles to both a `staticlib` (for later CGo linking) and an -`rlib` (for the in-tree test binary). +No event tap, no session recording. The crate compiles to both a +`staticlib` (consumed via CGo from the Go wrapper at +`packages/pam/handlers/rdp/`, see [Go wrapper](#go-wrapper) below) and +an `rlib` (for the in-tree test binary). ## Build @@ -82,23 +83,28 @@ and supply `infisical` as the password when prompted. ### macOS dev note On macOS, sspi's Kerberos DNS fallback via Bonjour adds ~4s of DNS -timeouts during CredSSP. This does not affect production gateway -deployments (Linux, where `hickory-resolver` returns NXDOMAIN in -milliseconds). If validating locally on macOS with strict clients, -prefer running the bridge inside a Linux container: +timeouts during CredSSP. Sessions still succeed (strict clients like +Windows App do not time out in that window), but CredSSP feels sluggish. +Production gateway deployments run on Linux, where `hickory-resolver` +returns NXDOMAIN in milliseconds, so this is a local-dev quirk only. + +## Go wrapper + +The static library in `target/release/libinfisical_rdp_bridge.a` +exports a C ABI (see [`include/rdp_bridge.h`](include/rdp_bridge.h)) +that is consumed from the Go package at +`packages/pam/handlers/rdp/` via CGo. Build order: ```sh -docker run --rm -v "$PWD":/work -v "$PWD/target-docker":/work/target \ - -w /work -p 127.0.0.1:3390:3390 rust:1-bookworm \ - bash -c "cargo build --release && \ - ./target/release/rdp-bridge-test \ - --listen 0.0.0.0:3390 \ - --target-host \ - --username --password ''" +# 1. Build the Rust static library first. +cd packages/pam/handlers/rdp/native && cargo build --release + +# 2. Build the Go binary or package with the rdp tag. +cd - && go build -tags rdp ./packages/pam/handlers/rdp/cmd/bridge-test ``` -The `target-docker` directory keeps the Linux build artifacts separate -from the host's macOS `target`. +Builds without `-tags rdp` (or on unsupported platforms) link against a +pure-Go stub that returns `ErrRdpUnavailable` from every constructor. ## Lints diff --git a/packages/pam/handlers/rdp/native/include/rdp_bridge.h b/packages/pam/handlers/rdp/native/include/rdp_bridge.h new file mode 100644 index 00000000..6535f9e9 --- /dev/null +++ b/packages/pam/handlers/rdp/native/include/rdp_bridge.h @@ -0,0 +1,108 @@ +/* + * infisical-rdp-bridge C ABI + * + * C-callable interface to the Rust MITM bridge. Consumed via CGo from the + * Go gateway and CLI code. All functions are thread-safe unless noted. + * + * Session lifecycle: + * 1. Caller hands in a connected TCP file descriptor / socket and target + * credentials to `rdp_bridge_start_*`. On success, the call returns + * immediately with an opaque `uint64_t` handle; the bridge runs on a + * dedicated OS thread. + * 2. `rdp_bridge_wait(handle)` blocks until the session ends. + * 3. `rdp_bridge_cancel(handle)` can be called at any time from any + * thread to signal the session to abort. Idempotent. + * 4. `rdp_bridge_free(handle)` releases the registry entry. Call after + * `wait` returns. + * + * Ownership of the client fd / socket transfers to the bridge on a + * successful `start_*` call. The bridge closes it when the session ends. + * Callers that need to keep their own reference should dup before calling + * (syscall.Dup on Unix, WSADuplicateSocket or equivalent on Windows). + */ + +#ifndef INFISICAL_RDP_BRIDGE_H +#define INFISICAL_RDP_BRIDGE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Status codes returned by all functions. */ +#define RDP_BRIDGE_OK 0 +#define RDP_BRIDGE_SESSION_ERROR 1 +#define RDP_BRIDGE_THREAD_PANIC 2 +#define RDP_BRIDGE_INVALID_HANDLE -1 +#define RDP_BRIDGE_BAD_ARG -2 +#define RDP_BRIDGE_RUNTIME_ERROR -3 + +#if defined(__unix__) || defined(__APPLE__) +/* + * Start a session consuming a Unix client file descriptor. + * + * client_fd — connected TCP socket; ownership transfers to the bridge. + * target_host — NUL-terminated UTF-8 hostname or IP. + * target_port — RDP port (usually 3389). + * username — NUL-terminated UTF-8 username to inject via CredSSP. + * password — NUL-terminated UTF-8 password. + * out_handle — written with the session handle on success. + * + * Returns RDP_BRIDGE_OK on success, RDP_BRIDGE_BAD_ARG for invalid + * arguments, RDP_BRIDGE_RUNTIME_ERROR if the session thread could not + * be started. + */ +int32_t rdp_bridge_start_unix_fd( + int client_fd, + const char *target_host, + uint16_t target_port, + const char *username, + const char *password, + uint64_t *out_handle +); +#endif /* unix */ + +#if defined(_WIN32) || defined(_WIN64) +/* + * Start a session consuming a Windows SOCKET handle (passed as uintptr_t + * so the ABI is fixed regardless of whether the caller uses SOCKET or + * HANDLE). Same semantics as `rdp_bridge_start_unix_fd`. + */ +int32_t rdp_bridge_start_windows_socket( + uintptr_t client_socket, + const char *target_host, + uint16_t target_port, + const char *username, + const char *password, + uint64_t *out_handle +); +#endif /* windows */ + +/* + * Block until the session on `handle` ends. Returns RDP_BRIDGE_OK on + * clean end, RDP_BRIDGE_SESSION_ERROR on handshake / forwarding error, + * RDP_BRIDGE_THREAD_PANIC if the session thread panicked, + * RDP_BRIDGE_INVALID_HANDLE if the handle is unknown. Calling a second + * time on the same handle returns RDP_BRIDGE_OK. + */ +int32_t rdp_bridge_wait(uint64_t handle); + +/* + * Signal the session to cancel. The session's tokio task is aborted at + * the next await point and `wait` will then return. Idempotent; safe + * from any thread. Returns RDP_BRIDGE_OK or RDP_BRIDGE_INVALID_HANDLE. + */ +int32_t rdp_bridge_cancel(uint64_t handle); + +/* + * Release the handle's registry entry. Must be called after `wait` has + * returned. Returns RDP_BRIDGE_OK or RDP_BRIDGE_INVALID_HANDLE. + */ +int32_t rdp_bridge_free(uint64_t handle); + +#ifdef __cplusplus +} +#endif + +#endif /* INFISICAL_RDP_BRIDGE_H */ diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 6795384c..b2fe3a00 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -24,6 +24,7 @@ use ironrdp_tokio::reqwest::ReqwestNetworkClient; use ironrdp_tokio::{FramedWrite, NetworkClient}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; +use tokio_util::sync::CancellationToken; use tracing::info; use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; @@ -42,8 +43,23 @@ pub struct TargetEndpoint { } /// Run a single RDP MITM session. Injects credentials at CredSSP and then -/// passes everything else through between the two TLS streams. -pub async fn run_mitm(client_tcp: TcpStream, target: TargetEndpoint) -> Result<()> { +/// passes everything else through between the two TLS streams. The caller +/// can abort the session by cancelling the token. +pub async fn run_mitm( + client_tcp: TcpStream, + target: TargetEndpoint, + cancel: CancellationToken, +) -> Result<()> { + tokio::select! { + result = run_mitm_inner(client_tcp, target) => result, + _ = cancel.cancelled() => { + info!("session canceled by caller"); + Ok(()) + } + } +} + +async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result<()> { // rustls 0.23 requires an explicit crypto provider when more than one is // compiled in. Our tree pulls both `ring` (direct) and `aws-lc-rs` // (transitively from reqwest). Install ring as the default on first call; diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs new file mode 100644 index 00000000..8df854f5 --- /dev/null +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -0,0 +1,274 @@ +//! C ABI for the bridge. Designed to be called from Go via CGo. +//! +//! Model: +//! - Each session runs on its own OS thread with a current-thread tokio +//! runtime. Sessions are fully isolated. +//! - `start_*` allocates an opaque `u64` handle, spawns the thread, and +//! returns immediately. The handshake and passthrough happen inside +//! the spawned thread. +//! - `wait` blocks the calling thread until the session ends, returning +//! 0 on clean exit and 1 on session error. +//! - `cancel` is idempotent: it signals the bridge's CancellationToken, +//! which interrupts `run_mitm` at the next await point. +//! - `free` removes the handle from the registry. Call after `wait`. +//! +//! Ownership of the client file descriptor / socket: Rust takes ownership +//! of what is passed in and closes it when the session ends. The Go +//! caller is expected to hand in a dup'd fd (syscall.Dup on Unix, the +//! Windows equivalent on Windows) so its own `net.Conn` lifetime stays +//! independent. + +use std::collections::HashMap; +use std::ffi::{c_char, CStr}; +use std::net::TcpStream as StdTcpStream; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{LazyLock, Mutex}; +use std::thread::JoinHandle; + +use tokio::net::TcpStream; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; + +use crate::bridge::{run_mitm, TargetEndpoint}; + +pub const RDP_BRIDGE_OK: i32 = 0; +pub const RDP_BRIDGE_SESSION_ERROR: i32 = 1; +pub const RDP_BRIDGE_THREAD_PANIC: i32 = 2; +pub const RDP_BRIDGE_INVALID_HANDLE: i32 = -1; +pub const RDP_BRIDGE_BAD_ARG: i32 = -2; +pub const RDP_BRIDGE_RUNTIME_ERROR: i32 = -3; + +struct BridgeEntry { + cancel: CancellationToken, + /// Taken by `wait`; `None` afterward. + join: Mutex>>>, +} + +static HANDLES: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); +static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1); + +fn register(entry: BridgeEntry) -> u64 { + let id = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed); + HANDLES.lock().expect("HANDLES poisoned").insert(id, entry); + id +} + +/// # Safety +/// +/// `ptr` must be either null or a valid NUL-terminated C string with the +/// `'static` borrow of the caller's buffer lasting for the duration of +/// this call. +unsafe fn c_str_to_owned(ptr: *const c_char) -> Option { + if ptr.is_null() { + return None; + } + unsafe { CStr::from_ptr(ptr) } + .to_str() + .ok() + .map(str::to_owned) +} + +fn spawn_session( + client_tcp: StdTcpStream, + host: String, + port: u16, + username: String, + password: String, +) -> anyhow::Result { + client_tcp.set_nonblocking(true)?; + let cancel = CancellationToken::new(); + let cancel_for_thread = cancel.clone(); + + let join = std::thread::Builder::new() + .name("rdp-bridge-session".to_owned()) + .spawn(move || -> anyhow::Result<()> { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + rt.block_on(async move { + let client = TcpStream::from_std(client_tcp)?; + let endpoint = TargetEndpoint { + host, + port, + username, + password, + }; + run_mitm(client, endpoint, cancel_for_thread).await + }) + })?; + + Ok(register(BridgeEntry { + cancel, + join: Mutex::new(Some(join)), + })) +} + +/// Start a new bridge session consuming a Unix client file descriptor. +/// +/// # Safety +/// +/// `client_fd` must be a valid open socket descriptor. Ownership transfers +/// to the bridge on success; the caller must not close it. On failure, +/// ownership stays with the caller. `target_host`, `username`, and +/// `password` must be NUL-terminated valid UTF-8 C strings. `out_handle` +/// must be a writable `uint64_t`. +#[cfg(unix)] +#[no_mangle] +pub unsafe extern "C" fn rdp_bridge_start_unix_fd( + client_fd: std::ffi::c_int, + target_host: *const c_char, + target_port: u16, + username: *const c_char, + password: *const c_char, + out_handle: *mut u64, +) -> i32 { + if out_handle.is_null() { + return RDP_BRIDGE_BAD_ARG; + } + let host = match unsafe { c_str_to_owned(target_host) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let username = match unsafe { c_str_to_owned(username) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let password = match unsafe { c_str_to_owned(password) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + + use std::os::unix::io::FromRawFd; + // Safety: contract states the caller transfers ownership of fd. + let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; + + match spawn_session(client_tcp, host, target_port, username, password) { + Ok(id) => { + // Safety: contract states out_handle is writable. + unsafe { *out_handle = id }; + RDP_BRIDGE_OK + } + Err(e) => { + error!(error = ?e, "rdp_bridge_start_unix_fd: failed to spawn session"); + RDP_BRIDGE_RUNTIME_ERROR + } + } +} + +/// Start a new bridge session consuming a Windows SOCKET. +/// +/// # Safety +/// +/// `client_socket` must be a valid open `SOCKET`. Ownership transfers +/// to the bridge on success. See `rdp_bridge_start_unix_fd` for shared +/// string and out-param contracts. +#[cfg(windows)] +#[no_mangle] +pub unsafe extern "C" fn rdp_bridge_start_windows_socket( + client_socket: usize, + target_host: *const c_char, + target_port: u16, + username: *const c_char, + password: *const c_char, + out_handle: *mut u64, +) -> i32 { + if out_handle.is_null() { + return RDP_BRIDGE_BAD_ARG; + } + let host = match unsafe { c_str_to_owned(target_host) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let username = match unsafe { c_str_to_owned(username) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + let password = match unsafe { c_str_to_owned(password) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; + + use std::os::windows::io::{FromRawSocket, RawSocket}; + // Safety: contract states caller transfers ownership. + let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) }; + + match spawn_session(client_tcp, host, target_port, username, password) { + Ok(id) => { + unsafe { *out_handle = id }; + RDP_BRIDGE_OK + } + Err(e) => { + error!(error = ?e, "rdp_bridge_start_windows_socket: failed to spawn session"); + RDP_BRIDGE_RUNTIME_ERROR + } + } +} + +/// Block until the session on `handle` finishes. +/// +/// Returns `RDP_BRIDGE_OK` on clean session end, +/// `RDP_BRIDGE_SESSION_ERROR` if the session ended with an error, +/// `RDP_BRIDGE_THREAD_PANIC` if the session thread panicked, +/// `RDP_BRIDGE_INVALID_HANDLE` if `handle` is unknown. +/// +/// Safe to call from any thread. Calling a second time on the same handle +/// returns `RDP_BRIDGE_OK` (the session is already done). +#[no_mangle] +pub extern "C" fn rdp_bridge_wait(handle: u64) -> i32 { + let join = { + let handles = HANDLES.lock().expect("HANDLES poisoned"); + match handles.get(&handle) { + Some(entry) => entry.join.lock().expect("join poisoned").take(), + None => return RDP_BRIDGE_INVALID_HANDLE, + } + }; + + match join { + Some(jh) => match jh.join() { + Ok(Ok(())) => { + info!(handle, "rdp_bridge_wait: session ended cleanly"); + RDP_BRIDGE_OK + } + Ok(Err(e)) => { + error!(handle, error = ?e, "rdp_bridge_wait: session failed"); + RDP_BRIDGE_SESSION_ERROR + } + Err(_) => { + error!(handle, "rdp_bridge_wait: session thread panicked"); + RDP_BRIDGE_THREAD_PANIC + } + }, + None => RDP_BRIDGE_OK, + } +} + +/// Signal the session to cancel. Idempotent: safe to call multiple times. +/// After `cancel`, the caller should still `wait` to observe the session +/// actually finishing, and then `free` to release the handle. +#[no_mangle] +pub extern "C" fn rdp_bridge_cancel(handle: u64) -> i32 { + let handles = HANDLES.lock().expect("HANDLES poisoned"); + match handles.get(&handle) { + Some(entry) => { + entry.cancel.cancel(); + RDP_BRIDGE_OK + } + None => RDP_BRIDGE_INVALID_HANDLE, + } +} + +/// Release the handle's resources. Must be called after `wait` has +/// returned. If the session thread is still running when `free` is +/// called, the handle is dropped and the thread becomes detached (still +/// owned by the registry entry; would leak). Callers should always pair +/// `wait` with `free`. +#[no_mangle] +pub extern "C" fn rdp_bridge_free(handle: u64) -> i32 { + let mut handles = HANDLES.lock().expect("HANDLES poisoned"); + if handles.remove(&handle).is_some() { + RDP_BRIDGE_OK + } else { + RDP_BRIDGE_INVALID_HANDLE + } +} diff --git a/packages/pam/handlers/rdp/native/src/lib.rs b/packages/pam/handlers/rdp/native/src/lib.rs index e21b59e8..bc775d43 100644 --- a/packages/pam/handlers/rdp/native/src/lib.rs +++ b/packages/pam/handlers/rdp/native/src/lib.rs @@ -12,3 +12,4 @@ pub mod bridge; pub mod config; +pub mod ffi; diff --git a/packages/pam/handlers/rdp/native/src/main.rs b/packages/pam/handlers/rdp/native/src/main.rs index 41b19fc6..f44cbbe5 100644 --- a/packages/pam/handlers/rdp/native/src/main.rs +++ b/packages/pam/handlers/rdp/native/src/main.rs @@ -1,20 +1,16 @@ //! Standalone test binary. Listens on a loopback port, accepts one //! connection, runs the MITM bridge to a Windows target, and exits. //! -//! Validate against a real Windows server with: -//! -//! ```sh -//! xfreerdp /v:127.0.0.1:3390 /u:infisical /p:infisical /sec:tls /cert:ignore -//! ``` -//! -//! (or `mstsc` with a `.rdp` file whose `authentication level:i:0` and -//! `enablecredsspsupport:i:0` are set). +//! Validate against a real Windows server with any native RDP client +//! using credentials `infisical`/`infisical`; see the crate README for +//! tested client commands. use std::net::SocketAddr; use anyhow::{Context, Result}; use clap::Parser; use tokio::net::TcpListener; +use tokio_util::sync::CancellationToken; use tracing::{error, info}; use tracing_subscriber::EnvFilter; @@ -74,7 +70,9 @@ async fn main() -> Result<()> { password: args.password, }; - match run_mitm(client_tcp, endpoint).await { + // Test binary never cancels; pass a fresh token that stays uncancelled. + let cancel = CancellationToken::new(); + match run_mitm(client_tcp, endpoint, cancel).await { Ok(()) => { info!("session ended cleanly"); Ok(()) From 39328fdcb144721c9e66888e7ec1128c5da0fc7d Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 22 Apr 2026 13:38:44 -0400 Subject: [PATCH 03/51] feat(pam): wire rdp handler into gateway dispatch Phase 3 of the native RDP client architecture. The gateway's existing PAM dispatcher now routes Windows/RDP sessions to the Rust bridge. Changes: - Add ResourceTypeRDP = "windows" constant (matches backend enum). - Extend GetSupportedResourceTypes to advertise RDP. - Add a session.ResourceTypeRDP case to HandlePAMProxy's switch that builds an rdp.RDPProxyConfig from the session credentials and calls proxy.HandleConnection. Target port is range-validated before downcast to uint16. New handler API in packages/pam/handlers/rdp: - RDPProxy + RDPProxyConfig + NewRDPProxy in proxy.go (shared across all builds so the dispatcher can always reference the type). - HandleConnection uses StartWithReadWriter (new) to set up a local loopback TCP pair: the gateway's *tls.Conn gets pumped through two io.Copy goroutines to/from the loopback peer, while the Rust bridge owns the other end as a raw kernel fd. This is required because the gateway's conn is TLS-wrapped over an SSH channel and does not expose syscall.Conn. - Bridge gains an optional cleanup hook called during Close, which StartWithReadWriter uses to tear down the loopback goroutines. - Supervisor in HandleConnection translates ctx.Done into bridge.Cancel so admin terminate and session expiry propagate cleanly. - Stub file gains a HandleConnection that closes the conn and returns ErrRdpUnavailable, keeping the dispatcher compilable on builds without -tags rdp. No gateway transport changes needed: RDP reuses the existing `infisical-pam-proxy` ALPN, and routing happens via the ResourceType field the backend puts in the client certificate extension. Full end-to-end validation deferred to Phase 4 when the CLI side exists. --- packages/pam/handlers/rdp/bridge.go | 3 +- packages/pam/handlers/rdp/bridge_cgo.go | 145 ++++++++++++++++++++++- packages/pam/handlers/rdp/bridge_stub.go | 20 +++- packages/pam/handlers/rdp/proxy.go | 35 ++++++ packages/pam/pam-proxy.go | 20 ++++ packages/pam/session/uploader.go | 4 + 6 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 packages/pam/handlers/rdp/proxy.go diff --git a/packages/pam/handlers/rdp/bridge.go b/packages/pam/handlers/rdp/bridge.go index e792b133..0654896b 100644 --- a/packages/pam/handlers/rdp/bridge.go +++ b/packages/pam/handlers/rdp/bridge.go @@ -23,5 +23,6 @@ var ErrSessionFailed = errors.New("rdp bridge: session ended with error") // called from any goroutine; Wait blocks until the session ends; Close // releases the handle and must be called after Wait returns. type Bridge struct { - handle uint64 + handle uint64 + cleanup func() // runs during Close after the handle is freed; nil for direct fd sessions } diff --git a/packages/pam/handlers/rdp/bridge_cgo.go b/packages/pam/handlers/rdp/bridge_cgo.go index ad716044..3272dd4c 100644 --- a/packages/pam/handlers/rdp/bridge_cgo.go +++ b/packages/pam/handlers/rdp/bridge_cgo.go @@ -13,7 +13,10 @@ package rdp import "C" import ( + "context" + "errors" "fmt" + "io" "net" "syscall" "unsafe" @@ -26,14 +29,22 @@ import ( // ends. // // `conn` must be a *net.TCPConn or any net.Conn that exposes a raw file -// descriptor via syscall.Conn. Passing a TLS-wrapped conn will fail; -// Phase 3 will introduce a loopback shim for that case. +// descriptor via syscall.Conn. For TLS-wrapped or otherwise non-fd-backed +// conns (like the ones the gateway receives), use [StartWithReadWriter] +// instead. func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { dupFd, err := dupConnFD(conn) if err != nil { return nil, fmt.Errorf("rdp bridge: dup client fd: %w", err) } - // If the start call fails below, we still own the dup; close it. + return startWithDupedFD(dupFd, targetHost, targetPort, username, password) +} + +// startWithDupedFD hands ownership of `dupFd` to the Rust bridge. On +// success the bridge closes the fd when the session ends; on failure +// this function closes the fd itself before returning. Shared by +// StartWithConn and StartWithReadWriter. +func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { success := false defer func() { if !success { @@ -64,6 +75,79 @@ func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username return &Bridge{handle: uint64(handle)}, nil } +// StartWithReadWriter starts a bridge session for a caller whose client +// stream is not fd-backed (e.g. *tls.Conn wrapping an mTLS'd virtual +// connection in the gateway). It creates a local loopback TCP pair, hands +// the kernel-backed accepted end to the Rust bridge, and pumps bytes +// between the other loopback end and the caller's `rw` via two io.Copy +// goroutines. The goroutines exit when either side closes; the bridge's +// Close method also tears them down. +// +// The caller retains ownership of `rw` and is responsible for closing it +// when done (the bridge does not close it). +func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) + } + // We only ever accept one connection; close the listener either way. + defer listener.Close() + + // Kick off the dial concurrently with accept. Either ordering would + // work but the goroutine avoids a deadlock if some future net stack + // decides accept must run first. + type dialResult struct { + conn net.Conn + err error + } + dialCh := make(chan dialResult, 1) + go func() { + c, err := net.Dial("tcp", listener.Addr().String()) + dialCh <- dialResult{c, err} + }() + + accepted, err := listener.Accept() + if err != nil { + return nil, fmt.Errorf("rdp bridge: loopback accept: %w", err) + } + dr := <-dialCh + if dr.err != nil { + _ = accepted.Close() + return nil, fmt.Errorf("rdp bridge: loopback dial: %w", dr.err) + } + peer := dr.conn + + // The accepted side gets handed to Rust. Dup its fd, then close our + // copy so only Rust owns the socket going forward. + dupFd, err := dupConnFD(accepted) + _ = accepted.Close() + if err != nil { + _ = peer.Close() + return nil, fmt.Errorf("rdp bridge: dup accepted fd: %w", err) + } + + bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password) + if err != nil { + _ = peer.Close() + return nil, err + } + + // Pump bytes between the caller's rw and the loopback peer. Each + // goroutine closes the peer on exit so the other side unblocks and + // exits too, regardless of which half EOFs first. + go func() { + _, _ = io.Copy(peer, rw) + _ = peer.Close() + }() + go func() { + _, _ = io.Copy(rw, peer) + _ = peer.Close() + }() + + bridge.cleanup = func() { _ = peer.Close() } + return bridge, nil +} + // dupConnFD returns a new file descriptor independent from `conn`'s // internal one. The caller becomes responsible for closing the returned // fd. Requires `conn` to implement syscall.Conn. @@ -119,9 +203,62 @@ func (b *Bridge) Cancel() error { return nil } -// Close releases the bridge handle. Call after Wait has returned. +// HandleConnection is the entry point the gateway's PAM dispatcher calls +// for a Windows/RDP session. It takes ownership of `clientConn` (closes +// it on return), spawns a bridge via the loopback shim, and blocks until +// the session ends or `ctx` is cancelled (admin terminate, session +// expiry). On cancellation the bridge is signalled to abort and we wait +// for it to actually finish before returning `ctx.Err()`. +func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { + defer clientConn.Close() + if p.config.SessionLogger != nil { + defer func() { + if err := p.config.SessionLogger.Close(); err != nil { + // Don't fail the session on logger close error; it's a + // best-effort flush of any buffered events. + _ = err + } + }() + } + + bridge, err := StartWithReadWriter( + clientConn, + p.config.TargetHost, + p.config.TargetPort, + p.config.InjectUsername, + p.config.InjectPassword, + ) + if err != nil { + return fmt.Errorf("rdp proxy: start bridge: %w", err) + } + defer bridge.Close() + + // Run Wait on a goroutine so we can also select on ctx.Done(). + waitErr := make(chan error, 1) + go func() { waitErr <- bridge.Wait() }() + + select { + case err := <-waitErr: + if err != nil && !errors.Is(err, ErrInvalidHandle) { + return fmt.Errorf("rdp proxy: session: %w", err) + } + return nil + case <-ctx.Done(): + _ = bridge.Cancel() + <-waitErr // let the session unwind before we return + return ctx.Err() + } +} + +// Close releases the bridge handle. Call after Wait has returned. If the +// bridge was created with a loopback shim (via StartWithReadWriter), +// Close also tears down the shim goroutines by closing their loopback +// endpoint. func (b *Bridge) Close() error { rc := C.rdp_bridge_free(C.uint64_t(b.handle)) + if b.cleanup != nil { + b.cleanup() + } if rc == C.RDP_BRIDGE_INVALID_HANDLE { return ErrInvalidHandle } diff --git a/packages/pam/handlers/rdp/bridge_stub.go b/packages/pam/handlers/rdp/bridge_stub.go index 4909c158..a1aac7a0 100644 --- a/packages/pam/handlers/rdp/bridge_stub.go +++ b/packages/pam/handlers/rdp/bridge_stub.go @@ -2,7 +2,11 @@ package rdp -import "net" +import ( + "context" + "io" + "net" +) // StartWithConn is a stub that reports the RDP bridge is unavailable in // this build. To enable the real implementation, build with `-tags rdp` @@ -12,6 +16,20 @@ func StartWithConn(_ net.Conn, _ string, _ uint16, _, _ string) (*Bridge, error) return nil, ErrRdpUnavailable } +// StartWithReadWriter is a stub for builds without the RDP bridge. +func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _ string) (*Bridge, error) { + return nil, ErrRdpUnavailable +} + +// HandleConnection is a stub for builds without the RDP bridge. The +// gateway dispatcher calls into this on an RDP session; returning +// ErrRdpUnavailable surfaces a clean "this gateway build does not +// support RDP" error to the caller. +func (p *RDPProxy) HandleConnection(_ context.Context, clientConn net.Conn) error { + _ = clientConn.Close() + return ErrRdpUnavailable +} + // Wait is a stub for builds without the RDP bridge. func (b *Bridge) Wait() error { return ErrRdpUnavailable } diff --git a/packages/pam/handlers/rdp/proxy.go b/packages/pam/handlers/rdp/proxy.go new file mode 100644 index 00000000..544f1272 --- /dev/null +++ b/packages/pam/handlers/rdp/proxy.go @@ -0,0 +1,35 @@ +package rdp + +import ( + "github.com/Infisical/infisical-merge/packages/pam/session" +) + +// RDPProxyConfig is what the gateway's PAM dispatcher passes to +// [NewRDPProxy] when routing a Windows/RDP session. +type RDPProxyConfig struct { + TargetHost string + TargetPort uint16 + InjectUsername string + InjectPassword string + SessionID string + + // SessionLogger is retained on the config for API symmetry with the + // other PAM handlers. The current bridge has no event tap (no RDP + // session recording yet) so nothing is actually written through it, + // but the dispatcher expects to hand one in per session and may start + // shipping events through it in a later phase. + SessionLogger session.SessionLogger +} + +// RDPProxy is the gateway-side handler for a Windows/RDP PAM session. +// It wraps an [RDPProxyConfig] and implements the same HandleConnection +// shape as SSH / Postgres / Redis / etc. +type RDPProxy struct { + config RDPProxyConfig +} + +// NewRDPProxy constructs a proxy. The actual session work happens in +// HandleConnection (whose implementation is in a platform-specific file). +func NewRDPProxy(config RDPProxyConfig) *RDPProxy { + return &RDPProxy{config: config} +} diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index 3c44db0d..9cf0944f 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -18,6 +18,7 @@ import ( "github.com/Infisical/infisical-merge/packages/pam/handlers/mongodb" "github.com/Infisical/infisical-merge/packages/pam/handlers/mssql" "github.com/Infisical/infisical-merge/packages/pam/handlers/mysql" + "github.com/Infisical/infisical-merge/packages/pam/handlers/rdp" "github.com/Infisical/infisical-merge/packages/pam/handlers/redis" "github.com/Infisical/infisical-merge/packages/pam/handlers/ssh" "github.com/Infisical/infisical-merge/packages/pam/session" @@ -53,6 +54,7 @@ func GetSupportedResourceTypes() []string { session.ResourceTypeKubernetes, session.ResourceTypeRedis, session.ResourceTypeMongodb, + session.ResourceTypeRDP, } } @@ -405,6 +407,24 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo } return proxy.HandleConnection(ctx, conn, sessionLogger) + case session.ResourceTypeRDP: + if credentials.Port < 0 || credentials.Port > 65535 { + return fmt.Errorf("rdp: target port %d out of range", credentials.Port) + } + rdpConfig := rdp.RDPProxyConfig{ + TargetHost: credentials.Host, + TargetPort: uint16(credentials.Port), + InjectUsername: credentials.Username, + InjectPassword: credentials.Password, + SessionID: pamConfig.SessionId, + SessionLogger: sessionLogger, + } + proxy := rdp.NewRDPProxy(rdpConfig) + log.Info(). + Str("sessionId", pamConfig.SessionId). + Str("target", fmt.Sprintf("%s:%d", credentials.Host, credentials.Port)). + Msg("Starting RDP PAM proxy") + return proxy.HandleConnection(ctx, conn) default: return fmt.Errorf("unsupported resource type: %s", pamConfig.ResourceType) } diff --git a/packages/pam/session/uploader.go b/packages/pam/session/uploader.go index e75d8b8b..59b45262 100644 --- a/packages/pam/session/uploader.go +++ b/packages/pam/session/uploader.go @@ -31,6 +31,10 @@ const ( ResourceTypeSSH = "ssh" ResourceTypeKubernetes = "kubernetes" ResourceTypeMongodb = "mongodb" + // ResourceTypeRDP maps to the backend's PamResource.Windows enum; the + // string is "windows" (not "rdp") so the gateway's resource-type tag + // lines up with session metadata the backend already writes. + ResourceTypeRDP = "windows" ) type SessionFileInfo struct { From e84febf60740bfc6032ae7324bd31068bce813bb Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 22 Apr 2026 13:48:48 -0400 Subject: [PATCH 04/51] feat(pam): add rdp access CLI subcommand Phase 4 of the native RDP client architecture. Wires up `infisical pam rdp access --resource --account `. The command: - Authenticates the user and creates a PAM session via the existing CallPAMAccessWithMFA / HandleApprovalWorkflow plumbing. - Validates that the connected gateway advertises RDP support in its capabilities response. - Binds a loopback TCP listener on 127.0.0.1 (port 0 by default, or user-supplied via --port). - Writes a minimal .rdp file to the OS temp dir pointing at the loopback, prefilled with the fixed acceptor username. - Auto-launches the system RDP client (`open` on macOS, `cmd /c start` on Windows, `xdg-open` on Linux) unless --no-launch is passed. - Accepts one RDP client connection at a time and forwards bytes to the gateway over the existing mTLS+SSH relay with the standard infisical-pam-proxy ALPN. The gateway's Phase 3 dispatcher routes to the Windows handler, which spawns the Rust bridge. - Translates Ctrl-C to a graceful session teardown: notify the gateway via the cancellation ALPN, drain active connections, exit. New files: - packages/pam/local/rdp-proxy.go: RDPProxyServer mirrors DatabaseProxyServer (accept loop + io.Copy forwarding), plus writeRDPFile / launchRDPClient helpers. Modified: - packages/cmd/pam.go: adds the pam rdp access cobra command. - packages/pam/handlers/rdp/bridge.go: exports AcceptorUsername and AcceptorPassword consts kept in sync with the Rust crate, so the CLI can print them and embed them in the .rdp file. End-to-end validation needs Phase 5 (backend support for the Windows resource type); the CLI is otherwise feature-complete and builds cleanly both with -tags rdp (real bridge) and without (stub returning ErrRdpUnavailable). --- packages/cmd/pam.go | 96 ++++++++ packages/pam/handlers/rdp/bridge.go | 11 + packages/pam/local/rdp-proxy.go | 327 ++++++++++++++++++++++++++++ 3 files changed, 434 insertions(+) create mode 100644 packages/pam/local/rdp-proxy.go diff --git a/packages/cmd/pam.go b/packages/cmd/pam.go index 2c5b5cb6..5c3c2067 100644 --- a/packages/cmd/pam.go +++ b/packages/cmd/pam.go @@ -376,6 +376,90 @@ var pamRedisAccessCmd = &cobra.Command{ }, } +// ==================== RDP Commands ==================== + +var pamRdpCmd = &cobra.Command{ + Use: "rdp", + Short: "RDP-related PAM commands", + Long: "RDP-related PAM commands for Infisical (Windows Server / Remote Desktop)", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, +} + +var pamRdpAccessCmd = &cobra.Command{ + Use: "access", + Short: "Access PAM Windows/RDP accounts", + Long: "Access a PAM-managed Windows target over RDP. This starts a local loopback proxy that your RDP client connects to; the session tunnels through the Infisical Gateway with credentials injected server-side.", + Example: "infisical pam rdp access --resource windows-prod --account Administrator --duration 1h --project-id ", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + util.RequireLogin() + + resourceName, _ := cmd.Flags().GetString("resource") + accountName, _ := cmd.Flags().GetString("account") + + if resourceName == "" || accountName == "" { + util.PrintErrorMessageAndExit("Both --resource and --account flags are required") + } + + projectID, err := cmd.Flags().GetString("project-id") + if err != nil { + util.HandleError(err, "Unable to parse project-id flag") + } + + if projectID == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --project-id flag") + } + projectID = workspaceFile.WorkspaceId + } + + durationStr, err := cmd.Flags().GetString("duration") + if err != nil { + util.HandleError(err, "Unable to parse duration flag") + } + + _, err = time.ParseDuration(durationStr) + if err != nil { + util.HandleError(err, "Invalid duration format. Use formats like '1h', '30m', '2h30m'") + } + + port, err := cmd.Flags().GetInt("port") + if err != nil { + util.HandleError(err, "Unable to parse port flag") + } + + noLaunch, err := cmd.Flags().GetBool("no-launch") + if err != nil { + util.HandleError(err, "Unable to parse no-launch flag") + } + + log.Debug().Msg("PAM RDP Access: Trying to start session using logged in details") + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + isConnected := util.ValidateInfisicalAPIConnection() + + if isConnected { + log.Debug().Msg("PAM RDP Access: Connected to Infisical instance, checking logged in creds") + } + + if err != nil { + util.HandleError(err, "Unable to get logged in user details") + } + + if isConnected && loggedInUserDetails.LoginExpired { + loggedInUserDetails = util.EstablishUserLoginSession() + } + + pam.StartRDPLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, pam.PAMAccessParams{ + ResourceName: resourceName, + AccountName: accountName, + }, projectID, durationStr, port, noLaunch) + }, +} + func init() { // Database commands pamDbCmd.AddCommand(pamDbAccessCmd) @@ -426,9 +510,21 @@ func init() { pamRedisAccessCmd.MarkFlagRequired("resource") pamRedisAccessCmd.MarkFlagRequired("account") + // RDP commands + pamRdpCmd.AddCommand(pamRdpAccessCmd) + pamRdpAccessCmd.Flags().String("resource", "", "Name of the PAM resource to access") + pamRdpAccessCmd.Flags().String("account", "", "Name of the account within the resource") + pamRdpAccessCmd.Flags().String("duration", "1h", "Duration for RDP access session (e.g., '1h', '30m', '2h30m')") + pamRdpAccessCmd.Flags().Int("port", 0, "Port for the local RDP proxy server (0 for auto-assign)") + pamRdpAccessCmd.Flags().String("project-id", "", "Project ID of the account to access") + pamRdpAccessCmd.Flags().Bool("no-launch", false, "Do not auto-launch the system RDP client; print connection details only") + pamRdpAccessCmd.MarkFlagRequired("resource") + pamRdpAccessCmd.MarkFlagRequired("account") + pamCmd.AddCommand(pamDbCmd) pamCmd.AddCommand(pamSshCmd) pamCmd.AddCommand(pamKubernetesCmd) pamCmd.AddCommand(pamRedisCmd) + pamCmd.AddCommand(pamRdpCmd) RootCmd.AddCommand(pamCmd) } diff --git a/packages/pam/handlers/rdp/bridge.go b/packages/pam/handlers/rdp/bridge.go index 0654896b..95088e78 100644 --- a/packages/pam/handlers/rdp/bridge.go +++ b/packages/pam/handlers/rdp/bridge.go @@ -19,6 +19,17 @@ var ErrInvalidHandle = errors.New("rdp bridge: invalid handle") // handshake or forwarding error (rather than a clean client disconnect). var ErrSessionFailed = errors.New("rdp bridge: session ended with error") +// AcceptorUsername and AcceptorPassword are the fixed placeholder +// credential the native RDP client must present to the acceptor side of +// the bridge. The real access gate is upstream (Infisical auth + the +// gateway tunnel); these values are echoed from the Rust crate's +// ACCEPTOR_USERNAME / ACCEPTOR_PASSWORD constants and must stay in +// sync. +const ( + AcceptorUsername = "infisical" + AcceptorPassword = "infisical" +) + // Bridge owns the handle to a running RDP MITM session. Cancel may be // called from any goroutine; Wait blocks until the session ends; Close // releases the handle and must be called after Wait returns. diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go new file mode 100644 index 00000000..2a8898df --- /dev/null +++ b/packages/pam/local/rdp-proxy.go @@ -0,0 +1,327 @@ +package pam + +import ( + "context" + "fmt" + "io" + "net" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "syscall" + "time" + + "github.com/Infisical/infisical-merge/packages/pam/handlers/rdp" + "github.com/Infisical/infisical-merge/packages/util" + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" +) + +// RDPProxyServer exposes a local loopback TCP listener that tunnels bytes +// to the gateway's RDP MITM bridge via the existing mTLS + SSH relay. The +// user's RDP client connects to the loopback port; the gateway takes care +// of credential injection and forwarding to the Windows target. +type RDPProxyServer struct { + BaseProxyServer + server net.Listener + port int + rdpFilePath string // path to the generated .rdp file, if any +} + +// StartRDPLocalProxy is the CLI entry point for `infisical pam rdp access`. +// It creates a PAM session with the backend, binds a loopback listener, +// writes a .rdp file pointing at that loopback, optionally launches the +// user's default RDP client, and forwards accepted connections to the +// gateway. +func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projectID string, durationStr string, port int, noLaunch bool) { + log.Info().Msgf("Starting RDP proxy for account: %s", accessParams.GetDisplayName()) + log.Info().Msgf("Session duration: %s", durationStr) + + httpClient := resty.New() + httpClient.SetAuthToken(accessToken) + httpClient.SetHeader("User-Agent", "infisical-cli") + + pamRequest := accessParams.ToAPIRequest(projectID, durationStr) + + pamResponse, err := CallPAMAccessWithMFA(httpClient, pamRequest) + if err != nil { + if HandleApprovalWorkflow(httpClient, err, projectID, accessParams, durationStr) { + return + } + util.HandleError(err, "Failed to access PAM account") + return + } + + log.Info().Msgf("RDP session created with ID: %s", pamResponse.SessionId) + + duration, err := time.ParseDuration(durationStr) + if err != nil { + util.HandleError(err, "Failed to parse duration") + return + } + + ctx, cancel := context.WithCancel(context.Background()) + + proxy := &RDPProxyServer{ + BaseProxyServer: BaseProxyServer{ + httpClient: httpClient, + relayHost: pamResponse.RelayHost, + relayClientCert: pamResponse.RelayClientCertificate, + relayClientKey: pamResponse.RelayClientPrivateKey, + relayServerCertChain: pamResponse.RelayServerCertificateChain, + gatewayClientCert: pamResponse.GatewayClientCertificate, + gatewayClientKey: pamResponse.GatewayClientPrivateKey, + gatewayServerCertChain: pamResponse.GatewayServerCertificateChain, + sessionExpiry: time.Now().Add(duration), + sessionId: pamResponse.SessionId, + resourceType: pamResponse.ResourceType, + ctx: ctx, + cancel: cancel, + shutdownCh: make(chan struct{}), + }, + } + + if err := proxy.ValidateResourceTypeSupported(); err != nil { + util.HandleError(err, "Gateway version outdated") + return + } + + if err := proxy.Start(port); err != nil { + util.HandleError(err, "Failed to start proxy server") + return + } + + rdpFilePath, err := writeRDPFile(proxy.port, pamResponse.SessionId) + if err != nil { + log.Warn().Err(err).Msg("Failed to write .rdp file; proxy still running") + } else { + proxy.rdpFilePath = rdpFilePath + } + + log.Info().Msgf("RDP proxy server listening on port %d", proxy.port) + fmt.Printf("\n") + fmt.Printf("**********************************************************************\n") + fmt.Printf(" RDP Proxy Session Started! \n") + fmt.Printf("----------------------------------------------------------------------\n") + fmt.Printf("Resource: %s\n", accessParams.ResourceName) + fmt.Printf("Account: %s\n", accessParams.AccountName) + fmt.Printf("\n") + fmt.Printf("Connect your RDP client to:\n") + util.PrintfStderr(" 127.0.0.1:%d\n", proxy.port) + fmt.Printf("With credentials:\n") + util.PrintfStderr(" username: %s\n", rdp.AcceptorUsername) + util.PrintfStderr(" password: %s\n", rdp.AcceptorPassword) + if proxy.rdpFilePath != "" { + fmt.Printf("\n") + fmt.Printf("Generated .rdp file:\n") + util.PrintfStderr(" %s\n", proxy.rdpFilePath) + } + util.PrintfStderr("\n") + util.PrintfStderr("Press Ctrl+C to terminate the session.\n") + util.PrintfStderr("**********************************************************************\n") + util.PrintfStderr("\n") + + if !noLaunch && proxy.rdpFilePath != "" { + if err := launchRDPClient(proxy.rdpFilePath); err != nil { + log.Warn().Err(err).Msg("Failed to auto-launch RDP client; connect manually using the details above") + } + } + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-sigChan + log.Info().Msgf("Received signal %v, initiating graceful shutdown...", sig) + proxy.gracefulShutdown() + }() + + proxy.Run() +} + +// Start binds the loopback listener. Port 0 picks a random free port. +func (p *RDPProxyServer) Start(port int) error { + var err error + if port == 0 { + p.server, err = net.Listen("tcp", "127.0.0.1:0") + } else { + p.server, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + } + if err != nil { + return fmt.Errorf("failed to start server: %w", err) + } + p.port = p.server.Addr().(*net.TCPAddr).Port + return nil +} + +func (p *RDPProxyServer) gracefulShutdown() { + p.shutdownOnce.Do(func() { + log.Info().Msg("Starting graceful shutdown of RDP proxy...") + + p.NotifySessionTermination() + + close(p.shutdownCh) + + if p.server != nil { + p.server.Close() + } + + p.cancel() + + p.WaitForConnectionsWithTimeout(10 * time.Second) + + log.Info().Msg("RDP proxy shutdown complete") + os.Exit(0) + }) +} + +func (p *RDPProxyServer) Run() { + defer p.server.Close() + + for { + select { + case <-p.ctx.Done(): + log.Info().Msg("Context cancelled, stopping proxy server") + return + case <-p.shutdownCh: + log.Info().Msg("Shutdown signal received, stopping proxy server") + return + default: + if time.Now().After(p.sessionExpiry) { + log.Warn().Msg("RDP session expired, shutting down proxy") + p.gracefulShutdown() + return + } + + if tcpListener, ok := p.server.(*net.TCPListener); ok { + tcpListener.SetDeadline(time.Now().Add(1 * time.Second)) + } + + conn, err := p.server.Accept() + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + continue + } + select { + case <-p.ctx.Done(): + return + case <-p.shutdownCh: + return + default: + log.Error().Err(err).Msg("Failed to accept connection") + continue + } + } + + p.activeConnections.Add(1) + go p.handleConnection(conn) + } + } +} + +// handleConnection forwards bytes between the RDP client and the gateway +// tunnel. Identical shape to the database proxy; the gateway's RDP +// handler takes over on the other side. +func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { + defer func() { + clientConn.Close() + p.activeConnections.Done() + }() + + log.Info().Msgf("New RDP connection from %s", clientConn.RemoteAddr()) + + select { + case <-p.ctx.Done(): + return + default: + } + + relayConn, err := p.CreateRelayConnection() + if err != nil { + log.Error().Err(err).Msg("Failed to connect to relay") + return + } + defer relayConn.Close() + + gatewayConn, err := p.CreateGatewayConnection(relayConn, ALPNInfisicalPAMProxy) + if err != nil { + log.Error().Err(err).Msg("Failed to connect to gateway") + return + } + defer gatewayConn.Close() + + log.Info().Msg("Established connection to RDP resource") + + connCtx, connCancel := context.WithCancel(p.ctx) + defer connCancel() + + gatewayErrCh, clientErrCh := p.NewDisconnectChannels() + + go func() { + defer connCancel() + _, err := io.Copy(clientConn, gatewayConn) + if err != nil { + select { + case <-connCtx.Done(): + default: + log.Debug().Err(err).Msg("Gateway to client copy ended") + } + } + gatewayErrCh <- err + }() + + go func() { + defer connCancel() + _, err := io.Copy(gatewayConn, clientConn) + if err != nil { + select { + case <-connCtx.Done(): + default: + log.Debug().Err(err).Msg("Client to gateway copy ended") + } + } + clientErrCh <- err + }() + + p.WaitForDisconnect(gatewayErrCh, clientErrCh, connCtx) + + log.Info().Msgf("RDP connection closed for client: %s", clientConn.RemoteAddr().String()) +} + +// writeRDPFile creates a .rdp file in the OS temp directory pointing at +// the local loopback listener. The filename includes the session ID so +// multiple concurrent sessions don't collide. The file is not deleted on +// exit; users sometimes want to re-open the same session a few times. +func writeRDPFile(listenPort int, sessionID string) (string, error) { + filename := fmt.Sprintf("infisical-rdp-%s.rdp", sessionID) + path := filepath.Join(os.TempDir(), filename) + + content := fmt.Sprintf( + "full address:s:127.0.0.1:%d\r\n"+ + "username:s:%s\r\n", + listenPort, + rdp.AcceptorUsername, + ) + + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + return "", fmt.Errorf("write rdp file: %w", err) + } + return path, nil +} + +// launchRDPClient opens the given .rdp file with the user's default RDP +// client. Failure is non-fatal; the caller can still manually connect +// using the printed connection details. +func launchRDPClient(rdpFilePath string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", rdpFilePath) + case "windows": + cmd = exec.Command("cmd", "/c", "start", "", rdpFilePath) + default: + cmd = exec.Command("xdg-open", rdpFilePath) + } + return cmd.Start() +} From 55ca651a7cc9c93ec8c150d8ab4a32c07b98a522 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 22 Apr 2026 15:42:20 -0400 Subject: [PATCH 05/51] chore(pam): rdp UX polish - Write .rdp files to ~/.infisical/rdp/ instead of the OS temp dir so they live next to the CLI's other per-user state (login config, update-check cache). Falls back to os.TempDir() if the home directory can't be resolved. Directory is created on first use with 0700 permissions. - Copy the fixed acceptor password to the system clipboard when the proxy starts, so the user can paste it into the RDP client prompt instead of typing `infisical` each time. No .rdp file format has a portable way to embed a plaintext password (mstsc's DPAPI field is Windows-local; Mac / freerdp clients ignore any password field at all), so the clipboard is the cleanest universal answer. Uses pbcopy / clip / xclip-or-xsel per-OS; failure is non-fatal. --- packages/pam/local/rdp-proxy.go | 78 ++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index 2a8898df..ae04670f 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -123,6 +123,16 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec util.PrintfStderr("**********************************************************************\n") util.PrintfStderr("\n") + // The .rdp file format has no portable way to embed a plaintext password + // (mstsc's `password 51:b:` is Windows-DPAPI-encrypted; Mac / freerdp + // clients ignore any password field). Put the fixed acceptor password + // on the clipboard so the user just pastes it when the client prompts. + if err := copyToClipboard(rdp.AcceptorPassword); err != nil { + log.Debug().Err(err).Msg("Could not copy password to clipboard; type it manually") + } else { + util.PrintfStderr("(Password copied to clipboard.)\n\n") + } + if !noLaunch && proxy.rdpFilePath != "" { if err := launchRDPClient(proxy.rdpFilePath); err != nil { log.Warn().Err(err).Msg("Failed to auto-launch RDP client; connect manually using the details above") @@ -289,13 +299,24 @@ func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { log.Info().Msgf("RDP connection closed for client: %s", clientConn.RemoteAddr().String()) } -// writeRDPFile creates a .rdp file in the OS temp directory pointing at -// the local loopback listener. The filename includes the session ID so -// multiple concurrent sessions don't collide. The file is not deleted on -// exit; users sometimes want to re-open the same session a few times. +// writeRDPFile creates a .rdp file pointing at the local loopback +// listener. Files live under `~/.infisical/rdp/` — matching the CLI's +// existing convention for per-user state (alongside the login config +// and update-check cache). Filename includes the session ID so +// concurrent sessions don't collide; the file is not deleted on exit +// so users can re-open a session a few times from Finder if they want. +// Falls back to the OS temp dir if the home directory can't be resolved. func writeRDPFile(listenPort int, sessionID string) (string, error) { filename := fmt.Sprintf("infisical-rdp-%s.rdp", sessionID) - path := filepath.Join(os.TempDir(), filename) + + dir, err := rdpFileDir() + if err != nil { + log.Debug().Err(err).Msg("Falling back to OS temp dir for .rdp file") + dir = os.TempDir() + } else if err := os.MkdirAll(dir, 0o700); err != nil { + return "", fmt.Errorf("create rdp dir %q: %w", dir, err) + } + path := filepath.Join(dir, filename) content := fmt.Sprintf( "full address:s:127.0.0.1:%d\r\n"+ @@ -310,6 +331,16 @@ func writeRDPFile(listenPort int, sessionID string) (string, error) { return path, nil } +// rdpFileDir returns ~/.infisical/rdp (the conventional per-user state +// location for CLI data; see util.CONFIG_FOLDER_NAME). +func rdpFileDir() (string, error) { + home, err := util.GetHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, util.CONFIG_FOLDER_NAME, "rdp"), nil +} + // launchRDPClient opens the given .rdp file with the user's default RDP // client. Failure is non-fatal; the caller can still manually connect // using the printed connection details. @@ -325,3 +356,40 @@ func launchRDPClient(rdpFilePath string) error { } return cmd.Start() } + +// copyToClipboard pipes `text` into the OS clipboard via the platform's +// standard CLI helper. Failure is non-fatal; the caller logs and moves on. +func copyToClipboard(text string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("pbcopy") + case "windows": + cmd = exec.Command("clip") + default: + // Try xclip first, then xsel. Neither is guaranteed to exist on + // headless servers, which is fine: we just return the error and + // the caller logs at debug level. + if _, err := exec.LookPath("xclip"); err == nil { + cmd = exec.Command("xclip", "-selection", "clipboard") + } else if _, err := exec.LookPath("xsel"); err == nil { + cmd = exec.Command("xsel", "--clipboard", "--input") + } else { + return fmt.Errorf("no clipboard tool found (install xclip or xsel)") + } + } + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + if _, err := stdin.Write([]byte(text)); err != nil { + return err + } + if err := stdin.Close(); err != nil { + return err + } + return cmd.Wait() +} From afb0ec2f632ec402361d44ffb5e1341f6dfe5c6d Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 22 Apr 2026 15:50:54 -0400 Subject: [PATCH 06/51] chore(pam): remove rdp native crate README --- packages/pam/handlers/rdp/native/README.md | 114 --------------------- 1 file changed, 114 deletions(-) delete mode 100644 packages/pam/handlers/rdp/native/README.md diff --git a/packages/pam/handlers/rdp/native/README.md b/packages/pam/handlers/rdp/native/README.md deleted file mode 100644 index 70538cd4..00000000 --- a/packages/pam/handlers/rdp/native/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# infisical-rdp-bridge - -Rust crate that implements the RDP MITM bridge for Infisical's PAM Windows -handler. The bridge terminates an inbound RDP connection from a native -client (Windows App, mstsc, xfreerdp, sdl-freerdp), opens an outbound -connection to a Windows target, injects credentials at CredSSP on the -target side, and then byte-forwards raw TLS traffic in both directions. - -## Architecture - -**Post-CredSSP passthrough.** The bridge drives each half of the -connection only far enough to complete credential injection: - -1. Inbound (acceptor): X.224 negotiation → TLS upgrade with a self-signed - cert → CredSSP/NLA with the fixed placeholder credential - `infisical`/`infisical`. -2. Outbound (connector): X.224 negotiation → TLS upgrade → CredSSP/NLA - with the real target credentials injected via sspi's NTLM. -3. Both halves run concurrently via `tokio::try_join!` so the client - doesn't sit waiting after its CredSSP completes. -4. After both CredSSP sequences finish, we drop the acceptor / connector - state machines and use `tokio::io::copy_bidirectional` on the raw TLS - streams. - -From this point, client and target negotiate MCS, channels, capabilities, -and share state **directly with each other through us**. We never -synthesize our own Connect Initial / Connect Response, so we avoid the -capability-drift and identifier-drift that naive acceptor+connector -handshake forwarding introduces. Strict clients (Windows App, mstsc) -that validate echoes like `ServerCoreData.clientRequestedProtocols` -accept the session because target's response reflects the values -**client** sent, not what our connector would have advertised. - -## Scope - -No event tap, no session recording. The crate compiles to both a -`staticlib` (consumed via CGo from the Go wrapper at -`packages/pam/handlers/rdp/`, see [Go wrapper](#go-wrapper) below) and -an `rlib` (for the in-tree test binary). - -## Build - -```sh -cargo build --release -``` - -## Manual validation - -Start the bridge pointing at a real Windows server: - -```sh -RUST_LOG=info cargo run --release -- \ - --listen 127.0.0.1:3390 \ - --target-host \ - --target-port 3389 \ - --username \ - --password -``` - -Then connect any native RDP client to `127.0.0.1:3390` with credentials -`infisical` / `infisical`. Examples: - -**Microsoft Windows App (macOS):** Add PC → `127.0.0.1:3390`, user -account `infisical` / `infisical`. Click through the self-signed cert -warning. This is the strict client and validates the full post-CredSSP -architecture end-to-end. - -**sdl-freerdp (Linux/macOS):** - -```sh -sdl-freerdp /v:127.0.0.1:3390 /u:infisical /p:infisical /cert:ignore -``` - -**mstsc (Windows):** Save a `.rdp` file with: - -``` -full address:s:127.0.0.1:3390 -username:s:infisical -``` - -and supply `infisical` as the password when prompted. - -### macOS dev note - -On macOS, sspi's Kerberos DNS fallback via Bonjour adds ~4s of DNS -timeouts during CredSSP. Sessions still succeed (strict clients like -Windows App do not time out in that window), but CredSSP feels sluggish. -Production gateway deployments run on Linux, where `hickory-resolver` -returns NXDOMAIN in milliseconds, so this is a local-dev quirk only. - -## Go wrapper - -The static library in `target/release/libinfisical_rdp_bridge.a` -exports a C ABI (see [`include/rdp_bridge.h`](include/rdp_bridge.h)) -that is consumed from the Go package at -`packages/pam/handlers/rdp/` via CGo. Build order: - -```sh -# 1. Build the Rust static library first. -cd packages/pam/handlers/rdp/native && cargo build --release - -# 2. Build the Go binary or package with the rdp tag. -cd - && go build -tags rdp ./packages/pam/handlers/rdp/cmd/bridge-test -``` - -Builds without `-tags rdp` (or on unsupported platforms) link against a -pure-Go stub that returns `ErrRdpUnavailable` from every constructor. - -## Lints - -```sh -cargo fmt --check -cargo clippy --all-targets -- -D warnings -``` From 5e2e46b8cf56389c8436cb9d51e219bbe6e90bd6 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 22 Apr 2026 18:39:40 -0400 Subject: [PATCH 07/51] fix: add .vscode folder to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9574a412..0d9eb2f8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ test/infisical-merge infisical -/agent-testing \ No newline at end of file +/agent-testing +.vscode/ \ No newline at end of file From 095b900ada61336970b25a7443732a21b1058ab4 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 10:26:38 -0400 Subject: [PATCH 08/51] ci(pam): pin Rust toolchain + add RDP bridge smoke test Pins the Rust channel in rust-toolchain.toml so local dev and the three Phase 6 cross-compile CI jobs (coming next) all agree on the compiler version. Adds run-cli-rdp-smoke.yml: a per-PR job that builds the Rust static library for linux/amd64, runs fmt/clippy, builds the CLI with -tags rdp, and smoke-tests the binary (--version, `pam rdp access --help`). Fast guard against regressions that would otherwise only surface when cutting a release. Path-filtered to packages/pam/** + packages/cmd/pam.go so it doesn't fire on unrelated frontend or backend CLI work. --- .github/workflows/run-cli-rdp-smoke.yml | 65 +++++++++++++++++++ .../handlers/rdp/native/rust-toolchain.toml | 8 +++ 2 files changed, 73 insertions(+) create mode 100644 .github/workflows/run-cli-rdp-smoke.yml create mode 100644 packages/pam/handlers/rdp/native/rust-toolchain.toml diff --git a/.github/workflows/run-cli-rdp-smoke.yml b/.github/workflows/run-cli-rdp-smoke.yml new file mode 100644 index 00000000..1132614e --- /dev/null +++ b/.github/workflows/run-cli-rdp-smoke.yml @@ -0,0 +1,65 @@ +name: RDP Bridge Smoke Test + +# Fast per-PR check that the Rust static lib + CGo wrapper still build +# and link against the Go CLI on linux/amd64. The full cross-compile +# matrix runs on tag push via the release workflow; this job just +# guards against regressions that would only surface at release time. + +on: + pull_request: + types: [opened, synchronize] + paths: + - "packages/pam/**" + - "packages/cmd/pam.go" + - ".github/workflows/run-cli-rdp-smoke.yml" + workflow_dispatch: + +jobs: + smoke: + name: Build + smoke test on linux/amd64 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25.9" + + - name: Cache cargo registry + target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + packages/pam/handlers/rdp/native/target + key: rdp-bridge-cargo-${{ runner.os }}-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} + restore-keys: rdp-bridge-cargo-${{ runner.os }}- + + # Running any cargo command inside the crate auto-installs the + # toolchain pinned in rust-toolchain.toml via rustup (pre-installed + # on GitHub-hosted ubuntu-latest). + - name: Install pinned Rust toolchain + working-directory: packages/pam/handlers/rdp/native + run: rustup show active-toolchain + + - name: cargo fmt --check + working-directory: packages/pam/handlers/rdp/native + run: cargo fmt --check + + - name: cargo clippy -D warnings + working-directory: packages/pam/handlers/rdp/native + run: cargo clippy --all-targets -- -D warnings + + - name: cargo build --release + working-directory: packages/pam/handlers/rdp/native + run: cargo build --release + + - name: go build -tags rdp + run: go build -tags rdp -o ./infisical-rdp . + + - name: Smoke test CLI + run: | + ./infisical-rdp --version + ./infisical-rdp pam rdp access --help diff --git a/packages/pam/handlers/rdp/native/rust-toolchain.toml b/packages/pam/handlers/rdp/native/rust-toolchain.toml new file mode 100644 index 00000000..3b7c36ee --- /dev/null +++ b/packages/pam/handlers/rdp/native/rust-toolchain.toml @@ -0,0 +1,8 @@ +[toolchain] +# Pin the Rust version used by both local dev and CI so the three +# cross-compile jobs (rust-cross on ubuntu, rust-darwin on macos, +# rust-winarm on windows-11-arm) agree on compiler behaviour. Bump by +# editing this file and letting CI fall through to the new toolchain. +channel = "1.95.0" +components = ["rustfmt", "clippy"] +profile = "minimal" From 8a04e543d0cea5c41e746016183eb9ea26ab8c6f Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 10:32:24 -0400 Subject: [PATCH 09/51] ci: retrigger From 492f65617ef6d9ba9355dc395cd7413373ca2f4b Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 10:35:12 -0400 Subject: [PATCH 10/51] ci(pam): add dry-run option to release workflow Adds a dry_run boolean input to workflow_dispatch. When triggered with dry_run=true (the default for manual runs), goreleaser runs in --snapshot --skip=publish mode on both the linux and windows jobs, the S3 upload / Cloudfront invalidation / npm publish steps are skipped, and the built binaries are uploaded as workflow artifacts (goreleaser-dist-linux + goreleaser-dist-windows). The normal tag-push flow (github.event_name == 'push') is unchanged: full release, all publish steps run. Purpose: lets us exercise the full cross-compile matrix end-to-end during Phase 6 work without cutting a real release. We trigger from the Actions tab, download the dist artifact, spot-check the binaries locally. --- .../workflows/release_build_infisical_cli.yml | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 2a6e6716..32fcf36c 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -2,6 +2,16 @@ name: Build and release CLI on: workflow_dispatch: + inputs: + dry_run: + description: >- + Run goreleaser in --snapshot --skip=publish mode. No git tag needed, + nothing is published. The built binaries are uploaded as a + workflow artifact so you can download and sanity-check them. Safe + way to exercise the full cross-compile matrix before cutting a + real release. + type: boolean + default: true push: # run only against tags @@ -14,6 +24,9 @@ permissions: jobs: validate-tag-branch: + # Tag validation only makes sense on actual tag push. Skipped on + # workflow_dispatch runs (dry-run or manual real release from any branch). + if: github.event_name == 'push' uses: ./.github/workflows/pre-tag-validation.yml cli-tests: uses: ./.github/workflows/run-cli-e2e-tests.yml @@ -33,6 +46,9 @@ jobs: # CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} npm-release: + # Never publish npm on dry-runs. Only on real tag pushes or a manual + # workflow_dispatch with dry_run=false. + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) runs-on: ubuntu-latest env: working-directory: ./npm @@ -84,6 +100,13 @@ jobs: needs: - validate-tag-branch - cli-tests + # `always()` lets this run even when validate-tag-branch was skipped + # (dry-run / dispatched-release case). The inner conditions enforce + # the cli-tests success and branch-validation-if-tag rules. + if: | + always() && + needs.cli-tests.result == 'success' && + (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') steps: - uses: actions/checkout@v3 with: @@ -115,7 +138,22 @@ jobs: run: | mkdir ../../osxcross git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target - - uses: goreleaser/goreleaser-action@v4 + - name: GoReleaser (dry-run snapshot) + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser-pro + version: v1.26.2-pro + args: release --clean --snapshot --skip=publish + env: + GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} + POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} + FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} + AUR_KEY: ${{ secrets.AUR_KEY }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + - name: GoReleaser (release) + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) + uses: goreleaser/goreleaser-action@v4 with: distribution: goreleaser-pro version: v1.26.2-pro @@ -126,6 +164,13 @@ jobs: FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} AUR_KEY: ${{ secrets.AUR_KEY }} GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + - name: Upload dry-run dist as workflow artifact + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + uses: actions/upload-artifact@v4 + with: + name: goreleaser-dist-linux + path: dist/ + retention-days: 7 - uses: actions/setup-python@v4 with: python-version: "3.12" @@ -153,6 +198,7 @@ jobs: env: APK_PRIVATE_KEY: ${{ secrets.APK_PRIVATE_KEY }} - name: Publish packages to repositories + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) run: bash upload_to_s3.sh env: INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }} @@ -161,6 +207,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} APK_PRIVATE_KEY_PATH: /tmp/infisical-apk.rsa - name: Invalidate Cloudfront cache + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/rpm/Packages/*' '/rpm/repodata/*' '/deb/dists/stable/*' '/apk/stable/main/*' env: AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }} @@ -173,6 +220,10 @@ jobs: needs: - validate-tag-branch - cli-tests + if: | + always() && + needs.cli-tests.result == 'success' && + (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') steps: - uses: actions/checkout@v3 with: @@ -194,7 +245,21 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - uses: goreleaser/goreleaser-action@v4 + - name: GoReleaser Windows (dry-run snapshot) + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser-pro + version: v1.26.2-pro + args: release --clean --config .goreleaser-windows.yaml --skip-validate --snapshot --skip=publish + env: + GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} + POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} + AUR_KEY: ${{ secrets.AUR_KEY }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + - name: GoReleaser Windows (release) + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) + uses: goreleaser/goreleaser-action@v4 with: distribution: goreleaser-pro version: v1.26.2-pro @@ -204,5 +269,12 @@ jobs: POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} AUR_KEY: ${{ secrets.AUR_KEY }} GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + - name: Upload dry-run dist as workflow artifact + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + uses: actions/upload-artifact@v4 + with: + name: goreleaser-dist-windows + path: dist/ + retention-days: 7 From 83dc9aba598ee2a121ba4b471e6fbc2be1f8f083 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 10:37:18 -0400 Subject: [PATCH 11/51] ci(pam): cross-compile RDP bridge static libs across 11 targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds build-rdp-bridge.yml with three jobs: - rust-cross (ubuntu-latest-8-cores): 8 targets via `cross` — linux/{386, amd64, arm64, armv6, armv7}, freebsd/amd64, netbsd/amd64, windows/amd64 - rust-darwin (macos-latest): darwin/amd64 + darwin/arm64 natively - rust-winarm (windows-11-arm): windows/arm64 natively via MSVC Each matrix leg uploads its static archive as a workflow artifact named rdp-bridge- that the release workflow will download and link per-target via CGO_LDFLAGS (wiring comes next). The workflow is workflow_call + workflow_dispatch: invoked as a prerequisite to goreleaser on real releases, and triggerable manually for iterating on the cross-compile matrix without a full release run. Also broadens run-cli-rdp-smoke.yml trigger to fire on every PR sync (dropping the packages/pam/** paths filter) so it stays useful during Phase 6 work where most commits touch .github/ rather than source. --- .github/workflows/build-rdp-bridge.yml | 148 ++++++++++++++++++++++++ .github/workflows/run-cli-rdp-smoke.yml | 4 - 2 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build-rdp-bridge.yml diff --git a/.github/workflows/build-rdp-bridge.yml b/.github/workflows/build-rdp-bridge.yml new file mode 100644 index 00000000..ca651074 --- /dev/null +++ b/.github/workflows/build-rdp-bridge.yml @@ -0,0 +1,148 @@ +name: Build RDP Bridge Static Libs + +# Cross-compile the Rust MITM bridge into static archives (.a) for +# every OS/arch the CLI targets at release time. Produces three +# groups of artifacts that the main release workflow picks up and +# links per-target via CGO_LDFLAGS: +# +# rdp-bridge-cross — 8 targets built via `cross` on ubuntu-latest +# (linux/386, linux/amd64, linux/arm64, +# linux/armv6, linux/armv7, freebsd/amd64, +# netbsd/amd64, windows/amd64) +# rdp-bridge-darwin — darwin/amd64 + darwin/arm64 built natively +# on macos-latest +# rdp-bridge-winarm — windows/arm64 built natively on +# windows-11-arm +# +# Each job uploads a single artifact per target, keyed by the Rust +# triple, so goreleaser can fetch them with predictable names. +# +# Called from: +# - Per-push PR smoke (not yet — this is the heavy job) +# - Release workflow as a prerequisite to goreleaser +# - Direct workflow_dispatch for iterating on the matrix + +on: + workflow_call: + workflow_dispatch: + +jobs: + rust-cross: + name: cross (${{ matrix.target }}) + runs-on: ubuntu-latest-8-cores + strategy: + fail-fast: false + matrix: + include: + - target: i686-unknown-linux-gnu # linux/386 + - target: x86_64-unknown-linux-gnu # linux/amd64 + - target: aarch64-unknown-linux-gnu # linux/arm64 + - target: arm-unknown-linux-gnueabi # linux/armv6 + - target: armv7-unknown-linux-gnueabihf # linux/armv7 + - target: x86_64-unknown-freebsd # freebsd/amd64 + - target: x86_64-unknown-netbsd # netbsd/amd64 + - target: x86_64-pc-windows-gnu # windows/amd64 + steps: + - uses: actions/checkout@v4 + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: rdp-cross-cargo-${{ matrix.target }}-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} + restore-keys: rdp-cross-cargo-${{ matrix.target }}- + + - name: Install cross + run: cargo install cross --locked --version 0.2.5 + + - name: Install pinned Rust toolchain + working-directory: packages/pam/handlers/rdp/native + run: rustup show active-toolchain + + - name: cross build --release --target ${{ matrix.target }} + working-directory: packages/pam/handlers/rdp/native + run: cross build --release --target ${{ matrix.target }} + + - name: Upload static library + uses: actions/upload-artifact@v4 + with: + name: rdp-bridge-${{ matrix.target }} + path: packages/pam/handlers/rdp/native/target/${{ matrix.target }}/release/libinfisical_rdp_bridge.a + if-no-files-found: error + retention-days: 7 + + rust-darwin: + name: macos-latest (${{ matrix.target }}) + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-apple-darwin # darwin/amd64 + - target: aarch64-apple-darwin # darwin/arm64 + steps: + - uses: actions/checkout@v4 + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: rdp-darwin-cargo-${{ matrix.target }}-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} + restore-keys: rdp-darwin-cargo-${{ matrix.target }}- + + - name: Install pinned Rust toolchain + target + working-directory: packages/pam/handlers/rdp/native + run: | + rustup show active-toolchain + rustup target add ${{ matrix.target }} + + - name: cargo build --release --target ${{ matrix.target }} + working-directory: packages/pam/handlers/rdp/native + run: cargo build --release --target ${{ matrix.target }} + + - name: Upload static library + uses: actions/upload-artifact@v4 + with: + name: rdp-bridge-${{ matrix.target }} + path: packages/pam/handlers/rdp/native/target/${{ matrix.target }}/release/libinfisical_rdp_bridge.a + if-no-files-found: error + retention-days: 7 + + rust-winarm: + name: windows-11-arm (windows/arm64) + runs-on: windows-11-arm + steps: + - uses: actions/checkout@v4 + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: rdp-winarm-cargo-aarch64-pc-windows-msvc-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} + restore-keys: rdp-winarm-cargo-aarch64-pc-windows-msvc- + + - name: Install pinned Rust toolchain + working-directory: packages/pam/handlers/rdp/native + shell: pwsh + run: | + rustup show active-toolchain + rustup target add aarch64-pc-windows-msvc + + - name: cargo build --release --target aarch64-pc-windows-msvc + working-directory: packages/pam/handlers/rdp/native + shell: pwsh + run: cargo build --release --target aarch64-pc-windows-msvc + + - name: Upload static library + uses: actions/upload-artifact@v4 + with: + name: rdp-bridge-aarch64-pc-windows-msvc + path: packages/pam/handlers/rdp/native/target/aarch64-pc-windows-msvc/release/infisical_rdp_bridge.lib + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/run-cli-rdp-smoke.yml b/.github/workflows/run-cli-rdp-smoke.yml index 1132614e..f2d0e9ac 100644 --- a/.github/workflows/run-cli-rdp-smoke.yml +++ b/.github/workflows/run-cli-rdp-smoke.yml @@ -8,10 +8,6 @@ name: RDP Bridge Smoke Test on: pull_request: types: [opened, synchronize] - paths: - - "packages/pam/**" - - "packages/cmd/pam.go" - - ".github/workflows/run-cli-rdp-smoke.yml" workflow_dispatch: jobs: From 6ef384f232b21391e5d69d4f4e0db9f96d5b20c3 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 11:03:01 -0400 Subject: [PATCH 12/51] feat(pam-rdp): add Windows CGo wrapper and extract shared bridge ops Windows support for the RDP bridge: bridge_cgo_windows.go uses DuplicateHandle for in-process SOCKET duplication (in-process, not WSADuplicateSocketW which is for cross-process sharing) and the same loopback shim for non-socket-backed streams. Extracted Wait/Cancel/Close into bridge_cgo_shared.go since the C calls are platform-independent. The stub build tag is narrowed to exclude windows now that windows has a real implementation. --- packages/pam/handlers/rdp/bridge_cgo.go | 44 ---- .../pam/handlers/rdp/bridge_cgo_shared.go | 56 +++++ .../pam/handlers/rdp/bridge_cgo_windows.go | 228 ++++++++++++++++++ packages/pam/handlers/rdp/bridge_stub.go | 5 +- 4 files changed, 286 insertions(+), 47 deletions(-) create mode 100644 packages/pam/handlers/rdp/bridge_cgo_shared.go create mode 100644 packages/pam/handlers/rdp/bridge_cgo_windows.go diff --git a/packages/pam/handlers/rdp/bridge_cgo.go b/packages/pam/handlers/rdp/bridge_cgo.go index 3272dd4c..4e9da7bb 100644 --- a/packages/pam/handlers/rdp/bridge_cgo.go +++ b/packages/pam/handlers/rdp/bridge_cgo.go @@ -174,35 +174,6 @@ func dupConnFD(conn net.Conn) (int, error) { return dup, nil } -// Wait blocks until the session ends. Returns nil on a clean end -// (including the client hard-closing the TCP connection after a normal -// session), [ErrSessionFailed] on handshake or forwarding failure, or -// [ErrInvalidHandle] if the handle is unknown. Calling Wait a second -// time on the same handle returns nil (the session is already done). -func (b *Bridge) Wait() error { - rc := C.rdp_bridge_wait(C.uint64_t(b.handle)) - switch rc { - case C.RDP_BRIDGE_OK: - return nil - case C.RDP_BRIDGE_INVALID_HANDLE: - return ErrInvalidHandle - case C.RDP_BRIDGE_SESSION_ERROR, C.RDP_BRIDGE_THREAD_PANIC: - return ErrSessionFailed - default: - return fmt.Errorf("rdp bridge: wait returned unexpected status %d", int32(rc)) - } -} - -// Cancel signals the session to stop. Idempotent; safe from any -// goroutine even while another goroutine is inside Wait. -func (b *Bridge) Cancel() error { - rc := C.rdp_bridge_cancel(C.uint64_t(b.handle)) - if rc == C.RDP_BRIDGE_INVALID_HANDLE { - return ErrInvalidHandle - } - return nil -} - // HandleConnection is the entry point the gateway's PAM dispatcher calls // for a Windows/RDP session. It takes ownership of `clientConn` (closes // it on return), spawns a bridge via the loopback shim, and blocks until @@ -249,18 +220,3 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er return ctx.Err() } } - -// Close releases the bridge handle. Call after Wait has returned. If the -// bridge was created with a loopback shim (via StartWithReadWriter), -// Close also tears down the shim goroutines by closing their loopback -// endpoint. -func (b *Bridge) Close() error { - rc := C.rdp_bridge_free(C.uint64_t(b.handle)) - if b.cleanup != nil { - b.cleanup() - } - if rc == C.RDP_BRIDGE_INVALID_HANDLE { - return ErrInvalidHandle - } - return nil -} diff --git a/packages/pam/handlers/rdp/bridge_cgo_shared.go b/packages/pam/handlers/rdp/bridge_cgo_shared.go new file mode 100644 index 00000000..9ef41ad1 --- /dev/null +++ b/packages/pam/handlers/rdp/bridge_cgo_shared.go @@ -0,0 +1,56 @@ +//go:build rdp && (linux || darwin || windows) + +package rdp + +/* +#cgo CFLAGS: -I${SRCDIR}/native/include + +#include "rdp_bridge.h" +*/ +import "C" + +import "fmt" + +// Wait blocks until the session ends. Returns nil on a clean end +// (including the client hard-closing the TCP connection after a normal +// session), [ErrSessionFailed] on handshake or forwarding failure, or +// [ErrInvalidHandle] if the handle is unknown. Calling Wait a second +// time on the same handle returns nil (the session is already done). +func (b *Bridge) Wait() error { + rc := C.rdp_bridge_wait(C.uint64_t(b.handle)) + switch rc { + case C.RDP_BRIDGE_OK: + return nil + case C.RDP_BRIDGE_INVALID_HANDLE: + return ErrInvalidHandle + case C.RDP_BRIDGE_SESSION_ERROR, C.RDP_BRIDGE_THREAD_PANIC: + return ErrSessionFailed + default: + return fmt.Errorf("rdp bridge: wait returned unexpected status %d", int32(rc)) + } +} + +// Cancel signals the session to stop. Idempotent; safe from any +// goroutine even while another goroutine is inside Wait. +func (b *Bridge) Cancel() error { + rc := C.rdp_bridge_cancel(C.uint64_t(b.handle)) + if rc == C.RDP_BRIDGE_INVALID_HANDLE { + return ErrInvalidHandle + } + return nil +} + +// Close releases the bridge handle. Call after Wait has returned. If the +// bridge was created with a loopback shim (via StartWithReadWriter), +// Close also tears down the shim goroutines by closing their loopback +// endpoint. +func (b *Bridge) Close() error { + rc := C.rdp_bridge_free(C.uint64_t(b.handle)) + if b.cleanup != nil { + b.cleanup() + } + if rc == C.RDP_BRIDGE_INVALID_HANDLE { + return ErrInvalidHandle + } + return nil +} diff --git a/packages/pam/handlers/rdp/bridge_cgo_windows.go b/packages/pam/handlers/rdp/bridge_cgo_windows.go new file mode 100644 index 00000000..4bc78672 --- /dev/null +++ b/packages/pam/handlers/rdp/bridge_cgo_windows.go @@ -0,0 +1,228 @@ +//go:build rdp && windows + +package rdp + +/* +#cgo CFLAGS: -I${SRCDIR}/native/include +#cgo windows LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -lws2_32 -luserenv -lbcrypt -lntdll -ladvapi32 -lcrypt32 -lsecur32 + +#include "rdp_bridge.h" +#include +*/ +import "C" + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// StartWithConn starts a bridge session for the given TCP connection. +// Internally, an independent duplicate of the underlying SOCKET is +// handed to the bridge via DuplicateHandle; the caller's conn stays +// fully usable and is not closed by this function. The bridge closes +// its dup when the session ends. +// +// `conn` must be a *net.TCPConn or any net.Conn that exposes a raw +// socket via syscall.Conn. For TLS-wrapped or otherwise non-socket-backed +// conns (like the ones the gateway receives), use [StartWithReadWriter] +// instead. +func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { + dupSocket, err := dupConnSocket(conn) + if err != nil { + return nil, fmt.Errorf("rdp bridge: dup client socket: %w", err) + } + return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password) +} + +// startWithDupedSocket hands ownership of `dupSocket` to the Rust bridge. +// On success the bridge closes the socket when the session ends; on +// failure this function closes the socket itself before returning. +// Shared by StartWithConn and StartWithReadWriter. +func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { + success := false + defer func() { + if !success { + _ = windows.Closesocket(dupSocket) + } + }() + + cHost := C.CString(targetHost) + defer C.free(unsafe.Pointer(cHost)) + cUser := C.CString(username) + defer C.free(unsafe.Pointer(cUser)) + cPass := C.CString(password) + defer C.free(unsafe.Pointer(cPass)) + + var handle C.uint64_t + rc := C.rdp_bridge_start_windows_socket( + C.uintptr_t(dupSocket), + cHost, + C.uint16_t(targetPort), + cUser, + cPass, + &handle, + ) + if rc != C.RDP_BRIDGE_OK { + return nil, fmt.Errorf("rdp bridge: start failed (status %d)", int32(rc)) + } + success = true + return &Bridge{handle: uint64(handle)}, nil +} + +// StartWithReadWriter starts a bridge session for a caller whose client +// stream is not socket-backed (e.g. *tls.Conn wrapping an mTLS'd virtual +// connection in the gateway). It creates a local loopback TCP pair, +// hands the kernel-backed accepted end to the Rust bridge, and pumps +// bytes between the other loopback end and the caller's `rw` via two +// io.Copy goroutines. The goroutines exit when either side closes; the +// bridge's Close method also tears them down. +// +// The caller retains ownership of `rw` and is responsible for closing +// it when done (the bridge does not close it). +func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) + } + defer listener.Close() + + type dialResult struct { + conn net.Conn + err error + } + dialCh := make(chan dialResult, 1) + go func() { + c, err := net.Dial("tcp", listener.Addr().String()) + dialCh <- dialResult{c, err} + }() + + accepted, err := listener.Accept() + if err != nil { + return nil, fmt.Errorf("rdp bridge: loopback accept: %w", err) + } + dr := <-dialCh + if dr.err != nil { + _ = accepted.Close() + return nil, fmt.Errorf("rdp bridge: loopback dial: %w", dr.err) + } + peer := dr.conn + + dupSocket, err := dupConnSocket(accepted) + _ = accepted.Close() + if err != nil { + _ = peer.Close() + return nil, fmt.Errorf("rdp bridge: dup accepted socket: %w", err) + } + + bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password) + if err != nil { + _ = peer.Close() + return nil, err + } + + go func() { + _, _ = io.Copy(peer, rw) + _ = peer.Close() + }() + go func() { + _, _ = io.Copy(rw, peer) + _ = peer.Close() + }() + + bridge.cleanup = func() { _ = peer.Close() } + return bridge, nil +} + +// dupConnSocket returns a new SOCKET handle independent from `conn`'s +// internal one, using DuplicateHandle against the current process. The +// caller becomes responsible for closing the returned handle via +// windows.Closesocket. Requires `conn` to implement syscall.Conn. +// +// Note: this uses DuplicateHandle, not WSADuplicateSocketW. +// WSADuplicateSocketW is for cross-process socket sharing and requires +// the peer to call WSASocket with a WSAPROTOCOL_INFOW. For in-process +// SOCKET duplication, DuplicateHandle on the SOCKET's underlying kernel +// handle is the standard approach (SOCKETs are kernel handles on modern +// Windows). +func dupConnSocket(conn net.Conn) (windows.Handle, error) { + sc, ok := conn.(syscall.Conn) + if !ok { + return 0, fmt.Errorf("conn %T does not expose syscall.Conn", conn) + } + raw, err := sc.SyscallConn() + if err != nil { + return 0, err + } + var dup windows.Handle + var dupErr error + proc := windows.CurrentProcess() + ctrlErr := raw.Control(func(fd uintptr) { + dupErr = windows.DuplicateHandle( + proc, + windows.Handle(fd), + proc, + &dup, + 0, + false, + windows.DUPLICATE_SAME_ACCESS, + ) + }) + if ctrlErr != nil { + return 0, ctrlErr + } + if dupErr != nil { + return 0, dupErr + } + return dup, nil +} + +// HandleConnection is the entry point the gateway's PAM dispatcher calls +// for a Windows/RDP session. It takes ownership of `clientConn` (closes +// it on return), spawns a bridge via the loopback shim, and blocks until +// the session ends or `ctx` is cancelled (admin terminate, session +// expiry). On cancellation the bridge is signalled to abort and we wait +// for it to actually finish before returning `ctx.Err()`. +func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { + defer clientConn.Close() + if p.config.SessionLogger != nil { + defer func() { + if err := p.config.SessionLogger.Close(); err != nil { + _ = err + } + }() + } + + bridge, err := StartWithReadWriter( + clientConn, + p.config.TargetHost, + p.config.TargetPort, + p.config.InjectUsername, + p.config.InjectPassword, + ) + if err != nil { + return fmt.Errorf("rdp proxy: start bridge: %w", err) + } + defer bridge.Close() + + waitErr := make(chan error, 1) + go func() { waitErr <- bridge.Wait() }() + + select { + case err := <-waitErr: + if err != nil && !errors.Is(err, ErrInvalidHandle) { + return fmt.Errorf("rdp proxy: session: %w", err) + } + return nil + case <-ctx.Done(): + _ = bridge.Cancel() + <-waitErr + return ctx.Err() + } +} diff --git a/packages/pam/handlers/rdp/bridge_stub.go b/packages/pam/handlers/rdp/bridge_stub.go index a1aac7a0..e24a8357 100644 --- a/packages/pam/handlers/rdp/bridge_stub.go +++ b/packages/pam/handlers/rdp/bridge_stub.go @@ -1,4 +1,4 @@ -//go:build !rdp || (!linux && !darwin) +//go:build !rdp || (!linux && !darwin && !windows) package rdp @@ -10,8 +10,7 @@ import ( // StartWithConn is a stub that reports the RDP bridge is unavailable in // this build. To enable the real implementation, build with `-tags rdp` -// on a supported platform (linux, darwin; windows and others land in -// later phases). +// on a supported platform (linux, darwin, windows). func StartWithConn(_ net.Conn, _ string, _ uint16, _, _ string) (*Bridge, error) { return nil, ErrRdpUnavailable } From 0611a97fdd464cc42434d8cbbbcc52fe18f7247f Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 11:03:14 -0400 Subject: [PATCH 13/51] ci(pam-rdp): wire RDP bridge static libs into goreleaser release Scope is 'client tier' only: linux amd64/arm64, darwin amd64/arm64, and windows amd64 via MinGW. Other targets (linux/386, 32-bit arm, BSDs, windows-arm/arm64) keep CGO=0 and ship the stub, which returns ErrRdpUnavailable at runtime. Changes: * build-rdp-bridge.yml pruned from 11 targets to 5 (windows/amd64 is now x86_64-pc-windows-gnu so cgo cross-compile from ubuntu can link against it with MinGW). The rust-winarm MSVC job is removed. * .goreleaser.yaml splits the old all-other-builds group. Five new per-arch builds (darwin-amd64-rdp, darwin-arm64-rdp, linux-amd64-rdp, linux-arm64-rdp, windows-amd64-rdp) compile CGO=1 -tags rdp against the corresponding cargo target//release/libinfisical_rdp_bridge.a via CGO_LDFLAGS. The remaining all-other-builds group covers the stub-tier platforms with CGO=0. * release_build_infisical_cli.yml adds build-rdp-bridge as a needs prereq for the goreleaser job, installs gcc-aarch64-linux-gnu and gcc-mingw-w64-x86_64 on the ubuntu runner, downloads the five artifacts, and stages each one at its expected cargo output path. --- .github/workflows/build-rdp-bridge.yml | 74 +++------- .../workflows/release_build_infisical_cli.yml | 36 ++++- .goreleaser.yaml | 127 ++++++++++++++++-- 3 files changed, 166 insertions(+), 71 deletions(-) diff --git a/.github/workflows/build-rdp-bridge.yml b/.github/workflows/build-rdp-bridge.yml index ca651074..909d7757 100644 --- a/.github/workflows/build-rdp-bridge.yml +++ b/.github/workflows/build-rdp-bridge.yml @@ -1,24 +1,26 @@ name: Build RDP Bridge Static Libs -# Cross-compile the Rust MITM bridge into static archives (.a) for -# every OS/arch the CLI targets at release time. Produces three -# groups of artifacts that the main release workflow picks up and -# links per-target via CGO_LDFLAGS: +# Cross-compile the Rust MITM bridge into static archives for every +# OS/arch the CLI targets with RDP support at release time. Scoped to +# the "client tier" platforms where users actually run `pam rdp access` +# (or the gateway serves RDP): linux amd64/arm64, darwin amd64/arm64, +# and windows amd64 via MinGW (so cgo cross-compile from ubuntu can +# link against it). # -# rdp-bridge-cross — 8 targets built via `cross` on ubuntu-latest -# (linux/386, linux/amd64, linux/arm64, -# linux/armv6, linux/armv7, freebsd/amd64, -# netbsd/amd64, windows/amd64) -# rdp-bridge-darwin — darwin/amd64 + darwin/arm64 built natively -# on macos-latest -# rdp-bridge-winarm — windows/arm64 built natively on -# windows-11-arm +# Other platforms the main CLI targets (linux/386, arm 32-bit, BSDs, +# windows-arm64) ship with the RDP stub: they keep CGO=0 and get +# ErrRdpUnavailable at runtime. That matches option 2 of the build +# design: RDP is only available on the tier where users realistically +# use it. # -# Each job uploads a single artifact per target, keyed by the Rust -# triple, so goreleaser can fetch them with predictable names. +# Artifacts: +# rdp-bridge-x86_64-unknown-linux-gnu : linux/amd64 +# rdp-bridge-aarch64-unknown-linux-gnu : linux/arm64 +# rdp-bridge-x86_64-pc-windows-gnu : windows/amd64 (MinGW) +# rdp-bridge-x86_64-apple-darwin : darwin/amd64 +# rdp-bridge-aarch64-apple-darwin : darwin/arm64 # # Called from: -# - Per-push PR smoke (not yet — this is the heavy job) # - Release workflow as a prerequisite to goreleaser # - Direct workflow_dispatch for iterating on the matrix @@ -34,14 +36,9 @@ jobs: fail-fast: false matrix: include: - - target: i686-unknown-linux-gnu # linux/386 - target: x86_64-unknown-linux-gnu # linux/amd64 - target: aarch64-unknown-linux-gnu # linux/arm64 - - target: arm-unknown-linux-gnueabi # linux/armv6 - - target: armv7-unknown-linux-gnueabihf # linux/armv7 - - target: x86_64-unknown-freebsd # freebsd/amd64 - - target: x86_64-unknown-netbsd # netbsd/amd64 - - target: x86_64-pc-windows-gnu # windows/amd64 + - target: x86_64-pc-windows-gnu # windows/amd64 (MinGW) steps: - uses: actions/checkout@v4 @@ -111,38 +108,3 @@ jobs: path: packages/pam/handlers/rdp/native/target/${{ matrix.target }}/release/libinfisical_rdp_bridge.a if-no-files-found: error retention-days: 7 - - rust-winarm: - name: windows-11-arm (windows/arm64) - runs-on: windows-11-arm - steps: - - uses: actions/checkout@v4 - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: rdp-winarm-cargo-aarch64-pc-windows-msvc-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} - restore-keys: rdp-winarm-cargo-aarch64-pc-windows-msvc- - - - name: Install pinned Rust toolchain - working-directory: packages/pam/handlers/rdp/native - shell: pwsh - run: | - rustup show active-toolchain - rustup target add aarch64-pc-windows-msvc - - - name: cargo build --release --target aarch64-pc-windows-msvc - working-directory: packages/pam/handlers/rdp/native - shell: pwsh - run: cargo build --release --target aarch64-pc-windows-msvc - - - name: Upload static library - uses: actions/upload-artifact@v4 - with: - name: rdp-bridge-aarch64-pc-windows-msvc - path: packages/pam/handlers/rdp/native/target/aarch64-pc-windows-msvc/release/infisical_rdp_bridge.lib - if-no-files-found: error - retention-days: 7 diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 32fcf36c..c857238d 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -32,6 +32,13 @@ jobs: uses: ./.github/workflows/run-cli-e2e-tests.yml secrets: inherit + build-rdp-bridge: + # Cross-compile the Rust RDP bridge static archives for the five + # client-tier targets (linux amd64/arm64, darwin amd64/arm64, + # windows amd64 via MinGW). Goreleaser picks these up and links + # them into the per-target `infisical` binary via CGO. + uses: ./.github/workflows/build-rdp-bridge.yml + # cli-integration-tests: # name: Run tests before deployment # uses: ./.github/workflows/run-cli-tests.yml @@ -100,12 +107,14 @@ jobs: needs: - validate-tag-branch - cli-tests + - build-rdp-bridge # `always()` lets this run even when validate-tag-branch was skipped # (dry-run / dispatched-release case). The inner conditions enforce - # the cli-tests success and branch-validation-if-tag rules. + # the cli-tests + rdp-bridge success and branch-validation-if-tag rules. if: | always() && needs.cli-tests.result == 'success' && + needs.build-rdp-bridge.result == 'success' && (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') steps: - uses: actions/checkout@v3 @@ -134,10 +143,35 @@ jobs: sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 sudo apt update sudo apt-get install -y libssl1.0-dev + - name: Install cross-compile toolchains for RDP tier + # aarch64-linux-gnu-gcc: linux/arm64 CGO cross-compile. + # x86_64-w64-mingw32-gcc: windows/amd64 CGO cross-compile. + run: sudo apt-get install -y gcc-aarch64-linux-gnu gcc-mingw-w64-x86_64 - name: OSXCross for CGO Support run: | mkdir ../../osxcross git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target + - name: Download RDP bridge static libs + uses: actions/download-artifact@v4 + with: + pattern: rdp-bridge-* + path: /tmp/rdp-bridge-artifacts/ + - name: Stage RDP bridge static libs into cargo target dirs + # Goreleaser's per-build CGO_LDFLAGS expects each target triple's + # libinfisical_rdp_bridge.a to live at the standard cargo output + # path so the linker's -L flag resolves. + run: | + set -euo pipefail + for triple in \ + x86_64-unknown-linux-gnu \ + aarch64-unknown-linux-gnu \ + x86_64-pc-windows-gnu \ + x86_64-apple-darwin \ + aarch64-apple-darwin; do + target_dir="packages/pam/handlers/rdp/native/target/$triple/release" + mkdir -p "$target_dir" + cp "/tmp/rdp-bridge-artifacts/rdp-bridge-$triple/libinfisical_rdp_bridge.a" "$target_dir/" + done - name: GoReleaser (dry-run snapshot) if: github.event_name == 'workflow_dispatch' && inputs.dry_run uses: goreleaser/goreleaser-action@v4 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 0e95d859..cca81f92 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,34 +1,123 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com -# before: -# hooks: -# # You may remove this if you don't use go modules. -# - cd cli && go mod tidy -# # you may remove this if you don't need go generate -# - cd cli && go generate ./... before: hooks: - ./scripts/completions.sh - ./scripts/manpages.sh +# --------------------------------------------------------------------- +# Builds. +# +# The CLI has two tiers: +# +# rdp tier : CGO=1 with `-tags rdp`, links the Rust RDP bridge +# static archive. Cross-compile toolchain per target: +# linux/amd64 : system gcc +# linux/arm64 : gcc-aarch64-linux-gnu +# darwin/amd64 : o64-clang via osxcross +# darwin/arm64 : o64-clang via osxcross +# windows/amd64: x86_64-w64-mingw32-gcc (MinGW) +# Static lib per target is staged at +# packages/pam/handlers/rdp/native/target//release/libinfisical_rdp_bridge.a +# by the `build-rdp-bridge` workflow job. CGO_LDFLAGS +# adds the target-triple -L dir; the #cgo directive in +# bridge_cgo.go / bridge_cgo_windows.go supplies the -l +# flags. +# +# stub tier : CGO=0, no rdp tag. Ships the RDP stub that returns +# ErrRdpUnavailable at runtime. Covers the long tail of +# platforms we build for but don't realistically run PAM +# RDP on: linux 386 / 32-bit arm, BSDs, windows arm/arm64. +# --------------------------------------------------------------------- builds: - - id: darwin-build + # --- rdp tier --- + - id: darwin-amd64-rdp binary: infisical ldflags: - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} flags: - -trimpath + - -tags=rdp env: - CGO_ENABLED=1 - CC=/home/runner/work/osxcross/target/bin/o64-clang - CXX=/home/runner/work/osxcross/target/bin/o64-clang++ + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-apple-darwin/release' goos: - darwin - ignore: - - goos: darwin - goarch: "386" + goarch: + - amd64 + - id: darwin-arm64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - CC=/home/runner/work/osxcross/target/bin/o64-clang + - CXX=/home/runner/work/osxcross/target/bin/o64-clang++ + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/aarch64-apple-darwin/release' + goos: + - darwin + goarch: + - arm64 + + - id: linux-amd64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - CC=gcc + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-unknown-linux-gnu/release' + goos: + - linux + goarch: + - amd64 + + - id: linux-arm64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/aarch64-unknown-linux-gnu/release' + goos: + - linux + goarch: + - arm64 + + - id: windows-amd64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - CC=x86_64-w64-mingw32-gcc + - CXX=x86_64-w64-mingw32-g++ + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-pc-windows-gnu/release' + goos: + - windows + goarch: + - amd64 + + # --- stub tier: CGO=0, no rdp support --- - id: all-other-builds env: - CGO_ENABLED=0 @@ -53,6 +142,14 @@ builds: - "6" - "7" ignore: + # linux/amd64, linux/arm64, windows/amd64 are handled by the + # per-arch rdp builds above: don't duplicate them here. + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: windows + goarch: amd64 - goos: windows goarch: "386" - goos: freebsd @@ -123,6 +220,8 @@ nfpms: - id: infisical package_name: infisical builds: + - linux-amd64-rdp + - linux-arm64-rdp - all-other-builds vendor: Infisical, Inc homepage: https://infisical.com/ @@ -205,7 +304,7 @@ dockers: goarch: amd64 use: buildx ids: - - all-other-builds + - linux-amd64-rdp image_templates: - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64" - "infisical/cli:latest-amd64" @@ -217,7 +316,7 @@ dockers: goarch: arm64 use: buildx ids: - - all-other-builds + - linux-arm64-rdp image_templates: - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64" - "infisical/cli:latest-arm64" From 9beb6358d8ef02919e4b7a9cb0b9a2f283389152 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 11:37:25 -0400 Subject: [PATCH 14/51] feat(pam-rdp): wire --reason flag through to RDP access Mirrors the pattern the other PAM proxies adopted on main (database / ssh / kubernetes / redis): resolveReason reads --reason or prompts the user when interactive, the reason is threaded into the PAMAccessParams, and CallPAMAccessWithMFA is called with interactive = true so the backend's reason-required policy triggers a prompt instead of failing silently. --- packages/cmd/pam.go | 4 ++++ packages/pam/local/rdp-proxy.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cmd/pam.go b/packages/cmd/pam.go index be09e8d0..86619856 100644 --- a/packages/cmd/pam.go +++ b/packages/cmd/pam.go @@ -475,6 +475,8 @@ var pamRdpAccessCmd = &cobra.Command{ util.HandleError(err, "Unable to parse no-launch flag") } + reason := resolveReason(cmd) + log.Debug().Msg("PAM RDP Access: Trying to start session using logged in details") loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) @@ -495,6 +497,7 @@ var pamRdpAccessCmd = &cobra.Command{ pam.StartRDPLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, pam.PAMAccessParams{ ResourceName: resourceName, AccountName: accountName, + Reason: reason, }, projectID, durationStr, port, noLaunch) }, } @@ -561,6 +564,7 @@ func init() { pamRdpAccessCmd.Flags().Int("port", 0, "Port for the local RDP proxy server (0 for auto-assign)") pamRdpAccessCmd.Flags().String("project-id", "", "Project ID of the account to access") pamRdpAccessCmd.Flags().Bool("no-launch", false, "Do not auto-launch the system RDP client; print connection details only") + pamRdpAccessCmd.Flags().String("reason", "", "Reason for accessing the account (stored for audit purposes)") pamRdpAccessCmd.MarkFlagRequired("resource") pamRdpAccessCmd.MarkFlagRequired("account") diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index ae04670f..d2345e6f 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -45,7 +45,7 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec pamRequest := accessParams.ToAPIRequest(projectID, durationStr) - pamResponse, err := CallPAMAccessWithMFA(httpClient, pamRequest) + pamResponse, err := CallPAMAccessWithMFA(httpClient, pamRequest, true) if err != nil { if HandleApprovalWorkflow(httpClient, err, projectID, accessParams, durationStr) { return From a3f440866aa9cbb276c0b7fc2a3930e1d5432e5b Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 11:40:12 -0400 Subject: [PATCH 15/51] fix(ci): correct mingw apt package name and skip docker on dry-run Two issues from the first dry-run of the RDP release pipeline: 1. gcc-mingw-w64-x86_64 doesn't exist as an Ubuntu apt package name (underscore vs hyphen). The correct name is gcc-mingw-w64-x86-64. 2. The windows-2022 runner doesn't always have a running docker daemon, so goreleaser fails trying to build the Windows Server container images in dry-run mode. Skip docker only for dry-run; real releases keep building them as before. --- .github/workflows/release_build_infisical_cli.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index c857238d..f46c964f 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -146,7 +146,7 @@ jobs: - name: Install cross-compile toolchains for RDP tier # aarch64-linux-gnu-gcc: linux/arm64 CGO cross-compile. # x86_64-w64-mingw32-gcc: windows/amd64 CGO cross-compile. - run: sudo apt-get install -y gcc-aarch64-linux-gnu gcc-mingw-w64-x86_64 + run: sudo apt-get install -y gcc-aarch64-linux-gnu gcc-mingw-w64-x86-64 - name: OSXCross for CGO Support run: | mkdir ../../osxcross @@ -285,7 +285,10 @@ jobs: with: distribution: goreleaser-pro version: v1.26.2-pro - args: release --clean --config .goreleaser-windows.yaml --skip-validate --snapshot --skip=publish + # Skip docker on dry-run: the windows-2022 runner doesn't + # always have a running docker daemon for building Windows + # Server images. Real-release invocations still build them. + args: release --clean --config .goreleaser-windows.yaml --skip-validate --snapshot --skip=publish,docker env: GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} From 3bc0a780002d3d972a103626f109a72a3e3801fc Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 11:41:37 -0400 Subject: [PATCH 16/51] Revert skip=docker change; keep only the mingw package name fix Previous commit claimed the windows docker build was pre-existing broken, but real releases do in fact produce those images. Drop the --skip=docker workaround and let the next dry-run show whether docker actually recurs or the first failure was transient. --- .github/workflows/release_build_infisical_cli.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index f46c964f..934a92b7 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -285,10 +285,7 @@ jobs: with: distribution: goreleaser-pro version: v1.26.2-pro - # Skip docker on dry-run: the windows-2022 runner doesn't - # always have a running docker daemon for building Windows - # Server images. Real-release invocations still build them. - args: release --clean --config .goreleaser-windows.yaml --skip-validate --snapshot --skip=publish,docker + args: release --clean --config .goreleaser-windows.yaml --skip-validate --snapshot --skip=publish env: GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} From 0c9424d2a910802679fbace3e72fce6ea88d39a2 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 12:26:49 -0400 Subject: [PATCH 17/51] ci(pam-rdp): use zig cc for darwin cross-compile instead of osxcross The previous osxcross-target clone only provided the macOS SDK headers, not a real linker. That worked when no cgo code was actually being linked, but with -tags rdp pulling in bridge_cgo.go the final link step falls through to /usr/bin/ld (GNU) which can't handle ld64-specific flags like -headerpad and -framework, producing 'unrecognised emulation mode: llvm'. zig cc ships a full clang frontend plus ld.lld (with ld64 compat) plus a bundled macOS SDK snapshot, so it can produce valid Mach-O from linux. Scoped to the two darwin rdp builds only. Linux and windows builds keep their existing native / mingw toolchains. --- .github/workflows/release_build_infisical_cli.yml | 14 ++++++++++---- .goreleaser.yaml | 6 ++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 934a92b7..cfeca58f 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -147,10 +147,16 @@ jobs: # aarch64-linux-gnu-gcc: linux/arm64 CGO cross-compile. # x86_64-w64-mingw32-gcc: windows/amd64 CGO cross-compile. run: sudo apt-get install -y gcc-aarch64-linux-gnu gcc-mingw-w64-x86-64 - - name: OSXCross for CGO Support - run: | - mkdir ../../osxcross - git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target + - name: Install zig for darwin cross-compile + # zig cc bundles clang + ld.lld + a macOS SDK snapshot, so it + # can produce valid Mach-O from linux. Used as the $CC for the + # darwin-amd64-rdp and darwin-arm64-rdp goreleaser builds. + # The previous osxcross-target clone only shipped SDK headers + # (no linker), which worked when no cgo code was actually being + # linked but breaks as soon as -tags rdp pulls in bridge_cgo.go. + uses: mlugg/setup-zig@v1 + with: + version: 0.13.0 - name: Download RDP bridge static libs uses: actions/download-artifact@v4 with: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index cca81f92..14d7d0eb 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -39,8 +39,7 @@ builds: - -tags=rdp env: - CGO_ENABLED=1 - - CC=/home/runner/work/osxcross/target/bin/o64-clang - - CXX=/home/runner/work/osxcross/target/bin/o64-clang++ + - 'CC=zig cc -target x86_64-macos-none' - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-apple-darwin/release' goos: - darwin @@ -57,8 +56,7 @@ builds: - -tags=rdp env: - CGO_ENABLED=1 - - CC=/home/runner/work/osxcross/target/bin/o64-clang - - CXX=/home/runner/work/osxcross/target/bin/o64-clang++ + - 'CC=zig cc -target aarch64-macos-none' - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/aarch64-apple-darwin/release' goos: - darwin From b0aff6c9cf2e4a2db1716ccd9ed8ff925575a330 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 12:54:55 -0400 Subject: [PATCH 18/51] ci(pam-rdp): expand RDP support from 5 to 11 targets (option 1) Every CLI target except openbsd now ships with RDP support. Openbsd stays on the stub tier since we don't have a rust/cargo cross for it and it's a negligible share of PAM users. Changes: * build-rdp-bridge.yml: add back i686-unknown-linux-gnu, arm-unknown-linux-gnueabi, armv7-unknown-linux-gnueabihf, x86_64-unknown-freebsd, x86_64-unknown-netbsd to the cross job matrix. Add a new rust-winarm job that cross-compiles aarch64-pc-windows-gnullvm via native cargo + rust-lld (the cross tool doesn't cover this target). * .goreleaser.yaml: six new per-arch rdp builds (linux-386, linux-armv6, linux-armv7, freebsd-amd64, netbsd-amd64, windows-arm64). BSDs and windows-arm64 use zig cc as the linker (same toolchain already used for darwin) because apt doesn't ship cgo-capable cross compilers for them. all-other-builds narrows to openbsd only; nfpms .deb/.rpm/.apk now covers all five linux arches. * release_build_infisical_cli.yml: install gcc-i686-linux-gnu, gcc-arm-linux-gnueabi, gcc-arm-linux-gnueabihf. Expand the bridge staging loop to all 11 triples. --- .github/workflows/build-rdp-bridge.yml | 76 ++++++++--- .../workflows/release_build_infisical_cli.yml | 23 +++- .goreleaser.yaml | 129 +++++++++++++++--- ...3f796b02098_windows_expires_1776967747.enc | 0 4 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 session/pam_session_62c3bee9-ef35-4b24-937b-13f796b02098_windows_expires_1776967747.enc diff --git a/.github/workflows/build-rdp-bridge.yml b/.github/workflows/build-rdp-bridge.yml index 909d7757..930a6156 100644 --- a/.github/workflows/build-rdp-bridge.yml +++ b/.github/workflows/build-rdp-bridge.yml @@ -1,24 +1,22 @@ name: Build RDP Bridge Static Libs # Cross-compile the Rust MITM bridge into static archives for every -# OS/arch the CLI targets with RDP support at release time. Scoped to -# the "client tier" platforms where users actually run `pam rdp access` -# (or the gateway serves RDP): linux amd64/arm64, darwin amd64/arm64, -# and windows amd64 via MinGW (so cgo cross-compile from ubuntu can -# link against it). +# OS/arch the CLI ships with RDP support. Goreleaser downloads these +# as artifacts at release time and links them into per-target +# `infisical` binaries via CGO. # -# Other platforms the main CLI targets (linux/386, arm 32-bit, BSDs, -# windows-arm64) ship with the RDP stub: they keep CGO=0 and get -# ErrRdpUnavailable at runtime. That matches option 2 of the build -# design: RDP is only available on the tier where users realistically -# use it. -# -# Artifacts: -# rdp-bridge-x86_64-unknown-linux-gnu : linux/amd64 -# rdp-bridge-aarch64-unknown-linux-gnu : linux/arm64 -# rdp-bridge-x86_64-pc-windows-gnu : windows/amd64 (MinGW) -# rdp-bridge-x86_64-apple-darwin : darwin/amd64 -# rdp-bridge-aarch64-apple-darwin : darwin/arm64 +# Artifacts (11 total): +# rdp-bridge-x86_64-unknown-linux-gnu : linux/amd64 +# rdp-bridge-aarch64-unknown-linux-gnu : linux/arm64 +# rdp-bridge-i686-unknown-linux-gnu : linux/386 +# rdp-bridge-arm-unknown-linux-gnueabi : linux/armv6 +# rdp-bridge-armv7-unknown-linux-gnueabihf : linux/armv7 +# rdp-bridge-x86_64-unknown-freebsd : freebsd/amd64 +# rdp-bridge-x86_64-unknown-netbsd : netbsd/amd64 +# rdp-bridge-x86_64-pc-windows-gnu : windows/amd64 (MinGW) +# rdp-bridge-aarch64-pc-windows-gnullvm : windows/arm64 (rust-lld) +# rdp-bridge-x86_64-apple-darwin : darwin/amd64 +# rdp-bridge-aarch64-apple-darwin : darwin/arm64 # # Called from: # - Release workflow as a prerequisite to goreleaser @@ -30,6 +28,8 @@ on: jobs: rust-cross: + # Cross-compile via the `cross` tool (docker-based sysroots). Covers + # Linux (all arches), BSDs, and windows/amd64-gnu. name: cross (${{ matrix.target }}) runs-on: ubuntu-latest-8-cores strategy: @@ -38,6 +38,11 @@ jobs: include: - target: x86_64-unknown-linux-gnu # linux/amd64 - target: aarch64-unknown-linux-gnu # linux/arm64 + - target: i686-unknown-linux-gnu # linux/386 + - target: arm-unknown-linux-gnueabi # linux/armv6 + - target: armv7-unknown-linux-gnueabihf # linux/armv7 + - target: x86_64-unknown-freebsd # freebsd/amd64 + - target: x86_64-unknown-netbsd # netbsd/amd64 - target: x86_64-pc-windows-gnu # windows/amd64 (MinGW) steps: - uses: actions/checkout@v4 @@ -70,6 +75,43 @@ jobs: if-no-files-found: error retention-days: 7 + rust-winarm: + # aarch64-pc-windows-gnullvm uses rust-lld (bundled with rustup) as + # its linker, so no external cross toolchain is needed. The `cross` + # tool doesn't cover this target, so we invoke cargo directly after + # adding the rustup target. + name: aarch64-pc-windows-gnullvm (native cargo + rust-lld) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: rdp-winarm-cargo-aarch64-pc-windows-gnullvm-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} + restore-keys: rdp-winarm-cargo-aarch64-pc-windows-gnullvm- + + - name: Install pinned Rust toolchain + target + working-directory: packages/pam/handlers/rdp/native + run: | + rustup show active-toolchain + rustup target add aarch64-pc-windows-gnullvm + + - name: cargo build --release --target aarch64-pc-windows-gnullvm + working-directory: packages/pam/handlers/rdp/native + run: cargo build --release --target aarch64-pc-windows-gnullvm + + - name: Upload static library + uses: actions/upload-artifact@v4 + with: + name: rdp-bridge-aarch64-pc-windows-gnullvm + path: packages/pam/handlers/rdp/native/target/aarch64-pc-windows-gnullvm/release/libinfisical_rdp_bridge.a + if-no-files-found: error + retention-days: 7 + rust-darwin: name: macos-latest (${{ matrix.target }}) runs-on: macos-latest diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index cfeca58f..e5f23bc2 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -144,9 +144,17 @@ jobs: sudo apt update sudo apt-get install -y libssl1.0-dev - name: Install cross-compile toolchains for RDP tier - # aarch64-linux-gnu-gcc: linux/arm64 CGO cross-compile. - # x86_64-w64-mingw32-gcc: windows/amd64 CGO cross-compile. - run: sudo apt-get install -y gcc-aarch64-linux-gnu gcc-mingw-w64-x86-64 + # Per-target C cross-compilers for the Linux/Windows RDP builds. + # Darwin, FreeBSD, NetBSD, and windows/arm64 use zig cc instead + # (installed below) because apt doesn't ship cgo-capable cross + # toolchains for those targets. + run: | + sudo apt-get install -y \ + gcc-aarch64-linux-gnu \ + gcc-i686-linux-gnu \ + gcc-arm-linux-gnueabi \ + gcc-arm-linux-gnueabihf \ + gcc-mingw-w64-x86-64 - name: Install zig for darwin cross-compile # zig cc bundles clang + ld.lld + a macOS SDK snapshot, so it # can produce valid Mach-O from linux. Used as the $CC for the @@ -165,13 +173,20 @@ jobs: - name: Stage RDP bridge static libs into cargo target dirs # Goreleaser's per-build CGO_LDFLAGS expects each target triple's # libinfisical_rdp_bridge.a to live at the standard cargo output - # path so the linker's -L flag resolves. + # path so the linker's -L flag resolves. 11 triples, one per + # RDP-enabled target. run: | set -euo pipefail for triple in \ x86_64-unknown-linux-gnu \ aarch64-unknown-linux-gnu \ + i686-unknown-linux-gnu \ + arm-unknown-linux-gnueabi \ + armv7-unknown-linux-gnueabihf \ + x86_64-unknown-freebsd \ + x86_64-unknown-netbsd \ x86_64-pc-windows-gnu \ + aarch64-pc-windows-gnullvm \ x86_64-apple-darwin \ aarch64-apple-darwin; do target_dir="packages/pam/handlers/rdp/native/target/$triple/release" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 14d7d0eb..c5ba89ec 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -115,7 +115,115 @@ builds: goarch: - amd64 + - id: linux-386-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - CC=i686-linux-gnu-gcc + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/i686-unknown-linux-gnu/release' + goos: + - linux + goarch: + - "386" + + - id: linux-armv6-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - CC=arm-linux-gnueabi-gcc + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/arm-unknown-linux-gnueabi/release' + goos: + - linux + goarch: + - arm + goarm: + - "6" + + - id: linux-armv7-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - CC=arm-linux-gnueabihf-gcc + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/armv7-unknown-linux-gnueabihf/release' + goos: + - linux + goarch: + - arm + goarm: + - "7" + + - id: freebsd-amd64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - 'CC=zig cc -target x86_64-freebsd-none' + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-unknown-freebsd/release' + goos: + - freebsd + goarch: + - amd64 + + - id: netbsd-amd64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - 'CC=zig cc -target x86_64-netbsd-none' + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-unknown-netbsd/release' + goos: + - netbsd + goarch: + - amd64 + + - id: windows-arm64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - 'CC=zig cc -target aarch64-windows-gnu' + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/aarch64-pc-windows-gnullvm/release' + goos: + - windows + goarch: + - arm64 + # --- stub tier: CGO=0, no rdp support --- + # Only openbsd ships on the stub tier now. Every other target the + # CLI builds for has a dedicated rdp build above. - id: all-other-builds env: - CGO_ENABLED=0 @@ -126,11 +234,7 @@ builds: flags: - -trimpath goos: - - freebsd - - linux - - netbsd - openbsd - - windows goarch: - "386" - amd64 @@ -139,19 +243,6 @@ builds: goarm: - "6" - "7" - ignore: - # linux/amd64, linux/arm64, windows/amd64 are handled by the - # per-arch rdp builds above: don't duplicate them here. - - goos: linux - goarch: amd64 - - goos: linux - goarch: arm64 - - goos: windows - goarch: amd64 - - goos: windows - goarch: "386" - - goos: freebsd - goarch: "386" archives: - format_overrides: @@ -220,7 +311,9 @@ nfpms: builds: - linux-amd64-rdp - linux-arm64-rdp - - all-other-builds + - linux-386-rdp + - linux-armv6-rdp + - linux-armv7-rdp vendor: Infisical, Inc homepage: https://infisical.com/ maintainer: Infisical, Inc diff --git a/session/pam_session_62c3bee9-ef35-4b24-937b-13f796b02098_windows_expires_1776967747.enc b/session/pam_session_62c3bee9-ef35-4b24-937b-13f796b02098_windows_expires_1776967747.enc new file mode 100644 index 00000000..e69de29b From 05fcb1199ae4a7bc18d0bfad901e7aab064b0325 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 12:55:03 -0400 Subject: [PATCH 19/51] chore: remove accidentally committed local PAM session artifact The session/ directory holds encrypted per-user session state written by `infisical pam rdp access` during local testing. It has no place in the repo; adding it to .gitignore so it doesn't happen again. --- .gitignore | 4 +++- ...ef35-4b24-937b-13f796b02098_windows_expires_1776967747.enc | 0 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 session/pam_session_62c3bee9-ef35-4b24-937b-13f796b02098_windows_expires_1776967747.enc diff --git a/.gitignore b/.gitignore index 0d9eb2f8..2891130d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ infisical /agent-testing -.vscode/ \ No newline at end of file +.vscode/ +# PAM CLI session artifacts (local testing only) +/session/ diff --git a/session/pam_session_62c3bee9-ef35-4b24-937b-13f796b02098_windows_expires_1776967747.enc b/session/pam_session_62c3bee9-ef35-4b24-937b-13f796b02098_windows_expires_1776967747.enc deleted file mode 100644 index e69de29b..00000000 From 8329f79ba65c4f1d0df0675a428a7cfd91986088 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 13:03:36 -0400 Subject: [PATCH 20/51] fix(pam-rdp): statically bundle zlib into the bridge archive The linux/arm64 (and by extension all linux cross-compile) goreleaser builds were failing with 'cannot find -lz' because Ubuntu's cross-gcc packages don't ship per-arch zlib. Pinning libz-sys with the static feature makes it compile zlib from source (via cc) and link it statically into libinfisical_rdp_bridge.a, so the consuming CGO link step doesn't need a system -lz for any target arch. libz-sys is a transitive dep of flate2, which ironrdp's smartcard / CredSSP paths pull in through winscard -> sspi. We don't use those code paths at runtime, but the symbols still get compiled into the static archive. Also drop -lz from the Linux and Darwin LDFLAGS directives in bridge_cgo.go since the symbols are now baked into the .a. --- packages/pam/handlers/rdp/bridge_cgo.go | 4 ++-- packages/pam/handlers/rdp/native/Cargo.lock | 2 ++ packages/pam/handlers/rdp/native/Cargo.toml | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/pam/handlers/rdp/bridge_cgo.go b/packages/pam/handlers/rdp/bridge_cgo.go index 4e9da7bb..970b1be5 100644 --- a/packages/pam/handlers/rdp/bridge_cgo.go +++ b/packages/pam/handlers/rdp/bridge_cgo.go @@ -4,8 +4,8 @@ package rdp /* #cgo CFLAGS: -I${SRCDIR}/native/include -#cgo linux LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -lm -ldl -lpthread -lz -#cgo darwin LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -lz -framework Security -framework CoreFoundation -framework SystemConfiguration +#cgo linux LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -lm -ldl -lpthread +#cgo darwin LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -framework Security -framework CoreFoundation -framework SystemConfiguration #include "rdp_bridge.h" #include diff --git a/packages/pam/handlers/rdp/native/Cargo.lock b/packages/pam/handlers/rdp/native/Cargo.lock index 0b7c85f9..7fad97bd 100644 --- a/packages/pam/handlers/rdp/native/Cargo.lock +++ b/packages/pam/handlers/rdp/native/Cargo.lock @@ -1418,6 +1418,7 @@ dependencies = [ "ironrdp-pdu", "ironrdp-tls", "ironrdp-tokio", + "libz-sys", "rcgen", "rustls", "tokio", @@ -1691,6 +1692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", + "libc", "pkg-config", "vcpkg", ] diff --git a/packages/pam/handlers/rdp/native/Cargo.toml b/packages/pam/handlers/rdp/native/Cargo.toml index 6e0144a3..26eaa84e 100644 --- a/packages/pam/handlers/rdp/native/Cargo.toml +++ b/packages/pam/handlers/rdp/native/Cargo.toml @@ -34,6 +34,14 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } clap = { version = "4", features = ["derive"] } +# Force libz-sys to bundle its own zlib and link it statically into +# our .a, so cross-compile linkers don't need a system `-lz` for the +# target arch. Without this the linux cross builds fail at link time +# with "cannot find -lz" because Ubuntu's cross-gcc packages don't +# ship per-arch zlib. libz-sys is a transitive dep of flate2, which +# ironrdp's smartcard / CredSSP paths pull in via winscard -> sspi. +libz-sys = { version = "1", features = ["static"] } + [profile.release] lto = true codegen-units = 1 From a515cbd56c1c77cb529ef13b06638f0156859af2 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 13:12:15 -0400 Subject: [PATCH 21/51] ci(pam-rdp): drop windows/arm64 from RDP tier aarch64-pc-windows-gnullvm cross-compile requires aarch64-w64-mingw32-clang from llvm-mingw (not in Ubuntu's apt repos) because libz-sys's cc crate needs it to build the bundled zlib. Between the extra toolchain surface and the negligible PAM user share on windows/arm64, it's cleaner to keep this target on the CGO=0 stub tier. Moves windows/arm64 back into all-other-builds alongside openbsd. 10 RDP-enabled targets total, down from 11. --- .github/workflows/build-rdp-bridge.yml | 45 +++---------------- .../workflows/release_build_infisical_cli.yml | 3 +- .goreleaser.yaml | 36 +++++++-------- 3 files changed, 24 insertions(+), 60 deletions(-) diff --git a/.github/workflows/build-rdp-bridge.yml b/.github/workflows/build-rdp-bridge.yml index 930a6156..debc8543 100644 --- a/.github/workflows/build-rdp-bridge.yml +++ b/.github/workflows/build-rdp-bridge.yml @@ -5,7 +5,7 @@ name: Build RDP Bridge Static Libs # as artifacts at release time and links them into per-target # `infisical` binaries via CGO. # -# Artifacts (11 total): +# Artifacts (10 total): # rdp-bridge-x86_64-unknown-linux-gnu : linux/amd64 # rdp-bridge-aarch64-unknown-linux-gnu : linux/arm64 # rdp-bridge-i686-unknown-linux-gnu : linux/386 @@ -14,10 +14,14 @@ name: Build RDP Bridge Static Libs # rdp-bridge-x86_64-unknown-freebsd : freebsd/amd64 # rdp-bridge-x86_64-unknown-netbsd : netbsd/amd64 # rdp-bridge-x86_64-pc-windows-gnu : windows/amd64 (MinGW) -# rdp-bridge-aarch64-pc-windows-gnullvm : windows/arm64 (rust-lld) # rdp-bridge-x86_64-apple-darwin : darwin/amd64 # rdp-bridge-aarch64-apple-darwin : darwin/arm64 # +# windows/arm64 is intentionally excluded: the gnullvm toolchain +# needs aarch64-w64-mingw32-clang (from llvm-mingw) that Ubuntu +# doesn't ship and the target has effectively no PAM user base. +# Users on windows/arm64 get the RDP stub at runtime. +# # Called from: # - Release workflow as a prerequisite to goreleaser # - Direct workflow_dispatch for iterating on the matrix @@ -75,43 +79,6 @@ jobs: if-no-files-found: error retention-days: 7 - rust-winarm: - # aarch64-pc-windows-gnullvm uses rust-lld (bundled with rustup) as - # its linker, so no external cross toolchain is needed. The `cross` - # tool doesn't cover this target, so we invoke cargo directly after - # adding the rustup target. - name: aarch64-pc-windows-gnullvm (native cargo + rust-lld) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: rdp-winarm-cargo-aarch64-pc-windows-gnullvm-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} - restore-keys: rdp-winarm-cargo-aarch64-pc-windows-gnullvm- - - - name: Install pinned Rust toolchain + target - working-directory: packages/pam/handlers/rdp/native - run: | - rustup show active-toolchain - rustup target add aarch64-pc-windows-gnullvm - - - name: cargo build --release --target aarch64-pc-windows-gnullvm - working-directory: packages/pam/handlers/rdp/native - run: cargo build --release --target aarch64-pc-windows-gnullvm - - - name: Upload static library - uses: actions/upload-artifact@v4 - with: - name: rdp-bridge-aarch64-pc-windows-gnullvm - path: packages/pam/handlers/rdp/native/target/aarch64-pc-windows-gnullvm/release/libinfisical_rdp_bridge.a - if-no-files-found: error - retention-days: 7 - rust-darwin: name: macos-latest (${{ matrix.target }}) runs-on: macos-latest diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index e5f23bc2..124c6913 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -173,7 +173,7 @@ jobs: - name: Stage RDP bridge static libs into cargo target dirs # Goreleaser's per-build CGO_LDFLAGS expects each target triple's # libinfisical_rdp_bridge.a to live at the standard cargo output - # path so the linker's -L flag resolves. 11 triples, one per + # path so the linker's -L flag resolves. 10 triples, one per # RDP-enabled target. run: | set -euo pipefail @@ -186,7 +186,6 @@ jobs: x86_64-unknown-freebsd \ x86_64-unknown-netbsd \ x86_64-pc-windows-gnu \ - aarch64-pc-windows-gnullvm \ x86_64-apple-darwin \ aarch64-apple-darwin; do target_dir="packages/pam/handlers/rdp/native/target/$triple/release" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c5ba89ec..82d04e8c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -204,26 +204,13 @@ builds: goarch: - amd64 - - id: windows-arm64-rdp - binary: infisical - ldflags: - - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} - flags: - - -trimpath - - -tags=rdp - env: - - CGO_ENABLED=1 - - 'CC=zig cc -target aarch64-windows-gnu' - - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/aarch64-pc-windows-gnullvm/release' - goos: - - windows - goarch: - - arm64 - # --- stub tier: CGO=0, no rdp support --- - # Only openbsd ships on the stub tier now. Every other target the - # CLI builds for has a dedicated rdp build above. + # Two targets stay on stub: + # - openbsd (no cargo / `cross` coverage, negligible PAM users) + # - windows/arm64 (gnullvm cross-compile needs llvm-mingw that + # Ubuntu doesn't ship; negligible PAM users) + # Every other target the CLI builds for has a dedicated rdp build + # above. - id: all-other-builds env: - CGO_ENABLED=0 @@ -235,6 +222,7 @@ builds: - -trimpath goos: - openbsd + - windows goarch: - "386" - amd64 @@ -243,6 +231,16 @@ builds: goarm: - "6" - "7" + ignore: + # windows/amd64 is handled by windows-amd64-rdp above. + # windows/386 isn't supported by Go+cgo anyway. + # windows/arm isn't a real target. + - goos: windows + goarch: amd64 + - goos: windows + goarch: "386" + - goos: windows + goarch: arm archives: - format_overrides: From 2b2ccab2da86313a14a1a6f3faaa164e3a954930 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 13:13:25 -0400 Subject: [PATCH 22/51] ci(pam-rdp): build only the staticlib target in cross-compile jobs The bridge crate has two targets: the libinfisical_rdp_bridge.a staticlib (what we ship) and an rdp-bridge-test dev binary (local convenience for exercising the MITM flow against a real RDP server). The bin target pulls in extra runtime deps that cross's default sysroots don't ship. Hit it first on NetBSD, which needs libexecinfo for backtrace support. Adding --lib skips the bin build entirely, makes the job faster across the board, and avoids the whole class of 'target sysroot missing dev-binary dep' issues. --- .github/workflows/build-rdp-bridge.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-rdp-bridge.yml b/.github/workflows/build-rdp-bridge.yml index debc8543..ea9a8ad0 100644 --- a/.github/workflows/build-rdp-bridge.yml +++ b/.github/workflows/build-rdp-bridge.yml @@ -67,9 +67,14 @@ jobs: working-directory: packages/pam/handlers/rdp/native run: rustup show active-toolchain - - name: cross build --release --target ${{ matrix.target }} + - name: cross build --release --lib --target ${{ matrix.target }} + # --lib: only build the staticlib crate-type, skip the + # rdp-bridge-test dev binary. The binary pulls in extra runtime + # deps (libexecinfo on BSDs, for instance) that cross's + # stock sysroots don't ship, and it's never needed in a + # release artifact. working-directory: packages/pam/handlers/rdp/native - run: cross build --release --target ${{ matrix.target }} + run: cross build --release --lib --target ${{ matrix.target }} - name: Upload static library uses: actions/upload-artifact@v4 @@ -106,9 +111,9 @@ jobs: rustup show active-toolchain rustup target add ${{ matrix.target }} - - name: cargo build --release --target ${{ matrix.target }} + - name: cargo build --release --lib --target ${{ matrix.target }} working-directory: packages/pam/handlers/rdp/native - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --release --lib --target ${{ matrix.target }} - name: Upload static library uses: actions/upload-artifact@v4 From eb75047bf5fdd166799121ca1976fa94b247aa78 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 13:34:55 -0400 Subject: [PATCH 23/51] chore(pam-rdp): strip redundant comments across the PR Most of the comment blocks I'd added were restating the code rather than explaining non-obvious WHY. Kept the ones that actually matter (Windows App protocol-echo requirement, DuplicateHandle vs WSADuplicateSocketW, the UnexpectedEof/close_notify quirk, why libz-sys static is needed for cross-compile) and dropped the rest. --- .github/workflows/build-rdp-bridge.yml | 57 ++++----------- .../workflows/release_build_infisical_cli.yml | 28 ++------ .github/workflows/run-cli-rdp-smoke.yml | 8 --- .goreleaser.yaml | 36 +--------- packages/pam/handlers/rdp/bridge.go | 37 +++------- packages/pam/handlers/rdp/bridge_cgo.go | 57 +++------------ .../pam/handlers/rdp/bridge_cgo_shared.go | 15 ++-- .../pam/handlers/rdp/bridge_cgo_windows.go | 48 ++----------- packages/pam/handlers/rdp/bridge_stub.go | 21 ++---- packages/pam/handlers/rdp/native/Cargo.toml | 8 +-- .../handlers/rdp/native/include/rdp_bridge.h | 67 ++---------------- .../handlers/rdp/native/rust-toolchain.toml | 4 -- .../pam/handlers/rdp/native/src/bridge.rs | 66 ++++-------------- .../pam/handlers/rdp/native/src/config.rs | 31 ++------- packages/pam/handlers/rdp/native/src/ffi.rs | 69 +++---------------- packages/pam/handlers/rdp/native/src/lib.rs | 14 +--- packages/pam/handlers/rdp/proxy.go | 15 +--- 17 files changed, 96 insertions(+), 485 deletions(-) diff --git a/.github/workflows/build-rdp-bridge.yml b/.github/workflows/build-rdp-bridge.yml index ea9a8ad0..3aefd841 100644 --- a/.github/workflows/build-rdp-bridge.yml +++ b/.github/workflows/build-rdp-bridge.yml @@ -1,30 +1,8 @@ name: Build RDP Bridge Static Libs -# Cross-compile the Rust MITM bridge into static archives for every -# OS/arch the CLI ships with RDP support. Goreleaser downloads these -# as artifacts at release time and links them into per-target -# `infisical` binaries via CGO. -# -# Artifacts (10 total): -# rdp-bridge-x86_64-unknown-linux-gnu : linux/amd64 -# rdp-bridge-aarch64-unknown-linux-gnu : linux/arm64 -# rdp-bridge-i686-unknown-linux-gnu : linux/386 -# rdp-bridge-arm-unknown-linux-gnueabi : linux/armv6 -# rdp-bridge-armv7-unknown-linux-gnueabihf : linux/armv7 -# rdp-bridge-x86_64-unknown-freebsd : freebsd/amd64 -# rdp-bridge-x86_64-unknown-netbsd : netbsd/amd64 -# rdp-bridge-x86_64-pc-windows-gnu : windows/amd64 (MinGW) -# rdp-bridge-x86_64-apple-darwin : darwin/amd64 -# rdp-bridge-aarch64-apple-darwin : darwin/arm64 -# -# windows/arm64 is intentionally excluded: the gnullvm toolchain -# needs aarch64-w64-mingw32-clang (from llvm-mingw) that Ubuntu -# doesn't ship and the target has effectively no PAM user base. -# Users on windows/arm64 get the RDP stub at runtime. -# -# Called from: -# - Release workflow as a prerequisite to goreleaser -# - Direct workflow_dispatch for iterating on the matrix +# windows/arm64 excluded: cross-compiling libz-sys for the gnullvm +# target needs aarch64-w64-mingw32-clang (from llvm-mingw), not +# available in Ubuntu repos. on: workflow_call: @@ -32,22 +10,20 @@ on: jobs: rust-cross: - # Cross-compile via the `cross` tool (docker-based sysroots). Covers - # Linux (all arches), BSDs, and windows/amd64-gnu. name: cross (${{ matrix.target }}) runs-on: ubuntu-latest-8-cores strategy: fail-fast: false matrix: include: - - target: x86_64-unknown-linux-gnu # linux/amd64 - - target: aarch64-unknown-linux-gnu # linux/arm64 - - target: i686-unknown-linux-gnu # linux/386 - - target: arm-unknown-linux-gnueabi # linux/armv6 - - target: armv7-unknown-linux-gnueabihf # linux/armv7 - - target: x86_64-unknown-freebsd # freebsd/amd64 - - target: x86_64-unknown-netbsd # netbsd/amd64 - - target: x86_64-pc-windows-gnu # windows/amd64 (MinGW) + - target: x86_64-unknown-linux-gnu + - target: aarch64-unknown-linux-gnu + - target: i686-unknown-linux-gnu + - target: arm-unknown-linux-gnueabi + - target: armv7-unknown-linux-gnueabihf + - target: x86_64-unknown-freebsd + - target: x86_64-unknown-netbsd + - target: x86_64-pc-windows-gnu steps: - uses: actions/checkout@v4 @@ -67,12 +43,9 @@ jobs: working-directory: packages/pam/handlers/rdp/native run: rustup show active-toolchain + # --lib skips the rdp-bridge-test dev binary, which pulls in + # runtime deps cross's BSD sysroots don't ship (e.g. libexecinfo). - name: cross build --release --lib --target ${{ matrix.target }} - # --lib: only build the staticlib crate-type, skip the - # rdp-bridge-test dev binary. The binary pulls in extra runtime - # deps (libexecinfo on BSDs, for instance) that cross's - # stock sysroots don't ship, and it's never needed in a - # release artifact. working-directory: packages/pam/handlers/rdp/native run: cross build --release --lib --target ${{ matrix.target }} @@ -91,8 +64,8 @@ jobs: fail-fast: false matrix: include: - - target: x86_64-apple-darwin # darwin/amd64 - - target: aarch64-apple-darwin # darwin/arm64 + - target: x86_64-apple-darwin + - target: aarch64-apple-darwin steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 124c6913..e7342e35 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -33,10 +33,6 @@ jobs: secrets: inherit build-rdp-bridge: - # Cross-compile the Rust RDP bridge static archives for the five - # client-tier targets (linux amd64/arm64, darwin amd64/arm64, - # windows amd64 via MinGW). Goreleaser picks these up and links - # them into the per-target `infisical` binary via CGO. uses: ./.github/workflows/build-rdp-bridge.yml # cli-integration-tests: @@ -53,8 +49,6 @@ jobs: # CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} npm-release: - # Never publish npm on dry-runs. Only on real tag pushes or a manual - # workflow_dispatch with dry_run=false. if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) runs-on: ubuntu-latest env: @@ -108,9 +102,6 @@ jobs: - validate-tag-branch - cli-tests - build-rdp-bridge - # `always()` lets this run even when validate-tag-branch was skipped - # (dry-run / dispatched-release case). The inner conditions enforce - # the cli-tests + rdp-bridge success and branch-validation-if-tag rules. if: | always() && needs.cli-tests.result == 'success' && @@ -144,10 +135,6 @@ jobs: sudo apt update sudo apt-get install -y libssl1.0-dev - name: Install cross-compile toolchains for RDP tier - # Per-target C cross-compilers for the Linux/Windows RDP builds. - # Darwin, FreeBSD, NetBSD, and windows/arm64 use zig cc instead - # (installed below) because apt doesn't ship cgo-capable cross - # toolchains for those targets. run: | sudo apt-get install -y \ gcc-aarch64-linux-gnu \ @@ -155,13 +142,10 @@ jobs: gcc-arm-linux-gnueabi \ gcc-arm-linux-gnueabihf \ gcc-mingw-w64-x86-64 - - name: Install zig for darwin cross-compile - # zig cc bundles clang + ld.lld + a macOS SDK snapshot, so it - # can produce valid Mach-O from linux. Used as the $CC for the - # darwin-amd64-rdp and darwin-arm64-rdp goreleaser builds. - # The previous osxcross-target clone only shipped SDK headers - # (no linker), which worked when no cgo code was actually being - # linked but breaks as soon as -tags rdp pulls in bridge_cgo.go. + # zig cc is used as the $CC for darwin + FreeBSD + NetBSD + # goreleaser builds; apt doesn't ship cgo-capable cross compilers + # for those targets. + - name: Install zig for darwin/BSD cross-compile uses: mlugg/setup-zig@v1 with: version: 0.13.0 @@ -171,10 +155,6 @@ jobs: pattern: rdp-bridge-* path: /tmp/rdp-bridge-artifacts/ - name: Stage RDP bridge static libs into cargo target dirs - # Goreleaser's per-build CGO_LDFLAGS expects each target triple's - # libinfisical_rdp_bridge.a to live at the standard cargo output - # path so the linker's -L flag resolves. 10 triples, one per - # RDP-enabled target. run: | set -euo pipefail for triple in \ diff --git a/.github/workflows/run-cli-rdp-smoke.yml b/.github/workflows/run-cli-rdp-smoke.yml index f2d0e9ac..ecb807de 100644 --- a/.github/workflows/run-cli-rdp-smoke.yml +++ b/.github/workflows/run-cli-rdp-smoke.yml @@ -1,10 +1,5 @@ name: RDP Bridge Smoke Test -# Fast per-PR check that the Rust static lib + CGo wrapper still build -# and link against the Go CLI on linux/amd64. The full cross-compile -# matrix runs on tag push via the release workflow; this job just -# guards against regressions that would only surface at release time. - on: pull_request: types: [opened, synchronize] @@ -33,9 +28,6 @@ jobs: key: rdp-bridge-cargo-${{ runner.os }}-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} restore-keys: rdp-bridge-cargo-${{ runner.os }}- - # Running any cargo command inside the crate auto-installs the - # toolchain pinned in rust-toolchain.toml via rustup (pre-installed - # on GitHub-hosted ubuntu-latest). - name: Install pinned Rust toolchain working-directory: packages/pam/handlers/rdp/native run: rustup show active-toolchain diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 82d04e8c..38c4677b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -3,32 +3,7 @@ before: - ./scripts/completions.sh - ./scripts/manpages.sh -# --------------------------------------------------------------------- -# Builds. -# -# The CLI has two tiers: -# -# rdp tier : CGO=1 with `-tags rdp`, links the Rust RDP bridge -# static archive. Cross-compile toolchain per target: -# linux/amd64 : system gcc -# linux/arm64 : gcc-aarch64-linux-gnu -# darwin/amd64 : o64-clang via osxcross -# darwin/arm64 : o64-clang via osxcross -# windows/amd64: x86_64-w64-mingw32-gcc (MinGW) -# Static lib per target is staged at -# packages/pam/handlers/rdp/native/target//release/libinfisical_rdp_bridge.a -# by the `build-rdp-bridge` workflow job. CGO_LDFLAGS -# adds the target-triple -L dir; the #cgo directive in -# bridge_cgo.go / bridge_cgo_windows.go supplies the -l -# flags. -# -# stub tier : CGO=0, no rdp tag. Ships the RDP stub that returns -# ErrRdpUnavailable at runtime. Covers the long tail of -# platforms we build for but don't realistically run PAM -# RDP on: linux 386 / 32-bit arm, BSDs, windows arm/arm64. -# --------------------------------------------------------------------- builds: - # --- rdp tier --- - id: darwin-amd64-rdp binary: infisical ldflags: @@ -204,13 +179,7 @@ builds: goarch: - amd64 - # --- stub tier: CGO=0, no rdp support --- - # Two targets stay on stub: - # - openbsd (no cargo / `cross` coverage, negligible PAM users) - # - windows/arm64 (gnullvm cross-compile needs llvm-mingw that - # Ubuntu doesn't ship; negligible PAM users) - # Every other target the CLI builds for has a dedicated rdp build - # above. + # openbsd and windows/arm64 stay on CGO=0 stub; see build-rdp-bridge.yml. - id: all-other-builds env: - CGO_ENABLED=0 @@ -232,9 +201,6 @@ builds: - "6" - "7" ignore: - # windows/amd64 is handled by windows-amd64-rdp above. - # windows/386 isn't supported by Go+cgo anyway. - # windows/arm isn't a real target. - goos: windows goarch: amd64 - goos: windows diff --git a/packages/pam/handlers/rdp/bridge.go b/packages/pam/handlers/rdp/bridge.go index 95088e78..9069f00a 100644 --- a/packages/pam/handlers/rdp/bridge.go +++ b/packages/pam/handlers/rdp/bridge.go @@ -1,39 +1,24 @@ -// Package rdp wraps the Rust MITM bridge for Infisical's PAM Windows -// handler. The real implementation is gated behind the `rdp` build tag -// and a supported platform; other builds receive stubs that return -// [ErrRdpUnavailable] from every constructor. +// Package rdp wraps the Rust MITM bridge behind the `rdp` build tag. +// Stub builds return ErrRdpUnavailable. package rdp import "errors" -// ErrRdpUnavailable is returned by constructors when the RDP bridge is -// not compiled in (built without `-tags rdp`, or on a platform that -// does not yet ship the Rust static library). -var ErrRdpUnavailable = errors.New("rdp bridge: not available in this build") - -// ErrInvalidHandle is returned when an operation references an unknown -// or already-freed bridge handle. -var ErrInvalidHandle = errors.New("rdp bridge: invalid handle") - -// ErrSessionFailed is returned from Wait when the session ended with a -// handshake or forwarding error (rather than a clean client disconnect). -var ErrSessionFailed = errors.New("rdp bridge: session ended with error") +var ( + ErrRdpUnavailable = errors.New("rdp bridge: not available in this build") + ErrInvalidHandle = errors.New("rdp bridge: invalid handle") + ErrSessionFailed = errors.New("rdp bridge: session ended with error") +) -// AcceptorUsername and AcceptorPassword are the fixed placeholder -// credential the native RDP client must present to the acceptor side of -// the bridge. The real access gate is upstream (Infisical auth + the -// gateway tunnel); these values are echoed from the Rust crate's -// ACCEPTOR_USERNAME / ACCEPTOR_PASSWORD constants and must stay in -// sync. +// Fixed placeholder credentials the RDP client presents to the acceptor +// side of the bridge. Must match ACCEPTOR_USERNAME / ACCEPTOR_PASSWORD in +// the Rust crate. Real authn happens upstream (Infisical + gateway). const ( AcceptorUsername = "infisical" AcceptorPassword = "infisical" ) -// Bridge owns the handle to a running RDP MITM session. Cancel may be -// called from any goroutine; Wait blocks until the session ends; Close -// releases the handle and must be called after Wait returns. type Bridge struct { handle uint64 - cleanup func() // runs during Close after the handle is freed; nil for direct fd sessions + cleanup func() } diff --git a/packages/pam/handlers/rdp/bridge_cgo.go b/packages/pam/handlers/rdp/bridge_cgo.go index 970b1be5..e7854e53 100644 --- a/packages/pam/handlers/rdp/bridge_cgo.go +++ b/packages/pam/handlers/rdp/bridge_cgo.go @@ -22,16 +22,8 @@ import ( "unsafe" ) -// StartWithConn starts a bridge session for the given TCP connection. -// Internally, an independent dup of the underlying file descriptor is -// handed to the bridge; the caller's conn stays fully usable and is not -// closed by this function. The bridge closes its dup when the session -// ends. -// -// `conn` must be a *net.TCPConn or any net.Conn that exposes a raw file -// descriptor via syscall.Conn. For TLS-wrapped or otherwise non-fd-backed -// conns (like the ones the gateway receives), use [StartWithReadWriter] -// instead. +// StartWithConn hands an independent dup of conn's fd to the bridge. +// For TLS-wrapped or otherwise non-fd-backed conns, use StartWithReadWriter. func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { dupFd, err := dupConnFD(conn) if err != nil { @@ -40,10 +32,7 @@ func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username return startWithDupedFD(dupFd, targetHost, targetPort, username, password) } -// startWithDupedFD hands ownership of `dupFd` to the Rust bridge. On -// success the bridge closes the fd when the session ends; on failure -// this function closes the fd itself before returning. Shared by -// StartWithConn and StartWithReadWriter. +// Ownership of dupFd transfers to Rust on success; we close it on failure. func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { success := false defer func() { @@ -75,27 +64,16 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, return &Bridge{handle: uint64(handle)}, nil } -// StartWithReadWriter starts a bridge session for a caller whose client -// stream is not fd-backed (e.g. *tls.Conn wrapping an mTLS'd virtual -// connection in the gateway). It creates a local loopback TCP pair, hands -// the kernel-backed accepted end to the Rust bridge, and pumps bytes -// between the other loopback end and the caller's `rw` via two io.Copy -// goroutines. The goroutines exit when either side closes; the bridge's -// Close method also tears them down. -// -// The caller retains ownership of `rw` and is responsible for closing it -// when done (the bridge does not close it). +// StartWithReadWriter creates a loopback TCP pair, hands the accepted +// kernel-backed end to the bridge, and pumps bytes between the other +// end and rw. Used for streams without a raw fd (e.g. *tls.Conn). func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) } - // We only ever accept one connection; close the listener either way. defer listener.Close() - // Kick off the dial concurrently with accept. Either ordering would - // work but the goroutine avoids a deadlock if some future net stack - // decides accept must run first. type dialResult struct { conn net.Conn err error @@ -117,8 +95,6 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, } peer := dr.conn - // The accepted side gets handed to Rust. Dup its fd, then close our - // copy so only Rust owns the socket going forward. dupFd, err := dupConnFD(accepted) _ = accepted.Close() if err != nil { @@ -132,9 +108,6 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return nil, err } - // Pump bytes between the caller's rw and the loopback peer. Each - // goroutine closes the peer on exit so the other side unblocks and - // exits too, regardless of which half EOFs first. go func() { _, _ = io.Copy(peer, rw) _ = peer.Close() @@ -148,9 +121,6 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return bridge, nil } -// dupConnFD returns a new file descriptor independent from `conn`'s -// internal one. The caller becomes responsible for closing the returned -// fd. Requires `conn` to implement syscall.Conn. func dupConnFD(conn net.Conn) (int, error) { sc, ok := conn.(syscall.Conn) if !ok { @@ -174,21 +144,11 @@ func dupConnFD(conn net.Conn) (int, error) { return dup, nil } -// HandleConnection is the entry point the gateway's PAM dispatcher calls -// for a Windows/RDP session. It takes ownership of `clientConn` (closes -// it on return), spawns a bridge via the loopback shim, and blocks until -// the session ends or `ctx` is cancelled (admin terminate, session -// expiry). On cancellation the bridge is signalled to abort and we wait -// for it to actually finish before returning `ctx.Err()`. func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { defer clientConn.Close() if p.config.SessionLogger != nil { defer func() { - if err := p.config.SessionLogger.Close(); err != nil { - // Don't fail the session on logger close error; it's a - // best-effort flush of any buffered events. - _ = err - } + _ = p.config.SessionLogger.Close() }() } @@ -204,7 +164,6 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er } defer bridge.Close() - // Run Wait on a goroutine so we can also select on ctx.Done(). waitErr := make(chan error, 1) go func() { waitErr <- bridge.Wait() }() @@ -216,7 +175,7 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er return nil case <-ctx.Done(): _ = bridge.Cancel() - <-waitErr // let the session unwind before we return + <-waitErr return ctx.Err() } } diff --git a/packages/pam/handlers/rdp/bridge_cgo_shared.go b/packages/pam/handlers/rdp/bridge_cgo_shared.go index 9ef41ad1..49c4e6fd 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_shared.go +++ b/packages/pam/handlers/rdp/bridge_cgo_shared.go @@ -11,11 +11,7 @@ import "C" import "fmt" -// Wait blocks until the session ends. Returns nil on a clean end -// (including the client hard-closing the TCP connection after a normal -// session), [ErrSessionFailed] on handshake or forwarding failure, or -// [ErrInvalidHandle] if the handle is unknown. Calling Wait a second -// time on the same handle returns nil (the session is already done). +// Wait blocks until the session ends. Idempotent. func (b *Bridge) Wait() error { rc := C.rdp_bridge_wait(C.uint64_t(b.handle)) switch rc { @@ -30,8 +26,8 @@ func (b *Bridge) Wait() error { } } -// Cancel signals the session to stop. Idempotent; safe from any -// goroutine even while another goroutine is inside Wait. +// Cancel is idempotent and safe from any goroutine, including +// concurrently with Wait. func (b *Bridge) Cancel() error { rc := C.rdp_bridge_cancel(C.uint64_t(b.handle)) if rc == C.RDP_BRIDGE_INVALID_HANDLE { @@ -40,10 +36,7 @@ func (b *Bridge) Cancel() error { return nil } -// Close releases the bridge handle. Call after Wait has returned. If the -// bridge was created with a loopback shim (via StartWithReadWriter), -// Close also tears down the shim goroutines by closing their loopback -// endpoint. +// Close must be called after Wait has returned. func (b *Bridge) Close() error { rc := C.rdp_bridge_free(C.uint64_t(b.handle)) if b.cleanup != nil { diff --git a/packages/pam/handlers/rdp/bridge_cgo_windows.go b/packages/pam/handlers/rdp/bridge_cgo_windows.go index 4bc78672..2af4a886 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_windows.go +++ b/packages/pam/handlers/rdp/bridge_cgo_windows.go @@ -23,16 +23,6 @@ import ( "golang.org/x/sys/windows" ) -// StartWithConn starts a bridge session for the given TCP connection. -// Internally, an independent duplicate of the underlying SOCKET is -// handed to the bridge via DuplicateHandle; the caller's conn stays -// fully usable and is not closed by this function. The bridge closes -// its dup when the session ends. -// -// `conn` must be a *net.TCPConn or any net.Conn that exposes a raw -// socket via syscall.Conn. For TLS-wrapped or otherwise non-socket-backed -// conns (like the ones the gateway receives), use [StartWithReadWriter] -// instead. func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { dupSocket, err := dupConnSocket(conn) if err != nil { @@ -41,10 +31,6 @@ func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password) } -// startWithDupedSocket hands ownership of `dupSocket` to the Rust bridge. -// On success the bridge closes the socket when the session ends; on -// failure this function closes the socket itself before returning. -// Shared by StartWithConn and StartWithReadWriter. func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { success := false defer func() { @@ -76,16 +62,6 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor return &Bridge{handle: uint64(handle)}, nil } -// StartWithReadWriter starts a bridge session for a caller whose client -// stream is not socket-backed (e.g. *tls.Conn wrapping an mTLS'd virtual -// connection in the gateway). It creates a local loopback TCP pair, -// hands the kernel-backed accepted end to the Rust bridge, and pumps -// bytes between the other loopback end and the caller's `rw` via two -// io.Copy goroutines. The goroutines exit when either side closes; the -// bridge's Close method also tears them down. -// -// The caller retains ownership of `rw` and is responsible for closing -// it when done (the bridge does not close it). func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -140,17 +116,9 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return bridge, nil } -// dupConnSocket returns a new SOCKET handle independent from `conn`'s -// internal one, using DuplicateHandle against the current process. The -// caller becomes responsible for closing the returned handle via -// windows.Closesocket. Requires `conn` to implement syscall.Conn. -// -// Note: this uses DuplicateHandle, not WSADuplicateSocketW. -// WSADuplicateSocketW is for cross-process socket sharing and requires -// the peer to call WSASocket with a WSAPROTOCOL_INFOW. For in-process -// SOCKET duplication, DuplicateHandle on the SOCKET's underlying kernel -// handle is the standard approach (SOCKETs are kernel handles on modern -// Windows). +// DuplicateHandle (not WSADuplicateSocketW, which is for cross-process +// sharing): SOCKETs are kernel handles on modern Windows, so DuplicateHandle +// gives us an independent in-process SOCKET the bridge can own and close. func dupConnSocket(conn net.Conn) (windows.Handle, error) { sc, ok := conn.(syscall.Conn) if !ok { @@ -183,19 +151,11 @@ func dupConnSocket(conn net.Conn) (windows.Handle, error) { return dup, nil } -// HandleConnection is the entry point the gateway's PAM dispatcher calls -// for a Windows/RDP session. It takes ownership of `clientConn` (closes -// it on return), spawns a bridge via the loopback shim, and blocks until -// the session ends or `ctx` is cancelled (admin terminate, session -// expiry). On cancellation the bridge is signalled to abort and we wait -// for it to actually finish before returning `ctx.Err()`. func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { defer clientConn.Close() if p.config.SessionLogger != nil { defer func() { - if err := p.config.SessionLogger.Close(); err != nil { - _ = err - } + _ = p.config.SessionLogger.Close() }() } diff --git a/packages/pam/handlers/rdp/bridge_stub.go b/packages/pam/handlers/rdp/bridge_stub.go index e24a8357..bbdfefbb 100644 --- a/packages/pam/handlers/rdp/bridge_stub.go +++ b/packages/pam/handlers/rdp/bridge_stub.go @@ -8,32 +8,23 @@ import ( "net" ) -// StartWithConn is a stub that reports the RDP bridge is unavailable in -// this build. To enable the real implementation, build with `-tags rdp` -// on a supported platform (linux, darwin, windows). +// Stub implementations for builds without `-tags rdp` or on platforms +// where the Rust bridge isn't compiled. All entry points return +// ErrRdpUnavailable. + func StartWithConn(_ net.Conn, _ string, _ uint16, _, _ string) (*Bridge, error) { return nil, ErrRdpUnavailable } -// StartWithReadWriter is a stub for builds without the RDP bridge. func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _ string) (*Bridge, error) { return nil, ErrRdpUnavailable } -// HandleConnection is a stub for builds without the RDP bridge. The -// gateway dispatcher calls into this on an RDP session; returning -// ErrRdpUnavailable surfaces a clean "this gateway build does not -// support RDP" error to the caller. func (p *RDPProxy) HandleConnection(_ context.Context, clientConn net.Conn) error { _ = clientConn.Close() return ErrRdpUnavailable } -// Wait is a stub for builds without the RDP bridge. -func (b *Bridge) Wait() error { return ErrRdpUnavailable } - -// Cancel is a stub for builds without the RDP bridge. +func (b *Bridge) Wait() error { return ErrRdpUnavailable } func (b *Bridge) Cancel() error { return ErrRdpUnavailable } - -// Close is a stub for builds without the RDP bridge. -func (b *Bridge) Close() error { return ErrRdpUnavailable } +func (b *Bridge) Close() error { return ErrRdpUnavailable } diff --git a/packages/pam/handlers/rdp/native/Cargo.toml b/packages/pam/handlers/rdp/native/Cargo.toml index 26eaa84e..b6ed10f2 100644 --- a/packages/pam/handlers/rdp/native/Cargo.toml +++ b/packages/pam/handlers/rdp/native/Cargo.toml @@ -34,12 +34,8 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } clap = { version = "4", features = ["derive"] } -# Force libz-sys to bundle its own zlib and link it statically into -# our .a, so cross-compile linkers don't need a system `-lz` for the -# target arch. Without this the linux cross builds fail at link time -# with "cannot find -lz" because Ubuntu's cross-gcc packages don't -# ship per-arch zlib. libz-sys is a transitive dep of flate2, which -# ironrdp's smartcard / CredSSP paths pull in via winscard -> sspi. +# Bundle zlib into the .a so cross-compile linkers don't need system -lz +# per target arch. Pulled in transitively via flate2 (winscard -> sspi). libz-sys = { version = "1", features = ["static"] } [profile.release] diff --git a/packages/pam/handlers/rdp/native/include/rdp_bridge.h b/packages/pam/handlers/rdp/native/include/rdp_bridge.h index 6535f9e9..83088768 100644 --- a/packages/pam/handlers/rdp/native/include/rdp_bridge.h +++ b/packages/pam/handlers/rdp/native/include/rdp_bridge.h @@ -1,24 +1,7 @@ /* - * infisical-rdp-bridge C ABI - * - * C-callable interface to the Rust MITM bridge. Consumed via CGo from the - * Go gateway and CLI code. All functions are thread-safe unless noted. - * - * Session lifecycle: - * 1. Caller hands in a connected TCP file descriptor / socket and target - * credentials to `rdp_bridge_start_*`. On success, the call returns - * immediately with an opaque `uint64_t` handle; the bridge runs on a - * dedicated OS thread. - * 2. `rdp_bridge_wait(handle)` blocks until the session ends. - * 3. `rdp_bridge_cancel(handle)` can be called at any time from any - * thread to signal the session to abort. Idempotent. - * 4. `rdp_bridge_free(handle)` releases the registry entry. Call after - * `wait` returns. - * - * Ownership of the client fd / socket transfers to the bridge on a - * successful `start_*` call. The bridge closes it when the session ends. - * Callers that need to keep their own reference should dup before calling - * (syscall.Dup on Unix, WSADuplicateSocket or equivalent on Windows). + * infisical-rdp-bridge C ABI. See ffi.rs for details. Lifecycle: + * start_* -> wait -> free; cancel may be called from any thread. + * start_* transfers ownership of the client fd/socket to the bridge. */ #ifndef INFISICAL_RDP_BRIDGE_H @@ -30,7 +13,6 @@ extern "C" { #endif -/* Status codes returned by all functions. */ #define RDP_BRIDGE_OK 0 #define RDP_BRIDGE_SESSION_ERROR 1 #define RDP_BRIDGE_THREAD_PANIC 2 @@ -39,20 +21,6 @@ extern "C" { #define RDP_BRIDGE_RUNTIME_ERROR -3 #if defined(__unix__) || defined(__APPLE__) -/* - * Start a session consuming a Unix client file descriptor. - * - * client_fd — connected TCP socket; ownership transfers to the bridge. - * target_host — NUL-terminated UTF-8 hostname or IP. - * target_port — RDP port (usually 3389). - * username — NUL-terminated UTF-8 username to inject via CredSSP. - * password — NUL-terminated UTF-8 password. - * out_handle — written with the session handle on success. - * - * Returns RDP_BRIDGE_OK on success, RDP_BRIDGE_BAD_ARG for invalid - * arguments, RDP_BRIDGE_RUNTIME_ERROR if the session thread could not - * be started. - */ int32_t rdp_bridge_start_unix_fd( int client_fd, const char *target_host, @@ -61,14 +29,9 @@ int32_t rdp_bridge_start_unix_fd( const char *password, uint64_t *out_handle ); -#endif /* unix */ +#endif #if defined(_WIN32) || defined(_WIN64) -/* - * Start a session consuming a Windows SOCKET handle (passed as uintptr_t - * so the ABI is fixed regardless of whether the caller uses SOCKET or - * HANDLE). Same semantics as `rdp_bridge_start_unix_fd`. - */ int32_t rdp_bridge_start_windows_socket( uintptr_t client_socket, const char *target_host, @@ -77,32 +40,14 @@ int32_t rdp_bridge_start_windows_socket( const char *password, uint64_t *out_handle ); -#endif /* windows */ +#endif -/* - * Block until the session on `handle` ends. Returns RDP_BRIDGE_OK on - * clean end, RDP_BRIDGE_SESSION_ERROR on handshake / forwarding error, - * RDP_BRIDGE_THREAD_PANIC if the session thread panicked, - * RDP_BRIDGE_INVALID_HANDLE if the handle is unknown. Calling a second - * time on the same handle returns RDP_BRIDGE_OK. - */ int32_t rdp_bridge_wait(uint64_t handle); - -/* - * Signal the session to cancel. The session's tokio task is aborted at - * the next await point and `wait` will then return. Idempotent; safe - * from any thread. Returns RDP_BRIDGE_OK or RDP_BRIDGE_INVALID_HANDLE. - */ int32_t rdp_bridge_cancel(uint64_t handle); - -/* - * Release the handle's registry entry. Must be called after `wait` has - * returned. Returns RDP_BRIDGE_OK or RDP_BRIDGE_INVALID_HANDLE. - */ int32_t rdp_bridge_free(uint64_t handle); #ifdef __cplusplus } #endif -#endif /* INFISICAL_RDP_BRIDGE_H */ +#endif diff --git a/packages/pam/handlers/rdp/native/rust-toolchain.toml b/packages/pam/handlers/rdp/native/rust-toolchain.toml index 3b7c36ee..3417ee59 100644 --- a/packages/pam/handlers/rdp/native/rust-toolchain.toml +++ b/packages/pam/handlers/rdp/native/rust-toolchain.toml @@ -1,8 +1,4 @@ [toolchain] -# Pin the Rust version used by both local dev and CI so the three -# cross-compile jobs (rust-cross on ubuntu, rust-darwin on macos, -# rust-winarm on windows-11-arm) agree on compiler behaviour. Bump by -# editing this file and letting CI fall through to the new toolchain. channel = "1.95.0" components = ["rustfmt", "clippy"] profile = "minimal" diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index b2fe3a00..b8db0850 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -1,13 +1,7 @@ -//! MITM bridge with post-CredSSP passthrough. -//! -//! We run the acceptor and connector only far enough to do credential -//! injection: accept client TLS + fixed-cred CredSSP on one side, connect -//! target TLS + real-cred CredSSP on the other. Once both CredSSP sequences -//! complete, we stop driving the IronRDP state machines and byte-forward -//! raw bytes between the two TLS streams. Client and target then negotiate -//! MCS, channels, capabilities, and share state directly with each other -//! through us, avoiding the feature-flag drift that breaks strict clients -//! (Windows App, mstsc) when acceptor and connector negotiate independently. +//! MITM bridge. Runs acceptor + connector only through CredSSP (to inject +//! credentials), then byte-forwards between the two TLS streams. Letting +//! client and target negotiate MCS/capabilities/share-state directly +//! avoids drift that breaks strict clients (Windows App, mstsc). use std::sync::Arc; @@ -29,9 +23,7 @@ use tracing::info; use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; -/// Fixed credential presented by the native client through the acceptor. -/// The real access gate is upstream (Infisical auth + the gateway tunnel); -/// this value only needs to match what the CLI bakes into the `.rdp` file. +// Must match what the CLI bakes into the generated .rdp file. pub const ACCEPTOR_USERNAME: &str = "infisical"; pub const ACCEPTOR_PASSWORD: &str = "infisical"; @@ -42,9 +34,6 @@ pub struct TargetEndpoint { pub password: String, } -/// Run a single RDP MITM session. Injects credentials at CredSSP and then -/// passes everything else through between the two TLS streams. The caller -/// can abort the session by cancelling the token. pub async fn run_mitm( client_tcp: TcpStream, target: TargetEndpoint, @@ -60,15 +49,10 @@ pub async fn run_mitm( } async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result<()> { - // rustls 0.23 requires an explicit crypto provider when more than one is - // compiled in. Our tree pulls both `ring` (direct) and `aws-lc-rs` - // (transitively from reqwest). Install ring as the default on first call; - // subsequent calls return Err("already installed") which we ignore. + // Our tree pulls both ring (direct) and aws-lc-rs (via reqwest); rustls + // 0.23 needs an explicit provider when more than one is compiled in. let _ = rustls::crypto::ring::default_provider().install_default(); - // Run the two halves concurrently so the client doesn't sit idle while - // the target side completes CredSSP. Functionally either order works; - // this is a latency optimization. let (acceptor_output, connector_output) = tokio::try_join!(run_acceptor_half(client_tcp), run_connector_half(target))?; @@ -88,10 +72,8 @@ async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result .context("flush target leftover to client")?; } - // Flush anything the CredSSP phase left buffered before handing off to - // copy_bidirectional. Belt-and-suspenders: tokio-rustls normally - // flushes on write_all, but being explicit here avoids a subtle stall - // if the final EarlyUserAuthResult PDU is sitting in the write buffer. + // Explicit flush before passthrough: avoids a stall if the final + // EarlyUserAuthResult PDU is sitting in the write buffer. client_stream .flush() .await @@ -101,11 +83,8 @@ async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result .await .context("flush target stream before passthrough")?; - // Passthrough: client and target negotiate MCS, channels, capabilities - // and share state directly through us. Real RDP clients hard-close the - // TCP connection on session end (no TLS close_notify), so rustls - // returns an UnexpectedEof. We treat that specific error as a clean - // shutdown; any other IO error propagates. + // Real RDP clients hard-close TCP without TLS close_notify, which + // rustls surfaces as UnexpectedEof. Treat that as clean shutdown. match tokio::io::copy_bidirectional(&mut client_stream, &mut target_stream).await { Ok(_) => info!("session ended cleanly"), Err(e) if is_unexpected_eof(&e) => info!("session ended (peer hard-closed)"), @@ -114,17 +93,10 @@ async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result Ok(()) } -/// rustls 0.23 raises `UnexpectedEof` when a peer closes the TCP connection -/// without sending `close_notify`. That's normal RDP client behavior and -/// should not surface as a session error. fn is_unexpected_eof(err: &std::io::Error) -> bool { err.kind() == std::io::ErrorKind::UnexpectedEof } -/// Accept the inbound connection, upgrade to TLS, and run CredSSP with the -/// fixed acceptor credential. Stops there: MCS and everything after is the -/// passthrough phase's job. Returns the underlying TLS stream and any bytes -/// the framed reader buffered beyond CredSSP. async fn run_acceptor_half(client_tcp: TcpStream) -> Result<(ErasedStream, bytes::BytesMut)> { let (server_tls, acceptor_public_key) = build_acceptor_tls().context("build acceptor TLS config")?; @@ -136,9 +108,7 @@ async fn run_acceptor_half(client_tcp: TcpStream) -> Result<(ErasedStream, bytes password: ACCEPTOR_PASSWORD.to_owned(), domain: None, }; - // Capabilities and desktop size passed here are unused because we never - // call `accept_finalize`. Acceptor::new requires them so we pass empty - // / sentinel values. + // Capabilities/desktop-size are shape-fillers; we never call accept_finalize. let mut acceptor = Acceptor::new( SecurityProtocol::HYBRID_EX | SecurityProtocol::HYBRID | SecurityProtocol::SSL, ironrdp_acceptor::DesktopSize { @@ -187,9 +157,6 @@ async fn run_acceptor_half(client_tcp: TcpStream) -> Result<(ErasedStream, bytes Ok(acceptor_framed.into_inner()) } -/// Connect to the target, upgrade to TLS, and run CredSSP with the injected -/// credentials. Stops there. Returns the underlying TLS stream and any -/// bytes the framed reader buffered beyond CredSSP. async fn run_connector_half(target: TargetEndpoint) -> Result<(ErasedStream, bytes::BytesMut)> { let target_addr = format!("{}:{}", target.host, target.port); let target_tcp = TcpStream::connect(&target_addr) @@ -235,11 +202,8 @@ async fn run_connector_half(target: TargetEndpoint) -> Result<(ErasedStream, byt Ok(target_framed.into_inner()) } -/// Drive the connector's CredSSP sequence to completion. Equivalent to -/// `perform_credssp_step` in `ironrdp-async`'s private module; replicated -/// here so we can stop before `connect_finalize` would start the MCS / -/// capability exchange (which is what we want client and target to do -/// directly via passthrough). +// Replicated from ironrdp-async's private perform_credssp_step so we can +// stop before connect_finalize (which would start MCS/capability exchange). async fn perform_connector_credssp( connector: &mut ClientConnector, framed: &mut ironrdp_tokio::TokioFramed, @@ -323,8 +287,6 @@ where Ok(()) } -/// Build the acceptor's TLS config and return the server's public key for -/// use as CredSSP TLS channel binding material. fn build_acceptor_tls() -> Result<(tokio_rustls::rustls::ServerConfig, Vec)> { use x509_cert::der::Decode; diff --git a/packages/pam/handlers/rdp/native/src/config.rs b/packages/pam/handlers/rdp/native/src/config.rs index e752cebf..b1f9a77a 100644 --- a/packages/pam/handlers/rdp/native/src/config.rs +++ b/packages/pam/handlers/rdp/native/src/config.rs @@ -1,11 +1,5 @@ -//! Connector config for the outbound half of the bridge. -//! -//! Post-CredSSP passthrough means we only need to drive the connector far -//! enough to complete CredSSP. After that, client and target negotiate -//! MCS / capabilities / share state directly through the byte-forwarding -//! pipe. Only CredSSP-relevant fields (credentials, security flags) are -//! load-bearing; other fields are required by `ironrdp_connector::Config` -//! but never hit the wire because we skip `connect_finalize`. +//! Connector config. Only CredSSP-relevant fields matter; after CredSSP +//! we switch to byte passthrough, so other fields are just shape-fillers. use ironrdp_connector::{BitmapConfig, Config, Credentials, DesktopSize}; use ironrdp_pdu::gcc::KeyboardType; @@ -23,28 +17,17 @@ pub fn connector_config(username: String, password: String) -> Config { }, desktop_scale_factor: 0, - // Advertise the same security-protocol set that native clients - // typically send (HYBRID_EX | HYBRID | SSL). Target echoes this - // set back in its ServerCoreData.clientRequestedProtocols; strict - // clients (Windows App) validate that echo against what THEY sent - // via the acceptor side. If the sets diverge, Windows App closes - // the session immediately after Connect Response. - // - // Target still picks HYBRID_EX (highest priority) so credential - // injection via NLA is unaffected. The MITM-downgrade concern - // described in ironrdp-connector's Config docs is real for a - // direct client-to-target connection, but here the outbound - // connection is to a known Windows server over a trusted path - // (gateway -> target), not a user-facing leg. + // Advertise HYBRID_EX|HYBRID|SSL to match what native clients send. + // Windows App validates the target's echoed clientRequestedProtocols + // against what it sent on the acceptor side; if the sets diverge it + // disconnects right after Connect Response. enable_tls: true, enable_credssp: true, credentials: Credentials::UsernamePassword { username, password }, domain: None, - // Unused after CredSSP because we switch to passthrough and target - // negotiates these values directly with the native client. Kept at - // sentinel values to satisfy the Config struct shape. + // Shape-fillers: unused after CredSSP (see module doc). client_build: 0, client_name: String::new(), keyboard_type: KeyboardType::IbmEnhanced, diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index 8df854f5..b2d1f9a0 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -1,22 +1,8 @@ -//! C ABI for the bridge. Designed to be called from Go via CGo. +//! C ABI for the bridge. Called from Go via CGo. //! -//! Model: -//! - Each session runs on its own OS thread with a current-thread tokio -//! runtime. Sessions are fully isolated. -//! - `start_*` allocates an opaque `u64` handle, spawns the thread, and -//! returns immediately. The handshake and passthrough happen inside -//! the spawned thread. -//! - `wait` blocks the calling thread until the session ends, returning -//! 0 on clean exit and 1 on session error. -//! - `cancel` is idempotent: it signals the bridge's CancellationToken, -//! which interrupts `run_mitm` at the next await point. -//! - `free` removes the handle from the registry. Call after `wait`. -//! -//! Ownership of the client file descriptor / socket: Rust takes ownership -//! of what is passed in and closes it when the session ends. The Go -//! caller is expected to hand in a dup'd fd (syscall.Dup on Unix, the -//! Windows equivalent on Windows) so its own `net.Conn` lifetime stays -//! independent. +//! Each session runs on its own OS thread with a current-thread tokio +//! runtime. `start_*` transfers ownership of the client fd/socket to +//! Rust (Go hands in a dup). Contract: wait, then free. use std::collections::HashMap; use std::ffi::{c_char, CStr}; @@ -40,7 +26,7 @@ pub const RDP_BRIDGE_RUNTIME_ERROR: i32 = -3; struct BridgeEntry { cancel: CancellationToken, - /// Taken by `wait`; `None` afterward. + // Taken by wait(); None afterward. join: Mutex>>>, } @@ -54,11 +40,7 @@ fn register(entry: BridgeEntry) -> u64 { id } -/// # Safety -/// -/// `ptr` must be either null or a valid NUL-terminated C string with the -/// `'static` borrow of the caller's buffer lasting for the duration of -/// this call. +/// # Safety: `ptr` must be null or a valid NUL-terminated C string. unsafe fn c_str_to_owned(ptr: *const c_char) -> Option { if ptr.is_null() { return None; @@ -104,15 +86,8 @@ fn spawn_session( })) } -/// Start a new bridge session consuming a Unix client file descriptor. -/// -/// # Safety -/// -/// `client_fd` must be a valid open socket descriptor. Ownership transfers -/// to the bridge on success; the caller must not close it. On failure, -/// ownership stays with the caller. `target_host`, `username`, and -/// `password` must be NUL-terminated valid UTF-8 C strings. `out_handle` -/// must be a writable `uint64_t`. +/// # Safety: `client_fd` ownership transfers to the bridge on OK, stays +/// with the caller on error. Strings must be NUL-terminated valid UTF-8. #[cfg(unix)] #[no_mangle] pub unsafe extern "C" fn rdp_bridge_start_unix_fd( @@ -140,12 +115,10 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( }; use std::os::unix::io::FromRawFd; - // Safety: contract states the caller transfers ownership of fd. let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; match spawn_session(client_tcp, host, target_port, username, password) { Ok(id) => { - // Safety: contract states out_handle is writable. unsafe { *out_handle = id }; RDP_BRIDGE_OK } @@ -156,13 +129,7 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( } } -/// Start a new bridge session consuming a Windows SOCKET. -/// -/// # Safety -/// -/// `client_socket` must be a valid open `SOCKET`. Ownership transfers -/// to the bridge on success. See `rdp_bridge_start_unix_fd` for shared -/// string and out-param contracts. +/// # Safety: see `rdp_bridge_start_unix_fd`. #[cfg(windows)] #[no_mangle] pub unsafe extern "C" fn rdp_bridge_start_windows_socket( @@ -190,7 +157,6 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( }; use std::os::windows::io::{FromRawSocket, RawSocket}; - // Safety: contract states caller transfers ownership. let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) }; match spawn_session(client_tcp, host, target_port, username, password) { @@ -205,15 +171,6 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( } } -/// Block until the session on `handle` finishes. -/// -/// Returns `RDP_BRIDGE_OK` on clean session end, -/// `RDP_BRIDGE_SESSION_ERROR` if the session ended with an error, -/// `RDP_BRIDGE_THREAD_PANIC` if the session thread panicked, -/// `RDP_BRIDGE_INVALID_HANDLE` if `handle` is unknown. -/// -/// Safe to call from any thread. Calling a second time on the same handle -/// returns `RDP_BRIDGE_OK` (the session is already done). #[no_mangle] pub extern "C" fn rdp_bridge_wait(handle: u64) -> i32 { let join = { @@ -243,9 +200,6 @@ pub extern "C" fn rdp_bridge_wait(handle: u64) -> i32 { } } -/// Signal the session to cancel. Idempotent: safe to call multiple times. -/// After `cancel`, the caller should still `wait` to observe the session -/// actually finishing, and then `free` to release the handle. #[no_mangle] pub extern "C" fn rdp_bridge_cancel(handle: u64) -> i32 { let handles = HANDLES.lock().expect("HANDLES poisoned"); @@ -258,11 +212,6 @@ pub extern "C" fn rdp_bridge_cancel(handle: u64) -> i32 { } } -/// Release the handle's resources. Must be called after `wait` has -/// returned. If the session thread is still running when `free` is -/// called, the handle is dropped and the thread becomes detached (still -/// owned by the registry entry; would leak). Callers should always pair -/// `wait` with `free`. #[no_mangle] pub extern "C" fn rdp_bridge_free(handle: u64) -> i32 { let mut handles = HANDLES.lock().expect("HANDLES poisoned"); diff --git a/packages/pam/handlers/rdp/native/src/lib.rs b/packages/pam/handlers/rdp/native/src/lib.rs index bc775d43..61c64480 100644 --- a/packages/pam/handlers/rdp/native/src/lib.rs +++ b/packages/pam/handlers/rdp/native/src/lib.rs @@ -1,14 +1,6 @@ -//! Infisical RDP MITM bridge. -//! -//! The bridge accepts an inbound RDP connection from a native client -//! (xfreerdp, mstsc) on one side and initiates an outbound RDP connection -//! to a Windows target on the other. The outbound handshake performs -//! CredSSP/NLA with credentials injected by the gateway, so the real -//! target credentials never reach the client. The inbound handshake -//! accepts a fixed placeholder credential (`infisical`/`infisical`) that -//! the CLI embeds in the generated .rdp file. -//! -//! Phase 1 scope: standalone test binary only, no FFI, no event tap. +//! Infisical RDP MITM bridge. Accepts inbound RDP with a placeholder +//! credential, connects outbound with gateway-injected credentials, then +//! passes bytes through. pub mod bridge; pub mod config; diff --git a/packages/pam/handlers/rdp/proxy.go b/packages/pam/handlers/rdp/proxy.go index 544f1272..e113902a 100644 --- a/packages/pam/handlers/rdp/proxy.go +++ b/packages/pam/handlers/rdp/proxy.go @@ -4,32 +4,21 @@ import ( "github.com/Infisical/infisical-merge/packages/pam/session" ) -// RDPProxyConfig is what the gateway's PAM dispatcher passes to -// [NewRDPProxy] when routing a Windows/RDP session. type RDPProxyConfig struct { TargetHost string TargetPort uint16 InjectUsername string InjectPassword string SessionID string - - // SessionLogger is retained on the config for API symmetry with the - // other PAM handlers. The current bridge has no event tap (no RDP - // session recording yet) so nothing is actually written through it, - // but the dispatcher expects to hand one in per session and may start - // shipping events through it in a later phase. + // Retained for API symmetry with other PAM handlers; not yet written + // through (no RDP session recording in this MVP). SessionLogger session.SessionLogger } -// RDPProxy is the gateway-side handler for a Windows/RDP PAM session. -// It wraps an [RDPProxyConfig] and implements the same HandleConnection -// shape as SSH / Postgres / Redis / etc. type RDPProxy struct { config RDPProxyConfig } -// NewRDPProxy constructs a proxy. The actual session work happens in -// HandleConnection (whose implementation is in a platform-specific file). func NewRDPProxy(config RDPProxyConfig) *RDPProxy { return &RDPProxy{config: config} } From 65aaa12d6a49ffdaefe0832e448a8d2de714635f Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 13:44:15 -0400 Subject: [PATCH 24/51] ci(pam-rdp): drop FreeBSD and NetBSD from RDP tier zig cc doesn't bundle FreeBSD/NetBSD libc headers, so cgo's include fails with 'stdlib.h file not found' during the Go link step. No standard Ubuntu apt package ships a cgo-capable BSD sysroot either. Moving both back to the CGO=0 stub alongside openbsd and windows/arm64. Final RDP matrix: 8 targets (5 linux arches + windows/amd64 + darwin amd64/arm64). --- .github/workflows/build-rdp-bridge.yml | 8 ++-- .../workflows/release_build_infisical_cli.yml | 2 - .goreleaser.yaml | 40 +++---------------- 3 files changed, 8 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build-rdp-bridge.yml b/.github/workflows/build-rdp-bridge.yml index 3aefd841..b36217e3 100644 --- a/.github/workflows/build-rdp-bridge.yml +++ b/.github/workflows/build-rdp-bridge.yml @@ -1,8 +1,8 @@ name: Build RDP Bridge Static Libs -# windows/arm64 excluded: cross-compiling libz-sys for the gnullvm -# target needs aarch64-w64-mingw32-clang (from llvm-mingw), not -# available in Ubuntu repos. +# windows/arm64, freebsd, netbsd, openbsd excluded: no cgo-capable +# cross toolchain we can reasonably install in CI. They ship the RDP +# stub at runtime. on: workflow_call: @@ -21,8 +21,6 @@ jobs: - target: i686-unknown-linux-gnu - target: arm-unknown-linux-gnueabi - target: armv7-unknown-linux-gnueabihf - - target: x86_64-unknown-freebsd - - target: x86_64-unknown-netbsd - target: x86_64-pc-windows-gnu steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index e7342e35..22e30b0b 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -163,8 +163,6 @@ jobs: i686-unknown-linux-gnu \ arm-unknown-linux-gnueabi \ armv7-unknown-linux-gnueabihf \ - x86_64-unknown-freebsd \ - x86_64-unknown-netbsd \ x86_64-pc-windows-gnu \ x86_64-apple-darwin \ aarch64-apple-darwin; do diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 38c4677b..5005c7c3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -145,41 +145,7 @@ builds: goarm: - "7" - - id: freebsd-amd64-rdp - binary: infisical - ldflags: - - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} - flags: - - -trimpath - - -tags=rdp - env: - - CGO_ENABLED=1 - - 'CC=zig cc -target x86_64-freebsd-none' - - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-unknown-freebsd/release' - goos: - - freebsd - goarch: - - amd64 - - - id: netbsd-amd64-rdp - binary: infisical - ldflags: - - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} - flags: - - -trimpath - - -tags=rdp - env: - - CGO_ENABLED=1 - - 'CC=zig cc -target x86_64-netbsd-none' - - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-unknown-netbsd/release' - goos: - - netbsd - goarch: - - amd64 - - # openbsd and windows/arm64 stay on CGO=0 stub; see build-rdp-bridge.yml. + # BSDs and windows/arm64 stay on CGO=0 stub; see build-rdp-bridge.yml. - id: all-other-builds env: - CGO_ENABLED=0 @@ -190,6 +156,8 @@ builds: flags: - -trimpath goos: + - freebsd + - netbsd - openbsd - windows goarch: @@ -201,6 +169,8 @@ builds: - "6" - "7" ignore: + - goos: freebsd + goarch: "386" - goos: windows goarch: amd64 - goos: windows From 50e29cec926ae5b4872cbad537bb1e7e494a0ba0 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 14:44:31 -0400 Subject: [PATCH 25/51] ci(pam-rdp): bump zig to 0.16.0 Verified locally that zig 0.16.0 resolves -lresolv correctly when cross-compiling cgo net package to darwin (the issue that broke darwin_amd64_v1 on the last dry-run). 0.13.0 didn't ship a libresolv SDK stub. --- .github/workflows/release_build_infisical_cli.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 22e30b0b..14218f15 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -145,10 +145,10 @@ jobs: # zig cc is used as the $CC for darwin + FreeBSD + NetBSD # goreleaser builds; apt doesn't ship cgo-capable cross compilers # for those targets. - - name: Install zig for darwin/BSD cross-compile + - name: Install zig for darwin cross-compile uses: mlugg/setup-zig@v1 with: - version: 0.13.0 + version: 0.16.0 - name: Download RDP bridge static libs uses: actions/download-artifact@v4 with: From b15371bdcab9e8bfe54abc0671e180004bff196a Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 15:22:24 -0400 Subject: [PATCH 26/51] fix(pam-rdp): clean up .rdp file on session end + pin zig 0.14.0 .rdp file cleanup: the generated file sticks in ~/.infisical/rdp/ indefinitely. It's dead weight once the CLI exits: the proxy port is closed, so double-clicking the file just fails. Added removal inside gracefulShutdown, placed before p.cancel() so main doesn't race ahead and exit the process before the removal completes. Ignores os.IsNotExist (if the user already deleted it manually) and any other error (best-effort, don't fail shutdown on filesystem hiccups). Zig pin: 0.16.0 isn't a stable release on ziglang.org (brew was shipping a dev build), so mlugg/setup-zig@v1 couldn't locate it. Verified locally that 0.14.0 correctly resolves -lresolv when cross-compiling cgo net package to darwin, which was the underlying libresolv issue we bumped zig to fix. --- .github/workflows/release_build_infisical_cli.yml | 2 +- packages/pam/local/rdp-proxy.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 14218f15..169cb551 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -148,7 +148,7 @@ jobs: - name: Install zig for darwin cross-compile uses: mlugg/setup-zig@v1 with: - version: 0.16.0 + version: 0.14.0 - name: Download RDP bridge static libs uses: actions/download-artifact@v4 with: diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index d2345e6f..b1643c14 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -169,6 +169,16 @@ func (p *RDPProxyServer) gracefulShutdown() { p.shutdownOnce.Do(func() { log.Info().Msg("Starting graceful shutdown of RDP proxy...") + // Remove the .rdp file first: p.cancel() below unblocks Run(), + // which returns to main, which may exit before the rest of this + // goroutine completes. Do the cleanup that has to happen before + // anything that could let main race ahead. + if p.rdpFilePath != "" { + if err := os.Remove(p.rdpFilePath); err != nil && !os.IsNotExist(err) { + log.Debug().Err(err).Str("path", p.rdpFilePath).Msg("Failed to remove .rdp file on exit") + } + } + p.NotifySessionTermination() close(p.shutdownCh) From 3887a5aff20a28027f9e6f92ab596da1e868d040 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 15:27:34 -0400 Subject: [PATCH 27/51] fix(ci): stub libresolv.tbd for darwin cross-compile My earlier claim that 0.14/0.16 'fixed' libresolv was wrong: I was fooled by Go's internal linker on a macOS host, which skipped the external zig cc invocation entirely. Direct invocation of 'zig cc -target x86_64-macos-none foo.c -lresolv' fails identically on all three versions (0.13, 0.14, 0.16). Actual fix: zig cc only searches the -L paths we provide when it can't find a system lib in its bundled SDK stubs. Drop a minimal libresolv.tbd (no exports) into the darwin cargo target dirs that are already on the -L path via CGO_LDFLAGS. Satisfies the link-time -lresolv; at runtime the resolv_* symbols come from libSystem (on real macOS, libresolv.9.dylib is itself a re-exporter of libSystem, so the binary behaves identically to one linked against the real libresolv). --- .github/workflows/release_build_infisical_cli.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 169cb551..e125e07b 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -170,6 +170,19 @@ jobs: mkdir -p "$target_dir" cp "/tmp/rdp-bridge-artifacts/rdp-bridge-$triple/libinfisical_rdp_bridge.a" "$target_dir/" done + # zig cc doesn't ship libresolv.tbd for macos targets, but Go's + # net package hardcodes -lresolv in its cgo directives. Drop a + # stub tbd with no exports into the darwin -L dirs: zig finds it + # at link time (satisfying -lresolv), and at runtime the resolv_* + # symbols resolve through libSystem on real macOS (libresolv.9 is + # itself just a re-exporter of libSystem). + - name: Stage libresolv stub for darwin cross-compile + run: | + set -euo pipefail + printf -- '--- !tapi-tbd\ntbd-version: 4\ntargets: [ x86_64-macos, arm64-macos ]\ninstall-name: /usr/lib/libresolv.9.dylib\nexports:\n - targets: [ x86_64-macos, arm64-macos ]\n symbols: [ ]\n' > /tmp/libresolv.tbd + for triple in x86_64-apple-darwin aarch64-apple-darwin; do + cp /tmp/libresolv.tbd "packages/pam/handlers/rdp/native/target/$triple/release/libresolv.tbd" + done - name: GoReleaser (dry-run snapshot) if: github.event_name == 'workflow_dispatch' && inputs.dry_run uses: goreleaser/goreleaser-action@v4 From 08f08096a4dc8c11a9218bd63370587989bc5d84 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 15:32:15 -0400 Subject: [PATCH 28/51] ci(pam-rdp): ditch zig for darwin, use macos-latest runner instead Three zig quirks in a row (osxcross initial fail, BSD sysroot gaps, libresolv missing from bundled stubs) made it clear cross-compiling cgo to darwin from linux is never going to be painless. Switching to the pattern your windows goreleaser already uses: dedicated runner for darwin, native toolchain, no hacks. Changes: * .goreleaser-darwin.yaml (new): darwin-amd64-rdp + darwin-arm64-rdp builds using default Apple clang on macos-latest. Apple's clang handles x86_64 cross-arch from Apple Silicon via -arch natively. Owns brews: (since darwin binaries are produced here). Uses release.mode: append so it adds archives to the draft the ubuntu job already created. * .goreleaser.yaml: drop darwin builds and the brews stanza. * release workflow: new goreleaser-darwin job on macos-latest. Depends on goreleaser (needs the draft to exist before append). Downloads only the two darwin bridge .a artifacts. Drops the zig install + libresolv stub from the ubuntu goreleaser job. * BSDs: gone from the rdp tier earlier in this branch; no new regression (still on stub via all-other-builds). Trade-offs: one more CI runner (macos is more expensive), extra needs-serialization between ubuntu and darwin jobs (was parallel). Worth it for a stable darwin pipeline. --- .../workflows/release_build_infisical_cli.yml | 100 ++++++++++++++---- .goreleaser-darwin.yaml | 89 ++++++++++++++++ .goreleaser.yaml | 68 +----------- 3 files changed, 167 insertions(+), 90 deletions(-) create mode 100644 .goreleaser-darwin.yaml diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index e125e07b..1eef3388 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -142,13 +142,6 @@ jobs: gcc-arm-linux-gnueabi \ gcc-arm-linux-gnueabihf \ gcc-mingw-w64-x86-64 - # zig cc is used as the $CC for darwin + FreeBSD + NetBSD - # goreleaser builds; apt doesn't ship cgo-capable cross compilers - # for those targets. - - name: Install zig for darwin cross-compile - uses: mlugg/setup-zig@v1 - with: - version: 0.14.0 - name: Download RDP bridge static libs uses: actions/download-artifact@v4 with: @@ -163,26 +156,11 @@ jobs: i686-unknown-linux-gnu \ arm-unknown-linux-gnueabi \ armv7-unknown-linux-gnueabihf \ - x86_64-pc-windows-gnu \ - x86_64-apple-darwin \ - aarch64-apple-darwin; do + x86_64-pc-windows-gnu; do target_dir="packages/pam/handlers/rdp/native/target/$triple/release" mkdir -p "$target_dir" cp "/tmp/rdp-bridge-artifacts/rdp-bridge-$triple/libinfisical_rdp_bridge.a" "$target_dir/" done - # zig cc doesn't ship libresolv.tbd for macos targets, but Go's - # net package hardcodes -lresolv in its cgo directives. Drop a - # stub tbd with no exports into the darwin -L dirs: zig finds it - # at link time (satisfying -lresolv), and at runtime the resolv_* - # symbols resolve through libSystem on real macOS (libresolv.9 is - # itself just a re-exporter of libSystem). - - name: Stage libresolv stub for darwin cross-compile - run: | - set -euo pipefail - printf -- '--- !tapi-tbd\ntbd-version: 4\ntargets: [ x86_64-macos, arm64-macos ]\ninstall-name: /usr/lib/libresolv.9.dylib\nexports:\n - targets: [ x86_64-macos, arm64-macos ]\n symbols: [ ]\n' > /tmp/libresolv.tbd - for triple in x86_64-apple-darwin aarch64-apple-darwin; do - cp /tmp/libresolv.tbd "packages/pam/handlers/rdp/native/target/$triple/release/libresolv.tbd" - done - name: GoReleaser (dry-run snapshot) if: github.event_name == 'workflow_dispatch' && inputs.dry_run uses: goreleaser/goreleaser-action@v4 @@ -259,6 +237,82 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }} CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }} + goreleaser-darwin: + runs-on: macos-latest + # Runs after the ubuntu goreleaser creates the release draft so this + # job can append darwin archives (release.mode: append). + needs: + - validate-tag-branch + - cli-tests + - build-rdp-bridge + - goreleaser + if: | + always() && + needs.cli-tests.result == 'success' && + needs.build-rdp-bridge.result == 'success' && + needs.goreleaser.result == 'success' && + (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch all tags + run: git fetch --force --tags + + - uses: actions/setup-go@v5 + with: + go-version: "1.25.9" + cache: true + cache-dependency-path: go.sum + + - name: Download darwin RDP bridge static libs + uses: actions/download-artifact@v4 + with: + pattern: rdp-bridge-*-apple-darwin + path: /tmp/rdp-bridge-artifacts/ + + - name: Stage darwin RDP bridge static libs + run: | + set -euo pipefail + for triple in x86_64-apple-darwin aarch64-apple-darwin; do + target_dir="packages/pam/handlers/rdp/native/target/$triple/release" + mkdir -p "$target_dir" + cp "/tmp/rdp-bridge-artifacts/rdp-bridge-$triple/libinfisical_rdp_bridge.a" "$target_dir/" + done + + - name: GoReleaser Darwin (dry-run snapshot) + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser-pro + version: v1.26.2-pro + args: release --clean --config .goreleaser-darwin.yaml --snapshot --skip=publish + env: + GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} + POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + + - name: GoReleaser Darwin (release) + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser-pro + version: v1.26.2-pro + args: release --clean --config .goreleaser-darwin.yaml + env: + GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} + POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + + - name: Upload dry-run dist as workflow artifact + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + uses: actions/upload-artifact@v4 + with: + name: goreleaser-dist-darwin + path: dist/ + retention-days: 7 + # Currently only supports Windows amd64 goreleaser-windows: runs-on: windows-2022 diff --git a/.goreleaser-darwin.yaml b/.goreleaser-darwin.yaml new file mode 100644 index 00000000..9871e640 --- /dev/null +++ b/.goreleaser-darwin.yaml @@ -0,0 +1,89 @@ +before: + hooks: + - ./scripts/completions.sh + - ./scripts/manpages.sh + +builds: + - id: darwin-amd64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-apple-darwin/release' + goos: + - darwin + goarch: + - amd64 + + - id: darwin-arm64-rdp + binary: infisical + ldflags: + - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} + - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} + flags: + - -trimpath + - -tags=rdp + env: + - CGO_ENABLED=1 + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/aarch64-apple-darwin/release' + goos: + - darwin + goarch: + - arm64 + +archives: + - files: + - README* + - LICENSE* + - manpages/* + - completions/* + +# Append to the release draft created by the ubuntu goreleaser job. +release: + replace_existing_draft: false + mode: append + +checksum: + name_template: "checksums-darwin.txt" + +snapshot: + name_template: "{{ .Version }}-devel" + +brews: + - name: infisical + tap: + owner: Infisical + name: homebrew-get-cli + commit_author: + name: "Infisical" + email: ai@infisical.com + folder: Formula + homepage: "https://infisical.com" + description: "The official Infisical CLI" + install: |- + bin.install "infisical" + bash_completion.install "completions/infisical.bash" => "infisical" + zsh_completion.install "completions/infisical.zsh" => "_infisical" + fish_completion.install "completions/infisical.fish" + man1.install "manpages/infisical.1.gz" + - name: "infisical@{{.Version}}" + tap: + owner: Infisical + name: homebrew-get-cli + commit_author: + name: "Infisical" + email: ai@infisical.com + folder: Formula + homepage: "https://infisical.com" + description: "The official Infisical CLI" + install: |- + bin.install "infisical" + bash_completion.install "completions/infisical.bash" => "infisical" + zsh_completion.install "completions/infisical.zsh" => "_infisical" + fish_completion.install "completions/infisical.fish" + man1.install "manpages/infisical.1.gz" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5005c7c3..78328b61 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -4,40 +4,6 @@ before: - ./scripts/manpages.sh builds: - - id: darwin-amd64-rdp - binary: infisical - ldflags: - - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} - flags: - - -trimpath - - -tags=rdp - env: - - CGO_ENABLED=1 - - 'CC=zig cc -target x86_64-macos-none' - - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-apple-darwin/release' - goos: - - darwin - goarch: - - amd64 - - - id: darwin-arm64-rdp - binary: infisical - ldflags: - - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} - flags: - - -trimpath - - -tags=rdp - env: - - CGO_ENABLED=1 - - 'CC=zig cc -target aarch64-macos-none' - - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/aarch64-apple-darwin/release' - goos: - - darwin - goarch: - - arm64 - - id: linux-amd64-rdp binary: infisical ldflags: @@ -205,39 +171,7 @@ snapshot: # dir: "{{ dir .ArtifactPath }}" # cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/ -brews: - - name: infisical - tap: - owner: Infisical - name: homebrew-get-cli - commit_author: - name: "Infisical" - email: ai@infisical.com - folder: Formula - homepage: "https://infisical.com" - description: "The official Infisical CLI" - install: |- - bin.install "infisical" - bash_completion.install "completions/infisical.bash" => "infisical" - zsh_completion.install "completions/infisical.zsh" => "_infisical" - fish_completion.install "completions/infisical.fish" - man1.install "manpages/infisical.1.gz" - - name: "infisical@{{.Version}}" - tap: - owner: Infisical - name: homebrew-get-cli - commit_author: - name: "Infisical" - email: ai@infisical.com - folder: Formula - homepage: "https://infisical.com" - description: "The official Infisical CLI" - install: |- - bin.install "infisical" - bash_completion.install "completions/infisical.bash" => "infisical" - zsh_completion.install "completions/infisical.zsh" => "_infisical" - fish_completion.install "completions/infisical.fish" - man1.install "manpages/infisical.1.gz" +# brews: moved to .goreleaser-darwin.yaml (where darwin binaries are built). nfpms: - id: infisical From cf674f105878080da81f50c9b6ff3ceb0c84259d Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 15:42:36 -0400 Subject: [PATCH 29/51] chore(pam-rdp): remove rdp-bridge-test dev binary Was a local MITM harness for manual testing against a real Windows server. Never shipped. We've moved past the point where it's useful (the feature is tested end-to-end via the CLI itself now), and keeping it around forces the -lib flag in every CI build command to work around deps (libexecinfo, tracing-subscriber, clap) that only matter for the dev binary. Drops main.rs, the [[bin]] section, tracing-subscriber and clap deps, and --lib from the cross/cargo build commands. --- .github/workflows/build-rdp-bridge.yml | 10 +- packages/pam/handlers/rdp/native/Cargo.lock | 226 ------------------- packages/pam/handlers/rdp/native/Cargo.toml | 6 - packages/pam/handlers/rdp/native/src/main.rs | 85 ------- 4 files changed, 4 insertions(+), 323 deletions(-) delete mode 100644 packages/pam/handlers/rdp/native/src/main.rs diff --git a/.github/workflows/build-rdp-bridge.yml b/.github/workflows/build-rdp-bridge.yml index b36217e3..2dfd7f79 100644 --- a/.github/workflows/build-rdp-bridge.yml +++ b/.github/workflows/build-rdp-bridge.yml @@ -41,11 +41,9 @@ jobs: working-directory: packages/pam/handlers/rdp/native run: rustup show active-toolchain - # --lib skips the rdp-bridge-test dev binary, which pulls in - # runtime deps cross's BSD sysroots don't ship (e.g. libexecinfo). - - name: cross build --release --lib --target ${{ matrix.target }} + - name: cross build --release --target ${{ matrix.target }} working-directory: packages/pam/handlers/rdp/native - run: cross build --release --lib --target ${{ matrix.target }} + run: cross build --release --target ${{ matrix.target }} - name: Upload static library uses: actions/upload-artifact@v4 @@ -82,9 +80,9 @@ jobs: rustup show active-toolchain rustup target add ${{ matrix.target }} - - name: cargo build --release --lib --target ${{ matrix.target }} + - name: cargo build --release --target ${{ matrix.target }} working-directory: packages/pam/handlers/rdp/native - run: cargo build --release --lib --target ${{ matrix.target }} + run: cargo build --release --target ${{ matrix.target }} - name: Upload static library uses: actions/upload-artifact@v4 diff --git a/packages/pam/handlers/rdp/native/Cargo.lock b/packages/pam/handlers/rdp/native/Cargo.lock index 7fad97bd..5c04a3e5 100644 --- a/packages/pam/handlers/rdp/native/Cargo.lock +++ b/packages/pam/handlers/rdp/native/Cargo.lock @@ -53,65 +53,6 @@ dependencies = [ "const-oid 0.10.2", ] -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -365,46 +306,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - [[package]] name = "cmake" version = "0.1.58" @@ -414,12 +315,6 @@ dependencies = [ "cc", ] -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - [[package]] name = "const-oid" version = "0.9.6" @@ -1412,7 +1307,6 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", - "clap", "ironrdp-acceptor", "ironrdp-connector", "ironrdp-pdu", @@ -1425,7 +1319,6 @@ dependencies = [ "tokio-rustls", "tokio-util", "tracing", - "tracing-subscriber", "x509-cert", ] @@ -1590,12 +1483,6 @@ dependencies = [ "url", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "iso7816" version = "0.1.4" @@ -1651,12 +1538,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -1718,15 +1599,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "md-5" version = "0.10.6" @@ -1816,15 +1688,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -1889,12 +1752,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -2406,23 +2263,6 @@ dependencies = [ "bitflags 2.11.1", ] -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - [[package]] name = "reqwest" version = "0.12.28" @@ -2775,15 +2615,6 @@ dependencies = [ "keccak", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2941,12 +2772,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subtle" version = "2.6.1" @@ -3037,15 +2862,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "time" version = "0.3.47" @@ -3250,36 +3066,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] @@ -3340,12 +3126,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.23.1" @@ -3358,12 +3138,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/packages/pam/handlers/rdp/native/Cargo.toml b/packages/pam/handlers/rdp/native/Cargo.toml index b6ed10f2..500a2117 100644 --- a/packages/pam/handlers/rdp/native/Cargo.toml +++ b/packages/pam/handlers/rdp/native/Cargo.toml @@ -10,10 +10,6 @@ name = "infisical_rdp_bridge" crate-type = ["staticlib", "rlib"] path = "src/lib.rs" -[[bin]] -name = "rdp-bridge-test" -path = "src/main.rs" - [dependencies] ironrdp-acceptor = "0.8" ironrdp-connector = "0.8" @@ -31,8 +27,6 @@ rcgen = "0.13" anyhow = "1" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -clap = { version = "4", features = ["derive"] } # Bundle zlib into the .a so cross-compile linkers don't need system -lz # per target arch. Pulled in transitively via flate2 (winscard -> sspi). diff --git a/packages/pam/handlers/rdp/native/src/main.rs b/packages/pam/handlers/rdp/native/src/main.rs deleted file mode 100644 index f44cbbe5..00000000 --- a/packages/pam/handlers/rdp/native/src/main.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Standalone test binary. Listens on a loopback port, accepts one -//! connection, runs the MITM bridge to a Windows target, and exits. -//! -//! Validate against a real Windows server with any native RDP client -//! using credentials `infisical`/`infisical`; see the crate README for -//! tested client commands. - -use std::net::SocketAddr; - -use anyhow::{Context, Result}; -use clap::Parser; -use tokio::net::TcpListener; -use tokio_util::sync::CancellationToken; -use tracing::{error, info}; -use tracing_subscriber::EnvFilter; - -use infisical_rdp_bridge::bridge::{run_mitm, TargetEndpoint}; - -#[derive(Parser, Debug)] -#[command(about = "Infisical RDP MITM bridge: manual validation harness")] -struct Args { - /// Loopback address to listen on for the native RDP client. - #[arg(long, default_value = "127.0.0.1:3390")] - listen: SocketAddr, - - /// Target Windows RDP server host. - #[arg(long)] - target_host: String, - - /// Target Windows RDP server port. - #[arg(long, default_value_t = 3389)] - target_port: u16, - - /// Username to inject on the outbound connection. - #[arg(long)] - username: String, - - /// Password to inject on the outbound connection. - #[arg(long)] - password: String, -} - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::fmt() - .with_env_filter( - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), - ) - .init(); - - let args = Args::parse(); - - let listener = TcpListener::bind(args.listen) - .await - .with_context(|| format!("bind {}", args.listen))?; - info!( - listen = %args.listen, - target = %format!("{}:{}", args.target_host, args.target_port), - "bridge ready; waiting for one RDP client connection" - ); - - let (client_tcp, peer) = listener.accept().await.context("accept")?; - info!(%peer, "inbound connection; starting MITM"); - drop(listener); - - let endpoint = TargetEndpoint { - host: args.target_host, - port: args.target_port, - username: args.username, - password: args.password, - }; - - // Test binary never cancels; pass a fresh token that stays uncancelled. - let cancel = CancellationToken::new(); - match run_mitm(client_tcp, endpoint, cancel).await { - Ok(()) => { - info!("session ended cleanly"); - Ok(()) - } - Err(e) => { - error!(error = ?e, "session failed"); - Err(e) - } - } -} From 348de421cca5e3903212720f0e0229a54fd4950e Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 16:14:32 -0400 Subject: [PATCH 30/51] ci(pam-rdp): parallelize goreleaser + goreleaser-darwin via shared draft Instead of serializing darwin on the full ubuntu goreleaser job, a new create-release-draft job creates the empty draft up front (~10 seconds). Both goreleaser jobs then depend on that job instead of on each other, and both use release.mode: append to upload into the shared draft in parallel. Chart is now: prereqs (cli-tests, validate-tag, rdp bridge) -> create-release-draft (only on real release) -> goreleaser (ubuntu) | -> goreleaser-darwin (macos) | both run in parallel Dry-run skips create-release-draft entirely; both goreleaser jobs still run under --snapshot (no GitHub API calls). goreleaser.release.mode is flipped from 'replace' to 'append' to match the new flow. Draft creation is idempotent: on a retry the existing draft is reused rather than overwritten. Saves ~5 min wall-clock on real releases by letting the darwin build overlap with ubuntu's S3/CloudFront publish steps. npm-release now needs goreleaser-darwin too so it doesn't publish the npm wrapper before all arch binaries are in the release. --- .../workflows/release_build_infisical_cli.yml | 36 ++++++++++++++++--- .goreleaser.yaml | 7 ++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 1eef3388..56898a2e 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -35,6 +35,33 @@ jobs: build-rdp-bridge: uses: ./.github/workflows/build-rdp-bridge.yml + # Create the GitHub release draft up front so both goreleaser + # (ubuntu) and goreleaser-darwin (macos) can append to it in + # parallel instead of serializing on ubuntu creating the draft. + # Skipped on dry-run since --snapshot doesn't touch GitHub at all. + create-release-draft: + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) + needs: + - validate-tag-branch + - cli-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Create GitHub release draft (idempotent) + run: | + if gh release view "${{ github.ref_name }}" >/dev/null 2>&1; then + echo "Release for ${{ github.ref_name }} already exists, skipping creation" + else + gh release create "${{ github.ref_name }}" \ + --draft \ + --title "${{ github.ref_name }}" \ + --generate-notes + fi + env: + GH_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} + # cli-integration-tests: # name: Run tests before deployment # uses: ./.github/workflows/run-cli-tests.yml @@ -56,6 +83,7 @@ jobs: needs: # - cli-integration-tests - goreleaser + - goreleaser-darwin - validate-tag-branch - cli-tests steps: @@ -102,10 +130,12 @@ jobs: - validate-tag-branch - cli-tests - build-rdp-bridge + - create-release-draft if: | always() && needs.cli-tests.result == 'success' && needs.build-rdp-bridge.result == 'success' && + (needs.create-release-draft.result == 'success' || needs.create-release-draft.result == 'skipped') && (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') steps: - uses: actions/checkout@v3 @@ -239,18 +269,16 @@ jobs: goreleaser-darwin: runs-on: macos-latest - # Runs after the ubuntu goreleaser creates the release draft so this - # job can append darwin archives (release.mode: append). needs: - validate-tag-branch - cli-tests - build-rdp-bridge - - goreleaser + - create-release-draft if: | always() && needs.cli-tests.result == 'success' && needs.build-rdp-bridge.result == 'success' && - needs.goreleaser.result == 'success' && + (needs.create-release-draft.result == 'success' || needs.create-release-draft.result == 'skipped') && (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') steps: - uses: actions/checkout@v4 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 78328b61..654258ef 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -155,8 +155,11 @@ archives: - completions/* release: - replace_existing_draft: true - mode: "replace" + # The draft is created up front by the create-release-draft workflow + # job, so both this config and .goreleaser-darwin.yaml use append mode + # to add their artifacts in parallel. + replace_existing_draft: false + mode: append checksum: name_template: "checksums.txt" From 3291a54b8ddfbf56dab01beb51ccd968d7659a3e Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 16:15:21 -0400 Subject: [PATCH 31/51] fix(ci): link libwinpthread for windows CGO The Rust bridge's static archive contains aws-lc-sys (pulled in transitively through reqwest's default rustls backend). For x86_64-pc-windows-gnu aws-lc-sys compiled its POSIX threading variant, leaving undefined references to pthread_*, sched_yield, and nanosleep. MinGW ships libwinpthread.a which provides these Windows-compatible implementations. --- packages/pam/handlers/rdp/bridge_cgo_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pam/handlers/rdp/bridge_cgo_windows.go b/packages/pam/handlers/rdp/bridge_cgo_windows.go index 2af4a886..5d80729c 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_windows.go +++ b/packages/pam/handlers/rdp/bridge_cgo_windows.go @@ -4,7 +4,7 @@ package rdp /* #cgo CFLAGS: -I${SRCDIR}/native/include -#cgo windows LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -lws2_32 -luserenv -lbcrypt -lntdll -ladvapi32 -lcrypt32 -lsecur32 +#cgo windows LDFLAGS: -L${SRCDIR}/native/target/release -linfisical_rdp_bridge -lws2_32 -luserenv -lbcrypt -lntdll -ladvapi32 -lcrypt32 -lsecur32 -lwinpthread #include "rdp_bridge.h" #include From 57438fee5bd71fcd98199c1c875150db87ab0447 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 16:40:18 -0400 Subject: [PATCH 32/51] ci: group goreleaser-windows into the create-release-draft column Doesn't technically need the draft (Docker Hub is a separate channel), but lining the three goreleaser jobs up visually behind the same prereq makes the workflow DAG easier to read. --- .github/workflows/release_build_infisical_cli.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 56898a2e..f1a707ea 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -341,15 +341,20 @@ jobs: path: dist/ retention-days: 7 - # Currently only supports Windows amd64 + # Currently only supports Windows amd64. + # Doesn't technically need create-release-draft (output goes to + # Docker Hub, not the GitHub release), but kept in needs for visual + # grouping with the other two goreleaser jobs in the workflow DAG. goreleaser-windows: runs-on: windows-2022 needs: - validate-tag-branch - cli-tests + - create-release-draft if: | always() && needs.cli-tests.result == 'success' && + (needs.create-release-draft.result == 'success' || needs.create-release-draft.result == 'skipped') && (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') steps: - uses: actions/checkout@v3 From 7511234535e21118da49a0b588123c0d3c823afb Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 16:55:27 -0400 Subject: [PATCH 33/51] ci(pam-rdp): enable RDP in Windows Docker container images Flips the windows-build goreleaser entry from CGO=0 to CGO=1 with -tags rdp, matching the user-facing Windows zip. Links the x86_64-pc-windows-gnu bridge staticlib via MinGW gcc (pre-installed on GitHub's windows-2022 runners). Use case: running 'infisical pam rdp access' inside a Windows container as a tunnel/relay, e.g. exposing the proxy port so an external RDP client can connect through the container to a gateway target. Niche but legit, and keeps the Windows container feature parity with the other tiers. goreleaser-windows now also depends on build-rdp-bridge, which has the side effect of aligning its needs: with goreleaser and goreleaser-darwin so all three render in the same DAG box. --- .github/workflows/release_build_infisical_cli.yml | 15 +++++++++++---- .goreleaser-windows.yaml | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index f1a707ea..a50136e9 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -341,19 +341,20 @@ jobs: path: dist/ retention-days: 7 - # Currently only supports Windows amd64. - # Doesn't technically need create-release-draft (output goes to - # Docker Hub, not the GitHub release), but kept in needs for visual - # grouping with the other two goreleaser jobs in the workflow DAG. + # Builds the Windows Server container images (→ Docker Hub). The + # binary baked in is RDP-enabled via MinGW CGO, matching the + # user-facing Windows zip. goreleaser-windows: runs-on: windows-2022 needs: - validate-tag-branch - cli-tests + - build-rdp-bridge - create-release-draft if: | always() && needs.cli-tests.result == 'success' && + needs.build-rdp-bridge.result == 'success' && (needs.create-release-draft.result == 'success' || needs.create-release-draft.result == 'skipped') && (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') steps: @@ -371,6 +372,12 @@ jobs: cache: true cache-dependency-path: go.sum + - name: Download windows RDP bridge static lib + uses: actions/download-artifact@v4 + with: + name: rdp-bridge-x86_64-pc-windows-gnu + path: packages/pam/handlers/rdp/native/target/x86_64-pc-windows-gnu/release + - name: 🐋 Login to Docker Hub uses: docker/login-action@v2 with: diff --git a/.goreleaser-windows.yaml b/.goreleaser-windows.yaml index a32f12fb..44b119d0 100644 --- a/.goreleaser-windows.yaml +++ b/.goreleaser-windows.yaml @@ -1,13 +1,16 @@ builds: - id: windows-build env: - - CGO_ENABLED=0 + - CGO_ENABLED=1 + - CC=gcc + - 'CGO_LDFLAGS=-L packages/pam/handlers/rdp/native/target/x86_64-pc-windows-gnu/release' binary: infisical ldflags: - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }} flags: - -trimpath + - -tags=rdp goos: - windows goarch: From 6f3ac4997a7ef7aed4437e65cb3429309b8aba2b Mon Sep 17 00:00:00 2001 From: bernie-g Date: Thu, 23 Apr 2026 17:35:10 -0400 Subject: [PATCH 34/51] ci(pam-rdp): build Rust bridge natively on windows-2022 runner cross-rs builds the x86_64-pc-windows-gnu .a with MSVCRT-linked MinGW, but the windows-2022 runner's bundled MinGW 14 defaults to UCRT. That ABI gap left the goreleaser-windows link with undefined references to __iob_func (an MSVCRT symbol; UCRT uses __acrt_iob_func). Fix: build the bridge archive on the same runner that will link it, so both the Rust staticlib and Go's cgo-invoked link step use the exact same MinGW installation. Adds a rustup target add + cargo build step, plus caching for the registry and target dir. Drops build-rdp-bridge from the job's needs since we're no longer consuming that job's artifact. Windows docker image build is now fully self-contained on its runner. --- .../workflows/release_build_infisical_cli.yml | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index a50136e9..acbd8c8b 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -344,17 +344,21 @@ jobs: # Builds the Windows Server container images (→ Docker Hub). The # binary baked in is RDP-enabled via MinGW CGO, matching the # user-facing Windows zip. + # + # The Rust bridge is built here natively rather than downloaded from + # build-rdp-bridge: cross-rs's MinGW produces MSVCRT-ABI archives, + # but windows-2022's MinGW 14 is UCRT-based, which doesn't resolve + # MSVCRT-only symbols like __iob_func. Building both the .a and the + # Go binary on the same runner guarantees the ABIs match. goreleaser-windows: runs-on: windows-2022 needs: - validate-tag-branch - cli-tests - - build-rdp-bridge - create-release-draft if: | always() && needs.cli-tests.result == 'success' && - needs.build-rdp-bridge.result == 'success' && (needs.create-release-draft.result == 'success' || needs.create-release-draft.result == 'skipped') && (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') steps: @@ -372,11 +376,27 @@ jobs: cache: true cache-dependency-path: go.sum - - name: Download windows RDP bridge static lib - uses: actions/download-artifact@v4 + - name: Cache cargo registry + target + uses: actions/cache@v4 with: - name: rdp-bridge-x86_64-pc-windows-gnu - path: packages/pam/handlers/rdp/native/target/x86_64-pc-windows-gnu/release + path: | + ~/.cargo/registry + ~/.cargo/git + packages/pam/handlers/rdp/native/target + key: rdp-bridge-windows-cargo-${{ hashFiles('packages/pam/handlers/rdp/native/Cargo.lock') }} + restore-keys: rdp-bridge-windows-cargo- + + - name: Install pinned Rust toolchain + gnu target + working-directory: packages/pam/handlers/rdp/native + shell: pwsh + run: | + rustup show active-toolchain + rustup target add x86_64-pc-windows-gnu + + - name: Build Rust bridge for x86_64-pc-windows-gnu + working-directory: packages/pam/handlers/rdp/native + shell: pwsh + run: cargo build --release --target x86_64-pc-windows-gnu - name: 🐋 Login to Docker Hub uses: docker/login-action@v2 From e22812acd553115ac93d768e69c161de19161851 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Fri, 24 Apr 2026 10:20:46 -0400 Subject: [PATCH 35/51] fix(pam-rdp): format Safety doc as proper heading for clippy The missing_safety_doc lint requires '# Safety' as a standalone markdown heading, not inline with a colon. Reformat all three unsafe-fn doc blocks (c_str_to_owned and the two FFI entry points) to separate the heading from the body. --- packages/pam/handlers/rdp/native/src/ffi.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index b2d1f9a0..ecef7782 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -40,7 +40,9 @@ fn register(entry: BridgeEntry) -> u64 { id } -/// # Safety: `ptr` must be null or a valid NUL-terminated C string. +/// # Safety +/// +/// `ptr` must be null or a valid NUL-terminated C string. unsafe fn c_str_to_owned(ptr: *const c_char) -> Option { if ptr.is_null() { return None; @@ -86,8 +88,10 @@ fn spawn_session( })) } -/// # Safety: `client_fd` ownership transfers to the bridge on OK, stays -/// with the caller on error. Strings must be NUL-terminated valid UTF-8. +/// # Safety +/// +/// `client_fd` ownership transfers to the bridge on OK, stays with the +/// caller on error. Strings must be NUL-terminated valid UTF-8. #[cfg(unix)] #[no_mangle] pub unsafe extern "C" fn rdp_bridge_start_unix_fd( @@ -129,7 +133,9 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( } } -/// # Safety: see `rdp_bridge_start_unix_fd`. +/// # Safety +/// +/// See `rdp_bridge_start_unix_fd`. #[cfg(windows)] #[no_mangle] pub unsafe extern "C" fn rdp_bridge_start_windows_socket( From b3754283eb737c35f7f62a5c4d6ffa0497679c36 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Fri, 24 Apr 2026 10:27:31 -0400 Subject: [PATCH 36/51] ci: shorten dry_run description in release workflow --- .github/workflows/release_build_infisical_cli.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index acbd8c8b..05789412 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -5,11 +5,7 @@ on: inputs: dry_run: description: >- - Run goreleaser in --snapshot --skip=publish mode. No git tag needed, - nothing is published. The built binaries are uploaded as a - workflow artifact so you can download and sanity-check them. Safe - way to exercise the full cross-compile matrix before cutting a - real release. + Do a dry-run (no artifacts are published and no release is created) type: boolean default: true From ef74e908d324273db11d7f83109278f816309f2d Mon Sep 17 00:00:00 2001 From: bernie-g Date: Fri, 24 Apr 2026 11:15:13 -0400 Subject: [PATCH 37/51] chore(pam-rdp): remove bridge-test harness The standalone harness exercised the Rust to CGo to Go chain without going through the CLI. Was useful during bring-up but 'infisical pam rdp access' now covers the same end-to-end path, so the harness is dead code. --- .../pam/handlers/rdp/cmd/bridge-test/main.go | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 packages/pam/handlers/rdp/cmd/bridge-test/main.go diff --git a/packages/pam/handlers/rdp/cmd/bridge-test/main.go b/packages/pam/handlers/rdp/cmd/bridge-test/main.go deleted file mode 100644 index 758182cb..00000000 --- a/packages/pam/handlers/rdp/cmd/bridge-test/main.go +++ /dev/null @@ -1,124 +0,0 @@ -// Standalone test harness for the Go bridge wrapper. -// -// Mirrors the Rust test binary's behavior but exercises the full -// Rust -> C ABI -> CGo -> Go path: -// -// 1. Bind a loopback TCP listener -// 2. Accept one RDP client connection -// 3. Hand it to rdp.StartWithConn -// 4. Block on bridge.Wait until the session ends -// 5. Exit -// -// Build with `-tags rdp` from the cli repo root: -// -// go run -tags rdp ./packages/pam/handlers/rdp/cmd/bridge-test -- \ -// -listen 127.0.0.1:3390 \ -// -target :3389 \ -// -user \ -// -pass -package main - -import ( - "errors" - "flag" - "fmt" - "log" - "net" - "os" - "os/signal" - "strconv" - "strings" - "syscall" - - "github.com/Infisical/infisical-merge/packages/pam/handlers/rdp" -) - -func main() { - listenAddr := flag.String("listen", "127.0.0.1:3390", "loopback address to accept the RDP client on") - target := flag.String("target", "", "target Windows server as host:port (port defaults to 3389)") - username := flag.String("user", "", "username to inject on the outbound connection") - password := flag.String("pass", "", "password to inject on the outbound connection") - flag.Parse() - - if *target == "" || *username == "" || *password == "" { - fmt.Fprintln(os.Stderr, "--target, --user, and --pass are required") - flag.Usage() - os.Exit(2) - } - - host, port, err := splitHostPort(*target) - if err != nil { - log.Fatalf("parse target: %v", err) - } - - listener, err := net.Listen("tcp", *listenAddr) - if err != nil { - log.Fatalf("bind %s: %v", *listenAddr, err) - } - log.Printf("bridge ready; listening on %s, target %s:%d", *listenAddr, host, port) - - // Accept one connection, then stop listening. - conn, err := listener.Accept() - if err != nil { - log.Fatalf("accept: %v", err) - } - _ = listener.Close() - log.Printf("inbound connection from %s; starting MITM", conn.RemoteAddr()) - - bridge, err := rdp.StartWithConn(conn, host, port, *username, *password) - if err != nil { - _ = conn.Close() - log.Fatalf("start bridge: %v", err) - } - // The bridge has its own dup of the fd; close the Go-side conn so we - // don't accidentally keep it alive. - _ = conn.Close() - - // If the user Ctrl-C's, cancel the session gracefully and let Wait - // return so Close can run. - sigc := make(chan os.Signal, 1) - signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) - go func() { - sig := <-sigc - log.Printf("received %s; cancelling session", sig) - if err := bridge.Cancel(); err != nil { - log.Printf("cancel: %v", err) - } - }() - - waitErr := bridge.Wait() - switch { - case waitErr == nil: - log.Printf("session ended cleanly") - case errors.Is(waitErr, rdp.ErrSessionFailed): - log.Printf("session ended with error") - default: - log.Printf("wait: %v", waitErr) - } - - if err := bridge.Close(); err != nil { - log.Printf("close: %v", err) - } - - if waitErr != nil && !errors.Is(waitErr, rdp.ErrInvalidHandle) { - os.Exit(1) - } -} - -// splitHostPort accepts "host", "host:port", or "[ipv6]:port" and returns -// host + port, defaulting port to 3389 if omitted. -func splitHostPort(s string) (string, uint16, error) { - // If no colon, it's just a host. - if !strings.Contains(s, ":") { - return s, 3389, nil - } - host, portStr, err := net.SplitHostPort(s) - if err != nil { - return "", 0, err - } - port, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - return "", 0, fmt.Errorf("parse port %q: %w", portStr, err) - } - return host, uint16(port), nil -} From 85c23d276676ce45b4ca873cc30df328612b959d Mon Sep 17 00:00:00 2001 From: bernie-g Date: Fri, 24 Apr 2026 12:10:37 -0400 Subject: [PATCH 38/51] fix(pam-rdp): address PR bot review comments Three related fixes from bot review of feat/pam-rdp-mvp: uploader.go: add ResourceTypeRDP to the new-format filename regex tuple. pam-proxy.go calls NewSessionLogger unconditionally before the resource-type switch, so RDP sessions do produce files on disk named pam_session__windows_expires_.enc. Without windows in the regex tuple, parsing fell back to the legacy format, which greedy-matched _windows as the session ID. Result: RegisterSession and CleanupPAMSession lookups missed for every RDP session, leaving orphan files until the timestamp-based sweep. pam-proxy.go: gate ResourceTypeRDP in GetSupportedResourceTypes on rdp.IsSupported(). Stub-tier builds were advertising RDP capability to the backend, so the backend would route RDP sessions to a gateway that could only fail them with ErrRdpUnavailable. Now capability advertisement matches runtime capability. rdp-proxy.go: update the writeRDPFile doc comment. Previous text said the .rdp file persists on exit so users can re-open from Finder, but gracefulShutdown explicitly removes it (correctly, since the embedded loopback port is ephemeral). Comment now matches behavior. Also includes the earlier un-committed StartWithReadWriter doc comment expansion explaining the loopback-shim rationale. --- packages/pam/handlers/rdp/bridge_cgo.go | 16 +++++++++++++--- packages/pam/handlers/rdp/bridge_cgo_shared.go | 6 ++++++ packages/pam/handlers/rdp/bridge_stub.go | 4 ++++ packages/pam/local/rdp-proxy.go | 8 +++++--- packages/pam/pam-proxy.go | 10 ++++++++-- packages/pam/session/uploader.go | 7 ++----- 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/pam/handlers/rdp/bridge_cgo.go b/packages/pam/handlers/rdp/bridge_cgo.go index e7854e53..a8787551 100644 --- a/packages/pam/handlers/rdp/bridge_cgo.go +++ b/packages/pam/handlers/rdp/bridge_cgo.go @@ -64,9 +64,19 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, return &Bridge{handle: uint64(handle)}, nil } -// StartWithReadWriter creates a loopback TCP pair, hands the accepted -// kernel-backed end to the bridge, and pumps bytes between the other -// end and rw. Used for streams without a raw fd (e.g. *tls.Conn). +// StartWithReadWriter adapts an fd-less Go byte stream (e.g. *tls.Conn +// from the gateway's mTLS-wrapped virtual connection) to the bridge, +// which needs a real file descriptor because the Rust side uses tokio's +// TcpStream::from_raw_fd and does direct async I/O on the socket. +// +// Trick: open a loopback TCP pair. Hand one end's fd to the bridge (it +// thinks it has a real client). Keep the other end in Go and shuttle +// bytes between it and rw with two io.Copy goroutines. +// +// rw (e.g. *tls.Conn) <-io.Copy-> peer <-kernel loopback-> accepted (fd -> Rust bridge) +// +// Cost: two extra in-process copies and a loopback round-trip per byte. +// Negligible vs. the TLS + CredSSP work on either side. func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { diff --git a/packages/pam/handlers/rdp/bridge_cgo_shared.go b/packages/pam/handlers/rdp/bridge_cgo_shared.go index 49c4e6fd..4bf72d92 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_shared.go +++ b/packages/pam/handlers/rdp/bridge_cgo_shared.go @@ -47,3 +47,9 @@ func (b *Bridge) Close() error { } return nil } + +// IsSupported reports whether this build has a real RDP bridge. Used +// by the gateway to decide whether to advertise RDP in the capabilities +// response: a stub-build gateway that advertises support would route +// RDP sessions only to fail them at connect time. +func IsSupported() bool { return true } diff --git a/packages/pam/handlers/rdp/bridge_stub.go b/packages/pam/handlers/rdp/bridge_stub.go index bbdfefbb..37a3bcdf 100644 --- a/packages/pam/handlers/rdp/bridge_stub.go +++ b/packages/pam/handlers/rdp/bridge_stub.go @@ -28,3 +28,7 @@ func (p *RDPProxy) HandleConnection(_ context.Context, clientConn net.Conn) erro func (b *Bridge) Wait() error { return ErrRdpUnavailable } func (b *Bridge) Cancel() error { return ErrRdpUnavailable } func (b *Bridge) Close() error { return ErrRdpUnavailable } + +// IsSupported reports whether this build has a real RDP bridge. See the +// rdp-enabled counterpart in bridge_cgo_shared.go for details. +func IsSupported() bool { return false } diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index b1643c14..37e2504b 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -310,11 +310,13 @@ func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { } // writeRDPFile creates a .rdp file pointing at the local loopback -// listener. Files live under `~/.infisical/rdp/` — matching the CLI's +// listener. Files live under `~/.infisical/rdp/` to match the CLI's // existing convention for per-user state (alongside the login config // and update-check cache). Filename includes the session ID so -// concurrent sessions don't collide; the file is not deleted on exit -// so users can re-open a session a few times from Finder if they want. +// concurrent sessions don't collide. The file is removed on graceful +// shutdown (see gracefulShutdown) since the embedded loopback port +// becomes invalid as soon as the CLI exits; reopening the file later +// would just dial a dead port. // Falls back to the OS temp dir if the home directory can't be resolved. func writeRDPFile(listenPort int, sessionID string) (string, error) { filename := fmt.Sprintf("infisical-rdp-%s.rdp", sessionID) diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index 9cf0944f..ebb00726 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -46,7 +46,7 @@ type PAMCapabilitiesResponse struct { } func GetSupportedResourceTypes() []string { - return []string{ + types := []string{ session.ResourceTypePostgres, session.ResourceTypeMysql, session.ResourceTypeMssql, @@ -54,8 +54,14 @@ func GetSupportedResourceTypes() []string { session.ResourceTypeKubernetes, session.ResourceTypeRedis, session.ResourceTypeMongodb, - session.ResourceTypeRDP, } + // Only advertise RDP when the real bridge is compiled in. A stub + // build would otherwise accept RDP session routing and fail every + // session at connect time with ErrRdpUnavailable. + if rdp.IsSupported() { + types = append(types, session.ResourceTypeRDP) + } + return types } // HandlePAMCapabilities handles the capabilities request from the client diff --git a/packages/pam/session/uploader.go b/packages/pam/session/uploader.go index 59b45262..95f37644 100644 --- a/packages/pam/session/uploader.go +++ b/packages/pam/session/uploader.go @@ -31,10 +31,7 @@ const ( ResourceTypeSSH = "ssh" ResourceTypeKubernetes = "kubernetes" ResourceTypeMongodb = "mongodb" - // ResourceTypeRDP maps to the backend's PamResource.Windows enum; the - // string is "windows" (not "rdp") so the gateway's resource-type tag - // lines up with session metadata the backend already writes. - ResourceTypeRDP = "windows" + ResourceTypeRDP = "windows" ) type SessionFileInfo struct { @@ -75,7 +72,7 @@ func NewSessionUploader(httpClient *resty.Client, credentialsManager *Credential func ParseSessionFilename(filename string) (*SessionFileInfo, error) { // Try new format first: pam_session_{sessionID}_{resourceType}_expires_{timestamp}.enc // Build regex pattern using constants - resourceTypePattern := fmt.Sprintf("(%s|%s|%s|%s|%s|%s|%s)", ResourceTypeSSH, ResourceTypePostgres, ResourceTypeRedis, ResourceTypeMysql, ResourceTypeMssql, ResourceTypeKubernetes, ResourceTypeMongodb) + resourceTypePattern := fmt.Sprintf("(%s|%s|%s|%s|%s|%s|%s|%s)", ResourceTypeSSH, ResourceTypePostgres, ResourceTypeRedis, ResourceTypeMysql, ResourceTypeMssql, ResourceTypeKubernetes, ResourceTypeMongodb, ResourceTypeRDP) newFormatRegex := regexp.MustCompile(fmt.Sprintf(`^pam_session_(.+)_%s_expires_(\d+)\.enc$`, resourceTypePattern)) matches := newFormatRegex.FindStringSubmatch(filename) From 6916ff6c1bd809901640f75647710cd677ea4e74 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Wed, 29 Apr 2026 12:48:10 -0400 Subject: [PATCH 39/51] fix(pam-rdp): address PR review feedback - Rename ResourceTypeRDP -> ResourceTypeWindows so the constant name matches the value ("windows") and leaves room for non-RDP Windows session protocols (e.g. WinRM) without overloading the resource type - Send the full RDP proxy startup banner to stderr so headers and values stay together when stdout is redirected - Tighten RDP target port validation to reject 0 (unset int zero-value) instead of dialing and failing later --- packages/pam/local/rdp-proxy.go | 22 +++++++++++----------- packages/pam/pam-proxy.go | 6 +++--- packages/pam/session/uploader.go | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index 37e2504b..c68be512 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -101,21 +101,21 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec } log.Info().Msgf("RDP proxy server listening on port %d", proxy.port) - fmt.Printf("\n") - fmt.Printf("**********************************************************************\n") - fmt.Printf(" RDP Proxy Session Started! \n") - fmt.Printf("----------------------------------------------------------------------\n") - fmt.Printf("Resource: %s\n", accessParams.ResourceName) - fmt.Printf("Account: %s\n", accessParams.AccountName) - fmt.Printf("\n") - fmt.Printf("Connect your RDP client to:\n") + util.PrintfStderr("\n") + util.PrintfStderr("**********************************************************************\n") + util.PrintfStderr(" RDP Proxy Session Started! \n") + util.PrintfStderr("----------------------------------------------------------------------\n") + util.PrintfStderr("Resource: %s\n", accessParams.ResourceName) + util.PrintfStderr("Account: %s\n", accessParams.AccountName) + util.PrintfStderr("\n") + util.PrintfStderr("Connect your RDP client to:\n") util.PrintfStderr(" 127.0.0.1:%d\n", proxy.port) - fmt.Printf("With credentials:\n") + util.PrintfStderr("With credentials:\n") util.PrintfStderr(" username: %s\n", rdp.AcceptorUsername) util.PrintfStderr(" password: %s\n", rdp.AcceptorPassword) if proxy.rdpFilePath != "" { - fmt.Printf("\n") - fmt.Printf("Generated .rdp file:\n") + util.PrintfStderr("\n") + util.PrintfStderr("Generated .rdp file:\n") util.PrintfStderr(" %s\n", proxy.rdpFilePath) } util.PrintfStderr("\n") diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index ebb00726..0cd6c29e 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -59,7 +59,7 @@ func GetSupportedResourceTypes() []string { // build would otherwise accept RDP session routing and fail every // session at connect time with ErrRdpUnavailable. if rdp.IsSupported() { - types = append(types, session.ResourceTypeRDP) + types = append(types, session.ResourceTypeWindows) } return types } @@ -413,8 +413,8 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo } return proxy.HandleConnection(ctx, conn, sessionLogger) - case session.ResourceTypeRDP: - if credentials.Port < 0 || credentials.Port > 65535 { + case session.ResourceTypeWindows: + if credentials.Port <= 0 || credentials.Port > 65535 { return fmt.Errorf("rdp: target port %d out of range", credentials.Port) } rdpConfig := rdp.RDPProxyConfig{ diff --git a/packages/pam/session/uploader.go b/packages/pam/session/uploader.go index 95f37644..798090b6 100644 --- a/packages/pam/session/uploader.go +++ b/packages/pam/session/uploader.go @@ -31,7 +31,7 @@ const ( ResourceTypeSSH = "ssh" ResourceTypeKubernetes = "kubernetes" ResourceTypeMongodb = "mongodb" - ResourceTypeRDP = "windows" + ResourceTypeWindows = "windows" ) type SessionFileInfo struct { @@ -72,7 +72,7 @@ func NewSessionUploader(httpClient *resty.Client, credentialsManager *Credential func ParseSessionFilename(filename string) (*SessionFileInfo, error) { // Try new format first: pam_session_{sessionID}_{resourceType}_expires_{timestamp}.enc // Build regex pattern using constants - resourceTypePattern := fmt.Sprintf("(%s|%s|%s|%s|%s|%s|%s|%s)", ResourceTypeSSH, ResourceTypePostgres, ResourceTypeRedis, ResourceTypeMysql, ResourceTypeMssql, ResourceTypeKubernetes, ResourceTypeMongodb, ResourceTypeRDP) + resourceTypePattern := fmt.Sprintf("(%s|%s|%s|%s|%s|%s|%s|%s)", ResourceTypeSSH, ResourceTypePostgres, ResourceTypeRedis, ResourceTypeMysql, ResourceTypeMssql, ResourceTypeKubernetes, ResourceTypeMongodb, ResourceTypeWindows) newFormatRegex := regexp.MustCompile(fmt.Sprintf(`^pam_session_(.+)_%s_expires_(\d+)\.enc$`, resourceTypePattern)) matches := newFormatRegex.FindStringSubmatch(filename) From e5d2acbaf1349c4e5fd251f21a7ce0a2cb9d7d7a Mon Sep 17 00:00:00 2001 From: bernie-g Date: Fri, 1 May 2026 09:34:46 -0400 Subject: [PATCH 40/51] docs(pam-rdp): add README for Rust bridge setup --- packages/pam/handlers/rdp/native/README.md | 72 ++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/pam/handlers/rdp/native/README.md diff --git a/packages/pam/handlers/rdp/native/README.md b/packages/pam/handlers/rdp/native/README.md new file mode 100644 index 00000000..83228343 --- /dev/null +++ b/packages/pam/handlers/rdp/native/README.md @@ -0,0 +1,72 @@ +# Infisical RDP Bridge + +Rust static library that provides the RDP MITM bridge for Infisical PAM Windows/RDP support. Uses [IronRDP](https://github.com/Devolutions/IronRDP) for protocol handling. + +## Prerequisites + +- Rust 1.95.0 (automatically selected via `rust-toolchain.toml`) +- For cross-compilation: [cross](https://github.com/cross-rs/cross) + +```bash +# Install Rust if not already installed +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# The rust-toolchain.toml will auto-install 1.95.0 on first build, +# or install manually: +rustup install 1.95.0 +``` + +## Building + +### Local development (macOS/Linux) + +```bash +cd packages/pam/handlers/rdp/native +cargo build --release +``` + +The static library is output to `target/release/libinfisical_rdp_bridge.a`. + +### Cross-compilation + +For Linux targets from any host: + +```bash +cargo install cross --locked --version 0.2.5 +cross build --release --target x86_64-unknown-linux-gnu +``` + +Supported targets: +- `x86_64-unknown-linux-gnu` +- `aarch64-unknown-linux-gnu` +- `x86_64-apple-darwin` +- `aarch64-apple-darwin` +- `x86_64-pc-windows-gnu` + +## Building the CLI with RDP support + +The Go CLI links against the static library via CGO. Build with the `rdp` tag: + +```bash +cd /path/to/cli +go build -tags rdp -o infisical ./cmd/infisical +``` + +Without `-tags rdp`, the CLI uses a stub that returns `ErrRdpUnavailable` for all RDP operations. + +## Verifying the build + +```bash +./infisical pam rdp --help +``` + +If you see help output, the bridge linked correctly. If you see "rdp bridge: not available in this build", the stub is active (missing `-tags rdp` or missing static library). + +## Architecture + +- `src/lib.rs` - Crate entry point, re-exports +- `src/ffi.rs` - C ABI exports (see `include/rdp_bridge.h`) +- `src/bridge.rs` - MITM logic: accepts client connection, injects credentials, connects to target +- `src/config.rs` - TLS and connection configuration + +The bridge runs async Tokio tasks but exposes a blocking C ABI. The Go side calls `rdp_bridge_start_*` to spawn the session, `rdp_bridge_wait` to block until completion, and `rdp_bridge_free` to release resources. From 4f0e3e296f7d5f47bed4ae4e19420ecde540eb1a Mon Sep 17 00:00:00 2001 From: bernie-g Date: Fri, 1 May 2026 16:06:23 -0400 Subject: [PATCH 41/51] refactor(pam-rdp): rename bridge_cgo.go to bridge_cgo_unix.go Consistent naming with bridge_cgo_windows.go. --- packages/pam/handlers/rdp/{bridge_cgo.go => bridge_cgo_unix.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/pam/handlers/rdp/{bridge_cgo.go => bridge_cgo_unix.go} (100%) diff --git a/packages/pam/handlers/rdp/bridge_cgo.go b/packages/pam/handlers/rdp/bridge_cgo_unix.go similarity index 100% rename from packages/pam/handlers/rdp/bridge_cgo.go rename to packages/pam/handlers/rdp/bridge_cgo_unix.go From 7675e95114bd179f48fccc90ccfca07946918d00 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Fri, 1 May 2026 16:26:52 -0400 Subject: [PATCH 42/51] feat(pam-rdp): use empty password for acceptor credentials Simplifies the user experience by only requiring username "infisical" with no password. Removes clipboard copy logic since there's nothing to paste. --- packages/pam/handlers/rdp/bridge.go | 2 +- .../pam/handlers/rdp/native/src/bridge.rs | 2 +- packages/pam/local/rdp-proxy.go | 49 +------------------ 3 files changed, 3 insertions(+), 50 deletions(-) diff --git a/packages/pam/handlers/rdp/bridge.go b/packages/pam/handlers/rdp/bridge.go index 9069f00a..6c696a2a 100644 --- a/packages/pam/handlers/rdp/bridge.go +++ b/packages/pam/handlers/rdp/bridge.go @@ -15,7 +15,7 @@ var ( // the Rust crate. Real authn happens upstream (Infisical + gateway). const ( AcceptorUsername = "infisical" - AcceptorPassword = "infisical" + AcceptorPassword = "" ) type Bridge struct { diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index b8db0850..3622ada8 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -25,7 +25,7 @@ use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; // Must match what the CLI bakes into the generated .rdp file. pub const ACCEPTOR_USERNAME: &str = "infisical"; -pub const ACCEPTOR_PASSWORD: &str = "infisical"; +pub const ACCEPTOR_PASSWORD: &str = ""; pub struct TargetEndpoint { pub host: String, diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index c68be512..483f911c 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -112,7 +112,7 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec util.PrintfStderr(" 127.0.0.1:%d\n", proxy.port) util.PrintfStderr("With credentials:\n") util.PrintfStderr(" username: %s\n", rdp.AcceptorUsername) - util.PrintfStderr(" password: %s\n", rdp.AcceptorPassword) + util.PrintfStderr(" password: (leave blank)\n") if proxy.rdpFilePath != "" { util.PrintfStderr("\n") util.PrintfStderr("Generated .rdp file:\n") @@ -123,16 +123,6 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec util.PrintfStderr("**********************************************************************\n") util.PrintfStderr("\n") - // The .rdp file format has no portable way to embed a plaintext password - // (mstsc's `password 51:b:` is Windows-DPAPI-encrypted; Mac / freerdp - // clients ignore any password field). Put the fixed acceptor password - // on the clipboard so the user just pastes it when the client prompts. - if err := copyToClipboard(rdp.AcceptorPassword); err != nil { - log.Debug().Err(err).Msg("Could not copy password to clipboard; type it manually") - } else { - util.PrintfStderr("(Password copied to clipboard.)\n\n") - } - if !noLaunch && proxy.rdpFilePath != "" { if err := launchRDPClient(proxy.rdpFilePath); err != nil { log.Warn().Err(err).Msg("Failed to auto-launch RDP client; connect manually using the details above") @@ -368,40 +358,3 @@ func launchRDPClient(rdpFilePath string) error { } return cmd.Start() } - -// copyToClipboard pipes `text` into the OS clipboard via the platform's -// standard CLI helper. Failure is non-fatal; the caller logs and moves on. -func copyToClipboard(text string) error { - var cmd *exec.Cmd - switch runtime.GOOS { - case "darwin": - cmd = exec.Command("pbcopy") - case "windows": - cmd = exec.Command("clip") - default: - // Try xclip first, then xsel. Neither is guaranteed to exist on - // headless servers, which is fine: we just return the error and - // the caller logs at debug level. - if _, err := exec.LookPath("xclip"); err == nil { - cmd = exec.Command("xclip", "-selection", "clipboard") - } else if _, err := exec.LookPath("xsel"); err == nil { - cmd = exec.Command("xsel", "--clipboard", "--input") - } else { - return fmt.Errorf("no clipboard tool found (install xclip or xsel)") - } - } - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - if err := cmd.Start(); err != nil { - return err - } - if _, err := stdin.Write([]byte(text)); err != nil { - return err - } - if err := stdin.Close(); err != nil { - return err - } - return cmd.Wait() -} From c2b68dac9390fc5059f4eca57b602641ce68a48a Mon Sep 17 00:00:00 2001 From: bernie-g Date: Fri, 1 May 2026 22:07:58 -0400 Subject: [PATCH 43/51] feat(pam-rdp): use account name as RDP acceptor username The bridge's CredSSP acceptor now expects the user to type the PAM account name (e.g. "test") rather than a fixed "infisical" placeholder, so users see a familiar value when their RDP client prompts. Plumbs accountName from the backend credentials response through the gateway and into the Rust bridge as a new acceptor_username FFI param, distinct from the actual Windows username injected to the target. Falls back to the target username if accountName is empty for older backends. --- packages/api/model.go | 1 + packages/pam/handlers/rdp/bridge.go | 7 ------- packages/pam/handlers/rdp/bridge_cgo_unix.go | 14 +++++++++----- .../pam/handlers/rdp/bridge_cgo_windows.go | 14 +++++++++----- packages/pam/handlers/rdp/bridge_stub.go | 4 ++-- .../handlers/rdp/native/include/rdp_bridge.h | 2 ++ packages/pam/handlers/rdp/native/src/bridge.rs | 18 +++++++++++++----- packages/pam/handlers/rdp/native/src/ffi.rs | 16 ++++++++++++++-- packages/pam/handlers/rdp/proxy.go | 5 ++++- packages/pam/local/rdp-proxy.go | 9 ++++----- packages/pam/pam-proxy.go | 13 +++++++------ packages/pam/session/credentials.go | 2 ++ 12 files changed, 67 insertions(+), 38 deletions(-) diff --git a/packages/api/model.go b/packages/api/model.go index 4000da29..4c48629c 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -890,6 +890,7 @@ type PAMSessionCredentials struct { ServiceAccountToken string `json:"serviceAccountToken,omitempty"` ServiceAccountName string `json:"serviceAccountName,omitempty"` Namespace string `json:"namespace,omitempty"` + AccountName string `json:"accountName,omitempty"` } type MFASessionStatus string diff --git a/packages/pam/handlers/rdp/bridge.go b/packages/pam/handlers/rdp/bridge.go index 6c696a2a..f582c864 100644 --- a/packages/pam/handlers/rdp/bridge.go +++ b/packages/pam/handlers/rdp/bridge.go @@ -10,13 +10,6 @@ var ( ErrSessionFailed = errors.New("rdp bridge: session ended with error") ) -// Fixed placeholder credentials the RDP client presents to the acceptor -// side of the bridge. Must match ACCEPTOR_USERNAME / ACCEPTOR_PASSWORD in -// the Rust crate. Real authn happens upstream (Infisical + gateway). -const ( - AcceptorUsername = "infisical" - AcceptorPassword = "" -) type Bridge struct { handle uint64 diff --git a/packages/pam/handlers/rdp/bridge_cgo_unix.go b/packages/pam/handlers/rdp/bridge_cgo_unix.go index a8787551..7f1973fa 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_unix.go +++ b/packages/pam/handlers/rdp/bridge_cgo_unix.go @@ -24,16 +24,16 @@ import ( // StartWithConn hands an independent dup of conn's fd to the bridge. // For TLS-wrapped or otherwise non-fd-backed conns, use StartWithReadWriter. -func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { +func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { dupFd, err := dupConnFD(conn) if err != nil { return nil, fmt.Errorf("rdp bridge: dup client fd: %w", err) } - return startWithDupedFD(dupFd, targetHost, targetPort, username, password) + return startWithDupedFD(dupFd, targetHost, targetPort, username, password, acceptorUsername) } // Ownership of dupFd transfers to Rust on success; we close it on failure. -func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { +func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { success := false defer func() { if !success { @@ -47,6 +47,8 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, defer C.free(unsafe.Pointer(cUser)) cPass := C.CString(password) defer C.free(unsafe.Pointer(cPass)) + cAcceptorUser := C.CString(acceptorUsername) + defer C.free(unsafe.Pointer(cAcceptorUser)) var handle C.uint64_t rc := C.rdp_bridge_start_unix_fd( @@ -55,6 +57,7 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, C.uint16_t(targetPort), cUser, cPass, + cAcceptorUser, &handle, ) if rc != C.RDP_BRIDGE_OK { @@ -77,7 +80,7 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, // // Cost: two extra in-process copies and a loopback round-trip per byte. // Negligible vs. the TLS + CredSSP work on either side. -func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { +func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) @@ -112,7 +115,7 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return nil, fmt.Errorf("rdp bridge: dup accepted fd: %w", err) } - bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password) + bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password, acceptorUsername) if err != nil { _ = peer.Close() return nil, err @@ -168,6 +171,7 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er p.config.TargetPort, p.config.InjectUsername, p.config.InjectPassword, + p.config.AcceptorUsername, ) if err != nil { return fmt.Errorf("rdp proxy: start bridge: %w", err) diff --git a/packages/pam/handlers/rdp/bridge_cgo_windows.go b/packages/pam/handlers/rdp/bridge_cgo_windows.go index 5d80729c..498f3523 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_windows.go +++ b/packages/pam/handlers/rdp/bridge_cgo_windows.go @@ -23,15 +23,15 @@ import ( "golang.org/x/sys/windows" ) -func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { +func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { dupSocket, err := dupConnSocket(conn) if err != nil { return nil, fmt.Errorf("rdp bridge: dup client socket: %w", err) } - return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password) + return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, acceptorUsername) } -func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { +func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { success := false defer func() { if !success { @@ -45,6 +45,8 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor defer C.free(unsafe.Pointer(cUser)) cPass := C.CString(password) defer C.free(unsafe.Pointer(cPass)) + cAcceptorUser := C.CString(acceptorUsername) + defer C.free(unsafe.Pointer(cAcceptorUser)) var handle C.uint64_t rc := C.rdp_bridge_start_windows_socket( @@ -53,6 +55,7 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor C.uint16_t(targetPort), cUser, cPass, + cAcceptorUser, &handle, ) if rc != C.RDP_BRIDGE_OK { @@ -62,7 +65,7 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor return &Bridge{handle: uint64(handle)}, nil } -func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { +func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) @@ -97,7 +100,7 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return nil, fmt.Errorf("rdp bridge: dup accepted socket: %w", err) } - bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password) + bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, acceptorUsername) if err != nil { _ = peer.Close() return nil, err @@ -165,6 +168,7 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er p.config.TargetPort, p.config.InjectUsername, p.config.InjectPassword, + p.config.AcceptorUsername, ) if err != nil { return fmt.Errorf("rdp proxy: start bridge: %w", err) diff --git a/packages/pam/handlers/rdp/bridge_stub.go b/packages/pam/handlers/rdp/bridge_stub.go index 37a3bcdf..2c488000 100644 --- a/packages/pam/handlers/rdp/bridge_stub.go +++ b/packages/pam/handlers/rdp/bridge_stub.go @@ -12,11 +12,11 @@ import ( // where the Rust bridge isn't compiled. All entry points return // ErrRdpUnavailable. -func StartWithConn(_ net.Conn, _ string, _ uint16, _, _ string) (*Bridge, error) { +func StartWithConn(_ net.Conn, _ string, _ uint16, _, _, _ string) (*Bridge, error) { return nil, ErrRdpUnavailable } -func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _ string) (*Bridge, error) { +func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _, _ string) (*Bridge, error) { return nil, ErrRdpUnavailable } diff --git a/packages/pam/handlers/rdp/native/include/rdp_bridge.h b/packages/pam/handlers/rdp/native/include/rdp_bridge.h index 83088768..ac12c008 100644 --- a/packages/pam/handlers/rdp/native/include/rdp_bridge.h +++ b/packages/pam/handlers/rdp/native/include/rdp_bridge.h @@ -27,6 +27,7 @@ int32_t rdp_bridge_start_unix_fd( uint16_t target_port, const char *username, const char *password, + const char *acceptor_username, uint64_t *out_handle ); #endif @@ -38,6 +39,7 @@ int32_t rdp_bridge_start_windows_socket( uint16_t target_port, const char *username, const char *password, + const char *acceptor_username, uint64_t *out_handle ); #endif diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 3622ada8..0fce3ab7 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -23,8 +23,7 @@ use tracing::info; use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; -// Must match what the CLI bakes into the generated .rdp file. -pub const ACCEPTOR_USERNAME: &str = "infisical"; +// Empty password for acceptor - the username comes from target credentials. pub const ACCEPTOR_PASSWORD: &str = ""; pub struct TargetEndpoint { @@ -32,6 +31,10 @@ pub struct TargetEndpoint { pub port: u16, pub username: String, pub password: String, + // Username the user types into their RDP client. Distinct from + // `username` (the real Windows account injected to the target). + // Falls back to `username` if empty. + pub acceptor_username: String, } pub async fn run_mitm( @@ -53,8 +56,13 @@ async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result // 0.23 needs an explicit provider when more than one is compiled in. let _ = rustls::crypto::ring::default_provider().install_default(); + let acceptor_username = if target.acceptor_username.is_empty() { + target.username.clone() + } else { + target.acceptor_username.clone() + }; let (acceptor_output, connector_output) = - tokio::try_join!(run_acceptor_half(client_tcp), run_connector_half(target))?; + tokio::try_join!(run_acceptor_half(client_tcp, acceptor_username), run_connector_half(target))?; let (mut client_stream, client_leftover) = acceptor_output; let (mut target_stream, target_leftover) = connector_output; @@ -97,14 +105,14 @@ fn is_unexpected_eof(err: &std::io::Error) -> bool { err.kind() == std::io::ErrorKind::UnexpectedEof } -async fn run_acceptor_half(client_tcp: TcpStream) -> Result<(ErasedStream, bytes::BytesMut)> { +async fn run_acceptor_half(client_tcp: TcpStream, username: String) -> Result<(ErasedStream, bytes::BytesMut)> { let (server_tls, acceptor_public_key) = build_acceptor_tls().context("build acceptor TLS config")?; let server_tls = Arc::new(server_tls); let acceptor_framed = ironrdp_tokio::TokioFramed::new(client_tcp); let expected_creds = AcceptorCredentials { - username: ACCEPTOR_USERNAME.to_owned(), + username, password: ACCEPTOR_PASSWORD.to_owned(), domain: None, }; diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index ecef7782..ca5b465f 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -59,6 +59,7 @@ fn spawn_session( port: u16, username: String, password: String, + acceptor_username: String, ) -> anyhow::Result { client_tcp.set_nonblocking(true)?; let cancel = CancellationToken::new(); @@ -77,6 +78,7 @@ fn spawn_session( port, username, password, + acceptor_username, }; run_mitm(client, endpoint, cancel_for_thread).await }) @@ -100,6 +102,7 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( target_port: u16, username: *const c_char, password: *const c_char, + acceptor_username: *const c_char, out_handle: *mut u64, ) -> i32 { if out_handle.is_null() { @@ -117,11 +120,15 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( Some(v) => v, None => return RDP_BRIDGE_BAD_ARG, }; + let acceptor_username = match unsafe { c_str_to_owned(acceptor_username) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; use std::os::unix::io::FromRawFd; let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; - match spawn_session(client_tcp, host, target_port, username, password) { + match spawn_session(client_tcp, host, target_port, username, password, acceptor_username) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK @@ -144,6 +151,7 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( target_port: u16, username: *const c_char, password: *const c_char, + acceptor_username: *const c_char, out_handle: *mut u64, ) -> i32 { if out_handle.is_null() { @@ -161,11 +169,15 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( Some(v) => v, None => return RDP_BRIDGE_BAD_ARG, }; + let acceptor_username = match unsafe { c_str_to_owned(acceptor_username) } { + Some(v) => v, + None => return RDP_BRIDGE_BAD_ARG, + }; use std::os::windows::io::{FromRawSocket, RawSocket}; let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) }; - match spawn_session(client_tcp, host, target_port, username, password) { + match spawn_session(client_tcp, host, target_port, username, password, acceptor_username) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK diff --git a/packages/pam/handlers/rdp/proxy.go b/packages/pam/handlers/rdp/proxy.go index e113902a..44fd7b77 100644 --- a/packages/pam/handlers/rdp/proxy.go +++ b/packages/pam/handlers/rdp/proxy.go @@ -9,7 +9,10 @@ type RDPProxyConfig struct { TargetPort uint16 InjectUsername string InjectPassword string - SessionID string + // Username the user types into their RDP client; the bridge's CredSSP + // acceptor expects this value. Falls back to InjectUsername if empty. + AcceptorUsername string + SessionID string // Retained for API symmetry with other PAM handlers; not yet written // through (no RDP session recording in this MVP). SessionLogger session.SessionLogger diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index 483f911c..781b08e2 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -13,7 +13,6 @@ import ( "syscall" "time" - "github.com/Infisical/infisical-merge/packages/pam/handlers/rdp" "github.com/Infisical/infisical-merge/packages/util" "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" @@ -93,7 +92,7 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec return } - rdpFilePath, err := writeRDPFile(proxy.port, pamResponse.SessionId) + rdpFilePath, err := writeRDPFile(proxy.port, pamResponse.SessionId, accessParams.AccountName) if err != nil { log.Warn().Err(err).Msg("Failed to write .rdp file; proxy still running") } else { @@ -111,7 +110,7 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec util.PrintfStderr("Connect your RDP client to:\n") util.PrintfStderr(" 127.0.0.1:%d\n", proxy.port) util.PrintfStderr("With credentials:\n") - util.PrintfStderr(" username: %s\n", rdp.AcceptorUsername) + util.PrintfStderr(" username: %s\n", accessParams.AccountName) util.PrintfStderr(" password: (leave blank)\n") if proxy.rdpFilePath != "" { util.PrintfStderr("\n") @@ -308,7 +307,7 @@ func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { // becomes invalid as soon as the CLI exits; reopening the file later // would just dial a dead port. // Falls back to the OS temp dir if the home directory can't be resolved. -func writeRDPFile(listenPort int, sessionID string) (string, error) { +func writeRDPFile(listenPort int, sessionID string, username string) (string, error) { filename := fmt.Sprintf("infisical-rdp-%s.rdp", sessionID) dir, err := rdpFileDir() @@ -324,7 +323,7 @@ func writeRDPFile(listenPort int, sessionID string) (string, error) { "full address:s:127.0.0.1:%d\r\n"+ "username:s:%s\r\n", listenPort, - rdp.AcceptorUsername, + username, ) if err := os.WriteFile(path, []byte(content), 0o600); err != nil { diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index 0cd6c29e..9129fcac 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -418,12 +418,13 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo return fmt.Errorf("rdp: target port %d out of range", credentials.Port) } rdpConfig := rdp.RDPProxyConfig{ - TargetHost: credentials.Host, - TargetPort: uint16(credentials.Port), - InjectUsername: credentials.Username, - InjectPassword: credentials.Password, - SessionID: pamConfig.SessionId, - SessionLogger: sessionLogger, + TargetHost: credentials.Host, + TargetPort: uint16(credentials.Port), + InjectUsername: credentials.Username, + InjectPassword: credentials.Password, + AcceptorUsername: credentials.AccountName, + SessionID: pamConfig.SessionId, + SessionLogger: sessionLogger, } proxy := rdp.NewRDPProxy(rdpConfig) log.Info(). diff --git a/packages/pam/session/credentials.go b/packages/pam/session/credentials.go index 008f9647..2b175f98 100644 --- a/packages/pam/session/credentials.go +++ b/packages/pam/session/credentials.go @@ -27,6 +27,7 @@ type PAMCredentials struct { ServiceAccountToken string ServiceAccountName string Namespace string + AccountName string PolicyRules *api.PAMPolicyRules } @@ -105,6 +106,7 @@ func (cm *CredentialsManager) GetPAMSessionCredentials(sessionId string, expiryT ServiceAccountToken: response.Credentials.ServiceAccountToken, ServiceAccountName: response.Credentials.ServiceAccountName, Namespace: response.Credentials.Namespace, + AccountName: response.Credentials.AccountName, PolicyRules: response.PolicyRules, } From e350e472bf454a7e6699a15831a4622ff9f8218f Mon Sep 17 00:00:00 2001 From: bernie-g Date: Mon, 4 May 2026 13:31:40 -0400 Subject: [PATCH 44/51] feat(pam-rdp): strip virtual channels from MCS Connect Initial Intercepts the client's MCS Connect Initial PDU after CredSSP and removes all virtual channels declared in its Client Network Data block before forwarding to the target. Blocks clipboard, drive, printer, audio, smart card, USB, and dynamic-vchan-based features at the gateway level. Mouse, keyboard, and screen ride the implicit MCS I/O channel and are unaffected. --- .../pam/handlers/rdp/native/src/bridge.rs | 103 ++++++++++++++++-- 1 file changed, 94 insertions(+), 9 deletions(-) diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 0fce3ab7..b6d4f100 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -10,13 +10,16 @@ use ironrdp_acceptor::{Acceptor, BeginResult}; use ironrdp_connector::credssp::{CredsspSequence, KerberosConfig}; use ironrdp_connector::sspi::credssp::ClientState; use ironrdp_connector::sspi::generator::GeneratorState; -use ironrdp_connector::{ClientConnector, ClientConnectorState}; -use ironrdp_pdu::ironrdp_core::WriteBuf; +use ironrdp_connector::{encode_x224_packet, ClientConnector, ClientConnectorState}; +use ironrdp_pdu::gcc::ConferenceCreateRequest; +use ironrdp_pdu::ironrdp_core::{decode, WriteBuf}; +use ironrdp_pdu::mcs::ConnectInitial; use ironrdp_pdu::nego::SecurityProtocol; use ironrdp_pdu::rdp::client_info::Credentials as AcceptorCredentials; +use ironrdp_pdu::x224::{X224Data, X224}; use ironrdp_tokio::reqwest::ReqwestNetworkClient; use ironrdp_tokio::{FramedWrite, NetworkClient}; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; use tokio_util::sync::CancellationToken; use tracing::info; @@ -67,12 +70,14 @@ async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result let (mut client_stream, client_leftover) = acceptor_output; let (mut target_stream, target_leftover) = connector_output; - if !client_leftover.is_empty() { - target_stream - .write_all(&client_leftover) - .await - .context("flush client leftover to target")?; - } + // Strip virtual channels (clipboard, drives, audio, USB, etc.) from the + // client's MCS Connect Initial before forwarding. Mouse/keyboard/screen + // ride the implicit MCS I/O channel, not virtual channels, so they're + // unaffected. + filter_client_mcs_connect_initial(&mut client_stream, &mut target_stream, client_leftover) + .await + .context("filter client MCS Connect Initial")?; + if !target_leftover.is_empty() { client_stream .write_all(&target_leftover) @@ -105,6 +110,86 @@ fn is_unexpected_eof(err: &std::io::Error) -> bool { err.kind() == std::io::ErrorKind::UnexpectedEof } +// Reads the client's MCS Connect Initial PDU, removes any virtual channels +// declared in its Client Network Data block, and forwards the rewritten PDU +// to the target. Any bytes after the PDU (rare; PDUs typically arrive one at +// a time at this stage) are forwarded unchanged. +async fn filter_client_mcs_connect_initial( + client_stream: &mut ErasedStream, + target_stream: &mut ErasedStream, + leftover: bytes::BytesMut, +) -> Result<()> { + let mut buf: Vec = leftover.to_vec(); + + // TPKT header: 0x03 0x00 [len_hi] [len_lo], len includes the header. + while buf.len() < 4 { + let mut chunk = [0u8; 1024]; + let n = client_stream + .read(&mut chunk) + .await + .context("read TPKT header")?; + if n == 0 { + anyhow::bail!("EOF before TPKT header for MCS Connect Initial"); + } + buf.extend_from_slice(&chunk[..n]); + } + if buf[0] != 0x03 { + anyhow::bail!("expected TPKT version 3, got 0x{:02x}", buf[0]); + } + let total_len = usize::from(u16::from_be_bytes([buf[2], buf[3]])); + + while buf.len() < total_len { + let mut chunk = vec![0u8; (total_len - buf.len()).max(1024)]; + let n = client_stream + .read(&mut chunk) + .await + .context("read MCS Connect Initial body")?; + if n == 0 { + anyhow::bail!("EOF mid MCS Connect Initial"); + } + buf.extend_from_slice(&chunk[..n]); + } + + let pdu_bytes = &buf[..total_len]; + let extra_bytes: Vec = buf[total_len..].to_vec(); + + let x224 = decode::>>(pdu_bytes) + .map_err(|e| anyhow::anyhow!("decode X.224 wrapper: {e:?}"))?; + let mut connect_initial = decode::(x224.0.data.as_ref()) + .map_err(|e| anyhow::anyhow!("decode MCS Connect Initial: {e:?}"))?; + + let mut gcc_blocks = connect_initial.conference_create_request.into_gcc_blocks(); + if let Some(network) = gcc_blocks.network.as_mut() { + let stripped: Vec = network + .channels + .iter() + .map(|c| c.name.as_str().unwrap_or("?").to_owned()) + .collect(); + if !stripped.is_empty() { + info!(?stripped, "stripped virtual channels from MCS Connect Initial"); + network.channels.clear(); + } + } + connect_initial.conference_create_request = ConferenceCreateRequest::new(gcc_blocks) + .map_err(|e| anyhow::anyhow!("rebuild ConferenceCreateRequest: {e:?}"))?; + + let mut out = WriteBuf::new(); + encode_x224_packet(&connect_initial, &mut out) + .map_err(|e| anyhow::anyhow!("re-encode MCS Connect Initial: {e:?}"))?; + + target_stream + .write_all(out.filled()) + .await + .context("write filtered MCS Connect Initial to target")?; + if !extra_bytes.is_empty() { + target_stream + .write_all(&extra_bytes) + .await + .context("forward bytes trailing MCS Connect Initial")?; + } + Ok(()) +} + async fn run_acceptor_half(client_tcp: TcpStream, username: String) -> Result<(ErasedStream, bytes::BytesMut)> { let (server_tls, acceptor_public_key) = build_acceptor_tls().context("build acceptor TLS config")?; From 8ffb72b44944846ed408e4b1792cc6e90510b36c Mon Sep 17 00:00:00 2001 From: bernie-g Date: Mon, 4 May 2026 13:52:57 -0400 Subject: [PATCH 45/51] refactor(pam-rdp): consolidate HandleConnection + cargo fmt Moves the platform-agnostic HandleConnection method out of the unix/windows cgo files into bridge_cgo_shared.go. Also applies cargo fmt to bridge.rs and ffi.rs to satisfy CI. --- .../pam/handlers/rdp/bridge_cgo_shared.go | 44 ++++++++++++++++++- packages/pam/handlers/rdp/bridge_cgo_unix.go | 39 ---------------- .../pam/handlers/rdp/bridge_cgo_windows.go | 39 ---------------- .../pam/handlers/rdp/native/src/bridge.rs | 16 +++++-- packages/pam/handlers/rdp/native/src/ffi.rs | 18 +++++++- 5 files changed, 71 insertions(+), 85 deletions(-) diff --git a/packages/pam/handlers/rdp/bridge_cgo_shared.go b/packages/pam/handlers/rdp/bridge_cgo_shared.go index 4bf72d92..042635a2 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_shared.go +++ b/packages/pam/handlers/rdp/bridge_cgo_shared.go @@ -9,7 +9,49 @@ package rdp */ import "C" -import "fmt" +import ( + "context" + "errors" + "fmt" + "net" +) + +func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { + defer clientConn.Close() + if p.config.SessionLogger != nil { + defer func() { + _ = p.config.SessionLogger.Close() + }() + } + + bridge, err := StartWithReadWriter( + clientConn, + p.config.TargetHost, + p.config.TargetPort, + p.config.InjectUsername, + p.config.InjectPassword, + p.config.AcceptorUsername, + ) + if err != nil { + return fmt.Errorf("rdp proxy: start bridge: %w", err) + } + defer bridge.Close() + + waitErr := make(chan error, 1) + go func() { waitErr <- bridge.Wait() }() + + select { + case err := <-waitErr: + if err != nil && !errors.Is(err, ErrInvalidHandle) { + return fmt.Errorf("rdp proxy: session: %w", err) + } + return nil + case <-ctx.Done(): + _ = bridge.Cancel() + <-waitErr + return ctx.Err() + } +} // Wait blocks until the session ends. Idempotent. func (b *Bridge) Wait() error { diff --git a/packages/pam/handlers/rdp/bridge_cgo_unix.go b/packages/pam/handlers/rdp/bridge_cgo_unix.go index 7f1973fa..4420ef18 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_unix.go +++ b/packages/pam/handlers/rdp/bridge_cgo_unix.go @@ -13,8 +13,6 @@ package rdp import "C" import ( - "context" - "errors" "fmt" "io" "net" @@ -156,40 +154,3 @@ func dupConnFD(conn net.Conn) (int, error) { } return dup, nil } - -func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { - defer clientConn.Close() - if p.config.SessionLogger != nil { - defer func() { - _ = p.config.SessionLogger.Close() - }() - } - - bridge, err := StartWithReadWriter( - clientConn, - p.config.TargetHost, - p.config.TargetPort, - p.config.InjectUsername, - p.config.InjectPassword, - p.config.AcceptorUsername, - ) - if err != nil { - return fmt.Errorf("rdp proxy: start bridge: %w", err) - } - defer bridge.Close() - - waitErr := make(chan error, 1) - go func() { waitErr <- bridge.Wait() }() - - select { - case err := <-waitErr: - if err != nil && !errors.Is(err, ErrInvalidHandle) { - return fmt.Errorf("rdp proxy: session: %w", err) - } - return nil - case <-ctx.Done(): - _ = bridge.Cancel() - <-waitErr - return ctx.Err() - } -} diff --git a/packages/pam/handlers/rdp/bridge_cgo_windows.go b/packages/pam/handlers/rdp/bridge_cgo_windows.go index 498f3523..e9f8c65e 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_windows.go +++ b/packages/pam/handlers/rdp/bridge_cgo_windows.go @@ -12,8 +12,6 @@ package rdp import "C" import ( - "context" - "errors" "fmt" "io" "net" @@ -153,40 +151,3 @@ func dupConnSocket(conn net.Conn) (windows.Handle, error) { } return dup, nil } - -func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error { - defer clientConn.Close() - if p.config.SessionLogger != nil { - defer func() { - _ = p.config.SessionLogger.Close() - }() - } - - bridge, err := StartWithReadWriter( - clientConn, - p.config.TargetHost, - p.config.TargetPort, - p.config.InjectUsername, - p.config.InjectPassword, - p.config.AcceptorUsername, - ) - if err != nil { - return fmt.Errorf("rdp proxy: start bridge: %w", err) - } - defer bridge.Close() - - waitErr := make(chan error, 1) - go func() { waitErr <- bridge.Wait() }() - - select { - case err := <-waitErr: - if err != nil && !errors.Is(err, ErrInvalidHandle) { - return fmt.Errorf("rdp proxy: session: %w", err) - } - return nil - case <-ctx.Done(): - _ = bridge.Cancel() - <-waitErr - return ctx.Err() - } -} diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index b6d4f100..8639dcaf 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -64,8 +64,10 @@ async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result } else { target.acceptor_username.clone() }; - let (acceptor_output, connector_output) = - tokio::try_join!(run_acceptor_half(client_tcp, acceptor_username), run_connector_half(target))?; + let (acceptor_output, connector_output) = tokio::try_join!( + run_acceptor_half(client_tcp, acceptor_username), + run_connector_half(target) + )?; let (mut client_stream, client_leftover) = acceptor_output; let (mut target_stream, target_leftover) = connector_output; @@ -166,7 +168,10 @@ async fn filter_client_mcs_connect_initial( .map(|c| c.name.as_str().unwrap_or("?").to_owned()) .collect(); if !stripped.is_empty() { - info!(?stripped, "stripped virtual channels from MCS Connect Initial"); + info!( + ?stripped, + "stripped virtual channels from MCS Connect Initial" + ); network.channels.clear(); } } @@ -190,7 +195,10 @@ async fn filter_client_mcs_connect_initial( Ok(()) } -async fn run_acceptor_half(client_tcp: TcpStream, username: String) -> Result<(ErasedStream, bytes::BytesMut)> { +async fn run_acceptor_half( + client_tcp: TcpStream, + username: String, +) -> Result<(ErasedStream, bytes::BytesMut)> { let (server_tls, acceptor_public_key) = build_acceptor_tls().context("build acceptor TLS config")?; let server_tls = Arc::new(server_tls); diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index ca5b465f..72d91e1a 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -128,7 +128,14 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( use std::os::unix::io::FromRawFd; let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; - match spawn_session(client_tcp, host, target_port, username, password, acceptor_username) { + match spawn_session( + client_tcp, + host, + target_port, + username, + password, + acceptor_username, + ) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK @@ -177,7 +184,14 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( use std::os::windows::io::{FromRawSocket, RawSocket}; let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) }; - match spawn_session(client_tcp, host, target_port, username, password, acceptor_username) { + match spawn_session( + client_tcp, + host, + target_port, + username, + password, + acceptor_username, + ) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK From e0a91579a7ae5118f79d24372a363112e254bb4e Mon Sep 17 00:00:00 2001 From: bernie-g Date: Mon, 4 May 2026 14:15:24 -0400 Subject: [PATCH 46/51] refactor(pam-rdp): use target username for acceptor, autofill via metadata Drops the separate acceptor_username plumbing and has the bridge's CredSSP acceptor expect the actual Windows username. The CLI reads pamResponse.Metadata["username"] (set by the backend, mirroring the existing SSH/Postgres pattern) and pre-fills it in the .rdp file so the user doesn't have to type it. --- packages/api/model.go | 1 - .../pam/handlers/rdp/bridge_cgo_shared.go | 1 - packages/pam/handlers/rdp/bridge_cgo_unix.go | 13 ++++---- .../pam/handlers/rdp/bridge_cgo_windows.go | 13 ++++---- packages/pam/handlers/rdp/bridge_stub.go | 4 +-- .../handlers/rdp/native/include/rdp_bridge.h | 2 -- .../pam/handlers/rdp/native/src/bridge.rs | 14 +++------ packages/pam/handlers/rdp/native/src/ffi.rs | 30 ++----------------- packages/pam/handlers/rdp/proxy.go | 5 +--- packages/pam/local/rdp-proxy.go | 12 ++++++-- packages/pam/pam-proxy.go | 13 ++++---- packages/pam/session/credentials.go | 2 -- 12 files changed, 34 insertions(+), 76 deletions(-) diff --git a/packages/api/model.go b/packages/api/model.go index 4c48629c..4000da29 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -890,7 +890,6 @@ type PAMSessionCredentials struct { ServiceAccountToken string `json:"serviceAccountToken,omitempty"` ServiceAccountName string `json:"serviceAccountName,omitempty"` Namespace string `json:"namespace,omitempty"` - AccountName string `json:"accountName,omitempty"` } type MFASessionStatus string diff --git a/packages/pam/handlers/rdp/bridge_cgo_shared.go b/packages/pam/handlers/rdp/bridge_cgo_shared.go index 042635a2..9a822e6f 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_shared.go +++ b/packages/pam/handlers/rdp/bridge_cgo_shared.go @@ -30,7 +30,6 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er p.config.TargetPort, p.config.InjectUsername, p.config.InjectPassword, - p.config.AcceptorUsername, ) if err != nil { return fmt.Errorf("rdp proxy: start bridge: %w", err) diff --git a/packages/pam/handlers/rdp/bridge_cgo_unix.go b/packages/pam/handlers/rdp/bridge_cgo_unix.go index 4420ef18..91b24d38 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_unix.go +++ b/packages/pam/handlers/rdp/bridge_cgo_unix.go @@ -22,16 +22,16 @@ import ( // StartWithConn hands an independent dup of conn's fd to the bridge. // For TLS-wrapped or otherwise non-fd-backed conns, use StartWithReadWriter. -func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { +func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { dupFd, err := dupConnFD(conn) if err != nil { return nil, fmt.Errorf("rdp bridge: dup client fd: %w", err) } - return startWithDupedFD(dupFd, targetHost, targetPort, username, password, acceptorUsername) + return startWithDupedFD(dupFd, targetHost, targetPort, username, password) } // Ownership of dupFd transfers to Rust on success; we close it on failure. -func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { +func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { success := false defer func() { if !success { @@ -45,8 +45,6 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, defer C.free(unsafe.Pointer(cUser)) cPass := C.CString(password) defer C.free(unsafe.Pointer(cPass)) - cAcceptorUser := C.CString(acceptorUsername) - defer C.free(unsafe.Pointer(cAcceptorUser)) var handle C.uint64_t rc := C.rdp_bridge_start_unix_fd( @@ -55,7 +53,6 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, C.uint16_t(targetPort), cUser, cPass, - cAcceptorUser, &handle, ) if rc != C.RDP_BRIDGE_OK { @@ -78,7 +75,7 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, // // Cost: two extra in-process copies and a loopback round-trip per byte. // Negligible vs. the TLS + CredSSP work on either side. -func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { +func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) @@ -113,7 +110,7 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return nil, fmt.Errorf("rdp bridge: dup accepted fd: %w", err) } - bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password, acceptorUsername) + bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password) if err != nil { _ = peer.Close() return nil, err diff --git a/packages/pam/handlers/rdp/bridge_cgo_windows.go b/packages/pam/handlers/rdp/bridge_cgo_windows.go index e9f8c65e..c28d5f89 100644 --- a/packages/pam/handlers/rdp/bridge_cgo_windows.go +++ b/packages/pam/handlers/rdp/bridge_cgo_windows.go @@ -21,15 +21,15 @@ import ( "golang.org/x/sys/windows" ) -func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { +func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { dupSocket, err := dupConnSocket(conn) if err != nil { return nil, fmt.Errorf("rdp bridge: dup client socket: %w", err) } - return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, acceptorUsername) + return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password) } -func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { +func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { success := false defer func() { if !success { @@ -43,8 +43,6 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor defer C.free(unsafe.Pointer(cUser)) cPass := C.CString(password) defer C.free(unsafe.Pointer(cPass)) - cAcceptorUser := C.CString(acceptorUsername) - defer C.free(unsafe.Pointer(cAcceptorUser)) var handle C.uint64_t rc := C.rdp_bridge_start_windows_socket( @@ -53,7 +51,6 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor C.uint16_t(targetPort), cUser, cPass, - cAcceptorUser, &handle, ) if rc != C.RDP_BRIDGE_OK { @@ -63,7 +60,7 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor return &Bridge{handle: uint64(handle)}, nil } -func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, acceptorUsername string) (*Bridge, error) { +func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err) @@ -98,7 +95,7 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, return nil, fmt.Errorf("rdp bridge: dup accepted socket: %w", err) } - bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, acceptorUsername) + bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password) if err != nil { _ = peer.Close() return nil, err diff --git a/packages/pam/handlers/rdp/bridge_stub.go b/packages/pam/handlers/rdp/bridge_stub.go index 2c488000..37a3bcdf 100644 --- a/packages/pam/handlers/rdp/bridge_stub.go +++ b/packages/pam/handlers/rdp/bridge_stub.go @@ -12,11 +12,11 @@ import ( // where the Rust bridge isn't compiled. All entry points return // ErrRdpUnavailable. -func StartWithConn(_ net.Conn, _ string, _ uint16, _, _, _ string) (*Bridge, error) { +func StartWithConn(_ net.Conn, _ string, _ uint16, _, _ string) (*Bridge, error) { return nil, ErrRdpUnavailable } -func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _, _ string) (*Bridge, error) { +func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _ string) (*Bridge, error) { return nil, ErrRdpUnavailable } diff --git a/packages/pam/handlers/rdp/native/include/rdp_bridge.h b/packages/pam/handlers/rdp/native/include/rdp_bridge.h index ac12c008..83088768 100644 --- a/packages/pam/handlers/rdp/native/include/rdp_bridge.h +++ b/packages/pam/handlers/rdp/native/include/rdp_bridge.h @@ -27,7 +27,6 @@ int32_t rdp_bridge_start_unix_fd( uint16_t target_port, const char *username, const char *password, - const char *acceptor_username, uint64_t *out_handle ); #endif @@ -39,7 +38,6 @@ int32_t rdp_bridge_start_windows_socket( uint16_t target_port, const char *username, const char *password, - const char *acceptor_username, uint64_t *out_handle ); #endif diff --git a/packages/pam/handlers/rdp/native/src/bridge.rs b/packages/pam/handlers/rdp/native/src/bridge.rs index 8639dcaf..cfe5e992 100644 --- a/packages/pam/handlers/rdp/native/src/bridge.rs +++ b/packages/pam/handlers/rdp/native/src/bridge.rs @@ -26,7 +26,9 @@ use tracing::info; use crate::config::{connector_config, DEFAULT_HEIGHT, DEFAULT_WIDTH}; -// Empty password for acceptor - the username comes from target credentials. +// The acceptor side of the bridge expects the user to type the target +// username with an empty password. The real password is injected by the +// connector side from the PAM vault. pub const ACCEPTOR_PASSWORD: &str = ""; pub struct TargetEndpoint { @@ -34,10 +36,6 @@ pub struct TargetEndpoint { pub port: u16, pub username: String, pub password: String, - // Username the user types into their RDP client. Distinct from - // `username` (the real Windows account injected to the target). - // Falls back to `username` if empty. - pub acceptor_username: String, } pub async fn run_mitm( @@ -59,11 +57,7 @@ async fn run_mitm_inner(client_tcp: TcpStream, target: TargetEndpoint) -> Result // 0.23 needs an explicit provider when more than one is compiled in. let _ = rustls::crypto::ring::default_provider().install_default(); - let acceptor_username = if target.acceptor_username.is_empty() { - target.username.clone() - } else { - target.acceptor_username.clone() - }; + let acceptor_username = target.username.clone(); let (acceptor_output, connector_output) = tokio::try_join!( run_acceptor_half(client_tcp, acceptor_username), run_connector_half(target) diff --git a/packages/pam/handlers/rdp/native/src/ffi.rs b/packages/pam/handlers/rdp/native/src/ffi.rs index 72d91e1a..ecef7782 100644 --- a/packages/pam/handlers/rdp/native/src/ffi.rs +++ b/packages/pam/handlers/rdp/native/src/ffi.rs @@ -59,7 +59,6 @@ fn spawn_session( port: u16, username: String, password: String, - acceptor_username: String, ) -> anyhow::Result { client_tcp.set_nonblocking(true)?; let cancel = CancellationToken::new(); @@ -78,7 +77,6 @@ fn spawn_session( port, username, password, - acceptor_username, }; run_mitm(client, endpoint, cancel_for_thread).await }) @@ -102,7 +100,6 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( target_port: u16, username: *const c_char, password: *const c_char, - acceptor_username: *const c_char, out_handle: *mut u64, ) -> i32 { if out_handle.is_null() { @@ -120,22 +117,11 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd( Some(v) => v, None => return RDP_BRIDGE_BAD_ARG, }; - let acceptor_username = match unsafe { c_str_to_owned(acceptor_username) } { - Some(v) => v, - None => return RDP_BRIDGE_BAD_ARG, - }; use std::os::unix::io::FromRawFd; let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) }; - match spawn_session( - client_tcp, - host, - target_port, - username, - password, - acceptor_username, - ) { + match spawn_session(client_tcp, host, target_port, username, password) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK @@ -158,7 +144,6 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( target_port: u16, username: *const c_char, password: *const c_char, - acceptor_username: *const c_char, out_handle: *mut u64, ) -> i32 { if out_handle.is_null() { @@ -176,22 +161,11 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket( Some(v) => v, None => return RDP_BRIDGE_BAD_ARG, }; - let acceptor_username = match unsafe { c_str_to_owned(acceptor_username) } { - Some(v) => v, - None => return RDP_BRIDGE_BAD_ARG, - }; use std::os::windows::io::{FromRawSocket, RawSocket}; let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) }; - match spawn_session( - client_tcp, - host, - target_port, - username, - password, - acceptor_username, - ) { + match spawn_session(client_tcp, host, target_port, username, password) { Ok(id) => { unsafe { *out_handle = id }; RDP_BRIDGE_OK diff --git a/packages/pam/handlers/rdp/proxy.go b/packages/pam/handlers/rdp/proxy.go index 44fd7b77..e113902a 100644 --- a/packages/pam/handlers/rdp/proxy.go +++ b/packages/pam/handlers/rdp/proxy.go @@ -9,10 +9,7 @@ type RDPProxyConfig struct { TargetPort uint16 InjectUsername string InjectPassword string - // Username the user types into their RDP client; the bridge's CredSSP - // acceptor expects this value. Falls back to InjectUsername if empty. - AcceptorUsername string - SessionID string + SessionID string // Retained for API symmetry with other PAM handlers; not yet written // through (no RDP session recording in this MVP). SessionLogger session.SessionLogger diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index 781b08e2..34d39ed1 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -92,7 +92,13 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec return } - rdpFilePath, err := writeRDPFile(proxy.port, pamResponse.SessionId, accessParams.AccountName) + username, ok := pamResponse.Metadata["username"] + if !ok { + util.HandleError(fmt.Errorf("PAM response metadata is missing 'username'"), "Failed to start proxy server") + return + } + + rdpFilePath, err := writeRDPFile(proxy.port, pamResponse.SessionId, username) if err != nil { log.Warn().Err(err).Msg("Failed to write .rdp file; proxy still running") } else { @@ -110,7 +116,7 @@ func StartRDPLocalProxy(accessToken string, accessParams PAMAccessParams, projec util.PrintfStderr("Connect your RDP client to:\n") util.PrintfStderr(" 127.0.0.1:%d\n", proxy.port) util.PrintfStderr("With credentials:\n") - util.PrintfStderr(" username: %s\n", accessParams.AccountName) + util.PrintfStderr(" username: %s\n", username) util.PrintfStderr(" password: (leave blank)\n") if proxy.rdpFilePath != "" { util.PrintfStderr("\n") @@ -307,7 +313,7 @@ func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { // becomes invalid as soon as the CLI exits; reopening the file later // would just dial a dead port. // Falls back to the OS temp dir if the home directory can't be resolved. -func writeRDPFile(listenPort int, sessionID string, username string) (string, error) { +func writeRDPFile(listenPort int, sessionID, username string) (string, error) { filename := fmt.Sprintf("infisical-rdp-%s.rdp", sessionID) dir, err := rdpFileDir() diff --git a/packages/pam/pam-proxy.go b/packages/pam/pam-proxy.go index 9129fcac..0cd6c29e 100644 --- a/packages/pam/pam-proxy.go +++ b/packages/pam/pam-proxy.go @@ -418,13 +418,12 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo return fmt.Errorf("rdp: target port %d out of range", credentials.Port) } rdpConfig := rdp.RDPProxyConfig{ - TargetHost: credentials.Host, - TargetPort: uint16(credentials.Port), - InjectUsername: credentials.Username, - InjectPassword: credentials.Password, - AcceptorUsername: credentials.AccountName, - SessionID: pamConfig.SessionId, - SessionLogger: sessionLogger, + TargetHost: credentials.Host, + TargetPort: uint16(credentials.Port), + InjectUsername: credentials.Username, + InjectPassword: credentials.Password, + SessionID: pamConfig.SessionId, + SessionLogger: sessionLogger, } proxy := rdp.NewRDPProxy(rdpConfig) log.Info(). diff --git a/packages/pam/session/credentials.go b/packages/pam/session/credentials.go index 2b175f98..008f9647 100644 --- a/packages/pam/session/credentials.go +++ b/packages/pam/session/credentials.go @@ -27,7 +27,6 @@ type PAMCredentials struct { ServiceAccountToken string ServiceAccountName string Namespace string - AccountName string PolicyRules *api.PAMPolicyRules } @@ -106,7 +105,6 @@ func (cm *CredentialsManager) GetPAMSessionCredentials(sessionId string, expiryT ServiceAccountToken: response.Credentials.ServiceAccountToken, ServiceAccountName: response.Credentials.ServiceAccountName, Namespace: response.Credentials.Namespace, - AccountName: response.Credentials.AccountName, PolicyRules: response.PolicyRules, } From ad8bcfc52d752a966ccc900512033d09c30f6736 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Mon, 4 May 2026 14:33:22 -0400 Subject: [PATCH 47/51] fix(release): make npm-release tolerate skipped validate-tag-branch npm-release's needs: includes validate-tag-branch, which is skipped on workflow_dispatch. Without an always() + needs.X.result pattern, the default needs: semantics propagate the skip to npm-release, so a manual non-dry-run release would upload goreleaser artifacts but silently skip npm publish. Mirrors the existing goreleaser job pattern. --- .github/workflows/release_build_infisical_cli.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml index 05789412..cea56363 100644 --- a/.github/workflows/release_build_infisical_cli.yml +++ b/.github/workflows/release_build_infisical_cli.yml @@ -72,7 +72,13 @@ jobs: # CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }} npm-release: - if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) + if: | + always() && + (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run)) && + needs.goreleaser.result == 'success' && + needs.goreleaser-darwin.result == 'success' && + needs.cli-tests.result == 'success' && + (github.event_name == 'workflow_dispatch' || needs.validate-tag-branch.result == 'success') runs-on: ubuntu-latest env: working-directory: ./npm From 92102e916397048cc2c7ef351bdb26b710e57056 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Mon, 4 May 2026 21:08:23 -0400 Subject: [PATCH 48/51] fix(pam-rdp): don't shut down proxy on per-connection gateway close The local proxy treated every gateway-side close as a session-level disconnect and shut down the CLI. A failed CredSSP attempt (e.g. wrong username typed in the RDP client) closes the gateway side too, so one typo killed the whole pam rdp access command. Mirrors the kubernetes proxy pattern: per-connection close just ends that connection, the proxy stays up so the user can retry. --- packages/pam/local/rdp-proxy.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/pam/local/rdp-proxy.go b/packages/pam/local/rdp-proxy.go index 34d39ed1..af3b43ef 100644 --- a/packages/pam/local/rdp-proxy.go +++ b/packages/pam/local/rdp-proxy.go @@ -271,7 +271,7 @@ func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { connCtx, connCancel := context.WithCancel(p.ctx) defer connCancel() - gatewayErrCh, clientErrCh := p.NewDisconnectChannels() + done := make(chan struct{}, 2) go func() { defer connCancel() @@ -283,7 +283,7 @@ func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { log.Debug().Err(err).Msg("Gateway to client copy ended") } } - gatewayErrCh <- err + done <- struct{}{} }() go func() { @@ -296,10 +296,14 @@ func (p *RDPProxyServer) handleConnection(clientConn net.Conn) { log.Debug().Err(err).Msg("Client to gateway copy ended") } } - clientErrCh <- err + done <- struct{}{} }() - p.WaitForDisconnect(gatewayErrCh, clientErrCh, connCtx) + select { + case <-done: + case <-connCtx.Done(): + log.Info().Msg("Connection cancelled by context") + } log.Info().Msgf("RDP connection closed for client: %s", clientConn.RemoteAddr().String()) } From e0619d7dcb6b1e368dfb0f5a692df573d23c2454 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Tue, 5 May 2026 09:39:39 -0400 Subject: [PATCH 49/51] chore(pam-rdp): lowercase example account name in help text --- packages/cmd/pam.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cmd/pam.go b/packages/cmd/pam.go index 86619856..5f9d99b0 100644 --- a/packages/cmd/pam.go +++ b/packages/cmd/pam.go @@ -429,7 +429,7 @@ var pamRdpAccessCmd = &cobra.Command{ Use: "access", Short: "Access PAM Windows/RDP accounts", Long: "Access a PAM-managed Windows target over RDP. This starts a local loopback proxy that your RDP client connects to; the session tunnels through the Infisical Gateway with credentials injected server-side.", - Example: "infisical pam rdp access --resource windows-prod --account Administrator --duration 1h --project-id ", + Example: "infisical pam rdp access --resource windows-prod --account administrator --duration 1h --project-id ", DisableFlagsInUseLine: true, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { From 98e076fce869764ab15d19268e539cf1beacc748 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Tue, 5 May 2026 09:42:30 -0400 Subject: [PATCH 50/51] chore(pam-rdp): drop unused /target-docker from .gitignore --- packages/pam/handlers/rdp/native/.gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pam/handlers/rdp/native/.gitignore b/packages/pam/handlers/rdp/native/.gitignore index 56071a5b..ea8c4bf7 100644 --- a/packages/pam/handlers/rdp/native/.gitignore +++ b/packages/pam/handlers/rdp/native/.gitignore @@ -1,2 +1 @@ /target -/target-docker From f2f4f99eedadadafcec3dbff66242baecfadd031 Mon Sep 17 00:00:00 2001 From: bernie-g Date: Tue, 5 May 2026 09:45:54 -0400 Subject: [PATCH 51/51] fix(release): append windows artifacts to shared draft Without an explicit release block, the windows goreleaser config defaults to mode: replace and clobbers the linux + darwin artifacts already uploaded to the shared draft (race-y; only manifests when windows finishes after the other two). Mirrors the append config the linux and darwin configs already use. --- .goreleaser-windows.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.goreleaser-windows.yaml b/.goreleaser-windows.yaml index 44b119d0..d73884d2 100644 --- a/.goreleaser-windows.yaml +++ b/.goreleaser-windows.yaml @@ -1,3 +1,8 @@ +# Append to the release draft created by the ubuntu goreleaser job. +release: + replace_existing_draft: false + mode: append + builds: - id: windows-build env: