From 1a1fe353eefa92e9029111df12bc7022cbb99e57 Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 7 Oct 2025 16:07:56 -0400 Subject: [PATCH 01/25] feat(ffi): add RDCleanPath gateway support for .NET - Add RDCleanPath FFI bindings - Implement WebSocket stream for gateway connections - Add GatewayConnection API with CredSSP hostname extraction - Support both direct and gateway connection modes --- Cargo.lock | 1 + crates/ironrdp-connector/src/credssp.rs | 11 +- ffi/Cargo.toml | 1 + ...Devolutions.IronRdp.AvaloniaExample.csproj | 12 +- .../MainWindow.axaml.cs | 127 ++++++- .../TokenGenerator.cs | 192 +++++++++++ .../Devolutions.IronRdp.ConnectExample.csproj | 2 +- .../Program.cs | 4 +- .../Generated/CertificateChainIterator.cs | 109 ++++++ .../Devolutions.IronRdp/Generated/Log.cs | 7 + .../Generated/RDCleanPathDetectionResult.cs | 129 ++++++++ .../Generated/RDCleanPathPdu.cs | 204 ++++++++++++ .../Generated/RDCleanPathResult.cs | 309 ++++++++++++++++++ .../Generated/RDCleanPathResultType.cs | 20 ++ .../Generated/RawCertificateChainIterator.cs | 31 ++ .../Devolutions.IronRdp/Generated/RawLog.cs | 7 + .../RawRDCleanPathDetectionResult.cs | 36 ++ .../Generated/RawRDCleanPathPdu.cs | 58 ++++ .../Generated/RawRDCleanPathResult.cs | 69 ++++ .../Generated/RawRDCleanPathResultType.cs | 20 ++ ...CertificateChainIteratorBoxIronRdpError.cs | 46 +++ ...iResultBoxRDCleanPathPduBoxIronRdpError.cs | 46 +++ ...sultBoxRDCleanPathResultBoxIronRdpError.cs | 46 +++ ...eanpathFfiResultBoxVecU8BoxIronRdpError.cs | 46 +++ ...wRdcleanpathFfiResultU16BoxIronRdpError.cs | 46 +++ ...dcleanpathFfiResultUsizeBoxIronRdpError.cs | 46 +++ .../Devolutions.IronRdp/src/Connection.cs | 22 +- ffi/dotnet/Devolutions.IronRdp/src/Framed.cs | 41 +++ .../src/GatewayConnection.cs | 309 ++++++++++++++++++ .../src/WebsocketStream.cs | 173 ++++++++++ ffi/src/error.rs | 27 ++ ffi/src/lib.rs | 1 + ffi/src/rdcleanpath.rs | 216 ++++++++++++ 33 files changed, 2387 insertions(+), 27 deletions(-) create mode 100644 ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/CertificateChainIterator.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathDetectionResult.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResult.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResultType.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawCertificateChainIterator.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathDetectionResult.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResult.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResultType.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxVecU8BoxIronRdpError.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultU16BoxIronRdpError.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultUsizeBoxIronRdpError.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs create mode 100644 ffi/src/rdcleanpath.rs diff --git a/Cargo.lock b/Cargo.lock index ea165ee61..582d179cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1534,6 +1534,7 @@ dependencies = [ "ironrdp-cliprdr-native", "ironrdp-core", "ironrdp-dvc-pipe-proxy", + "ironrdp-rdcleanpath", "sspi", "thiserror 2.0.17", "tracing", diff --git a/crates/ironrdp-connector/src/credssp.rs b/crates/ironrdp-connector/src/credssp.rs index 9750b6081..3bfc9de5c 100644 --- a/crates/ironrdp-connector/src/credssp.rs +++ b/crates/ironrdp-connector/src/credssp.rs @@ -6,7 +6,7 @@ use sspi::credssp::{self, ClientState, CredSspClient}; use sspi::generator::{Generator, NetworkRequest}; use sspi::negotiate::ProtocolConfig; use sspi::Username; -use tracing::debug; +use tracing::{debug, info}; use crate::{ custom_err, general_err, ConnectorError, ConnectorErrorKind, ConnectorResult, Credentials, ServerName, Written, @@ -100,6 +100,15 @@ impl CredsspSequence { server_public_key: Vec, kerberos_config: Option, ) -> ConnectorResult<(Self, credssp::TsRequest)> { + info!( + ?credentials, + ?domain, + ?protocol, + ?server_name, + ?server_public_key, + ?kerberos_config, + "Initialize CredSSP sequence" + ); let credentials: sspi::Credentials = match &credentials { Credentials::UsernamePassword { username, password } => { let username = Username::new(username, domain).map_err(|e| custom_err!("invalid username", e))?; diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 30d98d3db..8f17fa7b5 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -18,6 +18,7 @@ ironrdp = { path = "../crates/ironrdp", features = ["session", "connector", "dvc ironrdp-cliprdr-native.path = "../crates/ironrdp-cliprdr-native" ironrdp-dvc-pipe-proxy.path = "../crates/ironrdp-dvc-pipe-proxy" ironrdp-core = { path = "../crates/ironrdp-core", features = ["alloc"] } +ironrdp-rdcleanpath.path = "../crates/ironrdp-rdcleanpath" sspi = { version = "0.16", features = ["network_client"] } thiserror = "2" tracing = { version = "0.1", features = ["log"] } diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj index e29164c4e..802b8ad76 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/Devolutions.IronRdp.AvaloniaExample.csproj @@ -10,18 +10,18 @@ - - - - + + + + - + - + diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs index 341485271..ade51a205 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs @@ -7,6 +7,7 @@ using System; using System.ComponentModel; using System.Diagnostics; +using System.IO; using System.Net.Security; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -22,7 +23,7 @@ public partial class MainWindow : Window readonly InputDatabase? _inputDatabase = InputDatabase.New(); ActiveStage? _activeStage; DecodedImage? _decodedImage; - Framed? _framed; + Framed? _framed; // Changed to Stream to support both SslStream and WebSocketStream WinCliprdr? _cliprdr; private readonly RendererModel _renderModel; private Image? _imageControl; @@ -77,18 +78,48 @@ private void OnOpened(object? sender, EventArgs e) var username = Environment.GetEnvironmentVariable("IRONRDP_USERNAME"); var password = Environment.GetEnvironmentVariable("IRONRDP_PASSWORD"); - var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN"); + var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN"); // Optional var server = Environment.GetEnvironmentVariable("IRONRDP_SERVER"); + var portEnv = Environment.GetEnvironmentVariable("IRONRDP_PORT"); - if (username == null || password == null || domain == null || server == null) + // NEW: Gateway configuration (optional) + var gatewayUrl = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_URL"); + var gatewayToken = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_TOKEN"); + var tokengenUrl = Environment.GetEnvironmentVariable("IRONRDP_TOKENGEN_URL"); + + if (username == null || password == null || server == null) { var errorMessage = - "Please set the IRONRDP_USERNAME, IRONRDP_PASSWORD, IRONRDP_DOMAIN, and RONRDP_SERVER environment variables"; + "Please set the IRONRDP_USERNAME, IRONRDP_PASSWORD, and IRONRDP_SERVER environment variables"; Trace.TraceError(errorMessage); Close(); throw new InvalidProgramException(errorMessage); } + // Validate server is only domain or IP (no port allowed) + if (server.Contains(':')) + { + var errorMessage = $"IRONRDP_SERVER must be a domain or IP address only, not '{server}'. Use IRONRDP_PORT for the port."; + Trace.TraceError(errorMessage); + Close(); + throw new InvalidProgramException(errorMessage); + } + + // Parse port from environment variable or use default + int port = 3389; + if (!string.IsNullOrEmpty(portEnv)) + { + if (!int.TryParse(portEnv, out port) || port <= 0 || port > 65535) + { + var errorMessage = $"IRONRDP_PORT must be a valid port number (1-65535), got '{portEnv}'"; + Trace.TraceError(errorMessage); + Close(); + throw new InvalidProgramException(errorMessage); + } + } + + Trace.TraceInformation($"Target server: {server}:{port}"); + var config = BuildConfig(username, password, domain, _renderModel.Width, _renderModel.Height); CliprdrBackendFactory? factory = null; @@ -106,15 +137,79 @@ private void OnOpened(object? sender, EventArgs e) BeforeConnectSetup(); Task.Run(async () => { - var (res, framed) = await Connection.Connect(config, server, factory); - this._decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(), - res.GetDesktopSize().GetHeight()); - this._activeStage = ActiveStage.New(res); - this._framed = framed; - ReadPduAndProcessActiveStage(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + try { - HandleClipboardEvents(); + ConnectionResult res; + + // Determine connection mode: Gateway or Direct + if (!string.IsNullOrEmpty(gatewayUrl)) + { + Trace.TraceInformation("=== GATEWAY MODE ==="); + Trace.TraceInformation($"Gateway URL: {gatewayUrl}"); + Trace.TraceInformation($"Destination: {server}:{port}"); + + // Generate token if not provided + if (string.IsNullOrEmpty(gatewayToken)) + { + Trace.TraceInformation("No token provided, generating token..."); + var tokenGen = new TokenGenerator(tokengenUrl ?? "http://localhost:8080"); + + try + { + gatewayToken = await tokenGen.GenerateRdpTlsToken( + dstHost: server!, + proxyUser: $"{username}@{domain}", + proxyPassword: password!, + destUser: username!, + destPassword: password! + ); + Trace.TraceInformation($"Token generated successfully (length: {gatewayToken.Length})"); + } + catch (Exception ex) + { + Trace.TraceError($"Failed to generate token: {ex.Message}"); + Trace.TraceInformation("Make sure tokengen server is running:"); + Trace.TraceInformation($" cargo run --manifest-path D:/devolutions-gateway/tools/tokengen/Cargo.toml -- server"); + throw; + } + } + + // Connect via gateway - destination needs "hostname:port" format for RDCleanPath + string destination = $"{server}:{port}"; + var (gatewayRes, gatewayFramed) = await GatewayConnection.ConnectViaGateway( + config, gatewayUrl, gatewayToken!, destination, null, factory); + res = gatewayRes; + this._framed = new Framed(gatewayFramed.GetInner().Item1); + + Trace.TraceInformation("=== GATEWAY CONNECTION SUCCESSFUL ==="); + } + else + { + Trace.TraceInformation("=== DIRECT MODE ==="); + + // Direct connection (original behavior) + var (directRes, directFramed) = await Connection.Connect(config, server, factory, port); + res = directRes; + this._framed = new Framed(directFramed.GetInner().Item1); + + Trace.TraceInformation("=== DIRECT CONNECTION SUCCESSFUL ==="); + } + + this._decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(), + res.GetDesktopSize().GetHeight()); + this._activeStage = ActiveStage.New(res); + ReadPduAndProcessActiveStage(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + HandleClipboardEvents(); + } + } + catch (Exception ex) + { + Trace.TraceError($"Connection failed: {ex.Message}"); + Trace.TraceError($"Stack trace: {ex.StackTrace}"); + throw; } }); } @@ -260,12 +355,16 @@ private void HandleClipboardEvents() }); } - private static Config BuildConfig(string username, string password, string domain, int width, int height) + private static Config BuildConfig(string username, string password, string? domain, int width, int height) { ConfigBuilder configBuilder = ConfigBuilder.New(); configBuilder.WithUsernameAndPassword(username, password); - configBuilder.SetDomain(domain); + if (domain != null) + { + configBuilder.SetDomain(domain); + } + configBuilder.SetDesktopSize((ushort)height, (ushort)width); configBuilder.SetClientName("IronRdp"); configBuilder.SetClientDir("C:\\"); diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs new file mode 100644 index 000000000..00946c604 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs @@ -0,0 +1,192 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Devolutions.IronRdp.AvaloniaExample; + +/// +/// Client for requesting JWT tokens from a Devolutions Gateway tokengen server. +/// +public class TokenGenerator : IDisposable +{ + private readonly HttpClient _client; + private readonly string _tokengenUrl; + + /// + /// Creates a new TokenGenerator instance. + /// + /// The base URL of the tokengen server (e.g., "http://localhost:8080") + public TokenGenerator(string tokengenUrl = "http://localhost:8080") + { + _tokengenUrl = tokengenUrl; + _client = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30) + }; + } + + /// + /// Generates an RDP token with credential injection for gateway-based connections. + /// + /// Destination RDP server (e.g., "10.10.0.3:3389") + /// Gateway proxy username + /// Gateway proxy password + /// Destination RDP server username + /// Destination RDP server password + /// Optional session UUID + /// Token validity in seconds (default: 3600) + /// A JWT token string + public async Task GenerateRdpTlsToken( + string dstHost, + string proxyUser, + string proxyPassword, + string destUser, + string destPassword, + string? jetAid = null, + int validityDuration = 3600) + { + var request = new RdpTlsTokenRequest + { + DstHst = dstHost, + PrxUsr = proxyUser, + PrxPwd = proxyPassword, + DstUsr = destUser, + DstPwd = destPassword, + JetAid = jetAid, + ValidityDuration = validityDuration + }; + + try + { + var response = await _client.PostAsJsonAsync($"{_tokengenUrl}/rdp_tls", request); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + if (result?.Token == null) + { + throw new Exception("Token generation failed: Empty response"); + } + + return result.Token; + } + catch (HttpRequestException ex) + { + throw new Exception($"Failed to connect to tokengen server at {_tokengenUrl}: {ex.Message}", ex); + } + catch (TaskCanceledException ex) + { + throw new Exception($"Token generation request timed out: {ex.Message}", ex); + } + } + + /// + /// Generates a forward mode token for simple RDP forwarding without credential injection. + /// + /// Destination host + /// Application protocol (default: "rdp") + /// Enable recording + /// Token validity in seconds (default: 3600) + /// A JWT token string + public async Task GenerateForwardToken( + string dstHost, + string jetAp = "rdp", + bool jetRec = false, + int validityDuration = 3600) + { + var request = new ForwardTokenRequest + { + DstHst = dstHost, + JetAp = jetAp, + JetRec = jetRec, + ValidityDuration = validityDuration + }; + + try + { + var response = await _client.PostAsJsonAsync($"{_tokengenUrl}/forward", request); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + if (result?.Token == null) + { + throw new Exception("Token generation failed: Empty response"); + } + + return result.Token; + } + catch (HttpRequestException ex) + { + throw new Exception($"Failed to connect to tokengen server at {_tokengenUrl}: {ex.Message}", ex); + } + } + + /// + /// Checks if the tokengen server is reachable. + /// + /// True if server is reachable, false otherwise + public async Task IsServerReachable() + { + try + { + var response = await _client.GetAsync(_tokengenUrl); + return response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.NotFound; + } + catch + { + return false; + } + } + + public void Dispose() + { + _client?.Dispose(); + } + + // Request/Response DTOs + private class RdpTlsTokenRequest + { + [JsonPropertyName("dst_hst")] + public string DstHst { get; set; } = string.Empty; + + [JsonPropertyName("prx_usr")] + public string PrxUsr { get; set; } = string.Empty; + + [JsonPropertyName("prx_pwd")] + public string PrxPwd { get; set; } = string.Empty; + + [JsonPropertyName("dst_usr")] + public string DstUsr { get; set; } = string.Empty; + + [JsonPropertyName("dst_pwd")] + public string DstPwd { get; set; } = string.Empty; + + [JsonPropertyName("jet_aid")] + public string? JetAid { get; set; } + + [JsonPropertyName("validity_duration")] + public int ValidityDuration { get; set; } + } + + private class ForwardTokenRequest + { + [JsonPropertyName("dst_hst")] + public string DstHst { get; set; } = string.Empty; + + [JsonPropertyName("jet_ap")] + public string JetAp { get; set; } = "rdp"; + + [JsonPropertyName("jet_rec")] + public bool JetRec { get; set; } + + [JsonPropertyName("validity_duration")] + public int ValidityDuration { get; set; } + } + + private class TokenResponse + { + [JsonPropertyName("token")] + public string? Token { get; set; } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Devolutions.IronRdp.ConnectExample.csproj b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Devolutions.IronRdp.ConnectExample.csproj index 1ef474af5..61014ed13 100644 --- a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Devolutions.IronRdp.ConnectExample.csproj +++ b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Devolutions.IronRdp.ConnectExample.csproj @@ -16,7 +16,7 @@ https://learn.microsoft.com/en-us/dotnet/api/system.drawing?view=net-8.0 --> - + diff --git a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs index b685455ca..bff6cd9c4 100644 --- a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs +++ b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs @@ -23,7 +23,7 @@ static async Task Main(string[] args) try { - var (res, framed) = await Connection.Connect(buildConfig(serverName, username, password, domain, 1980, 1080), serverName, null); + var (res, framed) = await Connection.Connect(buildConfig( username, password, domain, 1980, 1080), serverName, null); var decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(), res.GetDesktopSize().GetHeight()); var activeState = ActiveStage.New(res); var keepLooping = true; @@ -175,7 +175,7 @@ static void PrintHelp() Console.WriteLine(" --help Show this message and exit."); } - private static Config buildConfig(string servername, string username, string password, string domain, int width, int height) + private static Config buildConfig(string username, string password, string domain, int width, int height) { ConfigBuilder configBuilder = ConfigBuilder.New(); diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/CertificateChainIterator.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/CertificateChainIterator.cs new file mode 100644 index 000000000..9830d732d --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/CertificateChainIterator.cs @@ -0,0 +1,109 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp; + +#nullable enable + +public partial class CertificateChainIterator: IDisposable +{ + private unsafe Raw.CertificateChainIterator* _inner; + + /// + /// Creates a managed CertificateChainIterator from a raw handle. + /// + /// + /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free). + ///
+ /// This constructor assumes the raw struct is allocated on Rust side. + /// If implemented, the custom Drop implementation on Rust side WILL run on destruction. + ///
+ public unsafe CertificateChainIterator(Raw.CertificateChainIterator* handle) + { + _inner = handle; + } + + /// + /// A VecU8 allocated on Rust side. + /// + public VecU8? Next() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("CertificateChainIterator"); + } + Raw.VecU8* retVal = Raw.CertificateChainIterator.Next(_inner); + if (retVal == null) + { + return null; + } + return new VecU8(retVal); + } + } + + public nuint Len() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("CertificateChainIterator"); + } + nuint retVal = Raw.CertificateChainIterator.Len(_inner); + return retVal; + } + } + + public bool IsEmpty() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("CertificateChainIterator"); + } + bool retVal = Raw.CertificateChainIterator.IsEmpty(_inner); + return retVal; + } + } + + /// + /// Returns the underlying raw handle. + /// + public unsafe Raw.CertificateChainIterator* AsFFI() + { + return _inner; + } + + /// + /// Destroys the underlying object immediately. + /// + public void Dispose() + { + unsafe + { + if (_inner == null) + { + return; + } + + Raw.CertificateChainIterator.Destroy(_inner); + _inner = null; + + GC.SuppressFinalize(this); + } + } + + ~CertificateChainIterator() + { + Dispose(); + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/Log.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/Log.cs index ec2769f3b..c0e3af52f 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/Log.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/Log.cs @@ -29,6 +29,13 @@ public unsafe Log(Raw.Log* handle) _inner = handle; } + /// + /// # Panics + /// + /// + /// - Panics if log directory creation fails. + /// - Panics if tracing initialization fails. + /// public static void InitWithEnv() { unsafe diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathDetectionResult.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathDetectionResult.cs new file mode 100644 index 000000000..644c997e6 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathDetectionResult.cs @@ -0,0 +1,129 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp; + +#nullable enable + +public partial class RDCleanPathDetectionResult: IDisposable +{ + private unsafe Raw.RDCleanPathDetectionResult* _inner; + + public nuint TotalLength + { + get + { + return GetTotalLength(); + } + } + + /// + /// Creates a managed RDCleanPathDetectionResult from a raw handle. + /// + /// + /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free). + ///
+ /// This constructor assumes the raw struct is allocated on Rust side. + /// If implemented, the custom Drop implementation on Rust side WILL run on destruction. + ///
+ public unsafe RDCleanPathDetectionResult(Raw.RDCleanPathDetectionResult* handle) + { + _inner = handle; + } + + public bool IsDetected() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathDetectionResult"); + } + bool retVal = Raw.RDCleanPathDetectionResult.IsDetected(_inner); + return retVal; + } + } + + public bool IsNotEnoughBytes() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathDetectionResult"); + } + bool retVal = Raw.RDCleanPathDetectionResult.IsNotEnoughBytes(_inner); + return retVal; + } + } + + public bool IsFailed() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathDetectionResult"); + } + bool retVal = Raw.RDCleanPathDetectionResult.IsFailed(_inner); + return retVal; + } + } + + /// + public nuint GetTotalLength() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathDetectionResult"); + } + Raw.RdcleanpathFfiResultUsizeBoxIronRdpError result = Raw.RDCleanPathDetectionResult.GetTotalLength(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + nuint retVal = result.Ok; + return retVal; + } + } + + /// + /// Returns the underlying raw handle. + /// + public unsafe Raw.RDCleanPathDetectionResult* AsFFI() + { + return _inner; + } + + /// + /// Destroys the underlying object immediately. + /// + public void Dispose() + { + unsafe + { + if (_inner == null) + { + return; + } + + Raw.RDCleanPathDetectionResult.Destroy(_inner); + _inner = null; + + GC.SuppressFinalize(this); + } + } + + ~RDCleanPathDetectionResult() + { + Dispose(); + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs new file mode 100644 index 000000000..d725b7733 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs @@ -0,0 +1,204 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp; + +#nullable enable + +public partial class RDCleanPathPdu: IDisposable +{ + private unsafe Raw.RDCleanPathPdu* _inner; + + /// + /// Creates a managed RDCleanPathPdu from a raw handle. + /// + /// + /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free). + ///
+ /// This constructor assumes the raw struct is allocated on Rust side. + /// If implemented, the custom Drop implementation on Rust side WILL run on destruction. + ///
+ public unsafe RDCleanPathPdu(Raw.RDCleanPathPdu* handle) + { + _inner = handle; + } + + /// + /// Creates a new RDCleanPath request PDU + /// + /// + /// # Arguments + /// * `x224_pdu` - The X.224 Connection Request PDU bytes + /// * `destination` - The destination RDP server address (e.g., "10.10.0.3:3389") + /// * `proxy_auth` - The JWT authentication token + /// * `pcb` - Optional preconnection blob (for Hyper-V VM connections, empty string if not needed) + /// + /// + /// + /// A RDCleanPathPdu allocated on Rust side. + /// + public static RDCleanPathPdu NewRequest(byte[] x224Pdu, string destination, string proxyAuth, string pcb) + { + unsafe + { + byte[] destinationBuf = DiplomatUtils.StringToUtf8(destination); + byte[] proxyAuthBuf = DiplomatUtils.StringToUtf8(proxyAuth); + byte[] pcbBuf = DiplomatUtils.StringToUtf8(pcb); + nuint x224PduLength = (nuint)x224Pdu.Length; + nuint destinationBufLength = (nuint)destinationBuf.Length; + nuint proxyAuthBufLength = (nuint)proxyAuthBuf.Length; + nuint pcbBufLength = (nuint)pcbBuf.Length; + fixed (byte* x224PduPtr = x224Pdu) + { + fixed (byte* destinationBufPtr = destinationBuf) + { + fixed (byte* proxyAuthBufPtr = proxyAuthBuf) + { + fixed (byte* pcbBufPtr = pcbBuf) + { + Raw.RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError result = Raw.RDCleanPathPdu.NewRequest(x224PduPtr, x224PduLength, destinationBufPtr, destinationBufLength, proxyAuthBufPtr, proxyAuthBufLength, pcbBufPtr, pcbBufLength); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.RDCleanPathPdu* retVal = result.Ok; + return new RDCleanPathPdu(retVal); + } + } + } + } + } + } + + /// + /// Decodes a RDCleanPath PDU from DER-encoded bytes + /// + /// + /// + /// A RDCleanPathPdu allocated on Rust side. + /// + public static RDCleanPathPdu FromDer(byte[] bytes) + { + unsafe + { + nuint bytesLength = (nuint)bytes.Length; + fixed (byte* bytesPtr = bytes) + { + Raw.RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError result = Raw.RDCleanPathPdu.FromDer(bytesPtr, bytesLength); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.RDCleanPathPdu* retVal = result.Ok; + return new RDCleanPathPdu(retVal); + } + } + } + + /// + /// Encodes the RDCleanPath PDU to DER-encoded bytes + /// + /// + /// + /// A VecU8 allocated on Rust side. + /// + public VecU8 ToDer() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + Raw.RdcleanpathFfiResultBoxVecU8BoxIronRdpError result = Raw.RDCleanPathPdu.ToDer(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.VecU8* retVal = result.Ok; + return new VecU8(retVal); + } + } + + /// + /// Detects if the bytes contain a valid RDCleanPath PDU and returns detection result + /// + /// + /// A RDCleanPathDetectionResult allocated on Rust side. + /// + public static RDCleanPathDetectionResult Detect(byte[] bytes) + { + unsafe + { + nuint bytesLength = (nuint)bytes.Length; + fixed (byte* bytesPtr = bytes) + { + Raw.RDCleanPathDetectionResult* retVal = Raw.RDCleanPathPdu.Detect(bytesPtr, bytesLength); + return new RDCleanPathDetectionResult(retVal); + } + } + } + + /// + /// Converts the PDU into a typed enum for pattern matching + /// + /// + /// + /// A RDCleanPathResult allocated on Rust side. + /// + public RDCleanPathResult IntoEnum() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + Raw.RdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError result = Raw.RDCleanPathPdu.IntoEnum(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.RDCleanPathResult* retVal = result.Ok; + return new RDCleanPathResult(retVal); + } + } + + /// + /// Returns the underlying raw handle. + /// + public unsafe Raw.RDCleanPathPdu* AsFFI() + { + return _inner; + } + + /// + /// Destroys the underlying object immediately. + /// + public void Dispose() + { + unsafe + { + if (_inner == null) + { + return; + } + + Raw.RDCleanPathPdu.Destroy(_inner); + _inner = null; + + GC.SuppressFinalize(this); + } + } + + ~RDCleanPathPdu() + { + Dispose(); + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResult.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResult.cs new file mode 100644 index 000000000..c620d6fb2 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResult.cs @@ -0,0 +1,309 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp; + +#nullable enable + +public partial class RDCleanPathResult: IDisposable +{ + private unsafe Raw.RDCleanPathResult* _inner; + + public ushort ErrorCode + { + get + { + return GetErrorCode(); + } + } + + public string ErrorMessage + { + get + { + return GetErrorMessage(); + } + } + + public ushort HttpStatusCode + { + get + { + return GetHttpStatusCode(); + } + } + + public string ServerAddr + { + get + { + return GetServerAddr(); + } + } + + public CertificateChainIterator ServerCertChain + { + get + { + return GetServerCertChain(); + } + } + + public RDCleanPathResultType Type + { + get + { + return GetType(); + } + } + + public VecU8 X224Response + { + get + { + return GetX224Response(); + } + } + + /// + /// Creates a managed RDCleanPathResult from a raw handle. + /// + /// + /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free). + ///
+ /// This constructor assumes the raw struct is allocated on Rust side. + /// If implemented, the custom Drop implementation on Rust side WILL run on destruction. + ///
+ public unsafe RDCleanPathResult(Raw.RDCleanPathResult* handle) + { + _inner = handle; + } + + /// + /// A RDCleanPathResultType allocated on C# side. + /// + public RDCleanPathResultType GetType() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + Raw.RDCleanPathResultType retVal = Raw.RDCleanPathResult.GetType(_inner); + return (RDCleanPathResultType)retVal; + } + } + + /// + /// Gets the X.224 connection response bytes (for Response variant) + /// + /// + /// + /// A VecU8 allocated on Rust side. + /// + public VecU8 GetX224Response() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + Raw.RdcleanpathFfiResultBoxVecU8BoxIronRdpError result = Raw.RDCleanPathResult.GetX224Response(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.VecU8* retVal = result.Ok; + return new VecU8(retVal); + } + } + + /// + /// Gets the server certificate chain (for Response variant) + /// Returns a vector iterator of certificate bytes + /// + /// + /// + /// A CertificateChainIterator allocated on Rust side. + /// + public CertificateChainIterator GetServerCertChain() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + Raw.RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError result = Raw.RDCleanPathResult.GetServerCertChain(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.CertificateChainIterator* retVal = result.Ok; + return new CertificateChainIterator(retVal); + } + } + + /// + /// Gets the server address string (for Response variant) + /// + public void GetServerAddr(DiplomatWriteable writeable) + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + Raw.RDCleanPathResult.GetServerAddr(_inner, &writeable); + } + } + + /// + /// Gets the server address string (for Response variant) + /// + public string GetServerAddr() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + DiplomatWriteable writeable = new DiplomatWriteable(); + Raw.RDCleanPathResult.GetServerAddr(_inner, &writeable); + string retVal = writeable.ToUnicode(); + writeable.Dispose(); + return retVal; + } + } + + /// + /// Gets error message (for GeneralError variant) + /// + public void GetErrorMessage(DiplomatWriteable writeable) + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + Raw.RDCleanPathResult.GetErrorMessage(_inner, &writeable); + } + } + + /// + /// Gets error message (for GeneralError variant) + /// + public string GetErrorMessage() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + DiplomatWriteable writeable = new DiplomatWriteable(); + Raw.RDCleanPathResult.GetErrorMessage(_inner, &writeable); + string retVal = writeable.ToUnicode(); + writeable.Dispose(); + return retVal; + } + } + + /// + /// Gets the error code (for GeneralError variant) + /// + /// + public ushort GetErrorCode() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + Raw.RdcleanpathFfiResultU16BoxIronRdpError result = Raw.RDCleanPathResult.GetErrorCode(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + ushort retVal = result.Ok; + return retVal; + } + } + + /// + /// Gets the HTTP status code if present (for GeneralError variant) + /// Returns 0 if not present or not a GeneralError variant + /// + public ushort GetHttpStatusCode() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + ushort retVal = Raw.RDCleanPathResult.GetHttpStatusCode(_inner); + return retVal; + } + } + + /// + /// Checks if HTTP status code is present + /// + public bool HasHttpStatusCode() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathResult"); + } + bool retVal = Raw.RDCleanPathResult.HasHttpStatusCode(_inner); + return retVal; + } + } + + /// + /// Returns the underlying raw handle. + /// + public unsafe Raw.RDCleanPathResult* AsFFI() + { + return _inner; + } + + /// + /// Destroys the underlying object immediately. + /// + public void Dispose() + { + unsafe + { + if (_inner == null) + { + return; + } + + Raw.RDCleanPathResult.Destroy(_inner); + _inner = null; + + GC.SuppressFinalize(this); + } + } + + ~RDCleanPathResult() + { + Dispose(); + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResultType.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResultType.cs new file mode 100644 index 000000000..4145574e1 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResultType.cs @@ -0,0 +1,20 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp; + +#nullable enable + +public enum RDCleanPathResultType +{ + Request = 0, + Response = 1, + GeneralError = 2, + NegotiationError = 3, +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCertificateChainIterator.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCertificateChainIterator.cs new file mode 100644 index 000000000..c451aa991 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCertificateChainIterator.cs @@ -0,0 +1,31 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct CertificateChainIterator +{ + private const string NativeLib = "DevolutionsIronRdp"; + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CertificateChainIterator_next", ExactSpelling = true)] + public static unsafe extern VecU8* Next(CertificateChainIterator* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CertificateChainIterator_len", ExactSpelling = true)] + public static unsafe extern nuint Len(CertificateChainIterator* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CertificateChainIterator_is_empty", ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U1)] + public static unsafe extern bool IsEmpty(CertificateChainIterator* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CertificateChainIterator_destroy", ExactSpelling = true)] + public static unsafe extern void Destroy(CertificateChainIterator* self); +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawLog.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawLog.cs index e1f649b99..404827024 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawLog.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawLog.cs @@ -16,6 +16,13 @@ public partial struct Log { private const string NativeLib = "DevolutionsIronRdp"; + /// + /// # Panics + /// + /// + /// - Panics if log directory creation fails. + /// - Panics if tracing initialization fails. + /// [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "Log_init_with_env", ExactSpelling = true)] public static unsafe extern void InitWithEnv(); diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathDetectionResult.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathDetectionResult.cs new file mode 100644 index 000000000..a5f7d88d0 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathDetectionResult.cs @@ -0,0 +1,36 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RDCleanPathDetectionResult +{ + private const string NativeLib = "DevolutionsIronRdp"; + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_is_detected", ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U1)] + public static unsafe extern bool IsDetected(RDCleanPathDetectionResult* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_is_not_enough_bytes", ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U1)] + public static unsafe extern bool IsNotEnoughBytes(RDCleanPathDetectionResult* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_is_failed", ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U1)] + public static unsafe extern bool IsFailed(RDCleanPathDetectionResult* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_get_total_length", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultUsizeBoxIronRdpError GetTotalLength(RDCleanPathDetectionResult* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathDetectionResult_destroy", ExactSpelling = true)] + public static unsafe extern void Destroy(RDCleanPathDetectionResult* self); +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs new file mode 100644 index 000000000..6624229f6 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs @@ -0,0 +1,58 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RDCleanPathPdu +{ + private const string NativeLib = "DevolutionsIronRdp"; + + /// + /// Creates a new RDCleanPath request PDU + /// + /// + /// # Arguments + /// * `x224_pdu` - The X.224 Connection Request PDU bytes + /// * `destination` - The destination RDP server address (e.g., "10.10.0.3:3389") + /// * `proxy_auth` - The JWT authentication token + /// * `pcb` - Optional preconnection blob (for Hyper-V VM connections, empty string if not needed) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_new_request", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError NewRequest(byte* x224Pdu, nuint x224PduSz, byte* destination, nuint destinationSz, byte* proxyAuth, nuint proxyAuthSz, byte* pcb, nuint pcbSz); + + /// + /// Decodes a RDCleanPath PDU from DER-encoded bytes + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_from_der", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError FromDer(byte* bytes, nuint bytesSz); + + /// + /// Encodes the RDCleanPath PDU to DER-encoded bytes + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_to_der", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultBoxVecU8BoxIronRdpError ToDer(RDCleanPathPdu* self); + + /// + /// Detects if the bytes contain a valid RDCleanPath PDU and returns detection result + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_detect", ExactSpelling = true)] + public static unsafe extern RDCleanPathDetectionResult* Detect(byte* bytes, nuint bytesSz); + + /// + /// Converts the PDU into a typed enum for pattern matching + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_into_enum", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError IntoEnum(RDCleanPathPdu* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_destroy", ExactSpelling = true)] + public static unsafe extern void Destroy(RDCleanPathPdu* self); +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResult.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResult.cs new file mode 100644 index 000000000..873b90158 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResult.cs @@ -0,0 +1,69 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RDCleanPathResult +{ + private const string NativeLib = "DevolutionsIronRdp"; + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_type", ExactSpelling = true)] + public static unsafe extern RDCleanPathResultType GetType(RDCleanPathResult* self); + + /// + /// Gets the X.224 connection response bytes (for Response variant) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_x224_response", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultBoxVecU8BoxIronRdpError GetX224Response(RDCleanPathResult* self); + + /// + /// Gets the server certificate chain (for Response variant) + /// Returns a vector iterator of certificate bytes + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_server_cert_chain", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError GetServerCertChain(RDCleanPathResult* self); + + /// + /// Gets the server address string (for Response variant) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_server_addr", ExactSpelling = true)] + public static unsafe extern void GetServerAddr(RDCleanPathResult* self, DiplomatWriteable* writeable); + + /// + /// Gets error message (for GeneralError variant) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_error_message", ExactSpelling = true)] + public static unsafe extern void GetErrorMessage(RDCleanPathResult* self, DiplomatWriteable* writeable); + + /// + /// Gets the error code (for GeneralError variant) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_error_code", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultU16BoxIronRdpError GetErrorCode(RDCleanPathResult* self); + + /// + /// Gets the HTTP status code if present (for GeneralError variant) + /// Returns 0 if not present or not a GeneralError variant + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_http_status_code", ExactSpelling = true)] + public static unsafe extern ushort GetHttpStatusCode(RDCleanPathResult* self); + + /// + /// Checks if HTTP status code is present + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_has_http_status_code", ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U1)] + public static unsafe extern bool HasHttpStatusCode(RDCleanPathResult* self); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_destroy", ExactSpelling = true)] + public static unsafe extern void Destroy(RDCleanPathResult* self); +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResultType.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResultType.cs new file mode 100644 index 000000000..393d65092 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResultType.cs @@ -0,0 +1,20 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +public enum RDCleanPathResultType +{ + Request = 0, + Response = 1, + GeneralError = 2, + NegotiationError = 3, +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError.cs new file mode 100644 index 000000000..01cb35908 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError.cs @@ -0,0 +1,46 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError +{ + [StructLayout(LayoutKind.Explicit)] + private unsafe struct InnerUnion + { + [FieldOffset(0)] + internal CertificateChainIterator* ok; + [FieldOffset(0)] + internal IronRdpError* err; + } + + private InnerUnion _inner; + + [MarshalAs(UnmanagedType.U1)] + public bool isOk; + + public unsafe CertificateChainIterator* Ok + { + get + { + return _inner.ok; + } + } + + public unsafe IronRdpError* Err + { + get + { + return _inner.err; + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError.cs new file mode 100644 index 000000000..c9f33a5da --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError.cs @@ -0,0 +1,46 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RdcleanpathFfiResultBoxRDCleanPathPduBoxIronRdpError +{ + [StructLayout(LayoutKind.Explicit)] + private unsafe struct InnerUnion + { + [FieldOffset(0)] + internal RDCleanPathPdu* ok; + [FieldOffset(0)] + internal IronRdpError* err; + } + + private InnerUnion _inner; + + [MarshalAs(UnmanagedType.U1)] + public bool isOk; + + public unsafe RDCleanPathPdu* Ok + { + get + { + return _inner.ok; + } + } + + public unsafe IronRdpError* Err + { + get + { + return _inner.err; + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError.cs new file mode 100644 index 000000000..cf63f3582 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError.cs @@ -0,0 +1,46 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError +{ + [StructLayout(LayoutKind.Explicit)] + private unsafe struct InnerUnion + { + [FieldOffset(0)] + internal RDCleanPathResult* ok; + [FieldOffset(0)] + internal IronRdpError* err; + } + + private InnerUnion _inner; + + [MarshalAs(UnmanagedType.U1)] + public bool isOk; + + public unsafe RDCleanPathResult* Ok + { + get + { + return _inner.ok; + } + } + + public unsafe IronRdpError* Err + { + get + { + return _inner.err; + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxVecU8BoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxVecU8BoxIronRdpError.cs new file mode 100644 index 000000000..7cce2e6ad --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxVecU8BoxIronRdpError.cs @@ -0,0 +1,46 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RdcleanpathFfiResultBoxVecU8BoxIronRdpError +{ + [StructLayout(LayoutKind.Explicit)] + private unsafe struct InnerUnion + { + [FieldOffset(0)] + internal VecU8* ok; + [FieldOffset(0)] + internal IronRdpError* err; + } + + private InnerUnion _inner; + + [MarshalAs(UnmanagedType.U1)] + public bool isOk; + + public unsafe VecU8* Ok + { + get + { + return _inner.ok; + } + } + + public unsafe IronRdpError* Err + { + get + { + return _inner.err; + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultU16BoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultU16BoxIronRdpError.cs new file mode 100644 index 000000000..3eaf32007 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultU16BoxIronRdpError.cs @@ -0,0 +1,46 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RdcleanpathFfiResultU16BoxIronRdpError +{ + [StructLayout(LayoutKind.Explicit)] + private unsafe struct InnerUnion + { + [FieldOffset(0)] + internal ushort ok; + [FieldOffset(0)] + internal IronRdpError* err; + } + + private InnerUnion _inner; + + [MarshalAs(UnmanagedType.U1)] + public bool isOk; + + public unsafe ushort Ok + { + get + { + return _inner.ok; + } + } + + public unsafe IronRdpError* Err + { + get + { + return _inner.err; + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultUsizeBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultUsizeBoxIronRdpError.cs new file mode 100644 index 000000000..eecbdaea4 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultUsizeBoxIronRdpError.cs @@ -0,0 +1,46 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct RdcleanpathFfiResultUsizeBoxIronRdpError +{ + [StructLayout(LayoutKind.Explicit)] + private unsafe struct InnerUnion + { + [FieldOffset(0)] + internal nuint ok; + [FieldOffset(0)] + internal IronRdpError* err; + } + + private InnerUnion _inner; + + [MarshalAs(UnmanagedType.U1)] + public bool isOk; + + public unsafe nuint Ok + { + get + { + return _inner.ok; + } + } + + public unsafe IronRdpError* Err + { + get + { + return _inner.err; + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs index 0a03fd6e0..d617a7136 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs @@ -135,7 +135,7 @@ private static async Task PerformCredsspSteps(ClientConnector connector, string } } - private static async Task ResolveGenerator(CredsspProcessGenerator generator, TcpClient tcpClient) + internal static async Task ResolveGenerator(CredsspProcessGenerator generator, System.Net.Sockets.TcpClient tcpClient) { var state = generator.Start(); NetworkStream? stream = null; @@ -169,10 +169,26 @@ private static async Task ResolveGenerator(CredsspProcessGenerator throw new Exception("Unimplemented protocol"); } } + else if (state.IsCompleted()) + { + try + { + var clientState = state.GetClientStateIfCompleted(); + return clientState; + } + catch (IronRdpException ex) + { + System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Error getting client state: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Error kind: {ex.Inner?.Kind}"); + System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Stack trace: {ex.StackTrace}"); + throw; + } + } else { - var clientState = state.GetClientStateIfCompleted(); - return clientState; + var errorMsg = $"[ResolveGenerator] Generator state is neither suspended nor completed. IsSuspended={state.IsSuspended()}, IsCompleted={state.IsCompleted()}"; + System.Diagnostics.Debug.WriteLine(errorMsg); + throw new InvalidOperationException(errorMsg); } } } diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Framed.cs b/ffi/dotnet/Devolutions.IronRdp/src/Framed.cs index f03530f3d..2e8772e79 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/Framed.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/Framed.cs @@ -134,4 +134,45 @@ public async Task ReadByHint(PduHint pduHint) } } } + + /// + /// Reads data from the buffer based on a custom PDU hint function. + /// + /// A custom hint object implementing IPduHint interface. + /// An asynchronous task that represents the operation. The task result contains the read data as a byte array. + public async Task ReadByHint(IPduHint customHint) + { + while (true) + { + var result = customHint.FindSize(this._buffer.ToArray()); + if (result.HasValue) + { + return await this.ReadExact((nuint)result.Value.Item2); + } + else + { + var len = await this.Read(); + if (len == 0) + { + throw new Exception("EOF"); + } + } + } + } +} + +/// +/// Interface for custom PDU hint implementations. +/// +public interface IPduHint +{ + /// + /// Finds the size of a PDU in the given byte array. + /// + /// The byte array to analyze. + /// + /// A tuple (detected, size) if PDU is detected, null if more bytes are needed. + /// Throws exception if invalid PDU is detected. + /// + (bool, int)? FindSize(byte[] bytes); } \ No newline at end of file diff --git a/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs new file mode 100644 index 000000000..8d03933f4 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs @@ -0,0 +1,309 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Devolutions.IronRdp; + +/// +/// Provides methods for connecting to RDP servers through Devolutions Gateway +/// using the RDCleanPath protocol over WebSocket. +/// +public static class GatewayConnection +{ + /// + /// Connects to an RDP server through a Devolutions Gateway using WebSocket and RDCleanPath protocol. + /// + /// The RDP connection configuration + /// The WebSocket URL to the gateway (e.g., "ws://localhost:7171/jet/rdp") + /// The JWT authentication token for the gateway + /// The destination RDP server address (e.g., "10.10.0.3:3389") + /// Optional preconnection blob for Hyper-V VM connections + /// Optional clipboard backend factory + /// A tuple containing the connection result and framed WebSocket stream + public static async Task<(ConnectionResult, Framed)> ConnectViaGateway( + Config config, + string gatewayUrl, + string authToken, + string destination, + string? pcb = null, + CliprdrBackendFactory? factory = null) + { + // Step 1: Connect WebSocket to gateway + Console.WriteLine($"Connecting to gateway at {gatewayUrl}..."); + var ws = await WebSocketStream.ConnectAsync(new Uri(gatewayUrl)); + var framed = new Framed(ws); + + // Step 2: Get client local address (dummy for WebSocket) + string clientAddr = "127.0.0.1:33899"; + + // Step 3: Setup ClientConnector + var connector = ClientConnector.New(config, clientAddr); + + // Attach optional dynamic/static channels + connector.WithDynamicChannelDisplayControl(); + var dvcPipeProxy = config.DvcPipeProxy; + if (dvcPipeProxy != null) + { + connector.WithDynamicChannelPipeProxy(dvcPipeProxy); + } + + if (factory != null) + { + var cliprdr = factory.BuildCliprdr(); + connector.AttachStaticCliprdr(cliprdr); + } + + // Step 4: Perform RDCleanPath handshake + Console.WriteLine("Performing RDCleanPath handshake..."); + var (serverPublicKey, framedAfterHandshake) = await ConnectRdCleanPath( + framed, connector, destination, authToken, pcb ?? ""); + + // Step 5: Mark security upgrade as done (WebSocket already has TLS) + connector.MarkSecurityUpgradeAsDone(); + + // Step 6: Finalize connection + Console.WriteLine("Finalizing RDP connection..."); + var result = await ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake); + + Console.WriteLine("Gateway connection established successfully!"); + return (result, framedAfterHandshake); + } + + /// + /// Performs the RDCleanPath handshake with the gateway. + /// + private static async Task<(byte[], Framed)> ConnectRdCleanPath( + Framed framed, + ClientConnector connector, + string destination, + string authToken, + string pcb) + { + var writeBuf = WriteBuf.New(); + + // Step 1: Generate X.224 Connection Request + Console.WriteLine("Generating X.224 Connection Request..."); + var written = connector.StepNoInput(writeBuf); + var x224PduSize = (int)written.GetSize().Get(); + var x224Pdu = new byte[x224PduSize]; + writeBuf.ReadIntoBuf(x224Pdu); + + // Step 2: Create and send RDCleanPath Request + Console.WriteLine($"Sending RDCleanPath request to {destination}..."); + var rdCleanPathReq = RDCleanPathPdu.NewRequest(x224Pdu, destination, authToken, pcb); + var reqBytes = rdCleanPathReq.ToDer(); + var reqBytesArray = new byte[reqBytes.GetSize()]; + reqBytes.Fill(reqBytesArray); + await framed.Write(reqBytesArray); + + // Step 3: Read RDCleanPath Response + Console.WriteLine("Waiting for RDCleanPath response..."); + var respBytes = await framed.ReadByHint(new RDCleanPathHint()); + var rdCleanPathResp = RDCleanPathPdu.FromDer(respBytes); + + // Step 4: Parse response + var result = rdCleanPathResp.IntoEnum(); + var resultType = result.GetType(); + + if (resultType == RDCleanPathResultType.Response) + { + Console.WriteLine("RDCleanPath handshake successful!"); + + // Extract X.224 response + var x224Response = result.GetX224Response(); + var x224ResponseBytes = new byte[x224Response.GetSize()]; + x224Response.Fill(x224ResponseBytes); + + // Process X.224 response with connector + writeBuf.Clear(); + connector.Step(x224ResponseBytes, writeBuf); + + // Extract server public key from certificate chain + var certChain = result.GetServerCertChain(); + if (certChain.IsEmpty()) + { + throw new IronRdpLibException( + IronRdpLibExceptionType.ConnectionFailed, + "Server certificate chain is empty"); + } + + var firstCert = certChain.Next(); + if (firstCert == null) + { + throw new IronRdpLibException( + IronRdpLibExceptionType.ConnectionFailed, + "Failed to get first certificate from chain"); + } + + var certBytes = new byte[firstCert.GetSize()]; + firstCert.Fill(certBytes); + + var serverPublicKey = ExtractPublicKeyFromX509(certBytes); + + Console.WriteLine($"Extracted server public key (length: {serverPublicKey.Length})"); + + return (serverPublicKey, framed); + } + else if (resultType == RDCleanPathResultType.GeneralError) + { + var errorCode = result.GetErrorCode(); + var errorMessage = result.GetErrorMessage(); + throw new IronRdpLibException( + IronRdpLibExceptionType.ConnectionFailed, + $"RDCleanPath error (code {errorCode}): {errorMessage}"); + } + else if (resultType == RDCleanPathResultType.NegotiationError) + { + throw new IronRdpLibException( + IronRdpLibExceptionType.ConnectionFailed, + "RDCleanPath negotiation error: Server rejected connection parameters"); + } + else + { + throw new IronRdpLibException( + IronRdpLibExceptionType.ConnectionFailed, + $"Unexpected RDCleanPath response type: {resultType}"); + } + } + + /// + /// Finalizes the RDP connection after RDCleanPath handshake. + /// + private static async Task ConnectFinalize( + string serverName, + ClientConnector connector, + byte[] serverPubKey, + Framed framedSsl) + { + var writeBuf = WriteBuf.New(); + + // Perform CredSSP if needed + if (connector.ShouldPerformCredssp()) + { + Console.WriteLine("Performing CredSSP authentication..."); + await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey); + } + + // Continue with remaining connection steps + Console.WriteLine("Completing connection sequence..."); + while (!connector.GetDynState().IsTerminal()) + { + await Connection.SingleSequenceStep(connector, writeBuf, framedSsl); + } + + // Get final connection result + ClientConnectorState state = connector.ConsumeAndCastToClientConnectorState(); + + if (state.GetEnumType() == ClientConnectorStateType.Connected) + { + return state.GetConnectedResult(); + } + else + { + throw new IronRdpLibException( + IronRdpLibExceptionType.ConnectionFailed, + "Connection failed after RDCleanPath handshake"); + } + } + + /// + /// Performs CredSSP authentication steps. + /// + private static async Task PerformCredsspSteps( + ClientConnector connector, + string serverName, + WriteBuf writeBuf, + Framed framedSsl, + byte[] serverpubkey) + { + // Extract hostname from "hostname:port" format for CredSSP + // CredSSP needs just the hostname for the service principal name (TERMSRV/hostname) + var hostname = serverName; + var colonIndex = serverName.IndexOf(':'); + if (colonIndex > 0) + { + hostname = serverName.Substring(0, colonIndex); + } + + var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, null); + var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); + var tsRequest = credsspSequenceInitResult.GetTsRequest(); + var tcpClient = new System.Net.Sockets.TcpClient(); + + while (true) + { + var generator = credsspSequence.ProcessTsRequest(tsRequest); + var clientState = await Connection.ResolveGenerator(generator, tcpClient); + writeBuf.Clear(); + var written = credsspSequence.HandleProcessResult(clientState, writeBuf); + + if (written.GetSize().IsSome()) + { + var actualSize = (int)written.GetSize().Get(); + var response = new byte[actualSize]; + writeBuf.ReadIntoBuf(response); + await framedSsl.Write(response); + } + + var pduHint = credsspSequence.NextPduHint(); + if (pduHint == null) + { + break; + } + + var pdu = await framedSsl.ReadByHint(pduHint); + var decoded = credsspSequence.DecodeServerMessage(pdu); + + if (null == decoded) + { + break; + } + + tsRequest = decoded; + } + } + + /// + /// Extracts the public key from an X.509 certificate in DER format. + /// + private static byte[] ExtractPublicKeyFromX509(byte[] certDer) + { + try + { + var cert = new X509Certificate2(certDer); + return cert.GetPublicKey(); + } + catch (Exception ex) + { + throw new IronRdpLibException( + IronRdpLibExceptionType.ConnectionFailed, + $"Failed to extract public key from certificate: {ex.Message}"); + } + } +} + +/// +/// PDU hint for detecting RDCleanPath PDUs in the stream. +/// +public class RDCleanPathHint : IPduHint +{ + public (bool, int)? FindSize(byte[] bytes) + { + var detection = RDCleanPathPdu.Detect(bytes); + + if (detection.IsDetected()) + { + var totalLength = (int)detection.GetTotalLength(); + return (true, totalLength); + } + + if (detection.IsNotEnoughBytes()) + { + return null; // Need more bytes + } + + // Detection failed + throw new IronRdpLibException( + IronRdpLibExceptionType.ConnectionFailed, + "Invalid RDCleanPath PDU detected"); + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs new file mode 100644 index 000000000..883b8d43d --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs @@ -0,0 +1,173 @@ +using System; +using System.Buffers; +using System.IO; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +public sealed class WebSocketStream : Stream +{ + private readonly ClientWebSocket _ws; + private readonly byte[] _recvBuf; + private int _recvPos; + private int _recvLen; + private bool _remoteClosed; + private bool _disposed; + + private const int DefaultRecvBufferSize = 64 * 1024; + private const int MaxSendFrame = 16 * 1024; // send in chunks + + private WebSocketStream(ClientWebSocket ws, int receiveBufferSize) + { + _ws = ws ?? throw new ArgumentNullException(nameof(ws)); + _recvBuf = ArrayPool.Shared.Rent(Math.Max(1024, receiveBufferSize)); + } + + public static async Task ConnectAsync( + Uri uri, + ClientWebSocket? ws = null, + int receiveBufferSize = DefaultRecvBufferSize, + CancellationToken ct = default) + { + ws ??= new ClientWebSocket(); + await ws.ConnectAsync(uri, ct).ConfigureAwait(false); + return new WebSocketStream(ws, receiveBufferSize); + } + + public ClientWebSocket Socket => _ws; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override void Flush() { /* no-op */ } + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public override int Read(byte[] buffer, int offset, int count) => + ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); + + public override void Write(byte[] buffer, int offset, int count) => + WriteAsync(buffer.AsMemory(offset, count)).GetAwaiter().GetResult(); + + public override async ValueTask ReadAsync( + Memory destination, CancellationToken cancellationToken = default) + { + if (_disposed) throw new ObjectDisposedException(nameof(WebSocketStream)); + if (_remoteClosed) return 0; + if (destination.Length == 0) return 0; + + // Fill local buffer if empty + if (_recvLen == 0) + { + var mem = _recvBuf.AsMemory(); + while (true) + { + var result = await _ws.ReceiveAsync(mem, cancellationToken).ConfigureAwait(false); + + // Close frame → signal EOF + if (result.MessageType == WebSocketMessageType.Close) + { + _remoteClosed = true; + try { await _ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "OK", cancellationToken).ConfigureAwait(false); } + catch { /* ignore */ } + return 0; + } + + if (result.MessageType == WebSocketMessageType.Text) + throw new InvalidOperationException("Received TEXT frame; this stream expects BINARY."); + + // Some data arrived + if (result.Count > 0) + { + _recvPos = 0; + _recvLen = result.Count; + break; + } + + // Keep looping if Count == 0 (can happen with pings/keepers) + } + } + + var toCopy = Math.Min(destination.Length, _recvLen); + new ReadOnlySpan(_recvBuf, _recvPos, toCopy).CopyTo(destination.Span); + _recvPos += toCopy; + _recvLen -= toCopy; + + // If we've drained local buffer, try to prefetch next chunk (non-blocking behavior not guaranteed) + if (_recvLen == 0 && _ws.State == WebSocketState.Open) + { + // optional prefetch: not strictly necessary—kept simple + } + + return toCopy; + } + + public override async Task WriteAsync( + byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await WriteAsync(buffer.AsMemory(offset, count), cancellationToken); + + public override async ValueTask WriteAsync( + ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + if (_disposed) throw new ObjectDisposedException(nameof(WebSocketStream)); + if (_ws.State != WebSocketState.Open) throw new IOException("WebSocket is not open."); + + // Treat each Write* as one complete WebSocket message (Binary). + // Chunk large payloads as continuation frames and set EndOfMessage on the last chunk. + int sent = 0; + while (sent < source.Length) + { + var chunkLen = Math.Min(MaxSendFrame, source.Length - sent); + var chunk = source.Slice(sent, chunkLen); + sent += chunkLen; + + bool end = (sent == source.Length); + await _ws.SendAsync(chunk, WebSocketMessageType.Binary, end, cancellationToken).ConfigureAwait(false); + } + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (_disposed) return; + if (disposing) + { + try + { + if (_ws.State == WebSocketState.Open) + { + _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disposing", CancellationToken.None) + .GetAwaiter().GetResult(); + } + } + catch { /* ignore on dispose */ } + _ws.Dispose(); + ArrayPool.Shared.Return(_recvBuf); + } + _disposed = true; + base.Dispose(disposing); + } + +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + public override async ValueTask DisposeAsync() + { + if (!_disposed) + { + try + { + if (_ws.State == WebSocketState.Open) + await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disposing", CancellationToken.None).ConfigureAwait(false); + } + catch { /* ignore */ } + _ws.Dispose(); + ArrayPool.Shared.Return(_recvBuf); + _disposed = true; + } + await base.DisposeAsync().ConfigureAwait(false); + } +#endif +} diff --git a/ffi/src/error.rs b/ffi/src/error.rs index 9312ba45f..476d8a379 100644 --- a/ffi/src/error.rs +++ b/ffi/src/error.rs @@ -6,6 +6,7 @@ use ironrdp::connector::ConnectorError; use ironrdp::session::SessionError; #[cfg(target_os = "windows")] use ironrdp_cliprdr_native::WinCliprdrError; +use ironrdp_rdcleanpath::der; use self::ffi::IronRdpErrorKind; @@ -93,6 +94,32 @@ impl From for IronRdpErrorKind { } } +impl From for IronRdpErrorKind { + fn from(_val: der::Error) -> Self { + IronRdpErrorKind::DecodeError + } +} + +impl From for IronRdpErrorKind { + fn from(_val: ironrdp_rdcleanpath::MissingRDCleanPathField) -> Self { + IronRdpErrorKind::Generic + } +} + +pub struct GenericError(pub String); + +impl Display for GenericError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for IronRdpErrorKind { + fn from(_val: GenericError) -> Self { + IronRdpErrorKind::Generic + } +} + impl From for Box where T: Into + ToString, diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 407847ce4..44009cd5a 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -11,6 +11,7 @@ pub mod graphics; pub mod input; pub mod log; pub mod pdu; +pub mod rdcleanpath; pub mod session; pub mod svc; pub mod utils; diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs new file mode 100644 index 000000000..142244730 --- /dev/null +++ b/ffi/src/rdcleanpath.rs @@ -0,0 +1,216 @@ +#[diplomat::bridge] +pub mod ffi { + use core::fmt::Write as _; + use diplomat_runtime::DiplomatWriteable; + + use crate::error::ffi::IronRdpError; + use crate::error::GenericError; + use crate::utils::ffi::VecU8; + + #[diplomat::opaque] + pub struct RDCleanPathPdu(pub ironrdp_rdcleanpath::RDCleanPathPdu); + + impl RDCleanPathPdu { + /// Creates a new RDCleanPath request PDU + /// + /// # Arguments + /// * `x224_pdu` - The X.224 Connection Request PDU bytes + /// * `destination` - The destination RDP server address (e.g., "10.10.0.3:3389") + /// * `proxy_auth` - The JWT authentication token + /// * `pcb` - Optional preconnection blob (for Hyper-V VM connections, empty string if not needed) + pub fn new_request( + x224_pdu: &[u8], + destination: &str, + proxy_auth: &str, + pcb: &str, + ) -> Result, Box> { + let pcb_opt = if pcb.is_empty() { None } else { Some(pcb.to_owned()) }; + + let pdu = ironrdp_rdcleanpath::RDCleanPathPdu::new_request( + x224_pdu.to_vec(), + destination.to_owned(), + proxy_auth.to_owned(), + pcb_opt, + ) + .map_err(|e| GenericError(format!("Failed to create RDCleanPath request: {e}")))?; + + Ok(Box::new(RDCleanPathPdu(pdu))) + } + + /// Decodes a RDCleanPath PDU from DER-encoded bytes + pub fn from_der(bytes: &[u8]) -> Result, Box> { + let pdu = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(bytes) + .map_err(|e| GenericError(format!("Failed to decode RDCleanPath PDU: {e}")))?; + + Ok(Box::new(RDCleanPathPdu(pdu))) + } + + /// Encodes the RDCleanPath PDU to DER-encoded bytes + pub fn to_der(&self) -> Result, Box> { + let bytes = self + .0 + .to_der() + .map_err(|e| GenericError(format!("Failed to encode RDCleanPath PDU: {e}")))?; + + Ok(Box::new(VecU8(bytes))) + } + + /// Detects if the bytes contain a valid RDCleanPath PDU and returns detection result + pub fn detect(bytes: &[u8]) -> Box { + let result = ironrdp_rdcleanpath::RDCleanPathPdu::detect(bytes); + Box::new(RDCleanPathDetectionResult(result)) + } + + /// Converts the PDU into a typed enum for pattern matching + pub fn into_enum(&self) -> Result, Box> { + let rdcleanpath = self + .0 + .clone() + .into_enum() + .map_err(|e| GenericError(format!("Missing RDCleanPath field: {e}")))?; + + Ok(Box::new(RDCleanPathResult(rdcleanpath))) + } + } + + #[diplomat::opaque] + pub struct RDCleanPathDetectionResult(pub ironrdp_rdcleanpath::DetectionResult); + + impl RDCleanPathDetectionResult { + pub fn is_detected(&self) -> bool { + matches!(self.0, ironrdp_rdcleanpath::DetectionResult::Detected { .. }) + } + + pub fn is_not_enough_bytes(&self) -> bool { + matches!(self.0, ironrdp_rdcleanpath::DetectionResult::NotEnoughBytes) + } + + pub fn is_failed(&self) -> bool { + matches!(self.0, ironrdp_rdcleanpath::DetectionResult::Failed) + } + + pub fn get_total_length(&self) -> Result> { + if let ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } = self.0 { + Ok(total_length) + } else { + Err(GenericError("Detection result is not Detected variant".into()).into()) + } + } + } + + #[diplomat::opaque] + pub struct RDCleanPathResult(pub ironrdp_rdcleanpath::RDCleanPath); + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum RDCleanPathResultType { + Request, + Response, + GeneralError, + NegotiationError, + } + + impl RDCleanPathResult { + pub fn get_type(&self) -> RDCleanPathResultType { + match &self.0 { + ironrdp_rdcleanpath::RDCleanPath::Request { .. } => RDCleanPathResultType::Request, + ironrdp_rdcleanpath::RDCleanPath::Response { .. } => RDCleanPathResultType::Response, + ironrdp_rdcleanpath::RDCleanPath::GeneralErr(_) => RDCleanPathResultType::GeneralError, + ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { .. } => RDCleanPathResultType::NegotiationError, + } + } + + /// Gets the X.224 connection response bytes (for Response variant) + pub fn get_x224_response(&self) -> Result, Box> { + match &self.0 { + ironrdp_rdcleanpath::RDCleanPath::Response { + x224_connection_response, + .. + } => Ok(Box::new(VecU8(x224_connection_response.as_bytes().to_vec()))), + ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { + x224_connection_response, + } => Ok(Box::new(VecU8(x224_connection_response.clone()))), + _ => Err(GenericError("RDCleanPath variant does not contain X.224 response".into()).into()), + } + } + + /// Gets the server certificate chain (for Response variant) + /// Returns a vector iterator of certificate bytes + pub fn get_server_cert_chain(&self) -> Result, Box> { + match &self.0 { + ironrdp_rdcleanpath::RDCleanPath::Response { server_cert_chain, .. } => { + let certs: Vec> = server_cert_chain.iter().map(|cert| cert.as_bytes().to_vec()).collect(); + Ok(Box::new(CertificateChainIterator { certs, index: 0 })) + } + _ => Err(GenericError("RDCleanPath variant does not contain certificate chain".into()).into()), + } + } + + /// Gets the server address string (for Response variant) + pub fn get_server_addr<'a>(&'a self, writeable: &'a mut DiplomatWriteable) { + if let ironrdp_rdcleanpath::RDCleanPath::Response { server_addr, .. } = &self.0 { + let _ = write!(writeable, "{server_addr}"); + } + } + + /// Gets error message (for GeneralError variant) + pub fn get_error_message<'a>(&'a self, writeable: &'a mut DiplomatWriteable) { + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { + let _ = write!(writeable, "{err}"); + } + } + + /// Gets the error code (for GeneralError variant) + pub fn get_error_code(&self) -> Result> { + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { + Ok(err.error_code) + } else { + Err(GenericError("Not a GeneralError variant".into()).into()) + } + } + + /// Gets the HTTP status code if present (for GeneralError variant) + /// Returns 0 if not present or not a GeneralError variant + pub fn get_http_status_code(&self) -> u16 { + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { + err.http_status_code.unwrap_or(0) + } else { + 0 + } + } + + /// Checks if HTTP status code is present + pub fn has_http_status_code(&self) -> bool { + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { + err.http_status_code.is_some() + } else { + false + } + } + } + + #[diplomat::opaque] + pub struct CertificateChainIterator { + certs: Vec>, + index: usize, + } + + impl CertificateChainIterator { + pub fn next(&mut self) -> Option> { + if self.index < self.certs.len() { + let cert = self.certs[self.index].clone(); + self.index += 1; + Some(Box::new(VecU8(cert))) + } else { + None + } + } + + pub fn len(&self) -> usize { + self.certs.len() + } + + pub fn is_empty(&self) -> bool { + self.certs.is_empty() + } + } +} From db87b15bc90543549131330f02952fae6abcf44c Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 7 Oct 2025 16:09:04 -0400 Subject: [PATCH 02/25] clean up --- crates/ironrdp-connector/src/credssp.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/ironrdp-connector/src/credssp.rs b/crates/ironrdp-connector/src/credssp.rs index 3bfc9de5c..5a5752aaa 100644 --- a/crates/ironrdp-connector/src/credssp.rs +++ b/crates/ironrdp-connector/src/credssp.rs @@ -100,15 +100,7 @@ impl CredsspSequence { server_public_key: Vec, kerberos_config: Option, ) -> ConnectorResult<(Self, credssp::TsRequest)> { - info!( - ?credentials, - ?domain, - ?protocol, - ?server_name, - ?server_public_key, - ?kerberos_config, - "Initialize CredSSP sequence" - ); + let credentials: sspi::Credentials = match &credentials { Credentials::UsernamePassword { username, password } => { let username = Username::new(username, domain).map_err(|e| custom_err!("invalid username", e))?; From a57e529d8a8ec3d643d465d6468befdfe9ae5530 Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 7 Oct 2025 17:14:19 -0400 Subject: [PATCH 03/25] feat(ffi): add KDC proxy support for .NET MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add KerberosConfig FFI bindings and HTTP/HTTPS network protocol handling for KDC proxy authentication via Devolutions Gateway. - Expose KerberosConfig::new() in FFI layer - Add GenerateKdcToken() method to TokenGenerator - Update GatewayConnection to accept KDC proxy parameters - Implement HTTP/HTTPS request handling in ResolveGenerator - Auto-generate client hostname for Kerberos authentication 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../MainWindow.axaml.cs | 63 +++++++++++++++++-- .../TokenGenerator.cs | 50 +++++++++++++++ .../Generated/KerberosConfig.cs | 36 +++++++++++ ...iResultBoxKerberosConfigBoxIronRdpError.cs | 46 ++++++++++++++ .../Generated/RawKerberosConfig.cs | 11 ++++ .../Devolutions.IronRdp/src/Connection.cs | 54 +++++++++++++--- .../src/GatewayConnection.cs | 28 +++++++-- ffi/src/credssp/mod.rs | 30 +++++++++ 8 files changed, 298 insertions(+), 20 deletions(-) create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs index ade51a205..40310aa4d 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs @@ -87,6 +87,11 @@ private void OnOpened(object? sender, EventArgs e) var gatewayToken = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_TOKEN"); var tokengenUrl = Environment.GetEnvironmentVariable("IRONRDP_TOKENGEN_URL"); + // NEW: KDC proxy configuration (optional) + var kdcProxyUrlBase = Environment.GetEnvironmentVariable("IRONRDP_KDC_PROXY_URL"); + var kdcRealm = Environment.GetEnvironmentVariable("IRONRDP_KDC_REALM"); + var kdcServer = Environment.GetEnvironmentVariable("IRONRDP_KDC_SERVER"); + if (username == null || password == null || server == null) { var errorMessage = @@ -148,11 +153,12 @@ private void OnOpened(object? sender, EventArgs e) Trace.TraceInformation($"Gateway URL: {gatewayUrl}"); Trace.TraceInformation($"Destination: {server}:{port}"); - // Generate token if not provided + var tokenGen = new TokenGenerator(tokengenUrl ?? "http://localhost:8080"); + + // Generate RDP token if not provided if (string.IsNullOrEmpty(gatewayToken)) { - Trace.TraceInformation("No token provided, generating token..."); - var tokenGen = new TokenGenerator(tokengenUrl ?? "http://localhost:8080"); + Trace.TraceInformation("No RDP token provided, generating token..."); try { @@ -163,21 +169,66 @@ private void OnOpened(object? sender, EventArgs e) destUser: username!, destPassword: password! ); - Trace.TraceInformation($"Token generated successfully (length: {gatewayToken.Length})"); + Trace.TraceInformation($"RDP token generated successfully (length: {gatewayToken.Length})"); } catch (Exception ex) { - Trace.TraceError($"Failed to generate token: {ex.Message}"); + Trace.TraceError($"Failed to generate RDP token: {ex.Message}"); Trace.TraceInformation("Make sure tokengen server is running:"); Trace.TraceInformation($" cargo run --manifest-path D:/devolutions-gateway/tools/tokengen/Cargo.toml -- server"); throw; } } + // Generate KDC token if KDC proxy is enabled + string? kdcProxyUrl = null; + if (!string.IsNullOrEmpty(kdcRealm) && !string.IsNullOrEmpty(kdcServer)) + { + Trace.TraceInformation("=== KDC PROXY MODE ENABLED ==="); + Trace.TraceInformation($"KDC Realm: {kdcRealm}"); + Trace.TraceInformation($"KDC Server: {kdcServer}"); + + try + { + var kdcToken = await tokenGen.GenerateKdcToken( + krbRealm: kdcRealm!, + krbKdc: kdcServer! + ); + Trace.TraceInformation($"KDC token generated successfully (length: {kdcToken.Length})"); + + // Build KDC proxy URL - use explicit URL if provided, otherwise auto-construct from gateway URL + if (!string.IsNullOrEmpty(kdcProxyUrlBase)) + { + kdcProxyUrl = $"{kdcProxyUrlBase.TrimEnd('/')}/{kdcToken}"; + Trace.TraceInformation($"Using explicit KDC Proxy URL: {kdcProxyUrl}"); + } + else + { + var gatewayBaseUrl = new Uri(gatewayUrl.Replace("/jet/rdp", "")).GetLeftPart(UriPartial.Authority); + kdcProxyUrl = $"{gatewayBaseUrl}/KdcProxy/{kdcToken}"; + Trace.TraceInformation($"Auto-constructed KDC Proxy URL: {kdcProxyUrl}"); + } + } + catch (Exception ex) + { + Trace.TraceError($"Failed to generate KDC token: {ex.Message}"); + Trace.TraceWarning("Continuing without KDC proxy..."); + } + } + // Connect via gateway - destination needs "hostname:port" format for RDCleanPath string destination = $"{server}:{port}"; + + // Get client hostname for Kerberos authentication + string? kdcClientHostname = null; + if (!string.IsNullOrEmpty(kdcProxyUrl)) + { + kdcClientHostname = System.Net.Dns.GetHostName(); + Trace.TraceInformation($"Client hostname for Kerberos: {kdcClientHostname}"); + } + var (gatewayRes, gatewayFramed) = await GatewayConnection.ConnectViaGateway( - config, gatewayUrl, gatewayToken!, destination, null, factory); + config, gatewayUrl, gatewayToken!, destination, null, factory, kdcProxyUrl, kdcClientHostname); res = gatewayRes; this._framed = new Framed(gatewayFramed.GetInner().Item1); diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs index 00946c604..f23a8094a 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs @@ -122,6 +122,44 @@ public async Task GenerateForwardToken( } } + /// + /// Generates a KDC proxy token for Kerberos authentication through the gateway. + /// + /// Kerberos realm (e.g., "AD.EXAMPLE.COM") + /// KDC address with protocol (e.g., "tcp://dc.ad.example.com:88") + /// Token validity in seconds (default: 3600) + /// A JWT token string + public async Task GenerateKdcToken( + string krbRealm, + string krbKdc, + int validityDuration = 3600) + { + var request = new KdcTokenRequest + { + KrbRealm = krbRealm, + KrbKdc = krbKdc, + ValidityDuration = validityDuration + }; + + try + { + var response = await _client.PostAsJsonAsync($"{_tokengenUrl}/kdc", request); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + if (result?.Token == null) + { + throw new Exception("KDC token generation failed: Empty response"); + } + + return result.Token; + } + catch (HttpRequestException ex) + { + throw new Exception($"Failed to generate KDC token from {_tokengenUrl}: {ex.Message}", ex); + } + } + /// /// Checks if the tokengen server is reachable. /// @@ -184,6 +222,18 @@ private class ForwardTokenRequest public int ValidityDuration { get; set; } } + private class KdcTokenRequest + { + [JsonPropertyName("krb_realm")] + public string KrbRealm { get; set; } = string.Empty; + + [JsonPropertyName("krb_kdc")] + public string KrbKdc { get; set; } = string.Empty; + + [JsonPropertyName("validity_duration")] + public int ValidityDuration { get; set; } + } + private class TokenResponse { [JsonPropertyName("token")] diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs index 0cb7f570b..ac310c85c 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs @@ -29,6 +29,42 @@ public unsafe KerberosConfig(Raw.KerberosConfig* handle) _inner = handle; } + /// + /// Creates a new KerberosConfig for KDC proxy support. + /// + /// + /// # Arguments + /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used + /// * `hostname` - Client hostname for Kerberos, empty string if not used + /// + /// + /// + /// A KerberosConfig allocated on Rust side. + /// + public static KerberosConfig New(string kdcProxyUrl, string hostname) + { + unsafe + { + byte[] kdcProxyUrlBuf = DiplomatUtils.StringToUtf8(kdcProxyUrl); + byte[] hostnameBuf = DiplomatUtils.StringToUtf8(hostname); + nuint kdcProxyUrlBufLength = (nuint)kdcProxyUrlBuf.Length; + nuint hostnameBufLength = (nuint)hostnameBuf.Length; + fixed (byte* kdcProxyUrlBufPtr = kdcProxyUrlBuf) + { + fixed (byte* hostnameBufPtr = hostnameBuf) + { + Raw.CredsspFfiResultBoxKerberosConfigBoxIronRdpError result = Raw.KerberosConfig.New(kdcProxyUrlBufPtr, kdcProxyUrlBufLength, hostnameBufPtr, hostnameBufLength); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.KerberosConfig* retVal = result.Ok; + return new KerberosConfig(retVal); + } + } + } + } + /// /// Returns the underlying raw handle. /// diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs new file mode 100644 index 000000000..1a08f6b2c --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs @@ -0,0 +1,46 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct CredsspFfiResultBoxKerberosConfigBoxIronRdpError +{ + [StructLayout(LayoutKind.Explicit)] + private unsafe struct InnerUnion + { + [FieldOffset(0)] + internal KerberosConfig* ok; + [FieldOffset(0)] + internal IronRdpError* err; + } + + private InnerUnion _inner; + + [MarshalAs(UnmanagedType.U1)] + public bool isOk; + + public unsafe KerberosConfig* Ok + { + get + { + return _inner.ok; + } + } + + public unsafe IronRdpError* Err + { + get + { + return _inner.err; + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs index 410ff439b..c50d8ca74 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs @@ -16,6 +16,17 @@ public partial struct KerberosConfig { private const string NativeLib = "DevolutionsIronRdp"; + /// + /// Creates a new KerberosConfig for KDC proxy support. + /// + /// + /// # Arguments + /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used + /// * `hostname` - Client hostname for Kerberos, empty string if not used + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "KerberosConfig_new", ExactSpelling = true)] + public static unsafe extern CredsspFfiResultBoxKerberosConfigBoxIronRdpError New(byte* kdcProxyUrl, nuint kdcProxyUrlSz, byte* hostname, nuint hostnameSz); + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "KerberosConfig_destroy", ExactSpelling = true)] public static unsafe extern void Destroy(KerberosConfig* self); } diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs index d617a7136..13a739cb9 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs @@ -139,6 +139,8 @@ internal static async Task ResolveGenerator(CredsspProcessGenerator { var state = generator.Start(); NetworkStream? stream = null; + HttpClient? httpClient = null; + while (true) { if (state.IsSuspended()) @@ -147,16 +149,17 @@ internal static async Task ResolveGenerator(CredsspProcessGenerator var protocol = request.GetProtocol(); var url = request.GetUrl(); var data = request.GetData(); - if (null == stream) - { - url = url.Replace("tcp://", ""); - var split = url.Split(":"); - await tcpClient.ConnectAsync(split[0], int.Parse(split[1])); - stream = tcpClient.GetStream(); - } if (protocol == NetworkRequestProtocol.Tcp) { + if (null == stream) + { + url = url.Replace("tcp://", ""); + var split = url.Split(":"); + await tcpClient.ConnectAsync(split[0], int.Parse(split[1])); + stream = tcpClient.GetStream(); + } + stream.Write(Utils.VecU8ToByte(data)); var readBuf = new byte[8096]; var readlen = await stream.ReadAsync(readBuf, 0, readBuf.Length); @@ -164,9 +167,44 @@ internal static async Task ResolveGenerator(CredsspProcessGenerator Array.Copy(readBuf, actuallyRead, readlen); state = generator.Resume(actuallyRead); } + else if (protocol == NetworkRequestProtocol.Http || protocol == NetworkRequestProtocol.Https) + { + // Handle HTTP/HTTPS requests for KDC proxy (mimics ironrdp-web implementation) + if (httpClient == null) + { + httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("keep-alive", "true"); + } + + System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Sending {protocol} request to {url}"); + + var bodyBytes = Utils.VecU8ToByte(data); + var content = new ByteArrayContent(bodyBytes); + + HttpResponseMessage response; + try + { + response = await httpClient.PostAsync(url, content); + } + catch (HttpRequestException ex) + { + throw new Exception($"Failed to send KDC request to {url}: {ex.Message}", ex); + } + + if (!response.IsSuccessStatusCode) + { + throw new Exception( + $"KdcProxy HTTP status error ({(int)response.StatusCode} {response.ReasonPhrase})"); + } + + var responseData = await response.Content.ReadAsByteArrayAsync(); + System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Received {responseData.Length} bytes from KDC proxy"); + + state = generator.Resume(responseData); + } else { - throw new Exception("Unimplemented protocol"); + throw new Exception($"Unimplemented protocol: {protocol}"); } } else if (state.IsCompleted()) diff --git a/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs index 8d03933f4..94a59f9af 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs @@ -18,6 +18,8 @@ public static class GatewayConnection /// The destination RDP server address (e.g., "10.10.0.3:3389") /// Optional preconnection blob for Hyper-V VM connections /// Optional clipboard backend factory + /// Optional KDC proxy URL with token (e.g., "https://gateway.example.com/KdcProxy/{token}") + /// Optional client hostname for Kerberos /// A tuple containing the connection result and framed WebSocket stream public static async Task<(ConnectionResult, Framed)> ConnectViaGateway( Config config, @@ -25,7 +27,9 @@ public static class GatewayConnection string authToken, string destination, string? pcb = null, - CliprdrBackendFactory? factory = null) + CliprdrBackendFactory? factory = null, + string? kdcProxyUrl = null, + string? kdcHostname = null) { // Step 1: Connect WebSocket to gateway Console.WriteLine($"Connecting to gateway at {gatewayUrl}..."); @@ -62,7 +66,7 @@ public static class GatewayConnection // Step 6: Finalize connection Console.WriteLine("Finalizing RDP connection..."); - var result = await ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake); + var result = await ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake, kdcProxyUrl, kdcHostname); Console.WriteLine("Gateway connection established successfully!"); return (result, framedAfterHandshake); @@ -172,7 +176,9 @@ private static async Task ConnectFinalize( string serverName, ClientConnector connector, byte[] serverPubKey, - Framed framedSsl) + Framed framedSsl, + string? kdcProxyUrl, + string? kdcHostname) { var writeBuf = WriteBuf.New(); @@ -180,7 +186,7 @@ private static async Task ConnectFinalize( if (connector.ShouldPerformCredssp()) { Console.WriteLine("Performing CredSSP authentication..."); - await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey); + await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey, kdcProxyUrl, kdcHostname); } // Continue with remaining connection steps @@ -213,7 +219,9 @@ private static async Task PerformCredsspSteps( string serverName, WriteBuf writeBuf, Framed framedSsl, - byte[] serverpubkey) + byte[] serverpubkey, + string? kdcProxyUrl, + string? kdcHostname) { // Extract hostname from "hostname:port" format for CredSSP // CredSSP needs just the hostname for the service principal name (TERMSRV/hostname) @@ -224,7 +232,15 @@ private static async Task PerformCredsspSteps( hostname = serverName.Substring(0, colonIndex); } - var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, null); + // Create KerberosConfig if KDC proxy URL is provided + KerberosConfig? kerberosConfig = null; + if (!string.IsNullOrEmpty(kdcProxyUrl)) + { + Console.WriteLine($"Using KDC proxy: {kdcProxyUrl}"); + kerberosConfig = KerberosConfig.New(kdcProxyUrl, kdcHostname ?? ""); + } + + var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, kerberosConfig); var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); var tsRequest = credsspSequenceInitResult.GetTsRequest(); var tcpClient = new System.Net.Sockets.TcpClient(); diff --git a/ffi/src/credssp/mod.rs b/ffi/src/credssp/mod.rs index b912d2685..ae6efd688 100644 --- a/ffi/src/credssp/mod.rs +++ b/ffi/src/credssp/mod.rs @@ -16,6 +16,36 @@ pub mod ffi { #[diplomat::opaque] pub struct KerberosConfig(pub ironrdp::connector::credssp::KerberosConfig); + impl KerberosConfig { + /// Creates a new KerberosConfig for KDC proxy support. + /// + /// # Arguments + /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used + /// * `hostname` - Client hostname for Kerberos, empty string if not used + pub fn new( + kdc_proxy_url: &str, + hostname: &str, + ) -> Result, Box> { + let kdc_proxy_url_opt = if kdc_proxy_url.is_empty() { + None + } else { + Some(kdc_proxy_url.to_owned()) + }; + + let hostname_opt = if hostname.is_empty() { + None + } else { + Some(hostname.to_owned()) + }; + + let config = ironrdp::connector::credssp::KerberosConfig::new( + kdc_proxy_url_opt, + hostname_opt, + )?; + Ok(Box::new(KerberosConfig(config))) + } + } + #[diplomat::opaque] pub struct CredsspSequence(pub ironrdp::connector::credssp::CredsspSequence); From 02b96ebb4d786cef609a25ff905810c33e0eb268 Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 7 Oct 2025 17:15:47 -0400 Subject: [PATCH 04/25] clean up --- crates/ironrdp-connector/src/credssp.rs | 3 +-- ffi/src/credssp/mod.rs | 10 ++-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/ironrdp-connector/src/credssp.rs b/crates/ironrdp-connector/src/credssp.rs index 5a5752aaa..9750b6081 100644 --- a/crates/ironrdp-connector/src/credssp.rs +++ b/crates/ironrdp-connector/src/credssp.rs @@ -6,7 +6,7 @@ use sspi::credssp::{self, ClientState, CredSspClient}; use sspi::generator::{Generator, NetworkRequest}; use sspi::negotiate::ProtocolConfig; use sspi::Username; -use tracing::{debug, info}; +use tracing::debug; use crate::{ custom_err, general_err, ConnectorError, ConnectorErrorKind, ConnectorResult, Credentials, ServerName, Written, @@ -100,7 +100,6 @@ impl CredsspSequence { server_public_key: Vec, kerberos_config: Option, ) -> ConnectorResult<(Self, credssp::TsRequest)> { - let credentials: sspi::Credentials = match &credentials { Credentials::UsernamePassword { username, password } => { let username = Username::new(username, domain).map_err(|e| custom_err!("invalid username", e))?; diff --git a/ffi/src/credssp/mod.rs b/ffi/src/credssp/mod.rs index ae6efd688..e0811d0cc 100644 --- a/ffi/src/credssp/mod.rs +++ b/ffi/src/credssp/mod.rs @@ -22,10 +22,7 @@ pub mod ffi { /// # Arguments /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used /// * `hostname` - Client hostname for Kerberos, empty string if not used - pub fn new( - kdc_proxy_url: &str, - hostname: &str, - ) -> Result, Box> { + pub fn new(kdc_proxy_url: &str, hostname: &str) -> Result, Box> { let kdc_proxy_url_opt = if kdc_proxy_url.is_empty() { None } else { @@ -38,10 +35,7 @@ pub mod ffi { Some(hostname.to_owned()) }; - let config = ironrdp::connector::credssp::KerberosConfig::new( - kdc_proxy_url_opt, - hostname_opt, - )?; + let config = ironrdp::connector::credssp::KerberosConfig::new(kdc_proxy_url_opt, hostname_opt)?; Ok(Box::new(KerberosConfig(config))) } } From 6d71d91dd51b251a6e34c1c3fe287b92efc3dcaf Mon Sep 17 00:00:00 2001 From: "irvingouj@Devolutions" Date: Wed, 8 Oct 2025 11:38:46 -0400 Subject: [PATCH 05/25] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs index 40310aa4d..9b215a028 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs @@ -164,7 +164,7 @@ private void OnOpened(object? sender, EventArgs e) { gatewayToken = await tokenGen.GenerateRdpTlsToken( dstHost: server!, - proxyUser: $"{username}@{domain}", + proxyUser: string.IsNullOrEmpty(domain) ? username : $"{username}@{domain}", proxyPassword: password!, destUser: username!, destPassword: password! From e93746bc05bb02075ea8a43e4b9872c870501b18 Mon Sep 17 00:00:00 2001 From: "irvingouj@Devolutions" Date: Wed, 8 Oct 2025 11:39:06 -0400 Subject: [PATCH 06/25] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs index 9b215a028..52e5b1c80 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs @@ -175,7 +175,7 @@ private void OnOpened(object? sender, EventArgs e) { Trace.TraceError($"Failed to generate RDP token: {ex.Message}"); Trace.TraceInformation("Make sure tokengen server is running:"); - Trace.TraceInformation($" cargo run --manifest-path D:/devolutions-gateway/tools/tokengen/Cargo.toml -- server"); + Trace.TraceInformation($" cargo run --manifest-path tools/tokengen/Cargo.toml -- server"); throw; } } From dbe7d4676ca95fa05b5a69817c4b88f12f0f9695 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 11:56:29 -0400 Subject: [PATCH 07/25] fix --- .../Devolutions.IronRdp/src/Connection.cs | 2 +- .../src/GatewayConnection.cs | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs index 13a739cb9..04cdc0926 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs @@ -11,7 +11,7 @@ public static class Connection { var client = await CreateTcpConnection(serverName, port); string clientAddr = client.Client.LocalEndPoint.ToString(); - Console.WriteLine(clientAddr); + System.Diagnostics.Debug.WriteLine(clientAddr); var framed = new Framed(client.GetStream()); diff --git a/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs index 94a59f9af..bc201ce8e 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs @@ -32,7 +32,7 @@ public static class GatewayConnection string? kdcHostname = null) { // Step 1: Connect WebSocket to gateway - Console.WriteLine($"Connecting to gateway at {gatewayUrl}..."); + System.Diagnostics.Debug.WriteLine($"Connecting to gateway at {gatewayUrl}..."); var ws = await WebSocketStream.ConnectAsync(new Uri(gatewayUrl)); var framed = new Framed(ws); @@ -57,7 +57,7 @@ public static class GatewayConnection } // Step 4: Perform RDCleanPath handshake - Console.WriteLine("Performing RDCleanPath handshake..."); + System.Diagnostics.Debug.WriteLine("Performing RDCleanPath handshake..."); var (serverPublicKey, framedAfterHandshake) = await ConnectRdCleanPath( framed, connector, destination, authToken, pcb ?? ""); @@ -65,10 +65,10 @@ public static class GatewayConnection connector.MarkSecurityUpgradeAsDone(); // Step 6: Finalize connection - Console.WriteLine("Finalizing RDP connection..."); + System.Diagnostics.Debug.WriteLine("Finalizing RDP connection..."); var result = await ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake, kdcProxyUrl, kdcHostname); - Console.WriteLine("Gateway connection established successfully!"); + System.Diagnostics.Debug.WriteLine("Gateway connection established successfully!"); return (result, framedAfterHandshake); } @@ -85,14 +85,14 @@ public static class GatewayConnection var writeBuf = WriteBuf.New(); // Step 1: Generate X.224 Connection Request - Console.WriteLine("Generating X.224 Connection Request..."); + System.Diagnostics.Debug.WriteLine("Generating X.224 Connection Request..."); var written = connector.StepNoInput(writeBuf); var x224PduSize = (int)written.GetSize().Get(); var x224Pdu = new byte[x224PduSize]; writeBuf.ReadIntoBuf(x224Pdu); // Step 2: Create and send RDCleanPath Request - Console.WriteLine($"Sending RDCleanPath request to {destination}..."); + System.Diagnostics.Debug.WriteLine($"Sending RDCleanPath request to {destination}..."); var rdCleanPathReq = RDCleanPathPdu.NewRequest(x224Pdu, destination, authToken, pcb); var reqBytes = rdCleanPathReq.ToDer(); var reqBytesArray = new byte[reqBytes.GetSize()]; @@ -100,7 +100,7 @@ public static class GatewayConnection await framed.Write(reqBytesArray); // Step 3: Read RDCleanPath Response - Console.WriteLine("Waiting for RDCleanPath response..."); + System.Diagnostics.Debug.WriteLine("Waiting for RDCleanPath response..."); var respBytes = await framed.ReadByHint(new RDCleanPathHint()); var rdCleanPathResp = RDCleanPathPdu.FromDer(respBytes); @@ -110,7 +110,7 @@ public static class GatewayConnection if (resultType == RDCleanPathResultType.Response) { - Console.WriteLine("RDCleanPath handshake successful!"); + System.Diagnostics.Debug.WriteLine("RDCleanPath handshake successful!"); // Extract X.224 response var x224Response = result.GetX224Response(); @@ -143,7 +143,7 @@ public static class GatewayConnection var serverPublicKey = ExtractPublicKeyFromX509(certBytes); - Console.WriteLine($"Extracted server public key (length: {serverPublicKey.Length})"); + System.Diagnostics.Debug.WriteLine($"Extracted server public key (length: {serverPublicKey.Length})"); return (serverPublicKey, framed); } @@ -185,12 +185,12 @@ private static async Task ConnectFinalize( // Perform CredSSP if needed if (connector.ShouldPerformCredssp()) { - Console.WriteLine("Performing CredSSP authentication..."); + System.Diagnostics.Debug.WriteLine("Performing CredSSP authentication..."); await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey, kdcProxyUrl, kdcHostname); } // Continue with remaining connection steps - Console.WriteLine("Completing connection sequence..."); + System.Diagnostics.Debug.WriteLine("Completing connection sequence..."); while (!connector.GetDynState().IsTerminal()) { await Connection.SingleSequenceStep(connector, writeBuf, framedSsl); @@ -236,7 +236,7 @@ private static async Task PerformCredsspSteps( KerberosConfig? kerberosConfig = null; if (!string.IsNullOrEmpty(kdcProxyUrl)) { - Console.WriteLine($"Using KDC proxy: {kdcProxyUrl}"); + System.Diagnostics.Debug.WriteLine($"Using KDC proxy: {kdcProxyUrl}"); kerberosConfig = KerberosConfig.New(kdcProxyUrl, kdcHostname ?? ""); } From ab87e25774aa9ed2d9d4ca1e7fae486f12bad3a4 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 12:45:09 -0400 Subject: [PATCH 08/25] clean --- Cargo.lock | 1 + ffi/Cargo.toml | 1 + ffi/src/clipboard/mod.rs | 4 ++-- ffi/src/connector/mod.rs | 4 ++-- ffi/src/error.rs | 11 ++++------- ffi/src/log.rs | 2 +- ffi/src/rdcleanpath.rs | 24 ++++++++++++++++-------- 7 files changed, 27 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 582d179cc..980260bba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,6 +1527,7 @@ dependencies = [ name = "ffi" version = "0.0.0" dependencies = [ + "anyhow", "diplomat", "diplomat-runtime", "embed-resource", diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 8f17fa7b5..1b6fb6ad2 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -23,6 +23,7 @@ sspi = { version = "0.16", features = ["network_client"] } thiserror = "2" tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1.0.100" [target.'cfg(windows)'.build-dependencies] embed-resource = "3.0" diff --git a/ffi/src/clipboard/mod.rs b/ffi/src/clipboard/mod.rs index e50aa1fec..386e7b855 100644 --- a/ffi/src/clipboard/mod.rs +++ b/ffi/src/clipboard/mod.rs @@ -34,8 +34,8 @@ pub struct FfiClipbarodMessageProxy { impl ironrdp::cliprdr::backend::ClipboardMessageProxy for FfiClipbarodMessageProxy { fn send_clipboard_message(&self, message: ironrdp::cliprdr::backend::ClipboardMessage) { - if let Err(err) = self.sender.send(message) { - error!("Failed to send clipboard message: {:?}", err); + if let Err(error) = self.sender.send(message) { + error!(?error, "Failed to send clipboard message"); } } } diff --git a/ffi/src/connector/mod.rs b/ffi/src/connector/mod.rs index e9226467c..332177af9 100644 --- a/ffi/src/connector/mod.rs +++ b/ffi/src/connector/mod.rs @@ -97,7 +97,7 @@ pub mod ffi { pub fn with_dynamic_channel_display_control(&mut self) -> Result<(), Box> { self.with_dvc(DisplayControlClient::new(|c| { - info!(DisplayCountrolCapabilities = ?c, "DisplayControl capabilities received"); + info!(display_control_capabilities = ?c, "DisplayControl capabilities received"); Ok(Vec::new()) })) } @@ -185,7 +185,7 @@ pub mod ffi { let Some(connector) = self.0.as_ref() else { return Err(ValueConsumedError::for_item("connector").into()); }; - tracing::trace!(pduhint=?connector.next_pdu_hint(), "Reading next PDU hint"); + tracing::trace!(pdu_hint=?connector.next_pdu_hint(), "Reading next PDU hint"); Ok(connector.next_pdu_hint().map(PduHint).map(Box::new)) } diff --git a/ffi/src/error.rs b/ffi/src/error.rs index 476d8a379..0bc40bdbc 100644 --- a/ffi/src/error.rs +++ b/ffi/src/error.rs @@ -106,11 +106,12 @@ impl From for IronRdpErrorKind { } } -pub struct GenericError(pub String); +pub struct GenericError(pub anyhow::Error); impl Display for GenericError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", self.0) + let repr = format!("{:#}", self.0); + write!(f, "{repr}") } } @@ -248,11 +249,7 @@ impl IncorrectEnumTypeErrorBuilder { impl Display for IncorrectEnumTypeError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!( - f, - "expected enum variable {}, of enum {}", - self.expected, self.enum_name - ) + write!(f, "expected enum variable {} of enum {}", self.expected, self.enum_name) } } diff --git a/ffi/src/log.rs b/ffi/src/log.rs index 7f7750a27..f050b1503 100644 --- a/ffi/src/log.rs +++ b/ffi/src/log.rs @@ -23,7 +23,7 @@ pub mod ffi { INIT_LOG.call_once(|| { let log_file = std::env::var(IRONRDP_LOG_PATH).ok(); let log_file = log_file.as_deref(); - setup_logging(log_file).expect("Failed to setup logging"); + setup_logging(log_file).expect("failed to setup logging"); }); } } diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index 142244730..61c7f5168 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -1,5 +1,6 @@ #[diplomat::bridge] pub mod ffi { + use anyhow::Context as _; use core::fmt::Write as _; use diplomat_runtime::DiplomatWriteable; @@ -32,7 +33,8 @@ pub mod ffi { proxy_auth.to_owned(), pcb_opt, ) - .map_err(|e| GenericError(format!("Failed to create RDCleanPath request: {e}")))?; + .context("failed to create RDCleanPath request") + .map_err(GenericError)?; Ok(Box::new(RDCleanPathPdu(pdu))) } @@ -40,7 +42,8 @@ pub mod ffi { /// Decodes a RDCleanPath PDU from DER-encoded bytes pub fn from_der(bytes: &[u8]) -> Result, Box> { let pdu = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(bytes) - .map_err(|e| GenericError(format!("Failed to decode RDCleanPath PDU: {e}")))?; + .context("failed to decode RDCleanPath PDU") + .map_err(GenericError)?; Ok(Box::new(RDCleanPathPdu(pdu))) } @@ -50,7 +53,8 @@ pub mod ffi { let bytes = self .0 .to_der() - .map_err(|e| GenericError(format!("Failed to encode RDCleanPath PDU: {e}")))?; + .context("failed to encode RDCleanPath PDU") + .map_err(GenericError)?; Ok(Box::new(VecU8(bytes))) } @@ -67,7 +71,8 @@ pub mod ffi { .0 .clone() .into_enum() - .map_err(|e| GenericError(format!("Missing RDCleanPath field: {e}")))?; + .context("missing RDCleanPath field") + .map_err(GenericError)?; Ok(Box::new(RDCleanPathResult(rdcleanpath))) } @@ -93,7 +98,7 @@ pub mod ffi { if let ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } = self.0 { Ok(total_length) } else { - Err(GenericError("Detection result is not Detected variant".into()).into()) + Err(GenericError(anyhow::anyhow!("detection result is not Detected variant")).into()) } } } @@ -129,7 +134,7 @@ pub mod ffi { ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { x224_connection_response, } => Ok(Box::new(VecU8(x224_connection_response.clone()))), - _ => Err(GenericError("RDCleanPath variant does not contain X.224 response".into()).into()), + _ => Err(GenericError(anyhow::anyhow!("RDCleanPath variant does not contain X.224 response")).into()), } } @@ -141,7 +146,10 @@ pub mod ffi { let certs: Vec> = server_cert_chain.iter().map(|cert| cert.as_bytes().to_vec()).collect(); Ok(Box::new(CertificateChainIterator { certs, index: 0 })) } - _ => Err(GenericError("RDCleanPath variant does not contain certificate chain".into()).into()), + _ => Err(GenericError(anyhow::anyhow!( + "RDCleanPath variant does not contain certificate chain" + )) + .into()), } } @@ -164,7 +172,7 @@ pub mod ffi { if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { Ok(err.error_code) } else { - Err(GenericError("Not a GeneralError variant".into()).into()) + Err(GenericError(anyhow::anyhow!("not a GeneralError variant")).into()) } } From 1da7a29754c6332d509a0eb3b4d2f48663b03167 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 13:07:05 -0400 Subject: [PATCH 09/25] review fix --- ...tewayConnection.cs => RDCleanPathConnection.cs} | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename ffi/dotnet/Devolutions.IronRdp/src/{GatewayConnection.cs => RDCleanPathConnection.cs} (95%) diff --git a/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs similarity index 95% rename from ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs rename to ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs index bc201ce8e..ced58c8f9 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/GatewayConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs @@ -4,17 +4,17 @@ namespace Devolutions.IronRdp; /// -/// Provides methods for connecting to RDP servers through Devolutions Gateway -/// using the RDCleanPath protocol over WebSocket. +/// Provides methods for connecting to RDP servers through an RDCleanPath-compatible gateway +/// (such as Devolutions Gateway or Cloudflare) using WebSocket. /// -public static class GatewayConnection +public static class RDCleanPathConnection { /// - /// Connects to an RDP server through a Devolutions Gateway using WebSocket and RDCleanPath protocol. + /// Connects to an RDP server through an RDCleanPath-compatible gateway using WebSocket. /// /// The RDP connection configuration - /// The WebSocket URL to the gateway (e.g., "ws://localhost:7171/jet/rdp") - /// The JWT authentication token for the gateway + /// The WebSocket URL to the RDCleanPath gateway (e.g., "ws://localhost:7171/jet/rdp") + /// The JWT authentication token for the RDCleanPath gateway /// The destination RDP server address (e.g., "10.10.0.3:3389") /// Optional preconnection blob for Hyper-V VM connections /// Optional clipboard backend factory @@ -73,7 +73,7 @@ public static class GatewayConnection } /// - /// Performs the RDCleanPath handshake with the gateway. + /// Performs the RDCleanPath handshake with the RDCleanPath-compatible gateway. /// private static async Task<(byte[], Framed)> ConnectRdCleanPath( Framed framed, From f14efa9eb0856712b1f507b94cfdc5f393972379 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 13:09:32 -0400 Subject: [PATCH 10/25] review fix --- .../MainWindow.axaml.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs index 52e5b1c80..f393e19a4 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs @@ -82,12 +82,12 @@ private void OnOpened(object? sender, EventArgs e) var server = Environment.GetEnvironmentVariable("IRONRDP_SERVER"); var portEnv = Environment.GetEnvironmentVariable("IRONRDP_PORT"); - // NEW: Gateway configuration (optional) + // Gateway configuration (optional) var gatewayUrl = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_URL"); var gatewayToken = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_TOKEN"); var tokengenUrl = Environment.GetEnvironmentVariable("IRONRDP_TOKENGEN_URL"); - // NEW: KDC proxy configuration (optional) + // KDC proxy configuration (optional) var kdcProxyUrlBase = Environment.GetEnvironmentVariable("IRONRDP_KDC_PROXY_URL"); var kdcRealm = Environment.GetEnvironmentVariable("IRONRDP_KDC_REALM"); var kdcServer = Environment.GetEnvironmentVariable("IRONRDP_KDC_SERVER"); @@ -102,6 +102,7 @@ private void OnOpened(object? sender, EventArgs e) } // Validate server is only domain or IP (no port allowed) + // i.e. "example.com" or "10.10.0.3" the port should go to the dedicated env var IRONRDP_PORT if (server.Contains(':')) { var errorMessage = $"IRONRDP_SERVER must be a domain or IP address only, not '{server}'. Use IRONRDP_PORT for the port."; @@ -568,7 +569,7 @@ private async Task HandleActiveStageOutput(ActiveStageOutputIterator outpu var writeBuf = WriteBuf.New(); while (true) { - await Connection.SingleSequenceStep(activationSequence, writeBuf,_framed!); + await Connection.SingleSequenceStep(activationSequence, writeBuf, _framed!); if (activationSequence.GetState().GetType() != ConnectionActivationStateType.Finalized) continue; From 83dc989c23bc90d016f3a5d89e414bbe6ef1de49 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 13:30:11 -0400 Subject: [PATCH 11/25] review fix --- ffi/Cargo.toml | 2 +- .../Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs | 2 +- ffi/src/error.rs | 3 +-- ffi/src/rdcleanpath.rs | 3 ++- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 1b6fb6ad2..22d5871ea 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -23,7 +23,7 @@ sspi = { version = "0.16", features = ["network_client"] } thiserror = "2" tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } -anyhow = "1.0.100" +anyhow = "1.0" [target.'cfg(windows)'.build-dependencies] embed-resource = "3.0" diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs index f393e19a4..794c0498a 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs @@ -23,7 +23,7 @@ public partial class MainWindow : Window readonly InputDatabase? _inputDatabase = InputDatabase.New(); ActiveStage? _activeStage; DecodedImage? _decodedImage; - Framed? _framed; // Changed to Stream to support both SslStream and WebSocketStream + Framed? _framed; WinCliprdr? _cliprdr; private readonly RendererModel _renderModel; private Image? _imageControl; diff --git a/ffi/src/error.rs b/ffi/src/error.rs index 0bc40bdbc..d21c3893e 100644 --- a/ffi/src/error.rs +++ b/ffi/src/error.rs @@ -110,8 +110,7 @@ pub struct GenericError(pub anyhow::Error); impl Display for GenericError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let repr = format!("{:#}", self.0); - write!(f, "{repr}") + write!(f, "{:#}", self.0) } } diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index 61c7f5168..ea74a490e 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -1,7 +1,8 @@ #[diplomat::bridge] pub mod ffi { - use anyhow::Context as _; use core::fmt::Write as _; + + use anyhow::Context as _; use diplomat_runtime::DiplomatWriteable; use crate::error::ffi::IronRdpError; From 2366696346fff1819ed66bbc59d4bd978840eb15 Mon Sep 17 00:00:00 2001 From: "irvingouj@Devolutions" Date: Wed, 8 Oct 2025 13:30:48 -0400 Subject: [PATCH 12/25] Update ffi/src/rdcleanpath.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ffi/src/rdcleanpath.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index ea74a490e..f90b88f40 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -131,7 +131,7 @@ pub mod ffi { ironrdp_rdcleanpath::RDCleanPath::Response { x224_connection_response, .. - } => Ok(Box::new(VecU8(x224_connection_response.as_bytes().to_vec()))), + } => Ok(Box::new(VecU8(x224_connection_response.clone()))), ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { x224_connection_response, } => Ok(Box::new(VecU8(x224_connection_response.clone()))), From 25361343db5e3e8119853450c696f50ba2d9879a Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 13:38:47 -0400 Subject: [PATCH 13/25] strip off kdc proxy --- .../src/RDCleanPathConnection.cs | 28 ++++--------------- ffi/src/rdcleanpath.rs | 2 +- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs index ced58c8f9..5dcee70c9 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs @@ -18,8 +18,6 @@ public static class RDCleanPathConnection /// The destination RDP server address (e.g., "10.10.0.3:3389") /// Optional preconnection blob for Hyper-V VM connections /// Optional clipboard backend factory - /// Optional KDC proxy URL with token (e.g., "https://gateway.example.com/KdcProxy/{token}") - /// Optional client hostname for Kerberos /// A tuple containing the connection result and framed WebSocket stream public static async Task<(ConnectionResult, Framed)> ConnectViaGateway( Config config, @@ -27,9 +25,7 @@ public static class RDCleanPathConnection string authToken, string destination, string? pcb = null, - CliprdrBackendFactory? factory = null, - string? kdcProxyUrl = null, - string? kdcHostname = null) + CliprdrBackendFactory? factory = null) { // Step 1: Connect WebSocket to gateway System.Diagnostics.Debug.WriteLine($"Connecting to gateway at {gatewayUrl}..."); @@ -66,7 +62,7 @@ public static class RDCleanPathConnection // Step 6: Finalize connection System.Diagnostics.Debug.WriteLine("Finalizing RDP connection..."); - var result = await ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake, kdcProxyUrl, kdcHostname); + var result = await ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake); System.Diagnostics.Debug.WriteLine("Gateway connection established successfully!"); return (result, framedAfterHandshake); @@ -176,9 +172,7 @@ private static async Task ConnectFinalize( string serverName, ClientConnector connector, byte[] serverPubKey, - Framed framedSsl, - string? kdcProxyUrl, - string? kdcHostname) + Framed framedSsl) { var writeBuf = WriteBuf.New(); @@ -186,7 +180,7 @@ private static async Task ConnectFinalize( if (connector.ShouldPerformCredssp()) { System.Diagnostics.Debug.WriteLine("Performing CredSSP authentication..."); - await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey, kdcProxyUrl, kdcHostname); + await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey); } // Continue with remaining connection steps @@ -219,9 +213,7 @@ private static async Task PerformCredsspSteps( string serverName, WriteBuf writeBuf, Framed framedSsl, - byte[] serverpubkey, - string? kdcProxyUrl, - string? kdcHostname) + byte[] serverpubkey) { // Extract hostname from "hostname:port" format for CredSSP // CredSSP needs just the hostname for the service principal name (TERMSRV/hostname) @@ -232,15 +224,7 @@ private static async Task PerformCredsspSteps( hostname = serverName.Substring(0, colonIndex); } - // Create KerberosConfig if KDC proxy URL is provided - KerberosConfig? kerberosConfig = null; - if (!string.IsNullOrEmpty(kdcProxyUrl)) - { - System.Diagnostics.Debug.WriteLine($"Using KDC proxy: {kdcProxyUrl}"); - kerberosConfig = KerberosConfig.New(kdcProxyUrl, kdcHostname ?? ""); - } - - var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, kerberosConfig); + var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, null); var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); var tsRequest = credsspSequenceInitResult.GetTsRequest(); var tcpClient = new System.Net.Sockets.TcpClient(); diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index f90b88f40..ea74a490e 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -131,7 +131,7 @@ pub mod ffi { ironrdp_rdcleanpath::RDCleanPath::Response { x224_connection_response, .. - } => Ok(Box::new(VecU8(x224_connection_response.clone()))), + } => Ok(Box::new(VecU8(x224_connection_response.as_bytes().to_vec()))), ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { x224_connection_response, } => Ok(Box::new(VecU8(x224_connection_response.clone()))), From ee7347e51ded341eadd61d434b58423d02cf9f73 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 14:37:30 -0400 Subject: [PATCH 14/25] remove proxy --- .../MainWindow.axaml.cs | 53 +--------- .../TokenGenerator.cs | 50 ---------- .../Generated/CredsspSequence.cs | 17 +--- .../Generated/KerberosConfig.cs | 99 ------------------- ...iResultBoxKerberosConfigBoxIronRdpError.cs | 46 --------- .../Generated/RawCredsspSequence.cs | 2 +- .../Generated/RawKerberosConfig.cs | 32 ------ .../Devolutions.IronRdp/src/Connection.cs | 38 +------ .../src/RDCleanPathConnection.cs | 2 +- ffi/src/credssp/mod.rs | 30 +----- 10 files changed, 8 insertions(+), 361 deletions(-) delete mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs delete mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs delete mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs index 794c0498a..486ce487b 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs @@ -87,11 +87,6 @@ private void OnOpened(object? sender, EventArgs e) var gatewayToken = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_TOKEN"); var tokengenUrl = Environment.GetEnvironmentVariable("IRONRDP_TOKENGEN_URL"); - // KDC proxy configuration (optional) - var kdcProxyUrlBase = Environment.GetEnvironmentVariable("IRONRDP_KDC_PROXY_URL"); - var kdcRealm = Environment.GetEnvironmentVariable("IRONRDP_KDC_REALM"); - var kdcServer = Environment.GetEnvironmentVariable("IRONRDP_KDC_SERVER"); - if (username == null || password == null || server == null) { var errorMessage = @@ -181,55 +176,11 @@ private void OnOpened(object? sender, EventArgs e) } } - // Generate KDC token if KDC proxy is enabled - string? kdcProxyUrl = null; - if (!string.IsNullOrEmpty(kdcRealm) && !string.IsNullOrEmpty(kdcServer)) - { - Trace.TraceInformation("=== KDC PROXY MODE ENABLED ==="); - Trace.TraceInformation($"KDC Realm: {kdcRealm}"); - Trace.TraceInformation($"KDC Server: {kdcServer}"); - - try - { - var kdcToken = await tokenGen.GenerateKdcToken( - krbRealm: kdcRealm!, - krbKdc: kdcServer! - ); - Trace.TraceInformation($"KDC token generated successfully (length: {kdcToken.Length})"); - - // Build KDC proxy URL - use explicit URL if provided, otherwise auto-construct from gateway URL - if (!string.IsNullOrEmpty(kdcProxyUrlBase)) - { - kdcProxyUrl = $"{kdcProxyUrlBase.TrimEnd('/')}/{kdcToken}"; - Trace.TraceInformation($"Using explicit KDC Proxy URL: {kdcProxyUrl}"); - } - else - { - var gatewayBaseUrl = new Uri(gatewayUrl.Replace("/jet/rdp", "")).GetLeftPart(UriPartial.Authority); - kdcProxyUrl = $"{gatewayBaseUrl}/KdcProxy/{kdcToken}"; - Trace.TraceInformation($"Auto-constructed KDC Proxy URL: {kdcProxyUrl}"); - } - } - catch (Exception ex) - { - Trace.TraceError($"Failed to generate KDC token: {ex.Message}"); - Trace.TraceWarning("Continuing without KDC proxy..."); - } - } - // Connect via gateway - destination needs "hostname:port" format for RDCleanPath string destination = $"{server}:{port}"; - // Get client hostname for Kerberos authentication - string? kdcClientHostname = null; - if (!string.IsNullOrEmpty(kdcProxyUrl)) - { - kdcClientHostname = System.Net.Dns.GetHostName(); - Trace.TraceInformation($"Client hostname for Kerberos: {kdcClientHostname}"); - } - - var (gatewayRes, gatewayFramed) = await GatewayConnection.ConnectViaGateway( - config, gatewayUrl, gatewayToken!, destination, null, factory, kdcProxyUrl, kdcClientHostname); + var (gatewayRes, gatewayFramed) = await RDCleanPathConnection.ConnectViaGateway( + config, gatewayUrl, gatewayToken!, destination, null, factory); res = gatewayRes; this._framed = new Framed(gatewayFramed.GetInner().Item1); diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs index f23a8094a..00946c604 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/TokenGenerator.cs @@ -122,44 +122,6 @@ public async Task GenerateForwardToken( } } - /// - /// Generates a KDC proxy token for Kerberos authentication through the gateway. - /// - /// Kerberos realm (e.g., "AD.EXAMPLE.COM") - /// KDC address with protocol (e.g., "tcp://dc.ad.example.com:88") - /// Token validity in seconds (default: 3600) - /// A JWT token string - public async Task GenerateKdcToken( - string krbRealm, - string krbKdc, - int validityDuration = 3600) - { - var request = new KdcTokenRequest - { - KrbRealm = krbRealm, - KrbKdc = krbKdc, - ValidityDuration = validityDuration - }; - - try - { - var response = await _client.PostAsJsonAsync($"{_tokengenUrl}/kdc", request); - response.EnsureSuccessStatusCode(); - - var result = await response.Content.ReadFromJsonAsync(); - if (result?.Token == null) - { - throw new Exception("KDC token generation failed: Empty response"); - } - - return result.Token; - } - catch (HttpRequestException ex) - { - throw new Exception($"Failed to generate KDC token from {_tokengenUrl}: {ex.Message}", ex); - } - } - /// /// Checks if the tokengen server is reachable. /// @@ -222,18 +184,6 @@ private class ForwardTokenRequest public int ValidityDuration { get; set; } } - private class KdcTokenRequest - { - [JsonPropertyName("krb_realm")] - public string KrbRealm { get; set; } = string.Empty; - - [JsonPropertyName("krb_kdc")] - public string KrbKdc { get; set; } = string.Empty; - - [JsonPropertyName("validity_duration")] - public int ValidityDuration { get; set; } - } - private class TokenResponse { [JsonPropertyName("token")] diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/CredsspSequence.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/CredsspSequence.cs index e750795ca..7f9214e05 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/CredsspSequence.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/CredsspSequence.cs @@ -53,7 +53,7 @@ public unsafe CredsspSequence(Raw.CredsspSequence* handle) /// /// A CredsspSequenceInitResult allocated on Rust side. /// - public static CredsspSequenceInitResult Init(ClientConnector connector, string serverName, byte[] serverPublicKey, KerberosConfig? kerberoConfigs) + public static CredsspSequenceInitResult Init(ClientConnector connector, string serverName, byte[] serverPublicKey) { unsafe { @@ -66,24 +66,11 @@ public static CredsspSequenceInitResult Init(ClientConnector connector, string s { throw new ObjectDisposedException("ClientConnector"); } - Raw.KerberosConfig* kerberoConfigsRaw; - if (kerberoConfigs == null) - { - kerberoConfigsRaw = null; - } - else - { - kerberoConfigsRaw = kerberoConfigs.AsFFI(); - if (kerberoConfigsRaw == null) - { - throw new ObjectDisposedException("KerberosConfig"); - } - } fixed (byte* serverPublicKeyPtr = serverPublicKey) { fixed (byte* serverNameBufPtr = serverNameBuf) { - Raw.CredsspFfiResultBoxCredsspSequenceInitResultBoxIronRdpError result = Raw.CredsspSequence.Init(connectorRaw, serverNameBufPtr, serverNameBufLength, serverPublicKeyPtr, serverPublicKeyLength, kerberoConfigsRaw); + Raw.CredsspFfiResultBoxCredsspSequenceInitResultBoxIronRdpError result = Raw.CredsspSequence.Init(connectorRaw, serverNameBufPtr, serverNameBufLength, serverPublicKeyPtr, serverPublicKeyLength); if (!result.isOk) { throw new IronRdpException(new IronRdpError(result.Err)); diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs deleted file mode 100644 index ac310c85c..000000000 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs +++ /dev/null @@ -1,99 +0,0 @@ -// by Diplomat - -#pragma warning disable 0105 -using System; -using System.Runtime.InteropServices; - -using Devolutions.IronRdp.Diplomat; -#pragma warning restore 0105 - -namespace Devolutions.IronRdp; - -#nullable enable - -public partial class KerberosConfig: IDisposable -{ - private unsafe Raw.KerberosConfig* _inner; - - /// - /// Creates a managed KerberosConfig from a raw handle. - /// - /// - /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free). - ///
- /// This constructor assumes the raw struct is allocated on Rust side. - /// If implemented, the custom Drop implementation on Rust side WILL run on destruction. - ///
- public unsafe KerberosConfig(Raw.KerberosConfig* handle) - { - _inner = handle; - } - - /// - /// Creates a new KerberosConfig for KDC proxy support. - /// - /// - /// # Arguments - /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used - /// * `hostname` - Client hostname for Kerberos, empty string if not used - /// - /// - /// - /// A KerberosConfig allocated on Rust side. - /// - public static KerberosConfig New(string kdcProxyUrl, string hostname) - { - unsafe - { - byte[] kdcProxyUrlBuf = DiplomatUtils.StringToUtf8(kdcProxyUrl); - byte[] hostnameBuf = DiplomatUtils.StringToUtf8(hostname); - nuint kdcProxyUrlBufLength = (nuint)kdcProxyUrlBuf.Length; - nuint hostnameBufLength = (nuint)hostnameBuf.Length; - fixed (byte* kdcProxyUrlBufPtr = kdcProxyUrlBuf) - { - fixed (byte* hostnameBufPtr = hostnameBuf) - { - Raw.CredsspFfiResultBoxKerberosConfigBoxIronRdpError result = Raw.KerberosConfig.New(kdcProxyUrlBufPtr, kdcProxyUrlBufLength, hostnameBufPtr, hostnameBufLength); - if (!result.isOk) - { - throw new IronRdpException(new IronRdpError(result.Err)); - } - Raw.KerberosConfig* retVal = result.Ok; - return new KerberosConfig(retVal); - } - } - } - } - - /// - /// Returns the underlying raw handle. - /// - public unsafe Raw.KerberosConfig* AsFFI() - { - return _inner; - } - - /// - /// Destroys the underlying object immediately. - /// - public void Dispose() - { - unsafe - { - if (_inner == null) - { - return; - } - - Raw.KerberosConfig.Destroy(_inner); - _inner = null; - - GC.SuppressFinalize(this); - } - } - - ~KerberosConfig() - { - Dispose(); - } -} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs deleted file mode 100644 index 1a08f6b2c..000000000 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs +++ /dev/null @@ -1,46 +0,0 @@ -// by Diplomat - -#pragma warning disable 0105 -using System; -using System.Runtime.InteropServices; - -using Devolutions.IronRdp.Diplomat; -#pragma warning restore 0105 - -namespace Devolutions.IronRdp.Raw; - -#nullable enable - -[StructLayout(LayoutKind.Sequential)] -public partial struct CredsspFfiResultBoxKerberosConfigBoxIronRdpError -{ - [StructLayout(LayoutKind.Explicit)] - private unsafe struct InnerUnion - { - [FieldOffset(0)] - internal KerberosConfig* ok; - [FieldOffset(0)] - internal IronRdpError* err; - } - - private InnerUnion _inner; - - [MarshalAs(UnmanagedType.U1)] - public bool isOk; - - public unsafe KerberosConfig* Ok - { - get - { - return _inner.ok; - } - } - - public unsafe IronRdpError* Err - { - get - { - return _inner.err; - } - } -} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspSequence.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspSequence.cs index 0029d7f23..dcd88a538 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspSequence.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspSequence.cs @@ -20,7 +20,7 @@ public partial struct CredsspSequence public static unsafe extern PduHint* NextPduHint(CredsspSequence* self); [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CredsspSequence_init", ExactSpelling = true)] - public static unsafe extern CredsspFfiResultBoxCredsspSequenceInitResultBoxIronRdpError Init(ClientConnector* connector, byte* serverName, nuint serverNameSz, byte* serverPublicKey, nuint serverPublicKeySz, KerberosConfig* kerberoConfigs); + public static unsafe extern CredsspFfiResultBoxCredsspSequenceInitResultBoxIronRdpError Init(ClientConnector* connector, byte* serverName, nuint serverNameSz, byte* serverPublicKey, nuint serverPublicKeySz); [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CredsspSequence_decode_server_message", ExactSpelling = true)] public static unsafe extern CredsspFfiResultOptBoxTsRequestBoxIronRdpError DecodeServerMessage(CredsspSequence* self, byte* pdu, nuint pduSz); diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs deleted file mode 100644 index c50d8ca74..000000000 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs +++ /dev/null @@ -1,32 +0,0 @@ -// by Diplomat - -#pragma warning disable 0105 -using System; -using System.Runtime.InteropServices; - -using Devolutions.IronRdp.Diplomat; -#pragma warning restore 0105 - -namespace Devolutions.IronRdp.Raw; - -#nullable enable - -[StructLayout(LayoutKind.Sequential)] -public partial struct KerberosConfig -{ - private const string NativeLib = "DevolutionsIronRdp"; - - /// - /// Creates a new KerberosConfig for KDC proxy support. - /// - /// - /// # Arguments - /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used - /// * `hostname` - Client hostname for Kerberos, empty string if not used - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "KerberosConfig_new", ExactSpelling = true)] - public static unsafe extern CredsspFfiResultBoxKerberosConfigBoxIronRdpError New(byte* kdcProxyUrl, nuint kdcProxyUrlSz, byte* hostname, nuint hostnameSz); - - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "KerberosConfig_destroy", ExactSpelling = true)] - public static unsafe extern void Destroy(KerberosConfig* self); -} diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs index 04cdc0926..9a5d61426 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs @@ -97,7 +97,7 @@ private static async Task ConnectFinalize(string serverName, C private static async Task PerformCredsspSteps(ClientConnector connector, string serverName, WriteBuf writeBuf, Framed framedSsl, byte[] serverpubkey) { - var credsspSequenceInitResult = CredsspSequence.Init(connector, serverName, serverpubkey, null); + var credsspSequenceInitResult = CredsspSequence.Init(connector, serverName, serverpubkey); var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); var tsRequest = credsspSequenceInitResult.GetTsRequest(); var tcpClient = new TcpClient(); @@ -139,7 +139,6 @@ internal static async Task ResolveGenerator(CredsspProcessGenerator { var state = generator.Start(); NetworkStream? stream = null; - HttpClient? httpClient = null; while (true) { @@ -167,41 +166,6 @@ internal static async Task ResolveGenerator(CredsspProcessGenerator Array.Copy(readBuf, actuallyRead, readlen); state = generator.Resume(actuallyRead); } - else if (protocol == NetworkRequestProtocol.Http || protocol == NetworkRequestProtocol.Https) - { - // Handle HTTP/HTTPS requests for KDC proxy (mimics ironrdp-web implementation) - if (httpClient == null) - { - httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Add("keep-alive", "true"); - } - - System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Sending {protocol} request to {url}"); - - var bodyBytes = Utils.VecU8ToByte(data); - var content = new ByteArrayContent(bodyBytes); - - HttpResponseMessage response; - try - { - response = await httpClient.PostAsync(url, content); - } - catch (HttpRequestException ex) - { - throw new Exception($"Failed to send KDC request to {url}: {ex.Message}", ex); - } - - if (!response.IsSuccessStatusCode) - { - throw new Exception( - $"KdcProxy HTTP status error ({(int)response.StatusCode} {response.ReasonPhrase})"); - } - - var responseData = await response.Content.ReadAsByteArrayAsync(); - System.Diagnostics.Debug.WriteLine($"[ResolveGenerator] Received {responseData.Length} bytes from KDC proxy"); - - state = generator.Resume(responseData); - } else { throw new Exception($"Unimplemented protocol: {protocol}"); diff --git a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs index 5dcee70c9..e4194d0dd 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs @@ -224,7 +224,7 @@ private static async Task PerformCredsspSteps( hostname = serverName.Substring(0, colonIndex); } - var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, null); + var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey); var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); var tsRequest = credsspSequenceInitResult.GetTsRequest(); var tcpClient = new System.Net.Sockets.TcpClient(); diff --git a/ffi/src/credssp/mod.rs b/ffi/src/credssp/mod.rs index e0811d0cc..582377fbf 100644 --- a/ffi/src/credssp/mod.rs +++ b/ffi/src/credssp/mod.rs @@ -13,33 +13,6 @@ pub mod ffi { use crate::error::ValueConsumedError; use crate::pdu::ffi::WriteBuf; - #[diplomat::opaque] - pub struct KerberosConfig(pub ironrdp::connector::credssp::KerberosConfig); - - impl KerberosConfig { - /// Creates a new KerberosConfig for KDC proxy support. - /// - /// # Arguments - /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used - /// * `hostname` - Client hostname for Kerberos, empty string if not used - pub fn new(kdc_proxy_url: &str, hostname: &str) -> Result, Box> { - let kdc_proxy_url_opt = if kdc_proxy_url.is_empty() { - None - } else { - Some(kdc_proxy_url.to_owned()) - }; - - let hostname_opt = if hostname.is_empty() { - None - } else { - Some(hostname.to_owned()) - }; - - let config = ironrdp::connector::credssp::KerberosConfig::new(kdc_proxy_url_opt, hostname_opt)?; - Ok(Box::new(KerberosConfig(config))) - } - } - #[diplomat::opaque] pub struct CredsspSequence(pub ironrdp::connector::credssp::CredsspSequence); @@ -77,7 +50,6 @@ pub mod ffi { connector: &ClientConnector, server_name: &str, server_public_key: &[u8], - kerbero_configs: Option<&KerberosConfig>, ) -> Result, Box> { let Some(connector) = connector.0.as_ref() else { return Err(ValueConsumedError::for_item("connector").into()); @@ -91,7 +63,7 @@ pub mod ffi { selected_protocol, server_name.into(), server_public_key.to_owned(), - kerbero_configs.map(|config| config.0.clone()), + None, )?; Ok(Box::new(CredsspSequenceInitResult { From cfd1d05152cc94c8cf835258274c91f090d1f345 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 14:47:42 -0400 Subject: [PATCH 15/25] undo changes --- .../Generated/CredsspSequence.cs | 17 +++- .../Generated/KerberosConfig.cs | 99 +++++++++++++++++++ ...iResultBoxKerberosConfigBoxIronRdpError.cs | 46 +++++++++ .../Generated/RawCredsspSequence.cs | 2 +- .../Generated/RawKerberosConfig.cs | 32 ++++++ .../Devolutions.IronRdp/src/Connection.cs | 2 +- .../src/RDCleanPathConnection.cs | 2 +- ffi/src/credssp/mod.rs | 30 +++++- 8 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs create mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/CredsspSequence.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/CredsspSequence.cs index 7f9214e05..e750795ca 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/CredsspSequence.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/CredsspSequence.cs @@ -53,7 +53,7 @@ public unsafe CredsspSequence(Raw.CredsspSequence* handle) /// /// A CredsspSequenceInitResult allocated on Rust side. /// - public static CredsspSequenceInitResult Init(ClientConnector connector, string serverName, byte[] serverPublicKey) + public static CredsspSequenceInitResult Init(ClientConnector connector, string serverName, byte[] serverPublicKey, KerberosConfig? kerberoConfigs) { unsafe { @@ -66,11 +66,24 @@ public static CredsspSequenceInitResult Init(ClientConnector connector, string s { throw new ObjectDisposedException("ClientConnector"); } + Raw.KerberosConfig* kerberoConfigsRaw; + if (kerberoConfigs == null) + { + kerberoConfigsRaw = null; + } + else + { + kerberoConfigsRaw = kerberoConfigs.AsFFI(); + if (kerberoConfigsRaw == null) + { + throw new ObjectDisposedException("KerberosConfig"); + } + } fixed (byte* serverPublicKeyPtr = serverPublicKey) { fixed (byte* serverNameBufPtr = serverNameBuf) { - Raw.CredsspFfiResultBoxCredsspSequenceInitResultBoxIronRdpError result = Raw.CredsspSequence.Init(connectorRaw, serverNameBufPtr, serverNameBufLength, serverPublicKeyPtr, serverPublicKeyLength); + Raw.CredsspFfiResultBoxCredsspSequenceInitResultBoxIronRdpError result = Raw.CredsspSequence.Init(connectorRaw, serverNameBufPtr, serverNameBufLength, serverPublicKeyPtr, serverPublicKeyLength, kerberoConfigsRaw); if (!result.isOk) { throw new IronRdpException(new IronRdpError(result.Err)); diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs new file mode 100644 index 000000000..ac310c85c --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs @@ -0,0 +1,99 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp; + +#nullable enable + +public partial class KerberosConfig: IDisposable +{ + private unsafe Raw.KerberosConfig* _inner; + + /// + /// Creates a managed KerberosConfig from a raw handle. + /// + /// + /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free). + ///
+ /// This constructor assumes the raw struct is allocated on Rust side. + /// If implemented, the custom Drop implementation on Rust side WILL run on destruction. + ///
+ public unsafe KerberosConfig(Raw.KerberosConfig* handle) + { + _inner = handle; + } + + /// + /// Creates a new KerberosConfig for KDC proxy support. + /// + /// + /// # Arguments + /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used + /// * `hostname` - Client hostname for Kerberos, empty string if not used + /// + /// + /// + /// A KerberosConfig allocated on Rust side. + /// + public static KerberosConfig New(string kdcProxyUrl, string hostname) + { + unsafe + { + byte[] kdcProxyUrlBuf = DiplomatUtils.StringToUtf8(kdcProxyUrl); + byte[] hostnameBuf = DiplomatUtils.StringToUtf8(hostname); + nuint kdcProxyUrlBufLength = (nuint)kdcProxyUrlBuf.Length; + nuint hostnameBufLength = (nuint)hostnameBuf.Length; + fixed (byte* kdcProxyUrlBufPtr = kdcProxyUrlBuf) + { + fixed (byte* hostnameBufPtr = hostnameBuf) + { + Raw.CredsspFfiResultBoxKerberosConfigBoxIronRdpError result = Raw.KerberosConfig.New(kdcProxyUrlBufPtr, kdcProxyUrlBufLength, hostnameBufPtr, hostnameBufLength); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.KerberosConfig* retVal = result.Ok; + return new KerberosConfig(retVal); + } + } + } + } + + /// + /// Returns the underlying raw handle. + /// + public unsafe Raw.KerberosConfig* AsFFI() + { + return _inner; + } + + /// + /// Destroys the underlying object immediately. + /// + public void Dispose() + { + unsafe + { + if (_inner == null) + { + return; + } + + Raw.KerberosConfig.Destroy(_inner); + _inner = null; + + GC.SuppressFinalize(this); + } + } + + ~KerberosConfig() + { + Dispose(); + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs new file mode 100644 index 000000000..1a08f6b2c --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs @@ -0,0 +1,46 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct CredsspFfiResultBoxKerberosConfigBoxIronRdpError +{ + [StructLayout(LayoutKind.Explicit)] + private unsafe struct InnerUnion + { + [FieldOffset(0)] + internal KerberosConfig* ok; + [FieldOffset(0)] + internal IronRdpError* err; + } + + private InnerUnion _inner; + + [MarshalAs(UnmanagedType.U1)] + public bool isOk; + + public unsafe KerberosConfig* Ok + { + get + { + return _inner.ok; + } + } + + public unsafe IronRdpError* Err + { + get + { + return _inner.err; + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspSequence.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspSequence.cs index dcd88a538..0029d7f23 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspSequence.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspSequence.cs @@ -20,7 +20,7 @@ public partial struct CredsspSequence public static unsafe extern PduHint* NextPduHint(CredsspSequence* self); [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CredsspSequence_init", ExactSpelling = true)] - public static unsafe extern CredsspFfiResultBoxCredsspSequenceInitResultBoxIronRdpError Init(ClientConnector* connector, byte* serverName, nuint serverNameSz, byte* serverPublicKey, nuint serverPublicKeySz); + public static unsafe extern CredsspFfiResultBoxCredsspSequenceInitResultBoxIronRdpError Init(ClientConnector* connector, byte* serverName, nuint serverNameSz, byte* serverPublicKey, nuint serverPublicKeySz, KerberosConfig* kerberoConfigs); [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "CredsspSequence_decode_server_message", ExactSpelling = true)] public static unsafe extern CredsspFfiResultOptBoxTsRequestBoxIronRdpError DecodeServerMessage(CredsspSequence* self, byte* pdu, nuint pduSz); diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs new file mode 100644 index 000000000..c50d8ca74 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs @@ -0,0 +1,32 @@ +// by Diplomat + +#pragma warning disable 0105 +using System; +using System.Runtime.InteropServices; + +using Devolutions.IronRdp.Diplomat; +#pragma warning restore 0105 + +namespace Devolutions.IronRdp.Raw; + +#nullable enable + +[StructLayout(LayoutKind.Sequential)] +public partial struct KerberosConfig +{ + private const string NativeLib = "DevolutionsIronRdp"; + + /// + /// Creates a new KerberosConfig for KDC proxy support. + /// + /// + /// # Arguments + /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used + /// * `hostname` - Client hostname for Kerberos, empty string if not used + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "KerberosConfig_new", ExactSpelling = true)] + public static unsafe extern CredsspFfiResultBoxKerberosConfigBoxIronRdpError New(byte* kdcProxyUrl, nuint kdcProxyUrlSz, byte* hostname, nuint hostnameSz); + + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "KerberosConfig_destroy", ExactSpelling = true)] + public static unsafe extern void Destroy(KerberosConfig* self); +} diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs index 9a5d61426..ca3b0e738 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs @@ -97,7 +97,7 @@ private static async Task ConnectFinalize(string serverName, C private static async Task PerformCredsspSteps(ClientConnector connector, string serverName, WriteBuf writeBuf, Framed framedSsl, byte[] serverpubkey) { - var credsspSequenceInitResult = CredsspSequence.Init(connector, serverName, serverpubkey); + var credsspSequenceInitResult = CredsspSequence.Init(connector, serverName, serverpubkey, null); var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); var tsRequest = credsspSequenceInitResult.GetTsRequest(); var tcpClient = new TcpClient(); diff --git a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs index e4194d0dd..5dcee70c9 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs @@ -224,7 +224,7 @@ private static async Task PerformCredsspSteps( hostname = serverName.Substring(0, colonIndex); } - var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey); + var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, null); var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); var tsRequest = credsspSequenceInitResult.GetTsRequest(); var tcpClient = new System.Net.Sockets.TcpClient(); diff --git a/ffi/src/credssp/mod.rs b/ffi/src/credssp/mod.rs index 582377fbf..e0811d0cc 100644 --- a/ffi/src/credssp/mod.rs +++ b/ffi/src/credssp/mod.rs @@ -13,6 +13,33 @@ pub mod ffi { use crate::error::ValueConsumedError; use crate::pdu::ffi::WriteBuf; + #[diplomat::opaque] + pub struct KerberosConfig(pub ironrdp::connector::credssp::KerberosConfig); + + impl KerberosConfig { + /// Creates a new KerberosConfig for KDC proxy support. + /// + /// # Arguments + /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used + /// * `hostname` - Client hostname for Kerberos, empty string if not used + pub fn new(kdc_proxy_url: &str, hostname: &str) -> Result, Box> { + let kdc_proxy_url_opt = if kdc_proxy_url.is_empty() { + None + } else { + Some(kdc_proxy_url.to_owned()) + }; + + let hostname_opt = if hostname.is_empty() { + None + } else { + Some(hostname.to_owned()) + }; + + let config = ironrdp::connector::credssp::KerberosConfig::new(kdc_proxy_url_opt, hostname_opt)?; + Ok(Box::new(KerberosConfig(config))) + } + } + #[diplomat::opaque] pub struct CredsspSequence(pub ironrdp::connector::credssp::CredsspSequence); @@ -50,6 +77,7 @@ pub mod ffi { connector: &ClientConnector, server_name: &str, server_public_key: &[u8], + kerbero_configs: Option<&KerberosConfig>, ) -> Result, Box> { let Some(connector) = connector.0.as_ref() else { return Err(ValueConsumedError::for_item("connector").into()); @@ -63,7 +91,7 @@ pub mod ffi { selected_protocol, server_name.into(), server_public_key.to_owned(), - None, + kerbero_configs.map(|config| config.0.clone()), )?; Ok(Box::new(CredsspSequenceInitResult { From 107ad4fa5075bf59459a196592b8ceef6387fe0c Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 14:53:14 -0400 Subject: [PATCH 16/25] clean up --- .../Generated/KerberosConfig.cs | 36 --------------- ...iResultBoxKerberosConfigBoxIronRdpError.cs | 46 ------------------- .../Generated/RawKerberosConfig.cs | 11 ----- ffi/src/credssp/mod.rs | 24 ---------- 4 files changed, 117 deletions(-) delete mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs index ac310c85c..0cb7f570b 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/KerberosConfig.cs @@ -29,42 +29,6 @@ public unsafe KerberosConfig(Raw.KerberosConfig* handle) _inner = handle; } - /// - /// Creates a new KerberosConfig for KDC proxy support. - /// - /// - /// # Arguments - /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used - /// * `hostname` - Client hostname for Kerberos, empty string if not used - /// - /// - /// - /// A KerberosConfig allocated on Rust side. - /// - public static KerberosConfig New(string kdcProxyUrl, string hostname) - { - unsafe - { - byte[] kdcProxyUrlBuf = DiplomatUtils.StringToUtf8(kdcProxyUrl); - byte[] hostnameBuf = DiplomatUtils.StringToUtf8(hostname); - nuint kdcProxyUrlBufLength = (nuint)kdcProxyUrlBuf.Length; - nuint hostnameBufLength = (nuint)hostnameBuf.Length; - fixed (byte* kdcProxyUrlBufPtr = kdcProxyUrlBuf) - { - fixed (byte* hostnameBufPtr = hostnameBuf) - { - Raw.CredsspFfiResultBoxKerberosConfigBoxIronRdpError result = Raw.KerberosConfig.New(kdcProxyUrlBufPtr, kdcProxyUrlBufLength, hostnameBufPtr, hostnameBufLength); - if (!result.isOk) - { - throw new IronRdpException(new IronRdpError(result.Err)); - } - Raw.KerberosConfig* retVal = result.Ok; - return new KerberosConfig(retVal); - } - } - } - } - /// /// Returns the underlying raw handle. /// diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs deleted file mode 100644 index 1a08f6b2c..000000000 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawCredsspFfiResultBoxKerberosConfigBoxIronRdpError.cs +++ /dev/null @@ -1,46 +0,0 @@ -// by Diplomat - -#pragma warning disable 0105 -using System; -using System.Runtime.InteropServices; - -using Devolutions.IronRdp.Diplomat; -#pragma warning restore 0105 - -namespace Devolutions.IronRdp.Raw; - -#nullable enable - -[StructLayout(LayoutKind.Sequential)] -public partial struct CredsspFfiResultBoxKerberosConfigBoxIronRdpError -{ - [StructLayout(LayoutKind.Explicit)] - private unsafe struct InnerUnion - { - [FieldOffset(0)] - internal KerberosConfig* ok; - [FieldOffset(0)] - internal IronRdpError* err; - } - - private InnerUnion _inner; - - [MarshalAs(UnmanagedType.U1)] - public bool isOk; - - public unsafe KerberosConfig* Ok - { - get - { - return _inner.ok; - } - } - - public unsafe IronRdpError* Err - { - get - { - return _inner.err; - } - } -} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs index c50d8ca74..410ff439b 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawKerberosConfig.cs @@ -16,17 +16,6 @@ public partial struct KerberosConfig { private const string NativeLib = "DevolutionsIronRdp"; - /// - /// Creates a new KerberosConfig for KDC proxy support. - /// - /// - /// # Arguments - /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used - /// * `hostname` - Client hostname for Kerberos, empty string if not used - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "KerberosConfig_new", ExactSpelling = true)] - public static unsafe extern CredsspFfiResultBoxKerberosConfigBoxIronRdpError New(byte* kdcProxyUrl, nuint kdcProxyUrlSz, byte* hostname, nuint hostnameSz); - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "KerberosConfig_destroy", ExactSpelling = true)] public static unsafe extern void Destroy(KerberosConfig* self); } diff --git a/ffi/src/credssp/mod.rs b/ffi/src/credssp/mod.rs index e0811d0cc..b912d2685 100644 --- a/ffi/src/credssp/mod.rs +++ b/ffi/src/credssp/mod.rs @@ -16,30 +16,6 @@ pub mod ffi { #[diplomat::opaque] pub struct KerberosConfig(pub ironrdp::connector::credssp::KerberosConfig); - impl KerberosConfig { - /// Creates a new KerberosConfig for KDC proxy support. - /// - /// # Arguments - /// * `kdc_proxy_url` - KDC proxy URL (e.g., "https://gateway.example.com/KdcProxy/{token}"), empty string if not used - /// * `hostname` - Client hostname for Kerberos, empty string if not used - pub fn new(kdc_proxy_url: &str, hostname: &str) -> Result, Box> { - let kdc_proxy_url_opt = if kdc_proxy_url.is_empty() { - None - } else { - Some(kdc_proxy_url.to_owned()) - }; - - let hostname_opt = if hostname.is_empty() { - None - } else { - Some(hostname.to_owned()) - }; - - let config = ironrdp::connector::credssp::KerberosConfig::new(kdc_proxy_url_opt, hostname_opt)?; - Ok(Box::new(KerberosConfig(config))) - } - } - #[diplomat::opaque] pub struct CredsspSequence(pub ironrdp::connector::credssp::CredsspSequence); From fb6313db5eae688ebea142ac9137a3ca280a0e56 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 14:54:29 -0400 Subject: [PATCH 17/25] clean up --- ffi/dotnet/Devolutions.IronRdp/src/Connection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs index ca3b0e738..5696fd83f 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs @@ -135,7 +135,7 @@ private static async Task PerformCredsspSteps(ClientConnector connector, string } } - internal static async Task ResolveGenerator(CredsspProcessGenerator generator, System.Net.Sockets.TcpClient tcpClient) + internal static async Task ResolveGenerator(CredsspProcessGenerator generator, TcpClient tcpClient) { var state = generator.Start(); NetworkStream? stream = null; From 7771c3cb05b0dceaaa5f5a276752831ad7200b50 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 8 Oct 2025 15:32:16 -0400 Subject: [PATCH 18/25] clean up --- .../Devolutions.IronRdp/src/Connection.cs | 83 +----------- .../src/ConnectionHelpers.cs | 121 ++++++++++++++++++ .../src/RDCleanPathConnection.cs | 114 +---------------- 3 files changed, 125 insertions(+), 193 deletions(-) create mode 100644 ffi/dotnet/Devolutions.IronRdp/src/ConnectionHelpers.cs diff --git a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs index 5696fd83f..497e70ab2 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/Connection.cs @@ -17,22 +17,11 @@ public static class Connection var connector = ClientConnector.New(config, clientAddr); - connector.WithDynamicChannelDisplayControl(); - var dvcPipeProxy = config.DvcPipeProxy; - if (dvcPipeProxy != null) - { - connector.WithDynamicChannelPipeProxy(dvcPipeProxy); - } - - if (factory != null) - { - var cliprdr = factory.BuildCliprdr(); - connector.AttachStaticCliprdr(cliprdr); - } + ConnectionHelpers.SetupConnector(connector, config, factory); await ConnectBegin(framed, connector); var (serverPublicKey, framedSsl) = await SecurityUpgrade(framed, connector); - var result = await ConnectFinalize(serverName, connector, serverPublicKey, framedSsl); + var result = await ConnectionHelpers.ConnectFinalize(serverName, connector, serverPublicKey, framedSsl); return (result, framedSsl); } @@ -67,74 +56,6 @@ private static async Task ConnectBegin(Framed framed, ClientConne } } - - private static async Task ConnectFinalize(string serverName, ClientConnector connector, - byte[] serverPubKey, Framed framedSsl) - { - var writeBuf2 = WriteBuf.New(); - if (connector.ShouldPerformCredssp()) - { - await PerformCredsspSteps(connector, serverName, writeBuf2, framedSsl, serverPubKey); - } - - while (!connector.GetDynState().IsTerminal()) - { - await SingleSequenceStep(connector, writeBuf2, framedSsl); - } - - ClientConnectorState state = connector.ConsumeAndCastToClientConnectorState(); - - if (state.GetEnumType() == ClientConnectorStateType.Connected) - { - return state.GetConnectedResult(); - } - else - { - throw new IronRdpLibException(IronRdpLibExceptionType.ConnectionFailed, "Connection failed"); - } - } - - private static async Task PerformCredsspSteps(ClientConnector connector, string serverName, WriteBuf writeBuf, - Framed framedSsl, byte[] serverpubkey) - { - var credsspSequenceInitResult = CredsspSequence.Init(connector, serverName, serverpubkey, null); - var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); - var tsRequest = credsspSequenceInitResult.GetTsRequest(); - var tcpClient = new TcpClient(); - while (true) - { - var generator = credsspSequence.ProcessTsRequest(tsRequest); - var clientState = await ResolveGenerator(generator, tcpClient); - writeBuf.Clear(); - var written = credsspSequence.HandleProcessResult(clientState, writeBuf); - - if (written.GetSize().IsSome()) - { - var actualSize = (int)written.GetSize().Get(); - var response = new byte[actualSize]; - writeBuf.ReadIntoBuf(response); - await framedSsl.Write(response); - } - - var pduHint = credsspSequence.NextPduHint(); - if (pduHint == null) - { - break; - } - - var pdu = await framedSsl.ReadByHint(pduHint); - var decoded = credsspSequence.DecodeServerMessage(pdu); - - // Don't remove, DecodeServerMessage is generated, and it can return null - if (null == decoded) - { - break; - } - - tsRequest = decoded; - } - } - internal static async Task ResolveGenerator(CredsspProcessGenerator generator, TcpClient tcpClient) { var state = generator.Start(); diff --git a/ffi/dotnet/Devolutions.IronRdp/src/ConnectionHelpers.cs b/ffi/dotnet/Devolutions.IronRdp/src/ConnectionHelpers.cs new file mode 100644 index 000000000..d57aaaca5 --- /dev/null +++ b/ffi/dotnet/Devolutions.IronRdp/src/ConnectionHelpers.cs @@ -0,0 +1,121 @@ +using System.Net.Sockets; + +namespace Devolutions.IronRdp; + +/// +/// Internal helper class providing shared connection logic for both direct and RDCleanPath connections. +/// +internal static class ConnectionHelpers +{ + /// + /// Sets up common connector configuration including dynamic channels and clipboard. + /// + internal static void SetupConnector(ClientConnector connector, Config config, CliprdrBackendFactory? factory) + { + connector.WithDynamicChannelDisplayControl(); + + var dvcPipeProxy = config.DvcPipeProxy; + if (dvcPipeProxy != null) + { + connector.WithDynamicChannelPipeProxy(dvcPipeProxy); + } + + if (factory != null) + { + var cliprdr = factory.BuildCliprdr(); + connector.AttachStaticCliprdr(cliprdr); + } + } + + /// + /// Performs CredSSP authentication steps over any stream type. + /// + internal static async Task PerformCredsspSteps( + ClientConnector connector, + string serverName, + WriteBuf writeBuf, + Framed framed, + byte[] serverpubkey) where T : Stream + { + // Extract hostname from "hostname:port" format if needed + // CredSSP needs just the hostname for the service principal name (TERMSRV/hostname) + var hostname = serverName; + var colonIndex = serverName.IndexOf(':'); + if (colonIndex > 0) + { + hostname = serverName.Substring(0, colonIndex); + } + + var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, null); + var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); + var tsRequest = credsspSequenceInitResult.GetTsRequest(); + var tcpClient = new TcpClient(); + + while (true) + { + var generator = credsspSequence.ProcessTsRequest(tsRequest); + var clientState = await Connection.ResolveGenerator(generator, tcpClient); + writeBuf.Clear(); + var written = credsspSequence.HandleProcessResult(clientState, writeBuf); + + if (written.GetSize().IsSome()) + { + var actualSize = (int)written.GetSize().Get(); + var response = new byte[actualSize]; + writeBuf.ReadIntoBuf(response); + await framed.Write(response); + } + + var pduHint = credsspSequence.NextPduHint(); + if (pduHint == null) + { + break; + } + + var pdu = await framed.ReadByHint(pduHint); + var decoded = credsspSequence.DecodeServerMessage(pdu); + + // Don't remove, DecodeServerMessage is generated, and it can return null + if (null == decoded) + { + break; + } + + tsRequest = decoded; + } + } + + /// + /// Finalizes the RDP connection after security upgrade, performing CredSSP if needed + /// and completing the connection sequence. + /// + internal static async Task ConnectFinalize( + string serverName, + ClientConnector connector, + byte[] serverPubKey, + Framed framedSsl) where T : Stream + { + var writeBuf = WriteBuf.New(); + + if (connector.ShouldPerformCredssp()) + { + await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey); + } + + while (!connector.GetDynState().IsTerminal()) + { + await Connection.SingleSequenceStep(connector, writeBuf, framedSsl); + } + + ClientConnectorState state = connector.ConsumeAndCastToClientConnectorState(); + + if (state.GetEnumType() == ClientConnectorStateType.Connected) + { + return state.GetConnectedResult(); + } + else + { + throw new IronRdpLibException(IronRdpLibExceptionType.ConnectionFailed, "Connection failed"); + } + } +} diff --git a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs index 5dcee70c9..aefdf95cc 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs @@ -37,20 +37,7 @@ public static class RDCleanPathConnection // Step 3: Setup ClientConnector var connector = ClientConnector.New(config, clientAddr); - - // Attach optional dynamic/static channels - connector.WithDynamicChannelDisplayControl(); - var dvcPipeProxy = config.DvcPipeProxy; - if (dvcPipeProxy != null) - { - connector.WithDynamicChannelPipeProxy(dvcPipeProxy); - } - - if (factory != null) - { - var cliprdr = factory.BuildCliprdr(); - connector.AttachStaticCliprdr(cliprdr); - } + ConnectionHelpers.SetupConnector(connector, config, factory); // Step 4: Perform RDCleanPath handshake System.Diagnostics.Debug.WriteLine("Performing RDCleanPath handshake..."); @@ -62,7 +49,7 @@ public static class RDCleanPathConnection // Step 6: Finalize connection System.Diagnostics.Debug.WriteLine("Finalizing RDP connection..."); - var result = await ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake); + var result = await ConnectionHelpers.ConnectFinalize(destination, connector, serverPublicKey, framedAfterHandshake); System.Diagnostics.Debug.WriteLine("Gateway connection established successfully!"); return (result, framedAfterHandshake); @@ -165,103 +152,6 @@ public static class RDCleanPathConnection } } - /// - /// Finalizes the RDP connection after RDCleanPath handshake. - /// - private static async Task ConnectFinalize( - string serverName, - ClientConnector connector, - byte[] serverPubKey, - Framed framedSsl) - { - var writeBuf = WriteBuf.New(); - - // Perform CredSSP if needed - if (connector.ShouldPerformCredssp()) - { - System.Diagnostics.Debug.WriteLine("Performing CredSSP authentication..."); - await PerformCredsspSteps(connector, serverName, writeBuf, framedSsl, serverPubKey); - } - - // Continue with remaining connection steps - System.Diagnostics.Debug.WriteLine("Completing connection sequence..."); - while (!connector.GetDynState().IsTerminal()) - { - await Connection.SingleSequenceStep(connector, writeBuf, framedSsl); - } - - // Get final connection result - ClientConnectorState state = connector.ConsumeAndCastToClientConnectorState(); - - if (state.GetEnumType() == ClientConnectorStateType.Connected) - { - return state.GetConnectedResult(); - } - else - { - throw new IronRdpLibException( - IronRdpLibExceptionType.ConnectionFailed, - "Connection failed after RDCleanPath handshake"); - } - } - - /// - /// Performs CredSSP authentication steps. - /// - private static async Task PerformCredsspSteps( - ClientConnector connector, - string serverName, - WriteBuf writeBuf, - Framed framedSsl, - byte[] serverpubkey) - { - // Extract hostname from "hostname:port" format for CredSSP - // CredSSP needs just the hostname for the service principal name (TERMSRV/hostname) - var hostname = serverName; - var colonIndex = serverName.IndexOf(':'); - if (colonIndex > 0) - { - hostname = serverName.Substring(0, colonIndex); - } - - var credsspSequenceInitResult = CredsspSequence.Init(connector, hostname, serverpubkey, null); - var credsspSequence = credsspSequenceInitResult.GetCredsspSequence(); - var tsRequest = credsspSequenceInitResult.GetTsRequest(); - var tcpClient = new System.Net.Sockets.TcpClient(); - - while (true) - { - var generator = credsspSequence.ProcessTsRequest(tsRequest); - var clientState = await Connection.ResolveGenerator(generator, tcpClient); - writeBuf.Clear(); - var written = credsspSequence.HandleProcessResult(clientState, writeBuf); - - if (written.GetSize().IsSome()) - { - var actualSize = (int)written.GetSize().Get(); - var response = new byte[actualSize]; - writeBuf.ReadIntoBuf(response); - await framedSsl.Write(response); - } - - var pduHint = credsspSequence.NextPduHint(); - if (pduHint == null) - { - break; - } - - var pdu = await framedSsl.ReadByHint(pduHint); - var decoded = credsspSequence.DecodeServerMessage(pdu); - - if (null == decoded) - { - break; - } - - tsRequest = decoded; - } - } - /// /// Extracts the public key from an X.509 certificate in DER format. /// From ca5a6ab1042b5d3b0b87cc71b5a3fc13760f5a56 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 22 Oct 2025 12:49:17 -0400 Subject: [PATCH 19/25] review fix --- .../Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs | 6 +++--- ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs | 2 +- ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs | 6 +++--- ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs | 2 +- ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs index 486ce487b..4ab8ef3f3 100644 --- a/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs +++ b/ffi/dotnet/Devolutions.IronRdp.AvaloniaExample/MainWindow.axaml.cs @@ -23,7 +23,7 @@ public partial class MainWindow : Window readonly InputDatabase? _inputDatabase = InputDatabase.New(); ActiveStage? _activeStage; DecodedImage? _decodedImage; - Framed? _framed; + Framed? _framed; WinCliprdr? _cliprdr; private readonly RendererModel _renderModel; private Image? _imageControl; @@ -80,7 +80,7 @@ private void OnOpened(object? sender, EventArgs e) var password = Environment.GetEnvironmentVariable("IRONRDP_PASSWORD"); var domain = Environment.GetEnvironmentVariable("IRONRDP_DOMAIN"); // Optional var server = Environment.GetEnvironmentVariable("IRONRDP_SERVER"); - var portEnv = Environment.GetEnvironmentVariable("IRONRDP_PORT"); + var portEnv = Environment.GetEnvironmentVariable("IRONRDP_PORT"); // Optional // Gateway configuration (optional) var gatewayUrl = Environment.GetEnvironmentVariable("IRONRDP_GATEWAY_URL"); @@ -179,7 +179,7 @@ private void OnOpened(object? sender, EventArgs e) // Connect via gateway - destination needs "hostname:port" format for RDCleanPath string destination = $"{server}:{port}"; - var (gatewayRes, gatewayFramed) = await RDCleanPathConnection.ConnectViaGateway( + var (gatewayRes, gatewayFramed) = await RDCleanPathConnection.ConnectRDCleanPath( config, gatewayUrl, gatewayToken!, destination, null, factory); res = gatewayRes; this._framed = new Framed(gatewayFramed.GetInner().Item1); diff --git a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs index bff6cd9c4..3c85f2587 100644 --- a/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs +++ b/ffi/dotnet/Devolutions.IronRdp.ConnectExample/Program.cs @@ -23,7 +23,7 @@ static async Task Main(string[] args) try { - var (res, framed) = await Connection.Connect(buildConfig( username, password, domain, 1980, 1080), serverName, null); + var (res, framed) = await Connection.Connect(buildConfig(username, password, domain, 1980, 1080), serverName, null); var decodedImage = DecodedImage.New(PixelFormat.RgbA32, res.GetDesktopSize().GetWidth(), res.GetDesktopSize().GetHeight()); var activeState = ActiveStage.New(res); var keepLooping = true; diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs index 18223e37b..158688dd6 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs @@ -46,11 +46,11 @@ public DiplomatWriteable() IntPtr flushFuncPtr = Marshal.GetFunctionPointerForDelegate(flushFunc); IntPtr growFuncPtr = Marshal.GetFunctionPointerForDelegate(growFunc); - + // flushFunc and growFunc are managed objects and might be disposed of by the garbage collector. // To prevent this, we make the context hold the references and protect the context itself // for automatic disposal by moving it behind a GCHandle. - DiplomatWriteableContext ctx = new DiplomatWriteableContext(); + DiplomatWriteableContext ctx = new DiplomatWriteableContext(); ctx.flushFunc = flushFunc; ctx.growFunc = growFunc; GCHandle ctxHandle = GCHandle.Alloc(ctx); @@ -81,7 +81,7 @@ public string ToUnicode() { throw new IndexOutOfRangeException("DiplomatWriteable buffer is too big"); } - return Marshal.PtrToStringUTF8(buf, (int) len); + return Marshal.PtrToStringUTF8(buf, (int)len); #else byte[] utf8 = ToUtf8Bytes(); return DiplomatUtils.Utf8ToString(utf8); diff --git a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs index aefdf95cc..94b4ae366 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs @@ -19,7 +19,7 @@ public static class RDCleanPathConnection /// Optional preconnection blob for Hyper-V VM connections /// Optional clipboard backend factory /// A tuple containing the connection result and framed WebSocket stream - public static async Task<(ConnectionResult, Framed)> ConnectViaGateway( + public static async Task<(ConnectionResult, Framed)> ConnectRDCleanPath( Config config, string gatewayUrl, string authToken, diff --git a/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs index 883b8d43d..cdafc2a1d 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs @@ -36,10 +36,10 @@ public static async Task ConnectAsync( public ClientWebSocket Socket => _ws; - public override bool CanRead => true; - public override bool CanSeek => false; + public override bool CanRead => true; + public override bool CanSeek => false; public override bool CanWrite => true; - public override long Length => throw new NotSupportedException(); + public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override void Flush() { /* no-op */ } From 93628d433fac5e578b5f2a4d945e9e1b01cdb704 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 22 Oct 2025 14:11:04 -0400 Subject: [PATCH 20/25] review fix --- .../Generated/DiplomatRuntime.cs | 6 +- .../Generated/RDCleanPathPdu.cs | 242 +++++++++++++- .../Generated/RDCleanPathResult.cs | 309 ------------------ .../Generated/RawRDCleanPathPdu.cs | 51 ++- .../Generated/RawRDCleanPathResult.cs | 69 ---- ...ltRDCleanPathResultTypeBoxIronRdpError.cs} | 6 +- .../src/RDCleanPathConnection.cs | 19 +- .../src/WebsocketStream.cs | 49 ++- ffi/src/rdcleanpath.rs | 144 ++++---- 9 files changed, 429 insertions(+), 466 deletions(-) delete mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResult.cs delete mode 100644 ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResult.cs rename ffi/dotnet/Devolutions.IronRdp/Generated/{RawRdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError.cs => RawRdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError.cs} (81%) diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs index 158688dd6..18223e37b 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/DiplomatRuntime.cs @@ -46,11 +46,11 @@ public DiplomatWriteable() IntPtr flushFuncPtr = Marshal.GetFunctionPointerForDelegate(flushFunc); IntPtr growFuncPtr = Marshal.GetFunctionPointerForDelegate(growFunc); - + // flushFunc and growFunc are managed objects and might be disposed of by the garbage collector. // To prevent this, we make the context hold the references and protect the context itself // for automatic disposal by moving it behind a GCHandle. - DiplomatWriteableContext ctx = new DiplomatWriteableContext(); + DiplomatWriteableContext ctx = new DiplomatWriteableContext(); ctx.flushFunc = flushFunc; ctx.growFunc = growFunc; GCHandle ctxHandle = GCHandle.Alloc(ctx); @@ -81,7 +81,7 @@ public string ToUnicode() { throw new IndexOutOfRangeException("DiplomatWriteable buffer is too big"); } - return Marshal.PtrToStringUTF8(buf, (int)len); + return Marshal.PtrToStringUTF8(buf, (int) len); #else byte[] utf8 = ToUtf8Bytes(); return DiplomatUtils.Utf8ToString(utf8); diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs index d725b7733..7dc0636b9 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs @@ -15,6 +15,62 @@ public partial class RDCleanPathPdu: IDisposable { private unsafe Raw.RDCleanPathPdu* _inner; + public ushort ErrorCode + { + get + { + return GetErrorCode(); + } + } + + public string ErrorMessage + { + get + { + return GetErrorMessage(); + } + } + + public ushort HttpStatusCode + { + get + { + return GetHttpStatusCode(); + } + } + + public string ServerAddr + { + get + { + return GetServerAddr(); + } + } + + public CertificateChainIterator ServerCertChain + { + get + { + return GetServerCertChain(); + } + } + + public RDCleanPathResultType Type + { + get + { + return GetType(); + } + } + + public VecU8 X224Response + { + get + { + return GetX224Response(); + } + } + /// /// Creates a managed RDCleanPathPdu from a raw handle. /// @@ -146,13 +202,13 @@ public static RDCleanPathDetectionResult Detect(byte[] bytes) } /// - /// Converts the PDU into a typed enum for pattern matching + /// Gets the type of this RDCleanPath PDU /// /// /// - /// A RDCleanPathResult allocated on Rust side. + /// A RDCleanPathResultType allocated on C# side. /// - public RDCleanPathResult IntoEnum() + public RDCleanPathResultType GetType() { unsafe { @@ -160,13 +216,187 @@ public RDCleanPathResult IntoEnum() { throw new ObjectDisposedException("RDCleanPathPdu"); } - Raw.RdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError result = Raw.RDCleanPathPdu.IntoEnum(_inner); + Raw.RdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError result = Raw.RDCleanPathPdu.GetType(_inner); if (!result.isOk) { throw new IronRdpException(new IronRdpError(result.Err)); } - Raw.RDCleanPathResult* retVal = result.Ok; - return new RDCleanPathResult(retVal); + Raw.RDCleanPathResultType retVal = result.Ok; + return (RDCleanPathResultType)retVal; + } + } + + /// + /// Gets the X.224 connection response bytes (for Response or NegotiationError variants) + /// + /// + /// + /// A VecU8 allocated on Rust side. + /// + public VecU8 GetX224Response() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + Raw.RdcleanpathFfiResultBoxVecU8BoxIronRdpError result = Raw.RDCleanPathPdu.GetX224Response(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.VecU8* retVal = result.Ok; + return new VecU8(retVal); + } + } + + /// + /// Gets the server certificate chain (for Response variant) + /// Returns a vector iterator of certificate bytes + /// + /// + /// + /// A CertificateChainIterator allocated on Rust side. + /// + public CertificateChainIterator GetServerCertChain() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + Raw.RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError result = Raw.RDCleanPathPdu.GetServerCertChain(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + Raw.CertificateChainIterator* retVal = result.Ok; + return new CertificateChainIterator(retVal); + } + } + + /// + /// Gets the server address string (for Response variant) + /// + public void GetServerAddr(DiplomatWriteable writeable) + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + Raw.RDCleanPathPdu.GetServerAddr(_inner, &writeable); + } + } + + /// + /// Gets the server address string (for Response variant) + /// + public string GetServerAddr() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + DiplomatWriteable writeable = new DiplomatWriteable(); + Raw.RDCleanPathPdu.GetServerAddr(_inner, &writeable); + string retVal = writeable.ToUnicode(); + writeable.Dispose(); + return retVal; + } + } + + /// + /// Gets error message (for GeneralError variant) + /// + public void GetErrorMessage(DiplomatWriteable writeable) + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + Raw.RDCleanPathPdu.GetErrorMessage(_inner, &writeable); + } + } + + /// + /// Gets error message (for GeneralError variant) + /// + public string GetErrorMessage() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + DiplomatWriteable writeable = new DiplomatWriteable(); + Raw.RDCleanPathPdu.GetErrorMessage(_inner, &writeable); + string retVal = writeable.ToUnicode(); + writeable.Dispose(); + return retVal; + } + } + + /// + /// Gets the error code (for GeneralError variant) + /// + /// + public ushort GetErrorCode() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + Raw.RdcleanpathFfiResultU16BoxIronRdpError result = Raw.RDCleanPathPdu.GetErrorCode(_inner); + if (!result.isOk) + { + throw new IronRdpException(new IronRdpError(result.Err)); + } + ushort retVal = result.Ok; + return retVal; + } + } + + /// + /// Gets the HTTP status code if present (for GeneralError variant) + /// Returns 0 if not present or not a GeneralError variant + /// + public ushort GetHttpStatusCode() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + ushort retVal = Raw.RDCleanPathPdu.GetHttpStatusCode(_inner); + return retVal; + } + } + + /// + /// Checks if HTTP status code is present + /// + public bool HasHttpStatusCode() + { + unsafe + { + if (_inner == null) + { + throw new ObjectDisposedException("RDCleanPathPdu"); + } + bool retVal = Raw.RDCleanPathPdu.HasHttpStatusCode(_inner); + return retVal; } } diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResult.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResult.cs deleted file mode 100644 index c620d6fb2..000000000 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathResult.cs +++ /dev/null @@ -1,309 +0,0 @@ -// by Diplomat - -#pragma warning disable 0105 -using System; -using System.Runtime.InteropServices; - -using Devolutions.IronRdp.Diplomat; -#pragma warning restore 0105 - -namespace Devolutions.IronRdp; - -#nullable enable - -public partial class RDCleanPathResult: IDisposable -{ - private unsafe Raw.RDCleanPathResult* _inner; - - public ushort ErrorCode - { - get - { - return GetErrorCode(); - } - } - - public string ErrorMessage - { - get - { - return GetErrorMessage(); - } - } - - public ushort HttpStatusCode - { - get - { - return GetHttpStatusCode(); - } - } - - public string ServerAddr - { - get - { - return GetServerAddr(); - } - } - - public CertificateChainIterator ServerCertChain - { - get - { - return GetServerCertChain(); - } - } - - public RDCleanPathResultType Type - { - get - { - return GetType(); - } - } - - public VecU8 X224Response - { - get - { - return GetX224Response(); - } - } - - /// - /// Creates a managed RDCleanPathResult from a raw handle. - /// - /// - /// Safety: you should not build two managed objects using the same raw handle (may causes use-after-free and double-free). - ///
- /// This constructor assumes the raw struct is allocated on Rust side. - /// If implemented, the custom Drop implementation on Rust side WILL run on destruction. - ///
- public unsafe RDCleanPathResult(Raw.RDCleanPathResult* handle) - { - _inner = handle; - } - - /// - /// A RDCleanPathResultType allocated on C# side. - /// - public RDCleanPathResultType GetType() - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - Raw.RDCleanPathResultType retVal = Raw.RDCleanPathResult.GetType(_inner); - return (RDCleanPathResultType)retVal; - } - } - - /// - /// Gets the X.224 connection response bytes (for Response variant) - /// - /// - /// - /// A VecU8 allocated on Rust side. - /// - public VecU8 GetX224Response() - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - Raw.RdcleanpathFfiResultBoxVecU8BoxIronRdpError result = Raw.RDCleanPathResult.GetX224Response(_inner); - if (!result.isOk) - { - throw new IronRdpException(new IronRdpError(result.Err)); - } - Raw.VecU8* retVal = result.Ok; - return new VecU8(retVal); - } - } - - /// - /// Gets the server certificate chain (for Response variant) - /// Returns a vector iterator of certificate bytes - /// - /// - /// - /// A CertificateChainIterator allocated on Rust side. - /// - public CertificateChainIterator GetServerCertChain() - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - Raw.RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError result = Raw.RDCleanPathResult.GetServerCertChain(_inner); - if (!result.isOk) - { - throw new IronRdpException(new IronRdpError(result.Err)); - } - Raw.CertificateChainIterator* retVal = result.Ok; - return new CertificateChainIterator(retVal); - } - } - - /// - /// Gets the server address string (for Response variant) - /// - public void GetServerAddr(DiplomatWriteable writeable) - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - Raw.RDCleanPathResult.GetServerAddr(_inner, &writeable); - } - } - - /// - /// Gets the server address string (for Response variant) - /// - public string GetServerAddr() - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - DiplomatWriteable writeable = new DiplomatWriteable(); - Raw.RDCleanPathResult.GetServerAddr(_inner, &writeable); - string retVal = writeable.ToUnicode(); - writeable.Dispose(); - return retVal; - } - } - - /// - /// Gets error message (for GeneralError variant) - /// - public void GetErrorMessage(DiplomatWriteable writeable) - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - Raw.RDCleanPathResult.GetErrorMessage(_inner, &writeable); - } - } - - /// - /// Gets error message (for GeneralError variant) - /// - public string GetErrorMessage() - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - DiplomatWriteable writeable = new DiplomatWriteable(); - Raw.RDCleanPathResult.GetErrorMessage(_inner, &writeable); - string retVal = writeable.ToUnicode(); - writeable.Dispose(); - return retVal; - } - } - - /// - /// Gets the error code (for GeneralError variant) - /// - /// - public ushort GetErrorCode() - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - Raw.RdcleanpathFfiResultU16BoxIronRdpError result = Raw.RDCleanPathResult.GetErrorCode(_inner); - if (!result.isOk) - { - throw new IronRdpException(new IronRdpError(result.Err)); - } - ushort retVal = result.Ok; - return retVal; - } - } - - /// - /// Gets the HTTP status code if present (for GeneralError variant) - /// Returns 0 if not present or not a GeneralError variant - /// - public ushort GetHttpStatusCode() - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - ushort retVal = Raw.RDCleanPathResult.GetHttpStatusCode(_inner); - return retVal; - } - } - - /// - /// Checks if HTTP status code is present - /// - public bool HasHttpStatusCode() - { - unsafe - { - if (_inner == null) - { - throw new ObjectDisposedException("RDCleanPathResult"); - } - bool retVal = Raw.RDCleanPathResult.HasHttpStatusCode(_inner); - return retVal; - } - } - - /// - /// Returns the underlying raw handle. - /// - public unsafe Raw.RDCleanPathResult* AsFFI() - { - return _inner; - } - - /// - /// Destroys the underlying object immediately. - /// - public void Dispose() - { - unsafe - { - if (_inner == null) - { - return; - } - - Raw.RDCleanPathResult.Destroy(_inner); - _inner = null; - - GC.SuppressFinalize(this); - } - } - - ~RDCleanPathResult() - { - Dispose(); - } -} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs index 6624229f6..c2310b74e 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs @@ -48,10 +48,55 @@ public partial struct RDCleanPathPdu public static unsafe extern RDCleanPathDetectionResult* Detect(byte* bytes, nuint bytesSz); /// - /// Converts the PDU into a typed enum for pattern matching + /// Gets the type of this RDCleanPath PDU /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_into_enum", ExactSpelling = true)] - public static unsafe extern RdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError IntoEnum(RDCleanPathPdu* self); + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_type", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError GetType(RDCleanPathPdu* self); + + /// + /// Gets the X.224 connection response bytes (for Response or NegotiationError variants) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_x224_response", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultBoxVecU8BoxIronRdpError GetX224Response(RDCleanPathPdu* self); + + /// + /// Gets the server certificate chain (for Response variant) + /// Returns a vector iterator of certificate bytes + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_server_cert_chain", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError GetServerCertChain(RDCleanPathPdu* self); + + /// + /// Gets the server address string (for Response variant) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_server_addr", ExactSpelling = true)] + public static unsafe extern void GetServerAddr(RDCleanPathPdu* self, DiplomatWriteable* writeable); + + /// + /// Gets error message (for GeneralError variant) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_error_message", ExactSpelling = true)] + public static unsafe extern void GetErrorMessage(RDCleanPathPdu* self, DiplomatWriteable* writeable); + + /// + /// Gets the error code (for GeneralError variant) + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_error_code", ExactSpelling = true)] + public static unsafe extern RdcleanpathFfiResultU16BoxIronRdpError GetErrorCode(RDCleanPathPdu* self); + + /// + /// Gets the HTTP status code if present (for GeneralError variant) + /// Returns 0 if not present or not a GeneralError variant + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_http_status_code", ExactSpelling = true)] + public static unsafe extern ushort GetHttpStatusCode(RDCleanPathPdu* self); + + /// + /// Checks if HTTP status code is present + /// + [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_has_http_status_code", ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.U1)] + public static unsafe extern bool HasHttpStatusCode(RDCleanPathPdu* self); [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_destroy", ExactSpelling = true)] public static unsafe extern void Destroy(RDCleanPathPdu* self); diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResult.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResult.cs deleted file mode 100644 index 873b90158..000000000 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathResult.cs +++ /dev/null @@ -1,69 +0,0 @@ -// by Diplomat - -#pragma warning disable 0105 -using System; -using System.Runtime.InteropServices; - -using Devolutions.IronRdp.Diplomat; -#pragma warning restore 0105 - -namespace Devolutions.IronRdp.Raw; - -#nullable enable - -[StructLayout(LayoutKind.Sequential)] -public partial struct RDCleanPathResult -{ - private const string NativeLib = "DevolutionsIronRdp"; - - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_type", ExactSpelling = true)] - public static unsafe extern RDCleanPathResultType GetType(RDCleanPathResult* self); - - /// - /// Gets the X.224 connection response bytes (for Response variant) - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_x224_response", ExactSpelling = true)] - public static unsafe extern RdcleanpathFfiResultBoxVecU8BoxIronRdpError GetX224Response(RDCleanPathResult* self); - - /// - /// Gets the server certificate chain (for Response variant) - /// Returns a vector iterator of certificate bytes - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_server_cert_chain", ExactSpelling = true)] - public static unsafe extern RdcleanpathFfiResultBoxCertificateChainIteratorBoxIronRdpError GetServerCertChain(RDCleanPathResult* self); - - /// - /// Gets the server address string (for Response variant) - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_server_addr", ExactSpelling = true)] - public static unsafe extern void GetServerAddr(RDCleanPathResult* self, DiplomatWriteable* writeable); - - /// - /// Gets error message (for GeneralError variant) - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_error_message", ExactSpelling = true)] - public static unsafe extern void GetErrorMessage(RDCleanPathResult* self, DiplomatWriteable* writeable); - - /// - /// Gets the error code (for GeneralError variant) - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_error_code", ExactSpelling = true)] - public static unsafe extern RdcleanpathFfiResultU16BoxIronRdpError GetErrorCode(RDCleanPathResult* self); - - /// - /// Gets the HTTP status code if present (for GeneralError variant) - /// Returns 0 if not present or not a GeneralError variant - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_get_http_status_code", ExactSpelling = true)] - public static unsafe extern ushort GetHttpStatusCode(RDCleanPathResult* self); - - /// - /// Checks if HTTP status code is present - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_has_http_status_code", ExactSpelling = true)] - [return: MarshalAs(UnmanagedType.U1)] - public static unsafe extern bool HasHttpStatusCode(RDCleanPathResult* self); - - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathResult_destroy", ExactSpelling = true)] - public static unsafe extern void Destroy(RDCleanPathResult* self); -} diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError.cs similarity index 81% rename from ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError.cs rename to ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError.cs index cf63f3582..6a9c6e8a9 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError.cs @@ -12,13 +12,13 @@ namespace Devolutions.IronRdp.Raw; #nullable enable [StructLayout(LayoutKind.Sequential)] -public partial struct RdcleanpathFfiResultBoxRDCleanPathResultBoxIronRdpError +public partial struct RdcleanpathFfiResultRDCleanPathResultTypeBoxIronRdpError { [StructLayout(LayoutKind.Explicit)] private unsafe struct InnerUnion { [FieldOffset(0)] - internal RDCleanPathResult* ok; + internal RDCleanPathResultType ok; [FieldOffset(0)] internal IronRdpError* err; } @@ -28,7 +28,7 @@ private unsafe struct InnerUnion [MarshalAs(UnmanagedType.U1)] public bool isOk; - public unsafe RDCleanPathResult* Ok + public unsafe RDCleanPathResultType Ok { get { diff --git a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs index 94b4ae366..725c0525f 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/RDCleanPathConnection.cs @@ -32,8 +32,10 @@ public static class RDCleanPathConnection var ws = await WebSocketStream.ConnectAsync(new Uri(gatewayUrl)); var framed = new Framed(ws); - // Step 2: Get client local address (dummy for WebSocket) - string clientAddr = "127.0.0.1:33899"; + // Step 2: Get client local address from the WebSocket connection + // This mimics Rust: let client_addr = socket.local_addr()?; + string clientAddr = ws.ClientAddr; + System.Diagnostics.Debug.WriteLine($"Client local address: {clientAddr}"); // Step 3: Setup ClientConnector var connector = ClientConnector.New(config, clientAddr); @@ -87,16 +89,15 @@ public static class RDCleanPathConnection var respBytes = await framed.ReadByHint(new RDCleanPathHint()); var rdCleanPathResp = RDCleanPathPdu.FromDer(respBytes); - // Step 4: Parse response - var result = rdCleanPathResp.IntoEnum(); - var resultType = result.GetType(); + // Step 4: Determine response type and handle accordingly + var resultType = rdCleanPathResp.GetType(); if (resultType == RDCleanPathResultType.Response) { System.Diagnostics.Debug.WriteLine("RDCleanPath handshake successful!"); // Extract X.224 response - var x224Response = result.GetX224Response(); + var x224Response = rdCleanPathResp.GetX224Response(); var x224ResponseBytes = new byte[x224Response.GetSize()]; x224Response.Fill(x224ResponseBytes); @@ -105,7 +106,7 @@ public static class RDCleanPathConnection connector.Step(x224ResponseBytes, writeBuf); // Extract server public key from certificate chain - var certChain = result.GetServerCertChain(); + var certChain = rdCleanPathResp.GetServerCertChain(); if (certChain.IsEmpty()) { throw new IronRdpLibException( @@ -132,8 +133,8 @@ public static class RDCleanPathConnection } else if (resultType == RDCleanPathResultType.GeneralError) { - var errorCode = result.GetErrorCode(); - var errorMessage = result.GetErrorMessage(); + var errorCode = rdCleanPathResp.GetErrorCode(); + var errorMessage = rdCleanPathResp.GetErrorMessage(); throw new IronRdpLibException( IronRdpLibExceptionType.ConnectionFailed, $"RDCleanPath error (code {errorCode}): {errorMessage}"); diff --git a/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs index cdafc2a1d..403c8fbd2 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs @@ -1,6 +1,8 @@ using System; using System.Buffers; using System.IO; +using System.Net; +using System.Net.Sockets; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; @@ -13,14 +15,16 @@ public sealed class WebSocketStream : Stream private int _recvLen; private bool _remoteClosed; private bool _disposed; + private readonly string _clientAddr; private const int DefaultRecvBufferSize = 64 * 1024; private const int MaxSendFrame = 16 * 1024; // send in chunks - private WebSocketStream(ClientWebSocket ws, int receiveBufferSize) + private WebSocketStream(ClientWebSocket ws, int receiveBufferSize, string clientAddr) { _ws = ws ?? throw new ArgumentNullException(nameof(ws)); _recvBuf = ArrayPool.Shared.Rent(Math.Max(1024, receiveBufferSize)); + _clientAddr = clientAddr; } public static async Task ConnectAsync( @@ -29,13 +33,54 @@ public static async Task ConnectAsync( int receiveBufferSize = DefaultRecvBufferSize, CancellationToken ct = default) { + // Follow the Rust native client approach: establish TCP connection first, + // get local address, then use it for WebSocket + var hostname = uri.Host; + var port = uri.Port > 0 ? uri.Port : (uri.Scheme == "wss" ? 443 : 80); + + string clientAddr; + + // Step 1: Create TCP socket and connect to get local address + // This mimics Rust: let socket = TcpStream::connect((hostname, port)).await?; + // let client_addr = socket.local_addr()?; + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + try + { + await socket.ConnectAsync(hostname, port, ct).ConfigureAwait(false); + + // Step 2: Set TCP_NODELAY (like Rust: socket.set_nodelay(true)?) + socket.NoDelay = true; + + // Step 3: Get local endpoint + var localEndPoint = socket.LocalEndPoint as IPEndPoint; + clientAddr = localEndPoint?.ToString() ?? "127.0.0.1:0"; + + // Step 4: Close this probe socket - we just needed the address + // (Unfortunately, .NET's ClientWebSocket doesn't support using an existing socket) + socket.Close(); + } + catch + { + socket.Dispose(); + throw; + } + + // Step 5: Now establish the actual WebSocket connection with TLS + // This mimics Rust: tokio_tungstenite::client_async_tls(rdcleanpath.url.as_str(), socket) ws ??= new ClientWebSocket(); await ws.ConnectAsync(uri, ct).ConfigureAwait(false); - return new WebSocketStream(ws, receiveBufferSize); + + return new WebSocketStream(ws, receiveBufferSize, clientAddr); } public ClientWebSocket Socket => _ws; + /// + /// Gets the local client address in "IP:port" format. + /// This is the address that was determined when establishing the TCP connection. + /// + public string ClientAddr => _clientAddr; + public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => true; diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index ea74a490e..56ee17c1a 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -66,8 +66,8 @@ pub mod ffi { Box::new(RDCleanPathDetectionResult(result)) } - /// Converts the PDU into a typed enum for pattern matching - pub fn into_enum(&self) -> Result, Box> { + /// Gets the type of this RDCleanPath PDU + pub fn get_type(&self) -> Result> { let rdcleanpath = self .0 .clone() @@ -75,66 +75,33 @@ pub mod ffi { .context("missing RDCleanPath field") .map_err(GenericError)?; - Ok(Box::new(RDCleanPathResult(rdcleanpath))) - } - } - - #[diplomat::opaque] - pub struct RDCleanPathDetectionResult(pub ironrdp_rdcleanpath::DetectionResult); - - impl RDCleanPathDetectionResult { - pub fn is_detected(&self) -> bool { - matches!(self.0, ironrdp_rdcleanpath::DetectionResult::Detected { .. }) - } - - pub fn is_not_enough_bytes(&self) -> bool { - matches!(self.0, ironrdp_rdcleanpath::DetectionResult::NotEnoughBytes) - } - - pub fn is_failed(&self) -> bool { - matches!(self.0, ironrdp_rdcleanpath::DetectionResult::Failed) - } - - pub fn get_total_length(&self) -> Result> { - if let ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } = self.0 { - Ok(total_length) - } else { - Err(GenericError(anyhow::anyhow!("detection result is not Detected variant")).into()) - } - } - } - - #[diplomat::opaque] - pub struct RDCleanPathResult(pub ironrdp_rdcleanpath::RDCleanPath); - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum RDCleanPathResultType { - Request, - Response, - GeneralError, - NegotiationError, - } - - impl RDCleanPathResult { - pub fn get_type(&self) -> RDCleanPathResultType { - match &self.0 { + let result_type = match rdcleanpath { ironrdp_rdcleanpath::RDCleanPath::Request { .. } => RDCleanPathResultType::Request, ironrdp_rdcleanpath::RDCleanPath::Response { .. } => RDCleanPathResultType::Response, ironrdp_rdcleanpath::RDCleanPath::GeneralErr(_) => RDCleanPathResultType::GeneralError, ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { .. } => RDCleanPathResultType::NegotiationError, - } + }; + + Ok(result_type) } - /// Gets the X.224 connection response bytes (for Response variant) + /// Gets the X.224 connection response bytes (for Response or NegotiationError variants) pub fn get_x224_response(&self) -> Result, Box> { - match &self.0 { + let rdcleanpath = self + .0 + .clone() + .into_enum() + .context("missing RDCleanPath field") + .map_err(GenericError)?; + + match rdcleanpath { ironrdp_rdcleanpath::RDCleanPath::Response { x224_connection_response, .. } => Ok(Box::new(VecU8(x224_connection_response.as_bytes().to_vec()))), ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { x224_connection_response, - } => Ok(Box::new(VecU8(x224_connection_response.clone()))), + } => Ok(Box::new(VecU8(x224_connection_response))), _ => Err(GenericError(anyhow::anyhow!("RDCleanPath variant does not contain X.224 response")).into()), } } @@ -142,7 +109,14 @@ pub mod ffi { /// Gets the server certificate chain (for Response variant) /// Returns a vector iterator of certificate bytes pub fn get_server_cert_chain(&self) -> Result, Box> { - match &self.0 { + let rdcleanpath = self + .0 + .clone() + .into_enum() + .context("missing RDCleanPath field") + .map_err(GenericError)?; + + match rdcleanpath { ironrdp_rdcleanpath::RDCleanPath::Response { server_cert_chain, .. } => { let certs: Vec> = server_cert_chain.iter().map(|cert| cert.as_bytes().to_vec()).collect(); Ok(Box::new(CertificateChainIterator { certs, index: 0 })) @@ -156,21 +130,32 @@ pub mod ffi { /// Gets the server address string (for Response variant) pub fn get_server_addr<'a>(&'a self, writeable: &'a mut DiplomatWriteable) { - if let ironrdp_rdcleanpath::RDCleanPath::Response { server_addr, .. } = &self.0 { - let _ = write!(writeable, "{server_addr}"); + if let Ok(rdcleanpath) = self.0.clone().into_enum() { + if let ironrdp_rdcleanpath::RDCleanPath::Response { server_addr, .. } = rdcleanpath { + let _ = write!(writeable, "{server_addr}"); + } } } /// Gets error message (for GeneralError variant) pub fn get_error_message<'a>(&'a self, writeable: &'a mut DiplomatWriteable) { - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { - let _ = write!(writeable, "{err}"); + if let Ok(rdcleanpath) = self.0.clone().into_enum() { + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { + let _ = write!(writeable, "{err}"); + } } } /// Gets the error code (for GeneralError variant) pub fn get_error_code(&self) -> Result> { - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { + let rdcleanpath = self + .0 + .clone() + .into_enum() + .context("missing RDCleanPath field") + .map_err(GenericError)?; + + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { Ok(err.error_code) } else { Err(GenericError(anyhow::anyhow!("not a GeneralError variant")).into()) @@ -180,19 +165,54 @@ pub mod ffi { /// Gets the HTTP status code if present (for GeneralError variant) /// Returns 0 if not present or not a GeneralError variant pub fn get_http_status_code(&self) -> u16 { - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { - err.http_status_code.unwrap_or(0) - } else { - 0 + if let Ok(rdcleanpath) = self.0.clone().into_enum() { + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { + return err.http_status_code.unwrap_or(0); + } } + 0 } /// Checks if HTTP status code is present pub fn has_http_status_code(&self) -> bool { - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = &self.0 { - err.http_status_code.is_some() + if let Ok(rdcleanpath) = self.0.clone().into_enum() { + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { + return err.http_status_code.is_some(); + } + } + false + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum RDCleanPathResultType { + Request, + Response, + GeneralError, + NegotiationError, + } + + #[diplomat::opaque] + pub struct RDCleanPathDetectionResult(pub ironrdp_rdcleanpath::DetectionResult); + + impl RDCleanPathDetectionResult { + pub fn is_detected(&self) -> bool { + matches!(self.0, ironrdp_rdcleanpath::DetectionResult::Detected { .. }) + } + + pub fn is_not_enough_bytes(&self) -> bool { + matches!(self.0, ironrdp_rdcleanpath::DetectionResult::NotEnoughBytes) + } + + pub fn is_failed(&self) -> bool { + matches!(self.0, ironrdp_rdcleanpath::DetectionResult::Failed) + } + + pub fn get_total_length(&self) -> Result> { + if let ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } = self.0 { + Ok(total_length) } else { - false + Err(GenericError(anyhow::anyhow!("detection result is not Detected variant")).into()) } } } From 9a1ff9f1513ffac2e46bda576c7219548e620f64 Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 22 Oct 2025 14:34:57 -0400 Subject: [PATCH 21/25] review fix --- .../src/WebsocketStream.cs | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs index 403c8fbd2..5f5281011 100644 --- a/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs +++ b/ffi/dotnet/Devolutions.IronRdp/src/WebsocketStream.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.IO; using System.Net; +using System.Net.Http; using System.Net.Sockets; using System.Net.WebSockets; using System.Threading; @@ -33,42 +34,35 @@ public static async Task ConnectAsync( int receiveBufferSize = DefaultRecvBufferSize, CancellationToken ct = default) { - // Follow the Rust native client approach: establish TCP connection first, - // get local address, then use it for WebSocket - var hostname = uri.Host; - var port = uri.Port > 0 ? uri.Port : (uri.Scheme == "wss" ? 443 : 80); - - string clientAddr; - - // Step 1: Create TCP socket and connect to get local address - // This mimics Rust: let socket = TcpStream::connect((hostname, port)).await?; - // let client_addr = socket.local_addr()?; - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - try + // Capture the local endpoint from the socket using SocketsHttpHandler.ConnectCallback + // This follows the Rust approach: socket.local_addr() + IPEndPoint? localEndPoint = null; + + var handler = new SocketsHttpHandler { - await socket.ConnectAsync(hostname, port, ct).ConfigureAwait(false); + ConnectCallback = async (context, cancellationToken) => + { + var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); - // Step 2: Set TCP_NODELAY (like Rust: socket.set_nodelay(true)?) - socket.NoDelay = true; + // Set TCP_NODELAY (matching Rust: socket.set_nodelay(true)) + socket.NoDelay = true; - // Step 3: Get local endpoint - var localEndPoint = socket.LocalEndPoint as IPEndPoint; - clientAddr = localEndPoint?.ToString() ?? "127.0.0.1:0"; + // Connect to the endpoint + await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false); - // Step 4: Close this probe socket - we just needed the address - // (Unfortunately, .NET's ClientWebSocket doesn't support using an existing socket) - socket.Close(); - } - catch - { - socket.Dispose(); - throw; - } + // Capture the local endpoint after connection + localEndPoint = socket.LocalEndPoint as IPEndPoint; + + return new NetworkStream(socket, ownsSocket: true); + } + }; + + var invoker = new HttpMessageInvoker(handler); - // Step 5: Now establish the actual WebSocket connection with TLS - // This mimics Rust: tokio_tungstenite::client_async_tls(rdcleanpath.url.as_str(), socket) ws ??= new ClientWebSocket(); - await ws.ConnectAsync(uri, ct).ConfigureAwait(false); + await ws.ConnectAsync(uri, invoker, ct).ConfigureAwait(false); + + string clientAddr = localEndPoint?.ToString() ?? "127.0.0.1:0"; return new WebSocketStream(ws, receiveBufferSize, clientAddr); } From b693a48d8c834caca673843a4ca79e6bb3dc99ce Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 22 Oct 2025 15:08:44 -0400 Subject: [PATCH 22/25] review fix --- .../Generated/RDCleanPathPdu.cs | 22 ++++---------- .../Generated/RawRDCleanPathPdu.cs | 11 ++----- ffi/src/rdcleanpath.rs | 29 +++++++++---------- 3 files changed, 21 insertions(+), 41 deletions(-) diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs index 7dc0636b9..b4c5d6278 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RDCleanPathPdu.cs @@ -369,8 +369,9 @@ public ushort GetErrorCode() /// /// Gets the HTTP status code if present (for GeneralError variant) - /// Returns 0 if not present or not a GeneralError variant + /// Returns error if not present or not a GeneralError variant /// + /// public ushort GetHttpStatusCode() { unsafe @@ -379,23 +380,12 @@ public ushort GetHttpStatusCode() { throw new ObjectDisposedException("RDCleanPathPdu"); } - ushort retVal = Raw.RDCleanPathPdu.GetHttpStatusCode(_inner); - return retVal; - } - } - - /// - /// Checks if HTTP status code is present - /// - public bool HasHttpStatusCode() - { - unsafe - { - if (_inner == null) + Raw.RdcleanpathFfiResultU16BoxIronRdpError result = Raw.RDCleanPathPdu.GetHttpStatusCode(_inner); + if (!result.isOk) { - throw new ObjectDisposedException("RDCleanPathPdu"); + throw new IronRdpException(new IronRdpError(result.Err)); } - bool retVal = Raw.RDCleanPathPdu.HasHttpStatusCode(_inner); + ushort retVal = result.Ok; return retVal; } } diff --git a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs index c2310b74e..5710356a1 100644 --- a/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs +++ b/ffi/dotnet/Devolutions.IronRdp/Generated/RawRDCleanPathPdu.cs @@ -86,17 +86,10 @@ public partial struct RDCleanPathPdu /// /// Gets the HTTP status code if present (for GeneralError variant) - /// Returns 0 if not present or not a GeneralError variant + /// Returns error if not present or not a GeneralError variant /// [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_get_http_status_code", ExactSpelling = true)] - public static unsafe extern ushort GetHttpStatusCode(RDCleanPathPdu* self); - - /// - /// Checks if HTTP status code is present - /// - [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_has_http_status_code", ExactSpelling = true)] - [return: MarshalAs(UnmanagedType.U1)] - public static unsafe extern bool HasHttpStatusCode(RDCleanPathPdu* self); + public static unsafe extern RdcleanpathFfiResultU16BoxIronRdpError GetHttpStatusCode(RDCleanPathPdu* self); [DllImport(NativeLib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "RDCleanPathPdu_destroy", ExactSpelling = true)] public static unsafe extern void Destroy(RDCleanPathPdu* self); diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index 56ee17c1a..560e87e20 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -163,24 +163,21 @@ pub mod ffi { } /// Gets the HTTP status code if present (for GeneralError variant) - /// Returns 0 if not present or not a GeneralError variant - pub fn get_http_status_code(&self) -> u16 { - if let Ok(rdcleanpath) = self.0.clone().into_enum() { - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { - return err.http_status_code.unwrap_or(0); - } - } - 0 - } + /// Returns error if not present or not a GeneralError variant + pub fn get_http_status_code(&self) -> Result> { + let rdcleanpath = self + .0 + .clone() + .into_enum() + .context("missing RDCleanPath field") + .map_err(GenericError)?; - /// Checks if HTTP status code is present - pub fn has_http_status_code(&self) -> bool { - if let Ok(rdcleanpath) = self.0.clone().into_enum() { - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { - return err.http_status_code.is_some(); - } + if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { + err.http_status_code + .ok_or_else(|| GenericError(anyhow::anyhow!("HTTP status code not present")).into()) + } else { + Err(GenericError(anyhow::anyhow!("not a GeneralError variant")).into()) } - false } } From a19b1c7ea71e50d9cada30e5bd1e558e2ef089cc Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 22 Oct 2025 15:31:30 -0400 Subject: [PATCH 23/25] CI fix --- ffi/src/rdcleanpath.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index 560e87e20..18bc2deeb 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -130,19 +130,15 @@ pub mod ffi { /// Gets the server address string (for Response variant) pub fn get_server_addr<'a>(&'a self, writeable: &'a mut DiplomatWriteable) { - if let Ok(rdcleanpath) = self.0.clone().into_enum() { - if let ironrdp_rdcleanpath::RDCleanPath::Response { server_addr, .. } = rdcleanpath { - let _ = write!(writeable, "{server_addr}"); - } + if let Ok(ironrdp_rdcleanpath::RDCleanPath::Response { server_addr, .. }) = self.0.clone().into_enum() { + let _ = write!(writeable, "{server_addr}"); } } /// Gets error message (for GeneralError variant) pub fn get_error_message<'a>(&'a self, writeable: &'a mut DiplomatWriteable) { - if let Ok(rdcleanpath) = self.0.clone().into_enum() { - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { - let _ = write!(writeable, "{err}"); - } + if let Ok(ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err)) = self.0.clone().into_enum() { + let _ = write!(writeable, "{err}"); } } From 9640a3eb249919edcdaeca6dbd5f5952f00aaf2c Mon Sep 17 00:00:00 2001 From: irving ou Date: Wed, 29 Oct 2025 16:34:02 -0400 Subject: [PATCH 24/25] fixing CI --- ffi/src/rdcleanpath.rs | 166 ++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 68 deletions(-) diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index 18bc2deeb..4418ae911 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -68,111 +68,141 @@ pub mod ffi { /// Gets the type of this RDCleanPath PDU pub fn get_type(&self) -> Result> { - let rdcleanpath = self - .0 - .clone() - .into_enum() - .context("missing RDCleanPath field") - .map_err(GenericError)?; + if self.0.destination.is_some() { + if self.0.proxy_auth.is_none() { + return Err(Self::missing_field("proxy_auth")); + } + + if self.0.x224_connection_pdu.is_none() { + return Err(Self::missing_field("x224_connection_pdu")); + } - let result_type = match rdcleanpath { - ironrdp_rdcleanpath::RDCleanPath::Request { .. } => RDCleanPathResultType::Request, - ironrdp_rdcleanpath::RDCleanPath::Response { .. } => RDCleanPathResultType::Response, - ironrdp_rdcleanpath::RDCleanPath::GeneralErr(_) => RDCleanPathResultType::GeneralError, - ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { .. } => RDCleanPathResultType::NegotiationError, - }; + Ok(RDCleanPathResultType::Request) + } else if self.0.server_addr.is_some() { + if self.0.x224_connection_pdu.is_none() { + return Err(Self::missing_field("x224_connection_pdu")); + } + + if self.0.server_cert_chain.is_none() { + return Err(Self::missing_field("server_cert_chain")); + } - Ok(result_type) + Ok(RDCleanPathResultType::Response) + } else if let Some(error) = &self.0.error { + if error.error_code == ironrdp_rdcleanpath::NEGOTIATION_ERROR_CODE { + if self.0.x224_connection_pdu.is_none() { + return Err(Self::missing_field("x224_connection_pdu")); + } + + Ok(RDCleanPathResultType::NegotiationError) + } else { + Ok(RDCleanPathResultType::GeneralError) + } + } else { + Err(Self::missing_field("error")) + } } /// Gets the X.224 connection response bytes (for Response or NegotiationError variants) pub fn get_x224_response(&self) -> Result, Box> { - let rdcleanpath = self - .0 - .clone() - .into_enum() - .context("missing RDCleanPath field") - .map_err(GenericError)?; - - match rdcleanpath { - ironrdp_rdcleanpath::RDCleanPath::Response { - x224_connection_response, - .. - } => Ok(Box::new(VecU8(x224_connection_response.as_bytes().to_vec()))), - ironrdp_rdcleanpath::RDCleanPath::NegotiationErr { - x224_connection_response, - } => Ok(Box::new(VecU8(x224_connection_response))), - _ => Err(GenericError(anyhow::anyhow!("RDCleanPath variant does not contain X.224 response")).into()), + if self.0.server_addr.is_some() { + let x224 = self + .0 + .x224_connection_pdu + .as_ref() + .ok_or_else(|| Self::missing_field("x224_connection_pdu"))?; + self.0 + .server_cert_chain + .as_ref() + .ok_or_else(|| Self::missing_field("server_cert_chain"))?; + + Ok(Box::new(VecU8(x224.as_bytes().to_vec()))) + } else if let Some(error) = &self.0.error { + if error.error_code == ironrdp_rdcleanpath::NEGOTIATION_ERROR_CODE { + let x224 = self + .0 + .x224_connection_pdu + .as_ref() + .ok_or_else(|| Self::missing_field("x224_connection_pdu"))?; + + Ok(Box::new(VecU8(x224.as_bytes().to_vec()))) + } else { + Err(GenericError(anyhow::anyhow!("RDCleanPath variant does not contain X.224 response")).into()) + } + } else { + Err(GenericError(anyhow::anyhow!("RDCleanPath variant does not contain X.224 response")).into()) } } /// Gets the server certificate chain (for Response variant) /// Returns a vector iterator of certificate bytes pub fn get_server_cert_chain(&self) -> Result, Box> { - let rdcleanpath = self - .0 - .clone() - .into_enum() - .context("missing RDCleanPath field") - .map_err(GenericError)?; - - match rdcleanpath { - ironrdp_rdcleanpath::RDCleanPath::Response { server_cert_chain, .. } => { - let certs: Vec> = server_cert_chain.iter().map(|cert| cert.as_bytes().to_vec()).collect(); - Ok(Box::new(CertificateChainIterator { certs, index: 0 })) - } - _ => Err(GenericError(anyhow::anyhow!( + if self.0.server_addr.is_some() { + self.0 + .x224_connection_pdu + .as_ref() + .ok_or_else(|| Self::missing_field("x224_connection_pdu"))?; + let certs = self + .0 + .server_cert_chain + .as_ref() + .ok_or_else(|| Self::missing_field("server_cert_chain"))?; + + let certs: Vec> = certs.iter().map(|cert| cert.as_bytes().to_vec()).collect(); + Ok(Box::new(CertificateChainIterator { certs, index: 0 })) + } else { + Err(GenericError(anyhow::anyhow!( "RDCleanPath variant does not contain certificate chain" )) - .into()), + .into()) } } /// Gets the server address string (for Response variant) pub fn get_server_addr<'a>(&'a self, writeable: &'a mut DiplomatWriteable) { - if let Ok(ironrdp_rdcleanpath::RDCleanPath::Response { server_addr, .. }) = self.0.clone().into_enum() { - let _ = write!(writeable, "{server_addr}"); + if self.0.server_addr.is_some() + && self.0.server_cert_chain.is_some() + && self.0.x224_connection_pdu.is_some() + { + if let Some(server_addr) = &self.0.server_addr { + let _ = write!(writeable, "{server_addr}"); + } } } /// Gets error message (for GeneralError variant) pub fn get_error_message<'a>(&'a self, writeable: &'a mut DiplomatWriteable) { - if let Ok(ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err)) = self.0.clone().into_enum() { + if let Ok(err) = self.general_error() { let _ = write!(writeable, "{err}"); } } /// Gets the error code (for GeneralError variant) pub fn get_error_code(&self) -> Result> { - let rdcleanpath = self - .0 - .clone() - .into_enum() - .context("missing RDCleanPath field") - .map_err(GenericError)?; - - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { - Ok(err.error_code) - } else { - Err(GenericError(anyhow::anyhow!("not a GeneralError variant")).into()) - } + let err = self.general_error()?; + Ok(err.error_code) } /// Gets the HTTP status code if present (for GeneralError variant) /// Returns error if not present or not a GeneralError variant pub fn get_http_status_code(&self) -> Result> { - let rdcleanpath = self - .0 - .clone() - .into_enum() - .context("missing RDCleanPath field") - .map_err(GenericError)?; + let err = self.general_error()?; - if let ironrdp_rdcleanpath::RDCleanPath::GeneralErr(err) = rdcleanpath { - err.http_status_code - .ok_or_else(|| GenericError(anyhow::anyhow!("HTTP status code not present")).into()) - } else { + err.http_status_code + .ok_or_else(|| GenericError(anyhow::anyhow!("HTTP status code not present")).into()) + } + + fn missing_field(field: &'static str) -> Box { + GenericError(anyhow::anyhow!("RDCleanPath is missing {} field", field)).into() + } + + fn general_error(&self) -> Result<&ironrdp_rdcleanpath::RDCleanPathErr, Box> { + let error = self.0.error.as_ref().ok_or_else(|| Self::missing_field("error"))?; + + if error.error_code == ironrdp_rdcleanpath::NEGOTIATION_ERROR_CODE { Err(GenericError(anyhow::anyhow!("not a GeneralError variant")).into()) + } else { + Ok(error) } } } From efb62a9fb8b6c8f284ab98f8504097e1641dd7e7 Mon Sep 17 00:00:00 2001 From: irving ou Date: Thu, 30 Oct 2025 12:23:43 -0400 Subject: [PATCH 25/25] fixing CI --- ffi/src/rdcleanpath.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/src/rdcleanpath.rs b/ffi/src/rdcleanpath.rs index 4418ae911..a842c747b 100644 --- a/ffi/src/rdcleanpath.rs +++ b/ffi/src/rdcleanpath.rs @@ -193,7 +193,7 @@ pub mod ffi { } fn missing_field(field: &'static str) -> Box { - GenericError(anyhow::anyhow!("RDCleanPath is missing {} field", field)).into() + GenericError(anyhow::anyhow!("RDCleanPath is missing {field} field")).into() } fn general_error(&self) -> Result<&ironrdp_rdcleanpath::RDCleanPathErr, Box> {