ironrdp_connector/
lib.rs

1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
3
4mod macros;
5
6pub mod legacy;
7
8mod channel_connection;
9mod connection;
10pub mod connection_activation;
11mod connection_finalization;
12pub mod credssp;
13mod license_exchange;
14mod server_name;
15
16use core::any::Any;
17use core::fmt;
18use std::sync::Arc;
19
20use ironrdp_core::{encode_buf, encode_vec, Encode, WriteBuf};
21use ironrdp_pdu::nego::NegoRequestData;
22use ironrdp_pdu::rdp::capability_sets::{self, BitmapCodecs};
23use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo};
24use ironrdp_pdu::x224::X224;
25use ironrdp_pdu::{gcc, x224, PduHint};
26pub use sspi;
27
28pub use self::channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
29pub use self::connection::{encode_send_data_request, ClientConnector, ClientConnectorState, ConnectionResult};
30pub use self::connection_finalization::{ConnectionFinalizationSequence, ConnectionFinalizationState};
31pub use self::license_exchange::{LicenseExchangeSequence, LicenseExchangeState};
32pub use self::server_name::ServerName;
33pub use crate::license_exchange::LicenseCache;
34
35/// Provides user-friendly error messages for RDP negotiation failures
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct NegotiationFailure(ironrdp_pdu::nego::FailureCode);
38
39impl NegotiationFailure {
40    pub fn code(self) -> ironrdp_pdu::nego::FailureCode {
41        self.0
42    }
43}
44
45impl core::error::Error for NegotiationFailure {}
46
47impl From<ironrdp_pdu::nego::FailureCode> for NegotiationFailure {
48    fn from(code: ironrdp_pdu::nego::FailureCode) -> Self {
49        Self(code)
50    }
51}
52
53impl fmt::Display for NegotiationFailure {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        use ironrdp_pdu::nego::FailureCode;
56
57        match self.0 {
58            FailureCode::SSL_REQUIRED_BY_SERVER => {
59                write!(f, "server requires Enhanced RDP Security with TLS or CredSSP")
60            }
61            FailureCode::SSL_NOT_ALLOWED_BY_SERVER => {
62                write!(f, "server only supports Standard RDP Security")
63            }
64            FailureCode::SSL_CERT_NOT_ON_SERVER => {
65                write!(f, "server lacks valid authentication certificate")
66            }
67            FailureCode::INCONSISTENT_FLAGS => {
68                write!(f, "inconsistent security protocol flags")
69            }
70            FailureCode::HYBRID_REQUIRED_BY_SERVER => {
71                write!(f, "server requires Enhanced RDP Security with CredSSP")
72            }
73            FailureCode::SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER => {
74                write!(
75                    f,
76                    "server requires Enhanced RDP Security with TLS and client certificate"
77                )
78            }
79            _ => write!(f, "unknown negotiation failure (code: 0x{:08x})", u32::from(self.0)),
80        }
81    }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
86pub struct DesktopSize {
87    pub width: u16,
88    pub height: u16,
89}
90
91#[derive(Debug, Clone)]
92#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
93pub struct BitmapConfig {
94    pub lossy_compression: bool,
95    pub color_depth: u32,
96    pub codecs: BitmapCodecs,
97}
98
99#[derive(Debug, Clone)]
100pub struct SmartCardIdentity {
101    /// DER-encoded X509 certificate
102    pub certificate: Vec<u8>,
103    /// Smart card reader name
104    pub reader_name: String,
105    /// Smart card key container name
106    pub container_name: String,
107    /// Smart card CSP name
108    pub csp_name: String,
109    /// DER-encoded RSA 2048-bit private key
110    pub private_key: Vec<u8>,
111}
112
113#[derive(Debug, Clone)]
114pub enum Credentials {
115    UsernamePassword {
116        username: String,
117        password: String,
118    },
119    SmartCard {
120        pin: String,
121        config: Option<SmartCardIdentity>,
122    },
123}
124
125impl Credentials {
126    fn username(&self) -> Option<&str> {
127        match self {
128            Self::UsernamePassword { username, .. } => Some(username),
129            Self::SmartCard { .. } => None, // Username is ultimately provided by the smart card certificate.
130        }
131    }
132
133    fn secret(&self) -> &str {
134        match self {
135            Self::UsernamePassword { password, .. } => password,
136            Self::SmartCard { pin, .. } => pin,
137        }
138    }
139}
140
141#[derive(Debug, Clone)]
142#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
143pub struct Config {
144    /// The initial desktop size to request
145    pub desktop_size: DesktopSize,
146    /// The initial desktop scale factor to request.
147    ///
148    /// This becomes the `desktop_scale_factor` in the [`TS_UD_CS_CORE`](gcc::ClientCoreOptionalData) structure.
149    pub desktop_scale_factor: u32,
150    /// TLS + Graphical login (legacy)
151    ///
152    /// Also called SSL or TLS security protocol.
153    /// The PROTOCOL_SSL flag will be set.
154    ///
155    /// When this security protocol is negotiated, the RDP server will show a graphical login screen.
156    /// For Windows, it means that the login subsystem (winlogon.exe) and the GDI graphics subsystem
157    /// will be initiated and the user will authenticate himself using LogonUI.exe, as if
158    /// using the physical machine directly.
159    ///
160    /// This security protocol is being phased out because it’s not great security-wise.
161    /// Indeed, the whole RDP connection sequence will be performed, allowing anyone to effectively
162    /// open a RDP session session with all static channels joined and active (e.g.: I/O, clipboard,
163    /// sound, drive redirection, etc). This exposes a wide attack surface with many impacts on both
164    /// the client and the server.
165    ///
166    /// - Man-in-the-middle (MITM)
167    /// - Server-side takeover
168    /// - Client-side file stealing
169    /// - Client-side takeover
170    ///
171    /// Recommended reads on this topic:
172    ///
173    /// - <https://www.gosecure.net/blog/2018/12/19/rdp-man-in-the-middle-smile-youre-on-camera/>
174    /// - <https://www.gosecure.net/divi_overlay/mitigating-the-risks-of-remote-desktop-protocols/>
175    /// - <https://gosecure.github.io/presentations/2021-08-05_blackhat-usa/BlackHat-USA-21-Arsenal-PyRDP-OlivierBilodeau.pdf>
176    /// - <https://gosecure.github.io/presentations/2022-10-06_sector/OlivierBilodeau-Purple_RDP.pdf>
177    ///
178    /// By setting this option to `false`, it’s possible to effectively enforce usage of NLA on client side.
179    pub enable_tls: bool,
180    /// TLS + Network Level Authentication (NLA) using CredSSP
181    ///
182    /// The PROTOCOL_HYBRID and PROTOCOL_HYBRID_EX flags will be set.
183    ///
184    /// NLA is allowing authentication to be performed before session establishment.
185    ///
186    /// This option includes the extended CredSSP early user authorization result PDU.
187    /// This PDU is used by the server to deny access before any credentials (except for the username)
188    /// have been submitted, e.g.: typically if the user does not have the necessary remote access
189    /// privileges.
190    ///
191    /// The attack surface is considerably reduced in comparison to the legacy "TLS" security protocol.
192    /// For this reason, it is recommended to set `enable_tls` to `false` when connecting to NLA-capable
193    /// computers.
194    #[doc(alias("enable_nla", "nla"))]
195    pub enable_credssp: bool,
196    pub credentials: Credentials,
197    pub domain: Option<String>,
198    /// The build number of the client.
199    pub client_build: u32,
200    /// Name of the client computer
201    ///
202    /// The name will be truncated to the 15 first characters.
203    pub client_name: String,
204    pub keyboard_type: gcc::KeyboardType,
205    pub keyboard_subtype: u32,
206    pub keyboard_functional_keys_count: u32,
207    pub keyboard_layout: u32,
208    pub ime_file_name: String,
209    pub bitmap: Option<BitmapConfig>,
210    pub dig_product_id: String,
211    pub client_dir: String,
212    pub platform: capability_sets::MajorPlatformType,
213    /// Unique identifier for the computer
214    ///
215    ///  Each 32-bit integer contains client hardware-specific data helping the server uniquely identify the client.
216    pub hardware_id: Option<[u32; 4]>,
217    /// Optional data for the x224 connection request.
218    ///
219    /// Fallbacks to a sensible default depending on the provided credentials:
220    ///
221    /// - A cookie containing the username for a username/password.
222    /// - Nothing for a smart card.
223    pub request_data: Option<NegoRequestData>,
224    /// If true, the INFO_AUTOLOGON flag is set in the [`ClientInfoPdu`](ironrdp_pdu::rdp::ClientInfoPdu)
225    pub autologon: bool,
226    /// If true, the INFO_NOAUDIOPLAYBACK flag is set in the [`ClientInfoPdu`](ironrdp_pdu::rdp::ClientInfoPdu)
227    pub enable_audio_playback: bool,
228    pub performance_flags: PerformanceFlags,
229
230    pub license_cache: Option<Arc<dyn LicenseCache>>,
231
232    // For Timezone Redirection to sync the server's timezone with the client's.
233    pub timezone_info: TimezoneInfo,
234
235    // FIXME(@CBenoit): these are client-only options, not part of the connector.
236    pub enable_server_pointer: bool,
237    pub pointer_software_rendering: bool,
238}
239
240ironrdp_core::assert_impl!(Config: Send, Sync);
241
242pub trait State: Send + fmt::Debug + 'static {
243    fn name(&self) -> &'static str;
244    fn is_terminal(&self) -> bool;
245    fn as_any(&self) -> &dyn Any;
246}
247
248ironrdp_core::assert_obj_safe!(State);
249
250pub fn state_downcast<T: State>(state: &dyn State) -> Option<&T> {
251    state.as_any().downcast_ref()
252}
253
254pub fn state_is<T: State>(state: &dyn State) -> bool {
255    state.as_any().is::<T>()
256}
257
258impl State for () {
259    fn name(&self) -> &'static str {
260        "()"
261    }
262
263    fn is_terminal(&self) -> bool {
264        true
265    }
266
267    fn as_any(&self) -> &dyn Any {
268        self
269    }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
273pub enum Written {
274    Nothing,
275    Size(core::num::NonZeroUsize),
276}
277
278impl Written {
279    #[inline]
280    pub fn from_size(value: usize) -> ConnectorResult<Self> {
281        core::num::NonZeroUsize::new(value)
282            .map(Self::Size)
283            .ok_or_else(|| ConnectorError::general("invalid written length (can't be zero)"))
284    }
285
286    #[inline]
287    pub fn is_nothing(self) -> bool {
288        matches!(self, Self::Nothing)
289    }
290
291    #[inline]
292    pub fn size(self) -> Option<usize> {
293        if let Self::Size(size) = self {
294            Some(size.get())
295        } else {
296            None
297        }
298    }
299}
300
301pub trait Sequence: Send {
302    fn next_pdu_hint(&self) -> Option<&dyn PduHint>;
303
304    fn state(&self) -> &dyn State;
305
306    fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult<Written>;
307
308    fn step_no_input(&mut self, output: &mut WriteBuf) -> ConnectorResult<Written> {
309        self.step(&[], output)
310    }
311}
312
313ironrdp_core::assert_obj_safe!(Sequence);
314
315pub type ConnectorResult<T> = Result<T, ConnectorError>;
316
317#[non_exhaustive]
318#[derive(Debug)]
319pub enum ConnectorErrorKind {
320    Encode(ironrdp_core::EncodeError),
321    Decode(ironrdp_core::DecodeError),
322    Credssp(sspi::Error),
323    Reason(String),
324    AccessDenied,
325    General,
326    Custom,
327    Negotiation(NegotiationFailure),
328}
329
330impl fmt::Display for ConnectorErrorKind {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        match &self {
333            ConnectorErrorKind::Encode(_) => write!(f, "encode error"),
334            ConnectorErrorKind::Decode(_) => write!(f, "decode error"),
335            ConnectorErrorKind::Credssp(_) => write!(f, "CredSSP"),
336            ConnectorErrorKind::Reason(description) => write!(f, "reason: {description}"),
337            ConnectorErrorKind::AccessDenied => write!(f, "access denied"),
338            ConnectorErrorKind::General => write!(f, "general error"),
339            ConnectorErrorKind::Custom => write!(f, "custom error"),
340            ConnectorErrorKind::Negotiation(failure) => write!(f, "negotiation failure: {failure}"),
341        }
342    }
343}
344
345impl core::error::Error for ConnectorErrorKind {
346    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
347        match &self {
348            ConnectorErrorKind::Encode(e) => Some(e),
349            ConnectorErrorKind::Decode(e) => Some(e),
350            ConnectorErrorKind::Credssp(e) => Some(e),
351            ConnectorErrorKind::Reason(_) => None,
352            ConnectorErrorKind::AccessDenied => None,
353            ConnectorErrorKind::Custom => None,
354            ConnectorErrorKind::General => None,
355            ConnectorErrorKind::Negotiation(failure) => Some(failure),
356        }
357    }
358}
359
360pub type ConnectorError = ironrdp_error::Error<ConnectorErrorKind>;
361
362pub trait ConnectorErrorExt {
363    fn encode(error: ironrdp_core::EncodeError) -> Self;
364    fn decode(error: ironrdp_core::DecodeError) -> Self;
365    fn general(context: &'static str) -> Self;
366    fn reason(context: &'static str, reason: impl Into<String>) -> Self;
367    fn custom<E>(context: &'static str, e: E) -> Self
368    where
369        E: core::error::Error + Sync + Send + 'static;
370}
371
372impl ConnectorErrorExt for ConnectorError {
373    fn encode(error: ironrdp_core::EncodeError) -> Self {
374        Self::new("encode error", ConnectorErrorKind::Encode(error))
375    }
376
377    fn decode(error: ironrdp_core::DecodeError) -> Self {
378        Self::new("decode error", ConnectorErrorKind::Decode(error))
379    }
380
381    fn general(context: &'static str) -> Self {
382        Self::new(context, ConnectorErrorKind::General)
383    }
384
385    fn reason(context: &'static str, reason: impl Into<String>) -> Self {
386        Self::new(context, ConnectorErrorKind::Reason(reason.into()))
387    }
388
389    fn custom<E>(context: &'static str, e: E) -> Self
390    where
391        E: core::error::Error + Sync + Send + 'static,
392    {
393        Self::new(context, ConnectorErrorKind::Custom).with_source(e)
394    }
395}
396
397pub trait ConnectorResultExt {
398    #[must_use]
399    fn with_context(self, context: &'static str) -> Self;
400    #[must_use]
401    fn with_source<E>(self, source: E) -> Self
402    where
403        E: core::error::Error + Sync + Send + 'static;
404}
405
406impl<T> ConnectorResultExt for ConnectorResult<T> {
407    fn with_context(self, context: &'static str) -> Self {
408        self.map_err(|mut e| {
409            e.context = context;
410            // e.set_context(context);
411            e
412        })
413    }
414
415    fn with_source<E>(self, source: E) -> Self
416    where
417        E: core::error::Error + Sync + Send + 'static,
418    {
419        self.map_err(|e| e.with_source(source))
420    }
421}
422
423pub fn encode_x224_packet<T>(x224_msg: &T, buf: &mut WriteBuf) -> ConnectorResult<usize>
424where
425    T: Encode,
426{
427    let x224_msg_buf = encode_vec(x224_msg).map_err(ConnectorError::encode)?;
428
429    let pdu = x224::X224Data {
430        data: std::borrow::Cow::Owned(x224_msg_buf),
431    };
432
433    let written = encode_buf(&X224(pdu), buf).map_err(ConnectorError::encode)?;
434
435    Ok(written)
436}