ironrdp_rdcleanpath/
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
4use core::fmt;
5
6use der::asn1::OctetString;
7
8// Re-export der crate for convenience
9#[rustfmt::skip] // do not re-order this pub use
10pub use der;
11
12pub const BASE_VERSION: u64 = 3389;
13pub const VERSION_1: u64 = BASE_VERSION + 1;
14
15pub const GENERAL_ERROR_CODE: u16 = 1;
16pub const NEGOTIATION_ERROR_CODE: u16 = 2;
17
18#[derive(Clone, Debug, Eq, PartialEq, der::Sequence)]
19#[asn1(tag_mode = "EXPLICIT")]
20pub struct RDCleanPathErr {
21    #[asn1(context_specific = "0")]
22    pub error_code: u16,
23    #[asn1(context_specific = "1", optional = "true")]
24    pub http_status_code: Option<u16>,
25    #[asn1(context_specific = "2", optional = "true")]
26    pub wsa_last_error: Option<u16>,
27    #[asn1(context_specific = "3", optional = "true")]
28    pub tls_alert_code: Option<u8>,
29}
30
31impl fmt::Display for RDCleanPathErr {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        let error_description = match self.error_code {
34            GENERAL_ERROR_CODE => "general error",
35            NEGOTIATION_ERROR_CODE => "negotiation error",
36            _ => "unknown error",
37        };
38        write!(f, "{error_description} (code {})", self.error_code)?;
39
40        if let Some(http_status_code) = self.http_status_code {
41            let description = match http_status_code {
42                200 => "OK",
43                400 => "bad request",
44                401 => "unauthorized",
45                403 => "forbidden",
46                404 => "not found",
47                405 => "method not allowed",
48                408 => "request timeout",
49                409 => "conflict",
50                410 => "gone",
51                413 => "payload too large",
52                414 => "URI too long",
53                422 => "unprocessable entity",
54                429 => "too many requests",
55                500 => "internal server error",
56                501 => "not implemented",
57                502 => "bad gateway",
58                503 => "service unavailable",
59                504 => "gateway timeout",
60                505 => "HTTP version not supported",
61                _ => "unknown HTTP status",
62            };
63            write!(f, "; HTTP {http_status_code} {description}")?;
64        }
65
66        if let Some(wsa_last_error) = self.wsa_last_error {
67            let description = match wsa_last_error {
68                10004 => "interrupted system call",
69                10009 => "bad file descriptor",
70                10013 => "permission denied",
71                10014 => "bad address",
72                10022 => "invalid argument",
73                10024 => "too many open files",
74                10035 => "resource temporarily unavailable",
75                10036 => "operation now in progress",
76                10037 => "operation already in progress",
77                10038 => "socket operation on nonsocket",
78                10039 => "destination address required",
79                10040 => "message too long",
80                10041 => "protocol wrong type for socket",
81                10042 => "bad protocol option",
82                10043 => "protocol not supported",
83                10044 => "socket type not supported",
84                10045 => "operation not supported",
85                10046 => "protocol family not supported",
86                10047 => "address family not supported by protocol family",
87                10048 => "address already in use",
88                10049 => "cannot assign requested address",
89                10050 => "network is down",
90                10051 => "network is unreachable",
91                10052 => "network dropped connection on reset",
92                10053 => "software caused connection abort",
93                10054 => "connection reset by peer",
94                10055 => "no buffer space available",
95                10056 => "socket is already connected",
96                10057 => "socket is not connected",
97                10058 => "cannot send after socket shutdown",
98                10060 => "connection timed out",
99                10061 => "connection refused",
100                10064 => "host is down",
101                10065 => "no route to host",
102                10067 => "too many processes",
103                10091 => "network subsystem is unavailable",
104                10092 => "Winsock version not supported",
105                10093 => "successful WSAStartup not yet performed",
106                10101 => "graceful shutdown in progress",
107                10109 => "class type not found",
108                11001 => "host not found",
109                11002 => "nonauthoritative host not found",
110                11003 => "this is a nonrecoverable error",
111                11004 => "valid name, no data record of requested type",
112                _ => "unknown WSA error",
113            };
114            write!(f, "; WSA {wsa_last_error} {description}")?;
115        }
116
117        if let Some(tls_alert_code) = self.tls_alert_code {
118            let description = match tls_alert_code {
119                0 => "close notify",
120                10 => "unexpected message",
121                20 => "bad record MAC",
122                21 => "decryption failed",
123                22 => "record overflow",
124                30 => "decompression failure",
125                40 => "handshake failure",
126                41 => "no certificate",
127                42 => "bad certificate",
128                43 => "unsupported certificate",
129                44 => "certificate revoked",
130                45 => "certificate expired",
131                46 => "certificate unknown",
132                47 => "illegal parameter",
133                48 => "unknown CA",
134                49 => "access denied",
135                50 => "decode error",
136                51 => "decrypt error",
137                60 => "export restriction",
138                70 => "protocol version",
139                71 => "insufficient security",
140                80 => "internal error",
141                90 => "user canceled",
142                100 => "no renegotiation",
143                109 => "missing extension",
144                110 => "unsupported extension",
145                111 => "certificate unobtainable",
146                112 => "unrecognized name",
147                113 => "bad certificate status response",
148                114 => "bad certificate hash value",
149                115 => "unknown PSK identity",
150                116 => "certificate required",
151                120 => "no application protocol",
152                _ => "unknown TLS alert",
153            };
154            write!(f, "; TLS alert {tls_alert_code} {description}")?;
155        }
156
157        Ok(())
158    }
159}
160
161impl core::error::Error for RDCleanPathErr {}
162
163#[derive(Clone, Debug, Eq, PartialEq, der::Sequence)]
164#[asn1(tag_mode = "EXPLICIT")]
165pub struct RDCleanPathPdu {
166    /// RDCleanPathPdu packet version.
167    #[asn1(context_specific = "0")]
168    pub version: u64,
169    /// The proxy error.
170    ///
171    /// Sent from proxy to client only.
172    #[asn1(context_specific = "1", optional = "true")]
173    pub error: Option<RDCleanPathErr>,
174    /// The RDP server address itself.
175    ///
176    /// Sent from client to proxy only.
177    #[asn1(context_specific = "2", optional = "true")]
178    pub destination: Option<String>,
179    /// Arbitrary string for authorization on proxy side.
180    ///
181    /// Sent from client to proxy only.
182    #[asn1(context_specific = "3", optional = "true")]
183    pub proxy_auth: Option<String>,
184    /// Currently unused. Could be used by a custom RDP server eventually.
185    #[asn1(context_specific = "4", optional = "true")]
186    pub server_auth: Option<String>,
187    /// The RDP PCB forwarded by the proxy to the RDP server.
188    ///
189    /// Sent from client to proxy only.
190    #[asn1(context_specific = "5", optional = "true")]
191    pub preconnection_blob: Option<String>,
192    /// Either the client handshake or the server handshake response.
193    ///
194    /// Both client and proxy will set this field.
195    #[asn1(context_specific = "6", optional = "true")]
196    pub x224_connection_pdu: Option<OctetString>,
197    /// The RDP server TLS chain.
198    ///
199    /// Sent from proxy to client only.
200    #[asn1(context_specific = "7", optional = "true")]
201    pub server_cert_chain: Option<Vec<OctetString>>,
202    // #[asn1(context_specific = "8", optional = "true")]
203    // pub ocsp_response: Option<String>,
204    /// IPv4 or IPv6 address of the server found by resolving the destination field on proxy side.
205    ///
206    /// Sent from proxy to client only.
207    #[asn1(context_specific = "9", optional = "true")]
208    pub server_addr: Option<String>,
209}
210
211impl Default for RDCleanPathPdu {
212    fn default() -> Self {
213        Self {
214            version: VERSION_1,
215            error: None,
216            destination: None,
217            proxy_auth: None,
218            server_auth: None,
219            preconnection_blob: None,
220            x224_connection_pdu: None,
221            server_cert_chain: None,
222            server_addr: None,
223        }
224    }
225}
226
227#[derive(Debug, Clone, PartialEq)]
228pub enum DetectionResult {
229    Detected { version: u64, total_length: usize },
230    NotEnoughBytes,
231    Failed,
232}
233
234impl RDCleanPathPdu {
235    /// Attempts to decode a RDCleanPath PDU from the provided buffer of bytes.
236    pub fn from_der(src: &[u8]) -> der::Result<Self> {
237        der::Decode::from_der(src)
238    }
239
240    /// Try to parse first few bytes in order to detect a RDCleanPath PDU
241    pub fn detect(src: &[u8]) -> DetectionResult {
242        use der::{Decode as _, Encode as _};
243
244        let Ok(mut slice_reader) = der::SliceReader::new(src) else {
245            return DetectionResult::Failed;
246        };
247
248        let header = match der::Header::decode(&mut slice_reader) {
249            Ok(header) => header,
250            Err(e) => match e.kind() {
251                der::ErrorKind::Incomplete { .. } => return DetectionResult::NotEnoughBytes,
252                _ => return DetectionResult::Failed,
253            },
254        };
255
256        let (Ok(header_encoded_len), Ok(body_length)) = (
257            header.encoded_len().and_then(usize::try_from),
258            usize::try_from(header.length),
259        ) else {
260            return DetectionResult::Failed;
261        };
262
263        let Some(total_length) = header_encoded_len.checked_add(body_length) else {
264            return DetectionResult::Failed;
265        };
266
267        match der::asn1::ContextSpecific::<u64>::decode_explicit(&mut slice_reader, der::TagNumber::N0) {
268            Ok(Some(version)) if version.value == VERSION_1 => DetectionResult::Detected {
269                version: VERSION_1,
270                total_length,
271            },
272            Ok(Some(_)) => DetectionResult::Failed,
273            Ok(None) => DetectionResult::NotEnoughBytes,
274            Err(e) => match e.kind() {
275                der::ErrorKind::Incomplete { .. } => DetectionResult::NotEnoughBytes,
276                _ => DetectionResult::Failed,
277            },
278        }
279    }
280
281    pub fn into_enum(self) -> Result<RDCleanPath, MissingRDCleanPathField> {
282        RDCleanPath::try_from(self)
283    }
284
285    pub fn new_general_error() -> Self {
286        Self {
287            version: VERSION_1,
288            error: Some(RDCleanPathErr {
289                error_code: GENERAL_ERROR_CODE,
290                http_status_code: None,
291                wsa_last_error: None,
292                tls_alert_code: None,
293            }),
294            ..Self::default()
295        }
296    }
297
298    pub fn new_http_error(status_code: u16) -> Self {
299        Self {
300            version: VERSION_1,
301            error: Some(RDCleanPathErr {
302                error_code: GENERAL_ERROR_CODE,
303                http_status_code: Some(status_code),
304                wsa_last_error: None,
305                tls_alert_code: None,
306            }),
307            ..Self::default()
308        }
309    }
310
311    pub fn new_request(
312        x224_pdu: Vec<u8>,
313        destination: String,
314        proxy_auth: String,
315        pcb: Option<String>,
316    ) -> der::Result<Self> {
317        Ok(Self {
318            version: VERSION_1,
319            destination: Some(destination),
320            proxy_auth: Some(proxy_auth),
321            preconnection_blob: pcb,
322            x224_connection_pdu: Some(OctetString::new(x224_pdu)?),
323            ..Self::default()
324        })
325    }
326
327    pub fn new_response(
328        server_addr: String,
329        x224_pdu: Vec<u8>,
330        x509_chain: impl IntoIterator<Item = Vec<u8>>,
331    ) -> der::Result<Self> {
332        Ok(Self {
333            version: VERSION_1,
334            x224_connection_pdu: Some(OctetString::new(x224_pdu)?),
335            server_cert_chain: Some(
336                x509_chain
337                    .into_iter()
338                    .map(OctetString::new)
339                    .collect::<der::Result<_>>()?,
340            ),
341            server_addr: Some(server_addr),
342            ..Self::default()
343        })
344    }
345
346    pub fn new_tls_error(alert_code: u8) -> Self {
347        Self {
348            version: VERSION_1,
349            error: Some(RDCleanPathErr {
350                error_code: GENERAL_ERROR_CODE,
351                http_status_code: None,
352                wsa_last_error: None,
353                tls_alert_code: Some(alert_code),
354            }),
355            ..Self::default()
356        }
357    }
358
359    pub fn new_wsa_error(wsa_error_code: u16) -> Self {
360        Self {
361            version: VERSION_1,
362            error: Some(RDCleanPathErr {
363                error_code: GENERAL_ERROR_CODE,
364                http_status_code: None,
365                wsa_last_error: Some(wsa_error_code),
366                tls_alert_code: None,
367            }),
368            ..Self::default()
369        }
370    }
371
372    /// Creates a negotiation error response that includes the server's X.224 negotiation response.
373    ///
374    /// This allows clients to extract specific negotiation failure details
375    /// (like "CredSSP required") from the server's original response.
376    ///
377    /// # Example
378    /// ```rust
379    /// use ironrdp_rdcleanpath::RDCleanPathPdu;
380    ///
381    /// // Server rejected connection with "CredSSP required" - preserve this info
382    /// let server_response = vec![/* X.224 Connection Confirm with failure code */];
383    /// let error_pdu = RDCleanPathPdu::new_negotiation_error(server_response)?;
384    /// # Ok::<(), der::Error>(())
385    /// ```
386    pub fn new_negotiation_error(server_x224_response: Vec<u8>) -> der::Result<Self> {
387        Ok(Self {
388            version: VERSION_1,
389            error: Some(RDCleanPathErr {
390                error_code: NEGOTIATION_ERROR_CODE,
391                http_status_code: None,
392                wsa_last_error: None,
393                tls_alert_code: None,
394            }),
395            x224_connection_pdu: Some(OctetString::new(server_x224_response)?),
396            ..Self::default()
397        })
398    }
399
400    pub fn to_der(&self) -> der::Result<Vec<u8>> {
401        der::Encode::to_der(self)
402    }
403}
404
405/// Helper enum to leverage Rust pattern matching feature.
406#[derive(Clone, Debug, Eq, PartialEq)]
407pub enum RDCleanPath {
408    Request {
409        destination: String,
410        proxy_auth: String,
411        server_auth: Option<String>,
412        preconnection_blob: Option<String>,
413        x224_connection_request: OctetString,
414    },
415    Response {
416        x224_connection_response: OctetString,
417        server_cert_chain: Vec<OctetString>,
418        server_addr: String,
419    },
420    GeneralErr(RDCleanPathErr),
421    NegotiationErr {
422        x224_connection_response: Vec<u8>,
423    },
424}
425
426impl RDCleanPath {
427    pub fn into_pdu(self) -> RDCleanPathPdu {
428        RDCleanPathPdu::from(self)
429    }
430}
431
432#[derive(Clone, Copy, Debug, Eq, PartialEq)]
433pub struct MissingRDCleanPathField(&'static str);
434
435impl fmt::Display for MissingRDCleanPathField {
436    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437        write!(f, "RDCleanPath is missing {} field", self.0)
438    }
439}
440
441impl core::error::Error for MissingRDCleanPathField {}
442
443impl TryFrom<RDCleanPathPdu> for RDCleanPath {
444    type Error = MissingRDCleanPathField;
445
446    fn try_from(pdu: RDCleanPathPdu) -> Result<Self, Self::Error> {
447        let rdcleanpath = if let Some(destination) = pdu.destination {
448            Self::Request {
449                destination,
450                proxy_auth: pdu.proxy_auth.ok_or(MissingRDCleanPathField("proxy_auth"))?,
451                server_auth: pdu.server_auth,
452                preconnection_blob: pdu.preconnection_blob,
453                x224_connection_request: pdu
454                    .x224_connection_pdu
455                    .ok_or(MissingRDCleanPathField("x224_connection_pdu"))?,
456            }
457        } else if let Some(server_addr) = pdu.server_addr {
458            Self::Response {
459                x224_connection_response: pdu
460                    .x224_connection_pdu
461                    .ok_or(MissingRDCleanPathField("x224_connection_pdu"))?,
462                server_cert_chain: pdu
463                    .server_cert_chain
464                    .ok_or(MissingRDCleanPathField("server_cert_chain"))?,
465                server_addr,
466            }
467        } else {
468            let error = pdu.error.ok_or(MissingRDCleanPathField("error"))?;
469            match (error.error_code, pdu.x224_connection_pdu) {
470                (NEGOTIATION_ERROR_CODE, Some(x224_pdu)) => Self::NegotiationErr {
471                    x224_connection_response: x224_pdu.as_bytes().to_vec(),
472                },
473                _ => Self::GeneralErr(error),
474            }
475        };
476
477        Ok(rdcleanpath)
478    }
479}
480
481impl From<RDCleanPath> for RDCleanPathPdu {
482    fn from(value: RDCleanPath) -> Self {
483        match value {
484            RDCleanPath::Request {
485                destination,
486                proxy_auth,
487                server_auth,
488                preconnection_blob,
489                x224_connection_request,
490            } => Self {
491                version: VERSION_1,
492                destination: Some(destination),
493                proxy_auth: Some(proxy_auth),
494                server_auth,
495                preconnection_blob,
496                x224_connection_pdu: Some(x224_connection_request),
497                ..Default::default()
498            },
499            RDCleanPath::Response {
500                x224_connection_response,
501                server_cert_chain,
502                server_addr,
503            } => Self {
504                version: VERSION_1,
505                x224_connection_pdu: Some(x224_connection_response),
506                server_cert_chain: Some(server_cert_chain),
507                server_addr: Some(server_addr),
508                ..Default::default()
509            },
510            RDCleanPath::GeneralErr(error) => Self {
511                version: VERSION_1,
512                error: Some(error),
513                ..Default::default()
514            },
515            RDCleanPath::NegotiationErr {
516                x224_connection_response,
517            } => Self {
518                version: VERSION_1,
519                error: Some(RDCleanPathErr {
520                    error_code: NEGOTIATION_ERROR_CODE,
521                    http_status_code: None,
522                    wsa_last_error: None,
523                    tls_alert_code: None,
524                }),
525                x224_connection_pdu: Some(
526                    OctetString::new(x224_connection_response)
527                        .expect("x224_connection_response smaller than u32::MAX (256 MiB)"),
528                ),
529                ..Default::default()
530            },
531        }
532    }
533}